changeset 3:de611b3b5ae8 draft default tip

planemo upload for repository https://github.com/BMCV/galaxy-image-analysis/tree/master/tools/points2labelimage/ commit 6fc9ab8db9ef72ac7ded30d7373768feeae9390d
author imgteam
date Fri, 27 Sep 2024 17:41:21 +0000
parents 30ca5d5d03ec
children
files creators.xml points2label.py points2label.xml test-data/galaxyIcon_noText.tif test-data/input1.tsv test-data/input2.tsv test-data/input3.tsv test-data/out.tif test-data/output1_binary.tif test-data/output2.tif test-data/output2_binary.tif test-data/output3.tif test-data/output3_binary.tif test-data/points.tsv tests.xml
diffstat 15 files changed, 382 insertions(+), 56 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/creators.xml	Fri Sep 27 17:41:21 2024 +0000
@@ -0,0 +1,28 @@
+<macros>
+
+    <xml name="creators/bmcv">
+        <organization name="Biomedical Computer Vision Group, Heidelberg Universtiy" alternateName="BMCV" url="http://www.bioquant.uni-heidelberg.de/research/groups/biomedical_computer_vision.html" />
+        <yield />
+    </xml>
+
+    <xml name="creators/rmassei">
+        <person givenName="Riccardo" familyName="Massei"/>
+        <yield/>
+    </xml>
+
+    <xml name="creators/alliecreason">
+        <person givenName="Allison" familyName="Creason"/>
+        <yield/>
+    </xml>
+
+    <xml name="creators/bugraoezdemir">
+        <person givenName="Bugra" familyName="Oezdemir"/>
+        <yield/>
+    </xml>
+
+    <xml name="creators/thawn">
+        <person givenName="Till" familyName="Korten"/>
+        <yield/>
+    </xml>
+    
+</macros>
--- a/points2label.py	Mon Nov 13 22:11:46 2023 +0000
+++ b/points2label.py	Fri Sep 27 17:41:21 2024 +0000
@@ -1,47 +1,125 @@
 import argparse
-import sys
+import os
 import warnings
 
+import giatools.pandas
 import numpy as np
 import pandas as pd
+import scipy.ndimage as ndi
 import skimage.io
+import skimage.segmentation
 
 
-def points2label(labels, shape, output_file=None, has_header=False, is_TSV=True):
-    labelimg = np.zeros([shape[0], shape[1]], dtype=np.int32)
+def rasterize(point_file, out_file, shape, has_header=False, swap_xy=False, bg_value=0, fg_value=None):
 
-    if is_TSV:
+    img = np.full(shape, dtype=np.uint16, fill_value=bg_value)
+    if os.path.exists(point_file) and os.path.getsize(point_file) > 0:
+
+        # Read the tabular file with information from the header
         if has_header:
-            df = pd.read_csv(labels, sep='\t', skiprows=1, header=None)
-        else:
-            df = pd.read_csv(labels, sep='\t', header=None)
-    else:
-        if has_header:
-            df = pd.read_csv(labels, skiprows=1, header=None)
+            df = pd.read_csv(point_file, delimiter='\t')
+
+            pos_x_column = giatools.pandas.find_column(df, ['pos_x', 'POS_X'])
+            pos_y_column = giatools.pandas.find_column(df, ['pos_y', 'POS_Y'])
+            pos_x_list = df[pos_x_column].round().astype(int)
+            pos_y_list = df[pos_y_column].round().astype(int)
+            assert len(pos_x_list) == len(pos_y_list)
+
+            try:
+                radius_column = giatools.pandas.find_column(df, ['radius', 'RADIUS'])
+                radius_list = df[radius_column]
+                assert len(pos_x_list) == len(radius_list)
+            except KeyError:
+                radius_list = [0] * len(pos_x_list)
+
+            try:
+                label_column = giatools.pandas.find_column(df, ['label', 'LABEL'])
+                label_list = df[label_column]
+                assert len(pos_x_list) == len(label_list)
+            except KeyError:
+                label_list = list(range(1, len(pos_x_list) + 1))
+
+        # Read the tabular file without header
         else:
-            df = pd.read_csv(labels, header=None)
+            df = pd.read_csv(point_file, header=None, delimiter='\t')
+            pos_x_list = df[0].round().astype(int)
+            pos_y_list = df[1].round().astype(int)
+            assert len(pos_x_list) == len(pos_y_list)
+            radius_list = [0] * len(pos_x_list)
+            label_list = list(range(1, len(pos_x_list) + 1))
+
+        # Optionally swap the coordinates
+        if swap_xy:
+            pos_x_list, pos_y_list = pos_y_list, pos_x_list
 
