# HG changeset patch # User imgteam # Date 1767451390 0 # Node ID 2ee04d2ebdcf0073ea6188a6eeece5de7350c82b # Parent 50fa6150e34008e88dfed4aa3ef547b594b95736 planemo upload for repository https://github.com/BMCV/galaxy-image-analysis/tree/master/tools/2d_auto_threshold/ commit 71f7ecabba78de48147d4a5e6ea380b6b70b16e8 diff -r 50fa6150e340 -r 2ee04d2ebdcf auto_threshold.py --- a/auto_threshold.py Sat Jun 07 18:38:31 2025 +0000 +++ b/auto_threshold.py Sat Jan 03 14:43:10 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 diff -r 50fa6150e340 -r 2ee04d2ebdcf auto_threshold.xml --- a/auto_threshold.xml Sat Jun 07 18:38:31 2025 +0000 +++ b/auto_threshold.xml Sat Jan 03 14:43:10 2026 +0000 @@ -3,48 +3,78 @@ creators.xml tests.xml - 0.25.0 + 0.25.2 0 - + + operation_3443 + galaxy_image_analysis + giatools scikit-image scikit-image scikit-image - giatools + giatools + ome-zarr - - + + + - + --output ./out.tiff + --params '$params' + --verbose + + ]]> + + + - + @@ -57,55 +87,37 @@ - - - - + + + - - - - + - - - - + - - - - + - - - - + - - - - + + - - - - + + - - - - + + - + @@ -113,73 +125,90 @@ - + - + - + - + - + - + - + - + - + - + - + + + + + + + + + - + - + - + - + - + + + + + + + + - **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. diff -r 50fa6150e340 -r 2ee04d2ebdcf creators.xml --- a/creators.xml Sat Jun 07 18:38:31 2025 +0000 +++ b/creators.xml Sat Jan 03 14:43:10 2026 +0000 @@ -5,6 +5,11 @@ + + + + + @@ -30,4 +35,9 @@ + + + + + diff -r 50fa6150e340 -r 2ee04d2ebdcf test-data/input/README.md --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test-data/input/README.md Sat Jan 03 14:43:10 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* diff -r 50fa6150e340 -r 2ee04d2ebdcf test-data/input/input5.jpg Binary file test-data/input/input5.jpg has changed diff -r 50fa6150e340 -r 2ee04d2ebdcf test-data/input/ome-zarr-examples/LICENSE --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test-data/input/ome-zarr-examples/LICENSE Sat Jan 03 14:43:10 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. diff -r 50fa6150e340 -r 2ee04d2ebdcf test-data/input/ome-zarr-examples/image-03.zarr/.zattrs --- /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:43:10 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 diff -r 50fa6150e340 -r 2ee04d2ebdcf test-data/input/ome-zarr-examples/image-03.zarr/.zgroup --- /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:43:10 2026 +0000 @@ -0,0 +1,3 @@ +{ + "zarr_format": 2 +} \ No newline at end of file diff -r 50fa6150e340 -r 2ee04d2ebdcf test-data/input/ome-zarr-examples/image-03.zarr/0/.zarray --- /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:43:10 2026 +0000 @@ -0,0 +1,25 @@ +{ + "chunks": [ + 1, + 50, + 100 + ], + "compressor": { + "blocksize": 0, + "clevel": 5, + "cname": "lz4", + "id": "blosc", + "shuffle": 1 + }, + "dimension_separator": "/", + "dtype": "