Mercurial > repos > imgteam > 2d_auto_threshold
changeset 5:1ae14e703e5b draft default tip
planemo upload for repository https://github.com/BMCV/galaxy-image-analysis/tree/master/tools/2d_auto_threshold/ commit 71f7ecabba78de48147d4a5e6ea380b6b70b16e8
line wrap: on
line diff
--- a/auto_threshold.py Sat Jun 07 18:38:16 2025 +0000 +++ b/auto_threshold.py Sat Jan 03 14:42:51 2026 +0000 @@ -1,45 +1,47 @@ """ -Copyright 2017-2024 Biomedical Computer Vision Group, Heidelberg University. +Copyright 2017-2025 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 giatools import numpy as np import skimage.filters import skimage.util -from giatools.image import Image + +# Fail early if an optional backend is not available +giatools.require_backend('omezarr') class DefaultThresholdingMethod: - def __init__(self, thres, accept: list[str] | None = None, **kwargs): + def __init__(self, thres, **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) + thres = self.thres(image, *args, **(self.kwargs | kwargs)) return image > thres + offset + def __str__(self): + return self.thres.__name__ + class ManualThresholding: - def __call__(self, image, thres1: float, thres2: float | None, **kwargs): - if thres2 is None: - return image > thres1 + def __call__(self, image, threshold1: float, threshold2: float | None, **kwargs): + if threshold2 is None: + return image > threshold1 else: - thres1, thres2 = sorted((thres1, thres2)) - return skimage.filters.apply_hysteresis_threshold(image, thres1, thres2) + threshold1, threshold2 = sorted((threshold1, threshold2)) + return skimage.filters.apply_hysteresis_threshold(image, threshold1, threshold2) + + def __str__(self): + return 'Manual' -th_methods = { +methods = { 'manual': ManualThresholding(), 'otsu': DefaultThresholdingMethod(skimage.filters.threshold_otsu), @@ -47,71 +49,37 @@ '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'), + 'loc_gaussian': DefaultThresholdingMethod(skimage.filters.threshold_local, method='gaussian'), + 'loc_median': DefaultThresholdingMethod(skimage.filters.threshold_local, method='median'), + 'loc_mean': DefaultThresholdingMethod(skimage.filters.threshold_local, method='mean'), } -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}"' +if __name__ == "__main__": + tool = giatools.ToolBaseplate() + tool.add_input_image('input') + tool.add_output_image('output') + tool.parse_args() - # Load image - img_in = Image.read(input_filepath) + # Retrieve general parameters + method = tool.args.params.pop('method') + invert = tool.args.params.pop('invert') # 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, + method_impl = methods[method] + print( + 'Thresholding:', + str(method_impl), + 'with', + ', '.join( + f'{key}={repr(value)}' for key, value in (tool.args.params | dict(invert=invert)).items() + ), ) - - -if __name__ == "__main__": - 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() - - do_thresholding( - args.input, - args.output, - args.th_method, - args.block_size, - args.offset, - args.threshold1, - args.threshold2, - args.invert_output, - ) + for section in tool.run('ZYX', output_dtype_hint='binary'): + section_output = method_impl( + image=np.asarray(section['input'].data), # some implementations have issues with Dask arrays + **tool.args.params, + ) + if invert: + section_output = np.logical_not(section_output) + section['output'] = section_output
--- a/auto_threshold.xml Sat Jun 07 18:38:16 2025 +0000 +++ b/auto_threshold.xml Sat Jan 03 14:42:51 2026 +0000 @@ -3,48 +3,78 @@ <macros> <import>creators.xml</import> <import>tests.xml</import> - <token name="@TOOL_VERSION@">0.25.0</token> + <token name="@TOOL_VERSION@">0.25.2</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)." /> + <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"/> + <expand macro="creators/kostrykin"/> </creator> <edam_operations> <edam_operation>operation_3443</edam_operation> </edam_operations> <xrefs> + <xref type="bio.tools">galaxy_image_analysis</xref> + <xref type="bio.tools">giatools</xref> <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> + <requirement type="package" version="0.7.3">giatools</requirement> + <requirement type="package" version="0.12.2">ome-zarr</requirement> </requirements> - <command detect_errors="aggressive"> - <![CDATA[ + <required_files> + <include type="literal" path="auto_threshold.py"/> + </required_files> + <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' + #if $input.extension == "zarr" + --input '$input.extra_files_path/$input.metadata.store_root' + #else + --input '$input' #end if - ]]> - </command> + --output ./out.tiff + --params '$params' + --verbose + + ]]></command> + <configfiles> + <configfile name="params"><![CDATA[ + { + + #if str($th_method.method_id).startswith('loc_') + "block_size": $th_method.block_size, + #end if + + #if $th_method.method_id != "manual" + "offset": $th_method.offset, + + #else + "threshold1": $th_method.threshold1, + #if str($th_method.threshold2) != "" + "threshold2": $th_method.threshold2, + #else + "threshold2": null, + #end if + + #end if + + "method": "$th_method.method_id", + "invert": $invert_output + + } + ]]></configfile> + </configfiles> <inputs> - <param name="input" type="data" format="tiff,png" label="Input image" /> + <param name="input" type="data" format="tiff,zarr,png,jpg" label="Input image"/> <conditional name="th_method"> <param name="method_id" type="select" label="Thresholding method"> <option value="manual">Manual</option> @@ -57,55 +87,37 @@ <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" /> + <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="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" /> + <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" /> + <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" /> + <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" /> + <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" /> + <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" /> + <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" /> + <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." /> + <param name="invert_output" type="boolean" checked="false" 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.tiff" /> @@ -113,73 +125,90 @@ <tests> <!-- Tests for single-channel images (TIFF) --> <test> - <param name="input" value="sample.tiff"/> + <param name="input" value="input/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"/> + <expand macro="tests/binary_image_diff" name="output" value="output/out1.tiff" ftype="tiff"/> </test> <test> - <param name="input" value="sample.tiff"/> + <param name="input" value="input/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"/> + <expand macro="tests/binary_image_diff" name="output" value="output/out2.tiff" ftype="tiff"/> </test> <test> - <param name="input" value="sample.tiff"/> + <param name="input" value="input/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"/> + <expand macro="tests/binary_image_diff" name="output" value="output/out3.tiff" ftype="tiff"/> </test> <test> - <param name="input" value="sample.tiff"/> + <param name="input" value="input/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"/> + <expand macro="tests/binary_image_diff" name="output" value="output/out4.tiff" ftype="tiff"/> </test> <test> - <param name="input" value="sample.tiff"/> + <param name="input" value="input/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"/> + <expand macro="tests/binary_image_diff" name="output" value="output/sample_manual_180_240.tiff" ftype="tiff"/> </test> <!-- Tests for multi-channel images (PNG) --> <test> - <param name="input" value="rgb.png"/> + <param name="input" value="input/rgb.png"/> + <conditional name="th_method"> + <param name="method_id" value="otsu"/> + </conditional> + <expand macro="tests/binary_image_diff" name="output" value="output/rgb_otsu.tiff" ftype="tiff"/> + </test> + <!-- Tests for irregular files (TODO: clear up what this means) --> + <test> + <param name="input" value="input/sample2.tiff"/> <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"/> + <expand macro="tests/binary_image_diff" name="output" value="output/out5.tiff" ftype="tiff"/> </test> - <!-- Tests for irregular files --> + <!-- Tests for exotic files --> <test> - <param name="input" value="sample2.tiff"/> + <param name="input" value="input/input5.jpg"/> <conditional name="th_method"> - <param name="method_id" value="otsu"/> + <param name="method_id" value="li"/> </conditional> - <expand macro="tests/binary_image_diff" name="output" value="out5.tiff" ftype="tiff"/> + <expand macro="tests/binary_image_diff" name="output" value="output/input5_li.tiff" ftype="tiff"/> + </test> + <test> + <param name="input" value="input/input8_zyx.zarr"/> + <conditional name="th_method"> + <param name="method_id" value="yen"/> + </conditional> + <expand macro="tests/binary_image_diff" name="output" value="output/input8_yen.tiff" ftype="tiff"/> </test> </tests> <help> - **Applies a standard thresholding algorithm to a 2-D single-channel image. Yields a binary image.** + **Applies a standard thresholding algorithm to an 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 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. For multi-channel images, each channel is processed separately, + which, for example, may also yield colors beyond black and white in case of RGB images. - The assignment of black and white to image regions below and above the threshold is inverted, if the corresponding option is set. + The assignment of the pixel values 0 and 255 (i.e. black and white) to image regions below and above the threshold is inverted, + if the corresponding option is set. </help> <citations>
--- a/creators.xml Sat Jun 07 18:38:16 2025 +0000 +++ b/creators.xml Sat Jan 03 14:42:51 2026 +0000 @@ -5,6 +5,11 @@ <yield /> </xml> + <xml name="creators/kostrykin"> + <person givenName="Leonid" familyName="Kostrykin"/> + <yield/> + </xml> + <xml name="creators/rmassei"> <person givenName="Riccardo" familyName="Massei"/> <yield/> @@ -30,4 +35,9 @@ <yield/> </xml> + <xml name="creators/tuncK"> + <person givenName="Tunc" familyName="Kayikcioglu"/> + <yield/> + </xml> + </macros>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test-data/input/README.md Sat Jan 03 14:42:51 2026 +0000 @@ -0,0 +1,37 @@ +# Overview of the test images + +## `input5.jpg`: + +- axes: `YX` +- resolution: `(10, 10, 3)` +- dtype: `uint8` + +## `input8_zyx.zarr`: + +- axes: `ZYX` +- resolution: `(2, 100, 100)` +- dtype: `float64` +- metadata: + - resolution: `(1.0, 1.0)` + - z-spacing: `1.0` + - unit: `um` + +## `rgb.png`: + +- axes: `YXC` +- resolution: `(6, 6, 3)` +- dtype: `uint8` + +## `sample.tiff`: + +- axes: `YX` +- resolution: `(265, 329)` +- dtype: `uint8` +- metadata: *unknown* + +## `sample2.tiff`: + +- axes: `YX` +- resolution: `(96, 97)` +- dtype: `uint8` +- metadata: *unknown*
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test-data/input/ome-zarr-examples/LICENSE Sat Jan 03 14:42:51 2026 +0000 @@ -0,0 +1,28 @@ +BSD 3-Clause License + +Copyright (c) 2023, Tommaso Comparin + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test-data/input/ome-zarr-examples/image-03.zarr/.zattrs Sat Jan 03 14:42:51 2026 +0000 @@ -0,0 +1,40 @@ +{ + "multiscales": [ + { + "axes": [ + { + "name": "z", + "type": "space", + "unit": "micrometer" + }, + { + "name": "y", + "type": "space", + "unit": "micrometer" + }, + { + "name": "x", + "type": "space", + "unit": "micrometer" + } + ], + "datasets": [ + { + "coordinateTransformations": [ + { + "scale": [ + 1.0, + 1.0, + 1.0 + ], + "type": "scale" + } + ], + "path": "0" + } + ], + "version": "0.4" + } + ], + "version": "0.4" +} \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test-data/input/ome-zarr-examples/image-03.zarr/.zgroup Sat Jan 03 14:42:51 2026 +0000 @@ -0,0 +1,3 @@ +{ + "zarr_format": 2 +} \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test-data/input/ome-zarr-examples/image-03.zarr/0/.zarray Sat Jan 03 14:42:51 2026 +0000 @@ -0,0 +1,25 @@ +{ + "chunks": [ + 1, + 50, + 100 + ], + "compressor": { + "blocksize": 0, + "clevel": 5, + "cname": "lz4", + "id": "blosc", + "shuffle": 1 + }, + "dimension_separator": "/", + "dtype": "<f8", + "fill_value": 0.0, + "filters": null, + "order": "C", + "shape": [ + 2, + 100, + 100 + ], + "zarr_format": 2 +} \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test-data/input/ome-zarr-examples/image-03.zarr/labels/.zattrs Sat Jan 03 14:42:51 2026 +0000 @@ -0,0 +1,3 @@ +{ + "labels": [] +} \ No newline at end of file