-    for i in range(0, len(df)):
-        a_row = df.iloc[i]
-        labelimg[a_row[0], a_row[1]] = i + 1
+        # Perform the rasterization
+        for y, x, radius, label in zip(pos_y_list, pos_x_list, radius_list, label_list):
+            if fg_value is not None:
+                label = fg_value
+
+            if y < 0 or x < 0 or y >= shape[0] or x >= shape[1]:
+                raise IndexError(f'The point x={x}, y={y} exceeds the bounds of the image (width: {shape[1]}, height: {shape[0]})')
+
+            # Rasterize circle and distribute overlapping image area
+            if radius > 0:
+                mask = np.ones(shape, dtype=bool)
+                mask[y, x] = False
+                mask = (ndi.distance_transform_edt(mask) <= radius)
 
-    if output_file is not None:
-        with warnings.catch_warnings():
-            warnings.simplefilter("ignore")
-            skimage.io.imsave(output_file, labelimg, plugin='tifffile')
+                # Compute the overlap (pretend there is none if the rasterization is binary)
+                if fg_value is None:
+                    overlap = np.logical_and(img > 0, mask)
+                else:
+                    overlap = np.zeros(shape, dtype=bool)
+
+                # Rasterize the part of the circle which is disjoint from other foreground.
+                #
+                # In the current implementation, the result depends on the order of the rasterized circles if somewhere
+                # more than two circles overlap. This is probably negligable for most applications. To achieve results
+                # that are invariant to the order, first all circles would need to be rasterized independently, and
+                # then blended together. This, however, would either strongly increase the memory consumption, or
+                # require a more complex implementation which exploits the sparsity of the rasterized masks.
+                #
+                disjoint_mask = np.logical_xor(mask, overlap)
+                if disjoint_mask.any():
+                    img[disjoint_mask] = label
+
+                    # Distribute the remaining part of the circle
+                    if overlap.any():
+                        dist = ndi.distance_transform_edt(overlap)
+                        foreground = (img > 0)
+                        img[overlap] = 0
+                        img = skimage.segmentation.watershed(dist, img, mask=foreground)
+
+            # Rasterize point (there is no overlapping area to be distributed)
+            else:
+                img[y, x] = label
+
     else:
-        return labelimg
+        raise Exception("{} is empty or does not exist.".format(point_file))  # appropriate built-in error?
+
+    with warnings.catch_warnings():
+        warnings.simplefilter("ignore")
+        skimage.io.imsave(out_file, img, plugin='tifffile')  # otherwise we get problems with the .dat extension
 
 
 if __name__ == "__main__":
     parser = argparse.ArgumentParser()
-    parser.add_argument('label_file', type=argparse.FileType('r'), default=sys.stdin, help='label file')
-    parser.add_argument('out_file', type=argparse.FileType('w'), default=sys.stdin, help='out file')
-    parser.add_argument('org_file', type=argparse.FileType('r'), default=sys.stdin, help='input original file')
-    parser.add_argument('--has_header', dest='has_header', type=bool, default=False, help='label file has header')
-    parser.add_argument('--is_tsv', dest='is_tsv', type=bool, default=True, help='label file is TSV')
+    parser.add_argument('point_file', type=argparse.FileType('r'), help='point file')
+    parser.add_argument('out_file', type=str, help='out file (TIFF)')
+    parser.add_argument('shapex', type=int, help='shapex')
+    parser.add_argument('shapey', type=int, help='shapey')
+    parser.add_argument('--has_header', dest='has_header', default=False, help='set True if point file has header')
+    parser.add_argument('--swap_xy', dest='swap_xy', default=False, help='Swap X and Y coordinates')
+    parser.add_argument('--binary', dest='binary', default=False, help='Produce binary image')
+
     args = parser.parse_args()
 
-    original_shape = skimage.io.imread(args.org_file.name, plugin='tifffile').shape
-
-    points2label(args.label_file.name, original_shape, args.out_file.name, args.has_header, args.is_tsv)
+    rasterize(
+        args.point_file.name,
+        args.out_file,
+        (args.shapey, args.shapex),
+        has_header=args.has_header,
+        swap_xy=args.swap_xy,
+        fg_value=0xffff if args.binary else None,
+    )
--- a/points2label.xml	Mon Nov 13 22:11:46 2023 +0000
+++ b/points2label.xml	Fri Sep 27 17:41:21 2024 +0000
@@ -1,41 +1,121 @@
-<tool id="ip_points_to_label" name="Convert point coordinates to label map" version="0.3-2"> 
+<tool id="ip_points_to_label" name="Convert point coordinates to label map" version="@TOOL_VERSION@+galaxy@VERSION_SUFFIX@" profile="20.05">
     <description></description>
