From c5441d841e2e0a2494653c316ba52f7732bcc638 Mon Sep 17 00:00:00 2001 From: Leonid Kostrykin Date: Tue, 24 Sep 2024 17:53:43 +0200 Subject: [PATCH 1/5] Update requirements of `points2binaryimage` tool --- .../points2binaryimage/points2binaryimage.xml | 24 +++++++++--------- .../test-data/{points.tsv => input1.tsv} | 0 .../test-data/{out.tiff => output1.tif} | Bin 3 files changed, 12 insertions(+), 12 deletions(-) rename tools/points2binaryimage/test-data/{points.tsv => input1.tsv} (100%) rename tools/points2binaryimage/test-data/{out.tiff => output1.tif} (100%) diff --git a/tools/points2binaryimage/points2binaryimage.xml b/tools/points2binaryimage/points2binaryimage.xml index f07c082a..1da1b239 100644 --- a/tools/points2binaryimage/points2binaryimage.xml +++ b/tools/points2binaryimage/points2binaryimage.xml @@ -15,12 +15,10 @@ galaxy_image_analysis - scikit-image - numpy - pandas - pytz - python-dateutil - tifffile + scikit-image + numpy + pandas + tifffile - + - + + - + - + + - This tool converts a CSV list of points to a binary image by rasterizing the point coordinates. + **Converts a tabular list of points to a binary image 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 value 32767 (white). - Pixels not corresponding to any points in the CSV are assigned the value 0 (black). + Pixels not corresponding to any points in the tabular file are assigned the value 0 (black). diff --git a/tools/points2binaryimage/test-data/points.tsv b/tools/points2binaryimage/test-data/input1.tsv similarity index 100% rename from tools/points2binaryimage/test-data/points.tsv rename to tools/points2binaryimage/test-data/input1.tsv diff --git a/tools/points2binaryimage/test-data/out.tiff b/tools/points2binaryimage/test-data/output1.tif similarity index 100% rename from tools/points2binaryimage/test-data/out.tiff rename to tools/points2binaryimage/test-data/output1.tif From 44ccfb92c38ab467ad93e7fa99fc6de62c8739ea Mon Sep 17 00:00:00 2001 From: Leonid Kostrykin Date: Tue, 24 Sep 2024 18:02:21 +0200 Subject: [PATCH 2/5] Migrate tests to `image_diff` --- .../points2binaryimage/points2binaryimage.py | 12 ++++---- .../points2binaryimage/points2binaryimage.xml | 29 ++++++++++++------- tools/points2binaryimage/tests.xml | 1 + 3 files changed, 25 insertions(+), 17 deletions(-) create mode 120000 tools/points2binaryimage/tests.xml diff --git a/tools/points2binaryimage/points2binaryimage.py b/tools/points2binaryimage/points2binaryimage.py index 11f8c659..a5815f9f 100644 --- a/tools/points2binaryimage/points2binaryimage.py +++ b/tools/points2binaryimage/points2binaryimage.py @@ -7,7 +7,7 @@ import skimage.io -def points2binaryimage(point_file, out_file, shape=[500, 500], has_header=False, invert_xy=False): +def points2binaryimage(point_file, out_file, shape=[500, 500], has_header=False, swap_xy=False): img = np.zeros(shape, dtype=np.int16) if os.path.exists(point_file) and os.path.getsize(point_file) > 0: @@ -21,7 +21,7 @@ def points2binaryimage(point_file, out_file, shape=[500, 500], has_header=False, if int(a_row[0]) < 0 or int(a_row[1]) < 0: raise IndexError("Point {},{} is out of image with bounds {},{}.".format(int(a_row[0]), int(a_row[1]), shape[0], shape[1])) - if invert_xy: + if swap_xy: if img.shape[0] <= int(a_row[0]) or img.shape[1] <= int(a_row[1]): raise IndexError("Point {},{} is out of image with bounds {},{}.".format(int(a_row[0]), int(a_row[1]), shape[0], shape[1])) else: @@ -41,14 +41,14 @@ def points2binaryimage(point_file, out_file, shape=[500, 500], has_header=False, if __name__ == "__main__": parser = argparse.ArgumentParser() - parser.add_argument('point_file', type=argparse.FileType('r'), help='label file') + 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 CSV has header') - parser.add_argument('--invert_xy', dest='invert_xy', default=False, help='invert x and y in CSV') + 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') args = parser.parse_args() # TOOL - points2binaryimage(args.point_file.name, args.out_file, [args.shapey, args.shapex], has_header=args.has_header, invert_xy=args.invert_xy) + points2binaryimage(args.point_file.name, args.out_file, [args.shapey, args.shapex], has_header=args.has_header, swap_xy=args.swap_xy) diff --git a/tools/points2binaryimage/points2binaryimage.xml b/tools/points2binaryimage/points2binaryimage.xml index 1da1b239..7e5ab3d0 100644 --- a/tools/points2binaryimage/points2binaryimage.xml +++ b/tools/points2binaryimage/points2binaryimage.xml @@ -2,6 +2,7 @@ creators.xml + tests.xml 0.2 3 @@ -20,17 +21,23 @@ pandas tifffile - - - + - - - - + + + + @@ -42,8 +49,8 @@ - - + + diff --git a/tools/points2binaryimage/tests.xml b/tools/points2binaryimage/tests.xml new file mode 120000 index 00000000..e20d710a --- /dev/null +++ b/tools/points2binaryimage/tests.xml @@ -0,0 +1 @@ +../../macros/tests.xml \ No newline at end of file From 135f95c6dc75d8ab23dde03734bd39e9cb403443 Mon Sep 17 00:00:00 2001 From: Leonid Kostrykin Date: Tue, 24 Sep 2024 19:07:19 +0200 Subject: [PATCH 3/5] Add support for arbitrary columns and fix bugs --- .../points2binaryimage/points2binaryimage.py | 79 +++++++++++++----- .../points2binaryimage/points2binaryimage.xml | 27 ++++-- tools/points2binaryimage/test-data/input2.tsv | 39 +++++++++ .../test-data/output1.original.tif | Bin 0 -> 1466 bytes .../points2binaryimage/test-data/output1.tif | Bin 1466 -> 1456 bytes .../points2binaryimage/test-data/output2.tif | Bin 0 -> 34696 bytes 6 files changed, 120 insertions(+), 25 deletions(-) create mode 100644 tools/points2binaryimage/test-data/input2.tsv create mode 100644 tools/points2binaryimage/test-data/output1.original.tif create mode 100644 tools/points2binaryimage/test-data/output2.tif diff --git a/tools/points2binaryimage/points2binaryimage.py b/tools/points2binaryimage/points2binaryimage.py index a5815f9f..dabce37b 100644 --- a/tools/points2binaryimage/points2binaryimage.py +++ b/tools/points2binaryimage/points2binaryimage.py @@ -1,36 +1,75 @@ import argparse import os import warnings +from typing import List import numpy as np import pandas as pd +import scipy.ndimage as ndi import skimage.io -def points2binaryimage(point_file, out_file, shape=[500, 500], has_header=False, swap_xy=False): +def find_column(df: pd.DataFrame, candidates: List[str]) -> str: + """ + Returns tje column name present in `df` and the list of `candidates`. - img = np.zeros(shape, dtype=np.int16) + Raises: + KeyError: If there is no candidate column name present in `df`, or more than one. + """ + intersection = frozenset(df.columns) & frozenset(candidates) + if len(intersection) == 0: + raise KeyError(f'No such column: {", ".join(candidates)}') + elif len(intersection) > 1: + raise KeyError(f'The column names {", ".join(intersection)} are ambiguous') + else: + return next(iter(intersection)) + + +def points2binaryimage(point_file, out_file, shape, has_header=False, swap_xy=False, bg_value=0, fg_value=0xffff): + + 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(point_file, skiprows=1, header=None, delimiter="\t") + df = pd.read_csv(point_file, delimiter='\t') + pos_x_column = find_column(df, ['pos_x', 'POS_X']) + pos_y_column = 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 = find_column(df, ['radius', 'RADIUS']) + radius_list = df[radius_column] + except KeyError: + radius_list = [0] * len(pos_x_list) + + # Read the tabular file without header else: - df = pd.read_csv(point_file, header=None, delimiter="\t") - - for i in range(0, len(df)): - a_row = df.iloc[i] - if int(a_row[0]) < 0 or int(a_row[1]) < 0: - raise IndexError("Point {},{} is out of image with bounds {},{}.".format(int(a_row[0]), int(a_row[1]), shape[0], shape[1])) - - if swap_xy: - if img.shape[0] <= int(a_row[0]) or img.shape[1] <= int(a_row[1]): - raise IndexError("Point {},{} is out of image with bounds {},{}.".format(int(a_row[0]), int(a_row[1]), shape[0], shape[1])) - else: - img[int(a_row[1]), int(a_row[0])] = 32767 + 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) + + # Optionally swap the coordinates + if swap_xy: + pos_x_list, pos_y_list = pos_y_list, pos_x_list + + # Perform the rasterization + for y, x, radius in zip(pos_y_list, pos_x_list, radius_list): + + 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]})') + + if radius > 0: + mask = np.ones(shape, dtype=bool) + mask[y, x] = False + mask = (ndi.distance_transform_edt(mask) <= radius) + img[mask] = fg_value else: - if img.shape[0] <= int(a_row[1]) or img.shape[1] <= int(a_row[0]): - raise IndexError("Point {},{} is out of image with bounds {},{}.".format(int(a_row[1]), int(a_row[0]), shape[0], shape[1])) - else: - img[int(a_row[0]), int(a_row[1])] = 32767 + img[y, x] = fg_value + else: raise Exception("{} is empty or does not exist.".format(point_file)) # appropriate built-in error? @@ -51,4 +90,4 @@ def points2binaryimage(point_file, out_file, shape=[500, 500], has_header=False, args = parser.parse_args() # TOOL - points2binaryimage(args.point_file.name, args.out_file, [args.shapey, args.shapex], has_header=args.has_header, swap_xy=args.swap_xy) + points2binaryimage(args.point_file.name, args.out_file, (args.shapey, args.shapex), has_header=args.has_header, swap_xy=args.swap_xy) diff --git a/tools/points2binaryimage/points2binaryimage.xml b/tools/points2binaryimage/points2binaryimage.xml index 7e5ab3d0..d890ef35 100644 --- a/tools/points2binaryimage/points2binaryimage.xml +++ b/tools/points2binaryimage/points2binaryimage.xml @@ -3,8 +3,8 @@ creators.xml tests.xml - 0.2 - 3 + 0.3 + 0 @@ -46,22 +46,39 @@ - - + + + + + + + + + + **Converts a tabular list of points to a binary image 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 value 32767 (white). + The points are rasterized with value 65535 (white). 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. + 10.1016/j.jbiotec.2017.07.019 diff --git a/tools/points2binaryimage/test-data/input2.tsv b/tools/points2binaryimage/test-data/input2.tsv new file mode 100644 index 00000000..47efffa6 --- /dev/null +++ b/tools/points2binaryimage/test-data/input2.tsv @@ -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 diff --git a/tools/points2binaryimage/test-data/output1.original.tif b/tools/points2binaryimage/test-data/output1.original.tif new file mode 100644 index 0000000000000000000000000000000000000000..99a69a37b735a660789b8f83285c43fe77788b7e GIT binary patch literal 1466 zcmebD)MDUZU|`^5_{YG)zzAf40V9Mh0^~44*>XTO6C*QNjR26%3}u7#upzPe7?~JA z`nCbFAXG0GP?0E-8Zjg@#i8sCEDQ{WK)pObeXoGn2x`tZAUhbUmkG!Qfoi4VjKqRe zB`bw!V*?!pBZJslhy)(8Br`27Ei)%oub`5_$iUFT%FxKl&_Kb+$jZdP%E$tbPFhO+ iuP5DfV)YCcb;uz^te=qe3lHi)f-NFb3QHzMPb%(S$$%$!ucf=VP+qfF96;6D-|UH7OCdWXRO|Ma$z PdiD)vx8n;1a{UDWGWH^7 literal 1466 zcmebD)MDUZU|`^5_{YG)zzAf40V9Mh0^~44*>XTO6C*QNjR26%3}u7#upzPe7?~JA z`nCbFAXG0GP?0E-8Zjg@#i8sCEDQ{WK)pObeXoGn2x`tZAUhbUmkG!Qfoi4VjKqRe zB`bw!V*?!pBZJslhy)(8Br`27Ei)%oub`5_$iUFT%FxKl&_Kb+$jZdP%E$tbPFhO+ iuP5DfV)YCcb;uz^te=qe3ekFhLY_VQQB?di`y{_yI>({G=A_sv(IKKb(VufPA@b?~Qn^uK?3d3AO5 z@~0P{z4{fP5seLu4U7$p4U7$p4U7$p4U7$p4U7$p4U7$p4U7$p4U7%k!3|u${@=*YK%Q|A)-(Ve$#^>Xe9r{iVX-n!2e z-bH^y;z}Ro{aA6s{XAHdL!ZbcvM>xxpAFz896$2$GJQ5)!^*!KcpKqxSq&RwnT>na zEbpO}oo{UQaU2}(Em4cFz6>qwEi)}Q?>n>n7Ih4%*Do=Ydg;yZgk)*lK2_&#UVDvO zFh@xfNFRl&M&+)cd_)*43(gR+R}M!Km0d7)9-m@kjugVxayYJs9^$U6Sam|a{9OMI z-=(a_*tT0RM_w<7<9g^J?z&1&Cymdcx2(s`%Ib_SV{=__u{l)vXCOb-zr+3|o7Z$R^SEUQb|ze0k*uV^ z=oqQfjGJSPdHPk@nMDT^7IZr%)IqLMnOjl%28u>qqVUBPwf`CVoO4u*pW`d{{jIXH zWaZg(H^fGA+_-3{lpfHV^EyAQ9*=Bu|KkZ__d&x21tmY;>DOt^Q_^DGr z@6?vMsPnufnr~L4%#-z-T{%91G}?)Y6r zwVcdsWq>p5#Bv-vwcQ^x%k;sc5%{!%6TS8bY>3*~dQZZmMDuF3_!48Di#w8)cy^Q~}u*~_cxATCJop8NZ7Q0SYV*MD${^P!@#7yxV zFwBB_Il@%TExqz|+y$RTF=pM0>dc?9ZagEHxT+QD#VlG;lsmN=@}XW6F}Gwj(}G>j zuv{_r;3_e5j7;A_aWR8mQBD!H&u~{~_+-r58EciYlrrNL!Bu41PoKa^ z1Wc|D#C*gY%s)S!`i-gQu;}=h*mJJpPif3pt>T>P11S;EwmPFq8JIOHq2r!T@fqXR ztgu!-%WJxsbf5Xsm@TU8L$W&6c)p&aPEpWl)v#L6>n6a{%COEHd`MKw$i0|>>fF1=aL=S yZJRfD$F|u$r}%C5bTYKV4d89U&dA3G#sJe{Ke literal 0 HcmV?d00001 From e3ff47a8ccb1f9634420b962098c42f86a752804 Mon Sep 17 00:00:00 2001 From: Leonid Kostrykin Date: Tue, 24 Sep 2024 20:00:52 +0200 Subject: [PATCH 4/5] Update field labels --- tools/points2binaryimage/points2binaryimage.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/points2binaryimage/points2binaryimage.xml b/tools/points2binaryimage/points2binaryimage.xml index d890ef35..7c13e798 100644 --- a/tools/points2binaryimage/points2binaryimage.xml +++ b/tools/points2binaryimage/points2binaryimage.xml @@ -33,10 +33,10 @@ ]]> - + - + From c0434cb492c51c2c844163ebbdd85e328e8e3e2a Mon Sep 17 00:00:00 2001 From: Leonid Kostrykin Date: Tue, 24 Sep 2024 20:49:17 +0200 Subject: [PATCH 5/5] Update tools/points2binaryimage/points2binaryimage.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Björn Grüning --- tools/points2binaryimage/points2binaryimage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/points2binaryimage/points2binaryimage.py b/tools/points2binaryimage/points2binaryimage.py index dabce37b..ba75a3e0 100644 --- a/tools/points2binaryimage/points2binaryimage.py +++ b/tools/points2binaryimage/points2binaryimage.py @@ -11,7 +11,7 @@ def find_column(df: pd.DataFrame, candidates: List[str]) -> str: """ - Returns tje column name present in `df` and the list of `candidates`. + Returns the column name present in `df` and the list of `candidates`. Raises: KeyError: If there is no candidate column name present in `df`, or more than one.