comparison filter.py @ 4:5ab62693dca5 draft default tip

planemo upload for repository https://github.com/BMCV/galaxy-image-analysis/tree/master/tools/2d_simple_filter/ commit c9cc62c508da3804872d105800993746c36cca48
author imgteam
date Sat, 20 Dec 2025 18:28:21 +0000
parents b2d9c92bc431
children
comparison
equal deleted inserted replaced
3:53c55776a974 4:5ab62693dca5
6 ) 6 )
7 7
8 import giatools 8 import giatools
9 import numpy as np 9 import numpy as np
10 import scipy.ndimage as ndi 10 import scipy.ndimage as ndi
11 from skimage.morphology import disk 11
12 12
13 13 def image_astype(image: giatools.Image, dtype: np.dtype) -> giatools.Image:
14 def image_astype(img: giatools.Image, dtype: np.dtype) -> giatools.Image: 14 if np.issubdtype(image.data.dtype, dtype):
15 return giatools.Image( 15 return image # no conversion needed
16 data=img.data.astype(dtype), 16 else:
17 axes=img.axes, 17 return giatools.Image(
18 original_axes=img.original_axes, 18 data=image.data.astype(dtype),
19 metadata=img.metadata, 19 axes=image.axes,
20 ) 20 original_axes=image.original_axes,
21 21 metadata=image.metadata,
22 22 )
23 filters = { 23
24 'gaussian': lambda img, sigma, order=0, axis=None: ( 24
25 apply_2d_filter( 25 def get_anisotropy(image: giatools.Image, axes: str) -> tuple[float, ...] | None:
26 """
27 Get the anisotropy of the image pixels/voxels along the given `axes`.
28
29 TODO: Migrate to `giatools.Image.get_anisotropy` with `giatools >=0.7`
30 """
31
32 # Determine the pixel/voxel size
33 voxel_size = list()
34 for axis in axes:
35 match axis:
36 case 'X':
37 if image.metadata.pixel_size is None:
38 return None # unknown size
39 else:
40 voxel_size.append(image.metadata.pixel_size[0])
41 case 'Y':
42 if image.metadata.pixel_size is None:
43 return None # unknown size
44 else:
45 voxel_size.append(image.metadata.pixel_size[1])
46 case 'Z':
47 if image.metadata.z_spacing is None:
48 return None # unknown size
49 else:
50 voxel_size.append(image.metadata.z_spacing)
51
52 # Check for unknown size and compute anisotropy
53 if any(abs(s) < 1e-8 for s in voxel_size):
54 print('Unknown pixel/voxel size')
55 return None
56 else:
57 denom = pow(np.prod(voxel_size), 1 / len(voxel_size)) # geometric mean
58 anisotropy = tuple(np.divide(voxel_size, denom).tolist())
59 print(f'Anisotropy of {axes} pixels/voxels:', anisotropy)
60 return anisotropy
61
62
63 def get_anisotropic_size(image: giatools.Image, axes: str, size: int) -> tuple[int, ...] | int:
64 if (anisotropy := get_anisotropy(image, axes)) is not None:
65 return tuple(
66 np.divide(size, anisotropy).round().clip(1, np.inf).astype(int).tolist(),
67 )
68 else:
69 return size
70
71
72 class Filters:
73
74 @staticmethod
75 def gaussian(
76 image: giatools.Image,
77 sigma: float,
78 anisotropic: bool,
79 axes: str,
80 order: int = 0,
81 direction: int | None = None,
82 **kwargs: Any,
83 ) -> giatools.Image:
84 if direction is None:
85 _order = 0
86 elif order >= 1:
87 _order = [0] * len(axes)
88 _order[direction] = order
89 _order = tuple(_order)
90 if anisotropic and (anisotropy := get_anisotropy(image, axes)) is not None:
91 _sigma = tuple(np.divide(sigma, anisotropy).tolist())
92 else:
93 _sigma = sigma
94 return apply_nd_filter(
26 ndi.gaussian_filter, 95 ndi.gaussian_filter,
27 img if order == 0 else image_astype(img, float), 96 image,
28 sigma=sigma, 97 sigma=_sigma,
29 order=order, 98 order=_order,
30 axes=axis, 99 axes=axes,
31 ) 100 **kwargs,
32 ), 101 )
33 'uniform': lambda img, size: ( 102
34 apply_2d_filter(ndi.uniform_filter, img, size=size) 103 @staticmethod
35 ), 104 def uniform(image: giatools.Image, size: int, anisotropic: bool, axes: str, **kwargs: Any) -> giatools.Image:
36 'median': lambda img, radius: ( 105 _size = get_anisotropic_size(image, axes, size) if anisotropic else size
37 apply_2d_filter(ndi.median_filter, img, footprint=disk(radius)) 106 return apply_nd_filter(
38 ), 107 ndi.uniform_filter,
39 'prewitt': lambda img, axis: ( 108 image,
40 apply_2d_filter(ndi.prewitt, img, axis=axis) 109 size=_size,
41 ), 110 axes=axes,
42 'sobel': lambda img, axis: ( 111 **kwargs,
43 apply_2d_filter(ndi.sobel, img, axis=axis) 112 )
44 ), 113
45 } 114 @staticmethod
46 115 def median(image: giatools.Image, size: int, anisotropic: bool, axes: str, **kwargs: Any) -> giatools.Image:
47 116 _size = get_anisotropic_size(image, axes, size) if anisotropic else size
48 def apply_2d_filter( 117 return apply_nd_filter(
118 ndi.median_filter,
119 image,
120 size=_size,
121 axes=axes,
122 **kwargs,
123 )
124
125 @staticmethod
126 def prewitt(image: giatools.Image, direction: int, **kwargs: Any) -> giatools.Image:
127 return apply_nd_filter(
128 ndi.prewitt,
129 image,
130 axis=direction,
131 **kwargs,
132 )
133
134 @staticmethod
135 def sobel(image: giatools.Image, direction: int, **kwargs: Any) -> giatools.Image:
136 return apply_nd_filter(
137 ndi.sobel,
138 image,
139 axis=direction,
140 **kwargs,
141 )
142
143
144 def apply_nd_filter(
49 filter_impl: Callable[[np.ndarray, Any, ...], np.ndarray], 145 filter_impl: Callable[[np.ndarray, Any, ...], np.ndarray],
50 img: giatools.Image, 146 image: giatools.Image,
147 axes: str,
51 **kwargs: Any, 148 **kwargs: Any,
52 ) -> giatools.Image: 149 ) -> giatools.Image:
53 """ 150 """
54 Apply the 2-D filter to the 2-D/3-D, potentially multi-frame and multi-channel image. 151 Apply the filter to the 2-D/3-D, potentially multi-frame and multi-channel image.
55 """ 152 """
153 print(
154 'Applying filter:',
155 filter_impl.__name__,
156 'with',
157 ', '.join(
158 f'{key}={repr(value)}' for key, value in (kwargs | dict(axes=axes)).items()
159 if not isinstance(value, np.ndarray)
160 ),
161 )
56 result_data = None 162 result_data = None
57 for qtzc in np.ndindex( 163 for section_sel, section_arr in image.iterate_jointly(axes):
58 img.data.shape[ 0], # Q axis 164 assert len(axes) == section_arr.ndim and section_arr.ndim in (2, 3) # sanity check, always True
59 img.data.shape[ 1], # T axis 165
60 img.data.shape[ 2], # Z axis 166 # Define the section using the requested axes layout (compatible with `kwargs`)
61 img.data.shape[-1], # C axis 167 joint_axes_original_order = ''.join(filter(lambda axis: axis in axes, image.axes))
62 ): 168 section = giatools.Image(section_arr, joint_axes_original_order).reorder_axes_like(axes)
63 sl = np.s_[*qtzc[:3], ..., qtzc[3]] # noqa: E999 169
64 arr = img.data[sl] 170 # Perform 2-D or 3-D filtering
65 assert arr.ndim == 2 # sanity check, should always be True 171 section_result = giatools.Image(
66 172 filter_impl(section.data, **kwargs),
67 # Perform 2-D filtering 173 axes, # axes layout compatible with `kwargs`
68 res = filter_impl(arr, **kwargs) 174 ).reorder_axes_like(
175 joint_axes_original_order, # axes order compatible to the input `image`
176 )
177
178 # Update the result image for the current section
69 if result_data is None: 179 if result_data is None:
70 result_data = np.empty(img.data.shape, res.dtype) 180 result_data = np.empty(image.data.shape, section_result.data.dtype)
71 result_data[sl] = res 181 result_data[section_sel] = section_result.data
72 182
73 # Return results 183 # Return results
74 return giatools.Image(result_data, img.axes) 184 return giatools.Image(result_data, image.axes, image.metadata)
75
76
77 def apply_filter(
78 input_filepath: str,
79 output_filepath: str,
80 filter_type: str,
81 **kwargs: Any,
82 ):
83 # Read the input image
84 img = giatools.Image.read(input_filepath)
85
86 # Perform filtering
87 filter_impl = filters[filter_type]
88 res = filter_impl(img, **kwargs).normalize_axes_like(img.original_axes)
89
90 # Adopt metadata and write the result
91 res.metadata = img.metadata
92 res.write(output_filepath, backend='tifffile')
93 185
94 186
95 if __name__ == "__main__": 187 if __name__ == "__main__":
96 parser = argparse.ArgumentParser() 188 parser = argparse.ArgumentParser()
97 parser.add_argument('input', type=str, help='Input image filepath') 189 parser.add_argument('input', type=str, help='Input image filepath')
100 args = parser.parse_args() 192 args = parser.parse_args()
101 193
102 # Read the config file 194 # Read the config file
103 with open(args.params) as cfgf: 195 with open(args.params) as cfgf:
104 cfg = json.load(cfgf) 196 cfg = json.load(cfgf)
105 197 cfg.setdefault('axes', 'YX')
106 apply_filter( 198
107 args.input, 199 # Read the input image
108 args.output, 200 image = giatools.Image.read(args.input)
109 **cfg, 201 print('Input image shape:', image.data.shape)
202 print('Input image axes:', image.axes)
203 print('Input image dtype:', image.data.dtype)
204
205 # Convert the image to the explicitly requested `dtype`, or the same as the input image
206 convert_to = getattr(np, cfg.pop('dtype', str(image.data.dtype)))
207 if np.issubdtype(image.data.dtype, convert_to):
208 convert_to = image.data.dtype # use the input image `dtype` if `convert_to` is a superset
209 elif convert_to == np.floating:
210 convert_to = np.float64 # use `float64` if conversion to *any* float is *required*
211
212 # If the input image is `float16` or conversion to `float16` is requested, ...
213 if convert_to == np.float16:
214 image = image_astype(image, np.float32) # ...convert to `float32` as an intermediate
215 else:
216 image = image_astype(image, convert_to) # ...otherwise, convert now
217
218 # Perform filtering
219 filter_type = cfg.pop('filter_type')
220 filter_impl = getattr(Filters, filter_type)
221 result = filter_impl(image, **cfg)
222
223 # Apply `dtype` conversion
224 result = image_astype(result, convert_to)
225
226 # Write the result
227 result = result.normalize_axes_like(
228 image.original_axes,
110 ) 229 )
230 print('Output image shape:', result.data.shape)
231 print('Output image axes:', result.axes)
232 print('Output image dtype:', result.data.dtype)
233 result.write(args.output, backend='tifffile')