+    <macros>
+        <import>creators.xml</import>
+        <import>tests.xml</import>
+        <token name="@TOOL_VERSION@">0.4</token>
+        <token name="@VERSION_SUFFIX@">0</token>
+    </macros>
+    <creator>
+        <expand macro="creators/bmcv" />
+    </creator>
     <edam_operations>
         <edam_operation>operation_3443</edam_operation>
     </edam_operations>
     <xrefs>
         <xref type="bio.tools">galaxy_image_analysis</xref>
     </xrefs>
-    <requirements>
-        <requirement type="package" version="1.15.4">numpy</requirement>
-        <requirement type="package" version="0.14.2">scikit-image</requirement>
-        <requirement type="package" version="0.23.4">pandas</requirement>
-        <requirement type="package" version="0.15.1">tifffile</requirement>
-    </requirements> 
-    <command detect_errors="aggressive">
-    <![CDATA[
-        python '$__tool_directory__/points2label.py' '$input' '$output' '$org_file' $has_header
-    ]]>
-    </command>
-    <inputs>
-        <param name="input" type="data" format="tabular" label="Point CSV file"/>
-        <param name="org_file" type="data" format="tiff" label="Original label image file"/>
-        <param name="has_header" type="boolean" checked="false" truevalue="--has_header True" falsevalue="" optional="true" label="Does point file contain header?" /> 
+    <requirements> 
+        <requirement type="package" version="0.21">scikit-image</requirement> 
+        <requirement type="package" version="1.26.4">numpy</requirement>
+        <requirement type="package" version="1.2.4">pandas</requirement>
+        <requirement type="package" version="2024.6.18">tifffile</requirement>
+        <requirement type="package" version="0.3.1">giatools</requirement> 
+    </requirements>
+    <command detect_errors="aggressive"><![CDATA[
+
+        python '$__tool_directory__/points2label.py'
+        '$input'
+        '$output'
+        $shapex
+        $shapey
+        $has_header
+        $swap_xy
+        $binary
+
+    ]]></command>
+    <inputs> 
+        <param name="input" type="data" format="tabular" label="Tabular list of points"/> 
+        <param name="shapex" type="integer" value="500" min="1" label="Width of output image" />
+        <param name="shapey" type="integer" value="500" min="1" label="Height of output image" />
+        <param name="has_header" type="boolean" checked="true" truevalue="--has_header True" falsevalue="" optional="true" label="Tabular list of points has header" help="Turning this off will ignore the first row and assume that the X and Y coordinates correspond to the first and second column, respectively." /> 
+        <param name="swap_xy" type="boolean" checked="false" falsevalue="" truevalue="--swap_xy True" optional="true" label="Swap X and Y coordinates" help="Swap the X and Y coordinates, regardless of whether the tabular list has a header or not." />
+        <param name="binary" type="boolean" checked="false" truevalue="--binary True" falsevalue="" optional="true" label="Produce binary image" help="Use the same label for all points (65535)." /> 
     </inputs>
     <outputs>
-        <data name="output" format="tiff"/>
+        <data name="output" format="tiff" />
     </outputs>
     <tests>
+        <!-- Binary / TSV without header -->
         <test>
