Mercurial > repos > imgteam > 2d_auto_threshold
changeset 4:7d80eb2411fb draft default tip
planemo upload for repository https://github.com/BMCV/galaxy-image-analysis/tree/master/tools/2d_auto_threshold/ commit 01343602708de3cc7fa4986af9000adc36dd0651
author | imgteam |
---|---|
date | Sat, 07 Jun 2025 18:38:16 +0000 |
parents | 5224cc463a97 |
children | |
files | auto_threshold.py auto_threshold.xml creators.xml test-data/out.tif test-data/out1.tiff test-data/out2.tif test-data/out2.tiff test-data/out3.tiff test-data/out4.tiff test-data/out5.tiff test-data/rgb.png test-data/rgb_otsu.tiff test-data/sample.tif test-data/sample.tiff test-data/sample2.tiff test-data/sample_manual_180_240.tiff tests.xml |
diffstat | 17 files changed, 397 insertions(+), 67 deletions(-) [+] |
line wrap: on
line diff
--- a/auto_threshold.py Sat Feb 19 15:16:52 2022 +0000 +++ b/auto_threshold.py Sat Jun 07 18:38:16 2025 +0000 @@ -1,47 +1,117 @@ """ -Copyright 2017-2022 Biomedical Computer Vision Group, Heidelberg University. +Copyright 2017-2024 Biomedical Computer Vision Group, Heidelberg University. Distributed under the MIT license. See file LICENSE for detail or copy at https://opensource.org/licenses/MIT - """ import argparse +import numpy as np import skimage.filters -import skimage.io import skimage.util -import tifffile +from giatools.image import Image + + +class DefaultThresholdingMethod: + + def __init__(self, thres, accept: list[str] | None = None, **kwargs): + self.thres = thres + self.accept = accept if accept else [] + self.kwargs = kwargs + + def __call__(self, image, *args, offset=0, **kwargs): + accepted_kwargs = self.kwargs.copy() + for key, val in kwargs.items(): + if key in self.accept: + accepted_kwargs[key] = val + thres = self.thres(image, *args, **accepted_kwargs) + return image > thres + offset + -thOptions = { - 'otsu': lambda img_raw, bz: skimage.filters.threshold_otsu(img_raw), - 'li': lambda img_raw, bz: skimage.filters.threshold_li(img_raw), - 'yen': lambda img_raw, bz: skimage.filters.threshold_yen(img_raw), - 'isodata': lambda img_raw, bz: skimage.filters.threshold_isodata(img_raw), +class ManualThresholding: + + def __call__(self, image, thres1: float, thres2: float | None, **kwargs): + if thres2 is None: + return image > thres1 + else: + thres1, thres2 = sorted((thres1, thres2)) + return skimage.filters.apply_hysteresis_threshold(image, thres1, thres2) + - 'loc_gaussian': lambda img_raw, bz: skimage.filters.threshold_local(img_raw, bz, method='gaussian'), - 'loc_median': lambda img_raw, bz: skimage.filters.threshold_local(img_raw, bz, method='median'), - 'loc_mean': lambda img_raw, bz: skimage.filters.threshold_local(img_raw, bz, method='mean') +th_methods = { + 'manual': ManualThresholding(), + + 'otsu': DefaultThresholdingMethod(skimage.filters.threshold_otsu), + 'li': DefaultThresholdingMethod(skimage.filters.threshold_li), + 'yen': DefaultThresholdingMethod(skimage.filters.threshold_yen), + 'isodata': DefaultThresholdingMethod(skimage.filters.threshold_isodata), + + 'loc_gaussian': DefaultThresholdingMethod(skimage.filters.threshold_local, accept=['block_size'], method='gaussian'), + 'loc_median': DefaultThresholdingMethod(skimage.filters.threshold_local, accept=['block_size'], method='median'), + 'loc_mean': DefaultThresholdingMethod(skimage.filters.threshold_local, accept=['block_size'], method='mean'), } -def auto_thresholding(in_fn, out_fn, th_method, block_size=5, dark_bg=True): - img = skimage.io.imread(in_fn) - th = thOptions[th_method](img, block_size) - if dark_bg: - res = img > th - else: - res = img <= th - tifffile.imwrite(out_fn, skimage.util.img_as_ubyte(res)) +def do_thresholding( + input_filepath: str, + output_filepath: str, + th_method: str, + block_size: int, + offset: float, + threshold1: float, + threshold2: float | None, + invert_output: bool, +): + assert th_method in th_methods, f'Unknown method "{th_method}"' + + # Load image + img_in = Image.read(input_filepath) + + # Perform thresholding + result = th_methods[th_method]( + image=img_in.data, + block_size=block_size, + offset=offset, + thres1=threshold1, + thres2=threshold2, + ) + if invert_output: + result = np.logical_not(result) + + # Convert to canonical representation for binary images + result = (result * 255).astype(np.uint8) + + # Write result + Image( + data=skimage.util.img_as_ubyte(result), + axes=img_in.axes, + ).normalize_axes_like( + img_in.original_axes, + ).write( + output_filepath, + ) if __name__ == "__main__": - parser = argparse.ArgumentParser(description='Automatic Image Thresholding') - parser.add_argument('im_in', help='Path to the input image') - parser.add_argument('im_out', help='Path to the output image (TIFF)') - parser.add_argument('th_method', choices=thOptions.keys(), help='Thresholding method') - parser.add_argument('block_size', type=int, default=5, help='Odd size of pixel neighborhood for calculating the threshold') - parser.add_argument('dark_bg', default=True, type=bool, help='True if background is dark') + parser = argparse.ArgumentParser(description='Automatic image thresholding') + parser.add_argument('input', type=str, help='Path to the input image') + parser.add_argument('output', type=str, help='Path to the output image (uint8)') + parser.add_argument('th_method', choices=th_methods.keys(), help='Thresholding method') + parser.add_argument('block_size', type=int, help='Odd size of pixel neighborhood for calculating the threshold') + parser.add_argument('offset', type=float, help='Offset of automatically determined threshold value') + parser.add_argument('threshold1', type=float, help='Manual threshold value') + parser.add_argument('--threshold2', type=float, help='Second manual threshold value (for hysteresis thresholding)') + parser.add_argument('--invert_output', default=False, action='store_true', help='Values below/above the threshold are labeled with 0/255 by default, and with 255/0 if this argument is used') args = parser.parse_args() - auto_thresholding(args.im_in, args.im_out, args.th_method, args.block_size, args.dark_bg) + do_thresholding( + args.input, + args.output, + args.th_method, + args.block_size, + args.offset, + args.threshold1, + args.threshold2, + args.invert_output, + )
--- a/auto_threshold.xml Sat Feb 19 15:16:52 2022 +0000 +++ b/auto_threshold.xml Sat Jun 07 18:38:16 2025 +0000 @@ -1,54 +1,186 @@ -<tool id="ip_threshold" name="Auto Threshold" version="0.0.5" profile="20.05"> - <description>applies a standard thresholding algorithm to an image</description> - <requirements> - <requirement type="package" version="0.18.1">scikit-image</requirement> - <requirement type="package" version="2020.10.1">tifffile</requirement> - </requirements> - <command detect_errors="aggressive"> - <![CDATA[ - python '$__tool_directory__/auto_threshold.py' - '$input' - ./out.tif - '$th_method' - '$block_size' - '$dark_bg' - ]]> - </command> - <inputs> +<tool id="ip_threshold" name="Threshold image" version="@TOOL_VERSION@+galaxy@VERSION_SUFFIX@" profile="20.05"> + <description>with scikit-image</description> + <macros> + <import>creators.xml</import> + <import>tests.xml</import> + <token name="@TOOL_VERSION@">0.25.0</token> + <token name="@VERSION_SUFFIX@">0</token> + <xml name="inputs/offset"> + <param name="offset" type="float" value="0" label="Offset" help="Offset to be added to the automatically determined threshold value. Positive values will increase the threshold (and thus reduce the amount of values above the threshold)." /> + </xml> + </macros> + <creator> + <expand macro="creators/bmcv"/> + </creator> + <edam_operations> + <edam_operation>operation_3443</edam_operation> + </edam_operations> + <xrefs> + <xref type="bio.tools">scikit-image</xref> + <xref type="biii">scikit-image</xref> + </xrefs> + <requirements> + <requirement type="package" version="@TOOL_VERSION@">scikit-image</requirement> + <requirement type="package" version="0.4.1">giatools</requirement> + </requirements> + <command detect_errors="aggressive"> + <![CDATA[ + + python '$__tool_directory__/auto_threshold.py' + + '$input' + ./out.tiff + + '$th_method.method_id' + '$th_method.block_size' + '$th_method.offset' + '$th_method.threshold1' + $invert_output + + #if str($th_method.threshold2) != '': + --threshold2 '$th_method.threshold2' + #end if + + ]]> + </command> + <inputs> <param name="input" type="data" format="tiff,png" label="Input image" /> - <param name="th_method" type="select" label="Thresholding method"> - <option value="otsu" selected="True">Otsu</option> - <option value="li">Li's Minimum Cross Entropy</option> - <option value="isodata">Isodata</option> - <option value="yen">Yen</option> - <option value="loc_gaussian">Adaptive (Gaussian)</option> - <option value="loc_median">Adaptive (Median)</option> - <option value="loc_mean">Adaptive (Mean)</option> - </param> - <param name="block_size" type="integer" value="5" label="Odd size of pixel neighborhood for determining the threshold (only valid for adaptive thresholding methods)" /> - <param name="dark_bg" type="boolean" checked="true" truevalue="True" falsevalue="False" label="Dark Background" /> + <conditional name="th_method"> + <param name="method_id" type="select" label="Thresholding method"> + <option value="manual">Manual</option> + <option value="otsu" selected="True">Globally adaptive / Otsu</option> + <option value="li">Globally adaptive / Li's Minimum Cross Entropy</option> + <option value="isodata">Globally adaptive / Isodata</option> + <option value="yen">Globally adaptive / Yen</option> + <option value="loc_gaussian">Locally adaptive / Gaussian</option> + <option value="loc_median">Locally adaptive / Median</option> + <option value="loc_mean">Locally adaptive / Mean</option> + </param> + <when value="manual"> + <param name="threshold1" type="float" value="0" label="Threshold value" /> + <param name="threshold2" type="float" value="" optional="true" label="Second threshold value for hysteresis thresholding" /> + <param name="block_size" type="hidden" value="0" /> + <param name="offset" type="hidden" value="0" /> + </when> + <when value="otsu"> + <param name="threshold1" type="hidden" value="0" /> + <param name="threshold2" type="hidden" value="" /> + <param name="block_size" type="hidden" value="0" /> + <expand macro="inputs/offset" /> + </when> + <when value="li"> + <param name="threshold1" type="hidden" value="0" /> + <param name="threshold2" type="hidden" value="" /> + <param name="block_size" type="hidden" value="0" /> + <expand macro="inputs/offset" /> + </when> + <when value="isodata"> + <param name="threshold1" type="hidden" value="0" /> + <param name="threshold2" type="hidden" value="" /> + <param name="block_size" type="hidden" value="0" /> + <expand macro="inputs/offset" /> + </when> + <when value="yen"> + <param name="threshold1" type="hidden" value="0" /> + <param name="threshold2" type="hidden" value="" /> + <param name="block_size" type="hidden" value="0" /> + <expand macro="inputs/offset" /> + </when> + <when value="loc_gaussian"> + <param name="threshold1" type="hidden" value="0" /> + <param name="threshold2" type="hidden" value="" /> + <param name="block_size" type="integer" value="5" label="Odd size of pixel neighborhood for determining the threshold" /> + <expand macro="inputs/offset" /> + </when> + <when value="loc_median"> + <param name="threshold1" type="hidden" value="0" /> + <param name="threshold2" type="hidden" value="" /> + <param name="block_size" type="integer" value="5" label="Odd size of pixel neighborhood for determining the threshold" /> + <expand macro="inputs/offset" /> + </when> + <when value="loc_mean"> + <param name="threshold1" type="hidden" value="0" /> + <param name="threshold2" type="hidden" value="" /> + <param name="block_size" type="integer" value="5" label="Odd size of pixel neighborhood for determining the threshold" /> + <expand macro="inputs/offset" /> + </when> + </conditional> + <param name="invert_output" type="boolean" checked="false" truevalue="--invert_output" falsevalue="" label="Invert output labels" help="Pixels are usually assigned the label 0 if the pixel value is below (or equal to) the threshold, and 255 if it is above the threshold. If this option is activated, pixels are assigned the label 255 if the pixel value is below (or equal to) the threshold, and 0 if it is above the threshold." /> </inputs> <outputs> - <data format="tiff" name="output" from_work_dir="out.tif" /> + <data format="tiff" name="output" from_work_dir="out.tiff" /> </outputs> <tests> + <!-- Tests for single-channel images (TIFF) --> <test> - <param name="input" value="sample.tif"/> - <output name="output" value="out.tif" ftype="tiff" compare="sim_size"/> - <param name="th_method" value="loc_gaussian"/> - <param name="block_size" value="3"/> - <param name="dark_bg" value="True"/> + <param name="input" value="sample.tiff"/> + <conditional name="th_method"> + <param name="method_id" value="loc_gaussian"/> + <param name="block_size" value="51"/> + </conditional> + <expand macro="tests/binary_image_diff" name="output" value="out1.tiff" ftype="tiff"/> + </test> + <test> + <param name="input" value="sample.tiff"/> + <conditional name="th_method"> + <param name="method_id" value="loc_gaussian"/> + <param name="block_size" value="51"/> + <param name="offset" value="1"/> + </conditional> + <expand macro="tests/binary_image_diff" name="output" value="out2.tiff" ftype="tiff"/> + </test> + <test> + <param name="input" value="sample.tiff"/> + <conditional name="th_method"> + <param name="method_id" value="otsu"/> + </conditional> + <expand macro="tests/binary_image_diff" name="output" value="out3.tiff" ftype="tiff"/> </test> <test> - <param name="input" value="sample.tif"/> - <output name="output" value="out2.tif" ftype="tiff" compare="sim_size"/> - <param name="th_method" value="otsu"/> - <param name="block_size" value="5"/> - <param name="dark_bg" value="True"/> + <param name="input" value="sample.tiff"/> + <param name="invert_output" value="True"/> + <conditional name="th_method"> + <param name="method_id" value="manual"/> + <param name="threshold1" value="64"/> + </conditional> + <expand macro="tests/binary_image_diff" name="output" value="out4.tiff" ftype="tiff"/> + </test> + <test> + <param name="input" value="sample.tiff"/> + <conditional name="th_method"> + <param name="method_id" value="manual"/> + <param name="threshold1" value="180"/> + <param name="threshold2" value="240"/> + </conditional> + <expand macro="tests/binary_image_diff" name="output" value="sample_manual_180_240.tiff" ftype="tiff"/> + </test> + <!-- Tests for multi-channel images (PNG) --> + <test> + <param name="input" value="rgb.png"/> + <conditional name="th_method"> + <param name="method_id" value="otsu"/> + </conditional> + <expand macro="tests/binary_image_diff" name="output" value="rgb_otsu.tiff" ftype="tiff"/> + </test> + <!-- Tests for irregular files --> + <test> + <param name="input" value="sample2.tiff"/> + <conditional name="th_method"> + <param name="method_id" value="otsu"/> + </conditional> + <expand macro="tests/binary_image_diff" name="output" value="out5.tiff" ftype="tiff"/> </test> </tests> <help> - Applies a standard thresholding algorithm to an image. + + **Applies a standard thresholding algorithm to a 2-D single-channel image. Yields a binary image.** + + The thresholding algorithm automatically determines a threshold value (unless manual thresholding is used). + The input image is then thresholded, by assigning white (pixel value 255) to image regions above the determined threshold, + and black (pixel value 0) to image regions below or equal to the determined threshold. + + The assignment of black and white to image regions below and above the threshold is inverted, if the corresponding option is set. + </help> <citations> <citation type="doi">10.1016/j.jbiotec.2017.07.019</citation>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/creators.xml Sat Jun 07 18:38:16 2025 +0000 @@ -0,0 +1,33 @@ +<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> + + <xml name="creators/pavanvidem"> + <person givenName="Pavan" familyName="Videm"/> + <yield/> + </xml> + +</macros>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests.xml Sat Jun 07 18:38:16 2025 +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>