Mercurial > repos > imgteam > 2d_simple_filter
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') |
