Skip to content

Commit

Permalink
Merge pull request #136 from kostrykin/points2label/dev
Browse files Browse the repository at this point in the history
Merge the `points2binaryimage` tool into `points2labelimage`
  • Loading branch information
bgruening authored Sep 27, 2024
2 parents a3c77a7 + 1ca0fa1 commit 6fc9ab8
Show file tree
Hide file tree
Showing 16 changed files with 261 additions and 56 deletions.
Binary file not shown.
1 change: 1 addition & 0 deletions tools/points2labelimage/creators.xml
132 changes: 105 additions & 27 deletions tools/points2labelimage/points2label.py
Original file line number Diff line number Diff line change
@@ -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:
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:
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, 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)

# 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

if output_file is not None:
with warnings.catch_warnings():
warnings.simplefilter("ignore")
skimage.io.imsave(output_file, labelimg, plugin='tifffile')
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')
args = parser.parse_args()
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')

original_shape = skimage.io.imread(args.org_file.name, plugin='tifffile').shape
args = parser.parse_args()

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,
)
124 changes: 102 additions & 22 deletions tools/points2labelimage/points2label.xml
Original file line number Diff line number Diff line change
@@ -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="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="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="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**

This tool converts points to a label image.
**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).

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>
Expand Down
Binary file removed tools/points2labelimage/test-data/galaxyIcon_noText.tif
Binary file not shown.
9 changes: 9 additions & 0 deletions tools/points2labelimage/test-data/input1.tsv
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
11.7555970149 10.4048507463
15 14
19 2
5 4
5 5
5 6
5 7
5 8
5 9
39 changes: 39 additions & 0 deletions tools/points2labelimage/test-data/input2.tsv
Original file line number Diff line number Diff line change
@@ -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
4 changes: 4 additions & 0 deletions tools/points2labelimage/test-data/input3.tsv
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
pos_x pos_y radius label
20 20 20 1
50 50 40 1
150 50 40 2
Binary file removed tools/points2labelimage/test-data/out.tif
Binary file not shown.
Binary file not shown.
Binary file added tools/points2labelimage/test-data/output2.tif
Binary file not shown.
Binary file not shown.
Binary file added tools/points2labelimage/test-data/output3.tif
Binary file not shown.
Binary file not shown.
7 changes: 0 additions & 7 deletions tools/points2labelimage/test-data/points.tsv

This file was deleted.

1 change: 1 addition & 0 deletions tools/points2labelimage/tests.xml

0 comments on commit 6fc9ab8

Please sign in to comment.