comparison auto_threshold.py @ 5:1ae14e703e5b draft default tip

planemo upload for repository https://github.com/BMCV/galaxy-image-analysis/tree/master/tools/2d_auto_threshold/ commit 71f7ecabba78de48147d4a5e6ea380b6b70b16e8
author imgteam
date Sat, 03 Jan 2026 14:42:51 +0000
parents 7d80eb2411fb
children
comparison
equal deleted inserted replaced
4:7d80eb2411fb 5:1ae14e703e5b
1 """ 1 """
2 Copyright 2017-2024 Biomedical Computer Vision Group, Heidelberg University. 2 Copyright 2017-2025 Biomedical Computer Vision Group, Heidelberg University.
3 3
4 Distributed under the MIT license. 4 Distributed under the MIT license.
5 See file LICENSE for detail or copy at https://opensource.org/licenses/MIT 5 See file LICENSE for detail or copy at https://opensource.org/licenses/MIT
6 """ 6 """
7 7
8 import argparse 8 import giatools
9
10 import numpy as np 9 import numpy as np
11 import skimage.filters 10 import skimage.filters
12 import skimage.util 11 import skimage.util
13 from giatools.image import Image 12
13 # Fail early if an optional backend is not available
14 giatools.require_backend('omezarr')
14 15
15 16
16 class DefaultThresholdingMethod: 17 class DefaultThresholdingMethod:
17 18
18 def __init__(self, thres, accept: list[str] | None = None, **kwargs): 19 def __init__(self, thres, **kwargs):
19 self.thres = thres 20 self.thres = thres
20 self.accept = accept if accept else []
21 self.kwargs = kwargs 21 self.kwargs = kwargs
22 22
23 def __call__(self, image, *args, offset=0, **kwargs): 23 def __call__(self, image, *args, offset=0, **kwargs):
24 accepted_kwargs = self.kwargs.copy() 24 thres = self.thres(image, *args, **(self.kwargs | kwargs))
25 for key, val in kwargs.items():
26 if key in self.accept:
27 accepted_kwargs[key] = val
28 thres = self.thres(image, *args, **accepted_kwargs)
29 return image > thres + offset 25 return image > thres + offset
26
27 def __str__(self):
28 return self.thres.__name__
30 29
31 30
32 class ManualThresholding: 31 class ManualThresholding:
33 32
34 def __call__(self, image, thres1: float, thres2: float | None, **kwargs): 33 def __call__(self, image, threshold1: float, threshold2: float | None, **kwargs):
35 if thres2 is None: 34 if threshold2 is None:
36 return image > thres1 35 return image > threshold1
37 else: 36 else:
38 thres1, thres2 = sorted((thres1, thres2)) 37 threshold1, threshold2 = sorted((threshold1, threshold2))
39 return skimage.filters.apply_hysteresis_threshold(image, thres1, thres2) 38 return skimage.filters.apply_hysteresis_threshold(image, threshold1, threshold2)
39
40 def __str__(self):
41 return 'Manual'
40 42
41 43
42 th_methods = { 44 methods = {
43 'manual': ManualThresholding(), 45 'manual': ManualThresholding(),
44 46
45 'otsu': DefaultThresholdingMethod(skimage.filters.threshold_otsu), 47 'otsu': DefaultThresholdingMethod(skimage.filters.threshold_otsu),
46 'li': DefaultThresholdingMethod(skimage.filters.threshold_li), 48 'li': DefaultThresholdingMethod(skimage.filters.threshold_li),
47 'yen': DefaultThresholdingMethod(skimage.filters.threshold_yen), 49 'yen': DefaultThresholdingMethod(skimage.filters.threshold_yen),
48 'isodata': DefaultThresholdingMethod(skimage.filters.threshold_isodata), 50 'isodata': DefaultThresholdingMethod(skimage.filters.threshold_isodata),
49 51
50 'loc_gaussian': DefaultThresholdingMethod(skimage.filters.threshold_local, accept=['block_size'], method='gaussian'), 52 'loc_gaussian': DefaultThresholdingMethod(skimage.filters.threshold_local, method='gaussian'),
51 'loc_median': DefaultThresholdingMethod(skimage.filters.threshold_local, accept=['block_size'], method='median'), 53 'loc_median': DefaultThresholdingMethod(skimage.filters.threshold_local, method='median'),
52 'loc_mean': DefaultThresholdingMethod(skimage.filters.threshold_local, accept=['block_size'], method='mean'), 54 'loc_mean': DefaultThresholdingMethod(skimage.filters.threshold_local, method='mean'),
53 } 55 }
54 56
55 57
56 def do_thresholding( 58 if __name__ == "__main__":
57 input_filepath: str, 59 tool = giatools.ToolBaseplate()
58 output_filepath: str, 60 tool.add_input_image('input')
59 th_method: str, 61 tool.add_output_image('output')
60 block_size: int, 62 tool.parse_args()
61 offset: float,
62 threshold1: float,
63 threshold2: float | None,
64 invert_output: bool,
65 ):
66 assert th_method in th_methods, f'Unknown method "{th_method}"'
67 63
68 # Load image 64 # Retrieve general parameters
69 img_in = Image.read(input_filepath) 65 method = tool.args.params.pop('method')
66 invert = tool.args.params.pop('invert')
70 67
71 # Perform thresholding 68 # Perform thresholding
72 result = th_methods[th_method]( 69 method_impl = methods[method]
73 image=img_in.data, 70 print(
74 block_size=block_size, 71 'Thresholding:',
75 offset=offset, 72 str(method_impl),
76 thres1=threshold1, 73 'with',
77 thres2=threshold2, 74 ', '.join(
75 f'{key}={repr(value)}' for key, value in (tool.args.params | dict(invert=invert)).items()
76 ),
78 ) 77 )
79 if invert_output: 78 for section in tool.run('ZYX', output_dtype_hint='binary'):
80 result = np.logical_not(result) 79 section_output = method_impl(
81 80 image=np.asarray(section['input'].data), # some implementations have issues with Dask arrays
82 # Convert to canonical representation for binary images 81 **tool.args.params,
83 result = (result * 255).astype(np.uint8) 82 )
84 83 if invert:
85 # Write result 84 section_output = np.logical_not(section_output)
86 Image( 85 section['output'] = section_output
87 data=skimage.util.img_as_ubyte(result),
88 axes=img_in.axes,
89 ).normalize_axes_like(
90 img_in.original_axes,
91 ).write(
92 output_filepath,
93 )
94
95
96 if __name__ == "__main__":
97 parser = argparse.ArgumentParser(description='Automatic image thresholding')
98 parser.add_argument('input', type=str, help='Path to the input image')
99 parser.add_argument('output', type=str, help='Path to the output image (uint8)')
100 parser.add_argument('th_method', choices=th_methods.keys(), help='Thresholding method')
101 parser.add_argument('block_size', type=int, help='Odd size of pixel neighborhood for calculating the threshold')
102 parser.add_argument('offset', type=float, help='Offset of automatically determined threshold value')
103 parser.add_argument('threshold1', type=float, help='Manual threshold value')
104 parser.add_argument('--threshold2', type=float, help='Second manual threshold value (for hysteresis thresholding)')
105 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')
106 args = parser.parse_args()
107
108 do_thresholding(
109 args.input,
110 args.output,
111 args.th_method,
112 args.block_size,
113 args.offset,
114 args.threshold1,
115 args.threshold2,
116 args.invert_output,
117 )