|
24 | 24 |
|
25 | 25 | from __future__ import annotations |
26 | 26 |
|
| 27 | +from numbers import Number |
| 28 | + |
27 | 29 | import numpy as np |
28 | | -from scipy.ndimage import median_filter |
| 30 | +from nibabel import Nifti1Image, load |
| 31 | +from scipy.ndimage import gaussian_filter as _gs |
| 32 | +from scipy.ndimage import map_coordinates, median_filter |
29 | 33 | from skimage.morphology import ball |
30 | 34 |
|
31 | 35 | DEFAULT_DTYPE = "int16" |
@@ -92,3 +96,145 @@ def advanced_clip( |
92 | 96 | data = np.round(255 * data).astype(dtype) |
93 | 97 |
|
94 | 98 | return data |
| 99 | + |
| 100 | + |
| 101 | +def gaussian_filter( |
| 102 | + data: np.ndarray, |
| 103 | + vox_width: float | tuple[float, float, float], |
| 104 | +) -> np.ndarray: |
| 105 | + """ |
| 106 | + Applies a Gaussian smoothing filter to a n-dimensional array. |
| 107 | +
|
| 108 | + This function smooths the input data using a Gaussian filter with a specified |
| 109 | + width (sigma) in voxels along each relevant dimension. It automatically |
| 110 | + handles different data dimensionalities (2D, 3D, or 4D) and ensures that |
| 111 | + smoothing is not applied along the time or orientation dimension (if present |
| 112 | + in 4D data). |
| 113 | +
|
| 114 | + Parameters |
| 115 | + ---------- |
| 116 | + data : :obj:`~numpy.ndarray` |
| 117 | + The input data array. |
| 118 | + vox_width : :obj:`float` or :obj:`tuple` of three :obj:`float` |
| 119 | + The smoothing kernel width (sigma) in voxels. If a single :obj:`float` is provided, |
| 120 | + it is applied uniformly across all spatial dimensions. Alternatively, a |
| 121 | + tuple of three floats can be provided to specify different sigma values |
| 122 | + for each spatial dimension (x, y, z). |
| 123 | +
|
| 124 | + Returns |
| 125 | + ------- |
| 126 | + :obj:`~numpy.ndarray` |
| 127 | + The smoothed data array. |
| 128 | +
|
| 129 | + """ |
| 130 | + |
| 131 | + data = np.squeeze(data) # Drop unused dimensions |
| 132 | + ndim = data.ndim |
| 133 | + |
| 134 | + if isinstance(vox_width, Number): |
| 135 | + vox_width = tuple([vox_width] * min(3, ndim)) |
| 136 | + |
| 137 | + # Do not smooth across time/orientation (if present in 4D data) |
| 138 | + if ndim == 4 and len(vox_width) == 3: |
| 139 | + vox_width = (*vox_width, 0) |
| 140 | + |
| 141 | + return _gs(data, vox_width) |
| 142 | + |
| 143 | + |
| 144 | +def decimate( |
| 145 | + in_file: str, |
| 146 | + factor: int | tuple[int, int, int], |
| 147 | + smooth: bool | tuple[int, int, int] = True, |
| 148 | + order: int = 3, |
| 149 | + nonnegative: bool = True, |
| 150 | +) -> Nifti1Image: |
| 151 | + """ |
| 152 | + Decimates a 3D or 4D Nifti image by a specified downsampling factor. |
| 153 | +
|
| 154 | + This function downsamples a Nifti image by averaging voxels within a user-defined |
| 155 | + factor in each spatial dimension. It optionally applies Gaussian smoothing |
| 156 | + before downsampling to reduce aliasing artifacts. The function also handles |
| 157 | + updating the affine transformation matrix to reflect the change in voxel size. |
| 158 | +
|
| 159 | + Parameters |
| 160 | + ---------- |
| 161 | + in_file : :obj:`str` |
| 162 | + Path to the input NIfTI image file. |
| 163 | + factor : :obj:`int` or :obj:`tuple` |
| 164 | + The downsampling factor. If a single integer is provided, it is applied |
| 165 | + uniformly across all spatial dimensions. Alternatively, a tuple of three |
| 166 | + integers can be provided to specify different downsampling factors for each |
| 167 | + spatial dimension (x, y, z). Values must be greater than 0. |
| 168 | + smooth : :obj:`bool` or :obj:`tuple`, optional (default=``True``) |
| 169 | + Controls application of Gaussian smoothing before downsampling. If True, |
| 170 | + a smoothing kernel size equal to the downsampling factor is applied. |
| 171 | + Alternatively, a tuple of three integers can be provided to specify |
| 172 | + different smoothing kernel sizes for each spatial dimension. Setting to |
| 173 | + False disables smoothing. |
| 174 | + order : :obj:`int`, optional (default=3) |
| 175 | + The order of the spline interpolation used for downsampling. Higher |
| 176 | + orders provide smoother results but are computationally more expensive. |
| 177 | + nonnegative : :obj:`bool`, optional (default=``True``) |
| 178 | + If True, negative values in the downsampled data are set to zero. |
| 179 | +
|
| 180 | + Returns |
| 181 | + ------- |
| 182 | + :obj:`~nibabel.Nifti1Image` |
| 183 | + The downsampled NIfTI image object. |
| 184 | +
|
| 185 | + """ |
| 186 | + |
| 187 | + imnii = load(in_file) |
| 188 | + data = np.squeeze(imnii.get_fdata()) # Remove unused dimensions |
| 189 | + datashape = data.shape |
| 190 | + ndim = data.ndim |
| 191 | + |
| 192 | + if isinstance(factor, Number): |
| 193 | + factor = tuple([factor] * min(3, ndim)) |
| 194 | + |
| 195 | + if any(f <= 0 for f in factor[:3]): |
| 196 | + raise ValueError("All spatial downsampling factors must be positive.") |
| 197 | + |
| 198 | + if ndim == 4 and len(factor) == 3: |
| 199 | + factor = (*factor, 0) |
| 200 | + |
| 201 | + if smooth: |
| 202 | + if smooth is True: |
| 203 | + smooth = factor |
| 204 | + data = gaussian_filter(data, smooth) |
| 205 | + |
| 206 | + # Create downsampled grid |
| 207 | + down_grid = np.array( |
| 208 | + np.meshgrid( |
| 209 | + *[np.arange(_s, step=int(_f) or 1) for _s, _f in zip(datashape, factor)], |
| 210 | + indexing="ij", |
| 211 | + ) |
| 212 | + ) |
| 213 | + new_shape = down_grid.shape[1:] |
| 214 | + |
| 215 | + # Update affine transformation |
| 216 | + newaffine = imnii.affine.copy() |
| 217 | + newaffine[:3, :3] = np.array(factor[:3]) * newaffine[:3, :3] |
| 218 | + |
| 219 | + # TODO: Update offset so new array is aligned with original |
| 220 | + |
| 221 | + # Resample data on the new grid |
| 222 | + resampled = map_coordinates( |
| 223 | + data, |
| 224 | + down_grid.reshape((ndim, np.prod(new_shape))), |
| 225 | + order=order, |
| 226 | + mode="constant", |
| 227 | + cval=0, |
| 228 | + prefilter=True, |
| 229 | + ).reshape(new_shape) |
| 230 | + |
| 231 | + # Set negative values to zero (optional) |
| 232 | + if order > 2 and nonnegative: |
| 233 | + resampled[resampled < 0] = 0 |
| 234 | + |
| 235 | + # Create new Nifti image with updated information |
| 236 | + newnii = Nifti1Image(resampled, newaffine, imnii.header) |
| 237 | + newnii.set_sform(newaffine, code=1) |
| 238 | + newnii.set_qform(newaffine, code=1) |
| 239 | + |
| 240 | + return newnii |
0 commit comments