-            <param name="input" value="points.tsv"/>
-            <param name="org_file" value="galaxyIcon_noText.tif"/>
-            <output name="output" file="out.tif" ftype="tiff" compare="sim_size"/>
+            <param name="input" value="input1.tsv" />
+            <param name="shapex" value="30" />
+            <param name="shapey" value="20" />
+            <param name="has_header" value="false" />
+            <param name="swap_xy" value="true" />
+            <param name="binary" value="true" />
+            <expand macro="tests/binary_image_diff" name="output" value="output1_binary.tif" ftype="tiff" />
+        </test>
+        <!-- Binary / TSV with header -->
+        <test>
+            <param name="input" value="input2.tsv" />
+            <param name="shapex" value="205" />
+            <param name="shapey" value="84" />
+            <param name="has_header" value="true" />
+            <param name="swap_xy" value="false" />
+            <param name="binary" value="true" />
+            <expand macro="tests/binary_image_diff" name="output" value="output2_binary.tif" ftype="tiff" />
+        </test>
+        <!-- Labeled / TSV with header -->
+        <test>
+            <param name="input" value="input2.tsv" />
+            <param name="shapex" value="205" />
+            <param name="shapey" value="84" />
+            <param name="has_header" value="true" />
+            <param name="swap_xy" value="false" />
+            <param name="binary" value="false" />
+            <expand macro="tests/label_image_diff" name="output" value="output2.tif" ftype="tiff" />
+        </test>
+        <!-- Binary / TSV with header / TSV with labels -->
+        <test>
+            <param name="input" value="input3.tsv" />
+            <param name="shapex" value="200" />
+            <param name="shapey" value="100" />
+            <param name="has_header" value="true" />
+            <param name="swap_xy" value="false" />
+            <param name="binary" value="true" />
+            <expand macro="tests/binary_image_diff" name="output" value="output3_binary.tif" ftype="tiff" />
+        </test>
+        <!-- Labeled / TSV with header / TSV with labels -->
+        <test>
+            <param name="input" value="input3.tsv" />
+            <param name="shapex" value="200" />
+            <param name="shapey" value="100" />
+            <param name="has_header" value="true" />
+            <param name="swap_xy" value="false" />
+            <param name="binary" value="false" />
+            <expand macro="tests/label_image_diff" name="output" value="output3.tif" ftype="tiff" />
         </test>
     </tests>
     <help>
-    **What it does**
+
+        **Converts a tabular list of points to a label map by rasterizing the point coordinates.**
+
+        The created image is a single-channel image with 16 bits per pixel (unsigned integer). The points are
+        rasterized with unique labels, or the value 65535 (white) for binary image output. Pixels not corresponding to
+        any points in the tabular file are assigned the value 0 (black).
 
-    This tool converts points to a label image.
+        The tabular list of points can either be header-less. In this case, the first and second columns are expected
+        to be the X and Y coordinates, respectively. Otherwise, if a header is present, it is searched for the
+        following column names:
+
+        - ``pos_x`` or ``POS_X``: This column corresponds to the X coordinates.
+        - ``pos_y`` or ``POS_Y``: This column corresponds to the Y coordinates.
+        - If a ``radius`` or ``RADIUS`` column is present, then the points will be rasterized as circles of the
+          corresponding radii.
+        - If a ``label`` or ``LABEL`` column is present, then the corresponding labels will be used for rasterization
+          (unless "Produce binary image" is activated). Different points are allowed to use the same label.
+
     </help>
     <citations>
         <citation type="doi">10.1016/j.jbiotec.2017.07.019</citation> 
Binary file test-data/galaxyIcon_noText.tif has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test-data/input1.tsv	Fri Sep 27 17:41:21 2024 +0000
@@ -0,0 +1,9 @@
+11.7555970149	10.4048507463
+15	14
+19	2
+5	4
+5	5
+5	6
+5	7
+5	8
+5	9
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test-data/input2.tsv	Fri Sep 27 17:41:21 2024 +0000
@@ -0,0 +1,39 @@
+frame	pos_x	pos_y	scale	radius	intensity
+1	85	32	1.33	3.77	18807.73
+1	190	25	1.78	5.03	24581.44
+1	137	26	1.44	4.09	19037.59
+1	63	42	1.44	4.09	22390.80
+1	107	44	1.33	3.77	23429.96
+1	61	27	1.56	4.40	18052.18
+1	158	39	1.44	4.09	18377.02
+1	190	14	1.33	3.77	18548.86
+1	182	33	1.78	5.03	26467.79
+1	39	39	1.44	4.09	14782.43
+1	169	26	1.33	3.77	14203.41
+1	61	54	1.33	3.77	23248.06
+1	95	52	1.33	3.77	21480.71
+1	23	60	1.89	5.34	25203.43
+1	84	24	1.56	4.40	16630.57
+1	121	47	1.67	4.71	15459.11
+1	66	49	1.11	3.14	23858.07
+1	115	36	2.00	5.66	16389.10
+1	55	51	1.33	3.77	23548.90
+1	130	72	1.67	4.71	15769.02
+1	117	23	1.33	3.77	16763.14
+1	45	52	1.56	4.40	22877.61
+1	36	71	1.56	4.40	20780.96
+1	78	17	1.33	3.77	16844.51
+1	101	38	1.56	4.40	21376.59
+1	147	31	1.78	5.03	16597.14
+1	163	55	2.00	5.66	18301.54
+1	164	23	1.33	3.77	17073.82
+1	150	24	1.56	4.40	15440.02
+1	151	67	1.78	5.03	18419.96
+1	26	53	2.00	5.66	20586.01
+1	79	62	1.33	3.77	15232.88
+1	69	17	1.11	3.14	15601.83
+1	83	52	1.33	3.77	18315.00
+1	16	54	2.00	5.66	22140.66
+1	166	61	1.78	5.03	18488.78
+1	163	43	1.44	4.09	16925.49
+1	130	53	1.78	5.03	15101.96
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test-data/input3.tsv	Fri Sep 27 17:41:21 2024 +0000
@@ -0,0 +1,4 @@
+pos_x	pos_y	radius	label
+20	20	20	1
+50	50	40	1
+150	50	40	2
Binary file test-data/out.tif has changed
Binary file test-data/output1_binary.tif has changed
Binary file test-data/output2.tif has changed
Binary file test-data/output2_binary.tif has changed
Binary file test-data/output3.tif has changed
Binary file test-data/output3_binary.tif has changed
--- a/test-data/points.tsv	Mon Nov 13 22:11:46 2023 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,7 +0,0 @@
-5	3
-10	2
-8	4
-15	15
-8	5
-8	6
-8	7
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests.xml	Fri Sep 27 17:41:21 2024 +0000
@@ -0,0 +1,95 @@
+<macros>
+
+    <!-- Macros for verification of image outputs -->
+
+    <xml
+        name="tests/binary_image_diff"
+        tokens="name,value,ftype,metric,eps"
+        token_metric="mae"
+        token_eps="0.01">
+
+        <output name="@NAME@" value="@VALUE@" ftype="@FTYPE@" compare="image_diff" metric="@METRIC@" eps="@EPS@" pin_labels="0">
+            <assert_contents>
+                <has_image_n_labels n="2"/>
+                <yield/>
+            </assert_contents>
+        </output>
+
+    </xml>
+
+    <xml
+        name="tests/label_image_diff"
+        tokens="name,value,ftype,metric,eps,pin_labels"
+        token_metric="iou"
+        token_eps="0.01"
+        token_pin_labels="0">
+
+        <output name="@NAME@" value="@VALUE@" ftype="@FTYPE@" compare="image_diff" metric="@METRIC@" eps="@EPS@" pin_labels="@PIN_LABELS@">
+            <assert_contents>
+                <yield/>
+            </assert_contents>
+        </output>
+
+    </xml>
+
+    <xml
+        name="tests/intensity_image_diff"
+        tokens="name,value,ftype,metric,eps"
+        token_metric="rms"
+        token_eps="0.01">
+
+        <output name="@NAME@" value="@VALUE@" ftype="@FTYPE@" compare="image_diff" metric="@METRIC@" eps="@EPS@">
+            <assert_contents>
+                <yield/>
+            </assert_contents>
+        </output>
+
+    </xml>
+
+    <!-- Variants of the above for verification of collection elements -->
+
+    <xml
+        name="tests/binary_image_diff/element"
+        tokens="name,value,ftype,metric,eps"
+        token_metric="mae"
+        token_eps="0.01">
+
+        <element name="@NAME@" value="@VALUE@" ftype="@FTYPE@" compare="image_diff" metric="@METRIC@" eps="@EPS@" pin_labels="0">
+            <assert_contents>
+                <has_image_n_labels n="2"/>
+                <yield/>
+            </assert_contents>
+        </element>
+
+    </xml>
+
+    <xml
+        name="tests/label_image_diff/element"
+        tokens="name,value,ftype,metric,eps"
+        token_metric="iou"
+        token_eps="0.01"
+        token_pin_labels="0">
+
+        <element name="@NAME@" value="@VALUE@" ftype="@FTYPE@" compare="image_diff" metric="@METRIC@" eps="@EPS@" pin_labels="@PIN_LABELS@">
+            <assert_contents>
+                <yield/>
+            </assert_contents>
+        </element>
+
+    </xml>
+
+    <xml
+        name="tests/intensity_image_diff/element"
+        tokens="name,value,ftype,metric,eps"
+        token_metric="rms"
+        token_eps="0.01">
+
+        <element name="@NAME@" value="@VALUE@" ftype="@FTYPE@" compare="image_diff" metric="@METRIC@" eps="@EPS@">
+            <assert_contents>
+                <yield/>
+            </assert_contents>
+        </element>
+
+    </xml>
+
+</macros>