Skip to content

Commit 590cdfb

Browse files
committed
Add antialiasing option for downsampling
1 parent 84d82b4 commit 590cdfb

1 file changed

Lines changed: 102 additions & 10 deletions

File tree

  • src/torchio/transforms/preprocessing/spatial

src/torchio/transforms/preprocessing/spatial/resample.py

Lines changed: 102 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
from ...spatial_transform import SpatialTransform
2121

2222
TypeSpacing = Union[float, tuple[float, float, float]]
23+
TypeTarget = Union[TypeSpacing, str, Path, Image, None]
24+
ONE_MILLIMITER_ISOTROPIC = 1
2325

2426

2527
class Resample(SpatialTransform):
@@ -50,6 +52,15 @@ class Resample(SpatialTransform):
5052
label_interpolation: See :ref:`Interpolation`.
5153
scalars_only: Apply only to instances of :class:`~torchio.ScalarImage`.
5254
Used internally by :class:`~torchio.transforms.RandomAnisotropy`.
55+
antialias: If ``True``, apply a Gaussian smoothing before
56+
downsampling, along any dimension that will be downsampled.
57+
This is useful to avoid aliasing artifacts when downsampling
58+
images. The standard deviation of the Gaussian kernel
59+
is computed according to the method described in Cardoso et al.,
60+
`Scale factor point spread function matching: beyond aliasing in
61+
image resampling
62+
<https://link.springer.com/chapter/10.1007/978-3-319-24571-3_81>`_,
63+
MICCAI 2015.
5364
**kwargs: See :class:`~torchio.transforms.Transform` for additional
5465
keyword arguments.
5566
@@ -79,11 +90,12 @@ class Resample(SpatialTransform):
7990

8091
def __init__(
8192
self,
82-
target: TypeSpacing | str | Path | Image | None = 1,
93+
target: TypeTarget = ONE_MILLIMITER_ISOTROPIC,
8394
image_interpolation: str = 'linear',
8495
label_interpolation: str = 'nearest',
8596
pre_affine_name: str | None = None,
8697
scalars_only: bool = False,
98+
antialias: bool = False,
8799
**kwargs,
88100
):
89101
super().__init__(**kwargs)
@@ -96,12 +108,14 @@ def __init__(
96108
)
97109
self.pre_affine_name = pre_affine_name
98110
self.scalars_only = scalars_only
111+
self.antialias = antialias
99112
self.args_names = [
100113
'target',
101114
'image_interpolation',
102115
'label_interpolation',
103116
'pre_affine_name',
104117
'scalars_only',
118+
'antialias',
105119
]
106120

107121
@staticmethod
@@ -190,21 +204,93 @@ def apply_transform(self, subject: Subject) -> Subject:
190204

191205
floating_sitk = image.as_sitk(force_3d=True)
192206

193-
resampler = sitk.ResampleImageFilter()
194-
resampler.SetInterpolator(interpolator)
195-
self._set_resampler_reference(
196-
resampler,
197-
self.target, # type: ignore[arg-type]
207+
resampler = self._get_resampler(
208+
interpolator,
198209
floating_sitk,
199210
subject,
211+
self.target,
200212
)
213+
if self.antialias and isinstance(image, ScalarImage):
214+
downsampling_factor = self._get_downsampling_factor(
215+
floating_sitk,
216+
resampler,
217+
)
218+
sigmas = self._get_sigmas(
219+
downsampling_factor,
220+
floating_sitk.GetSpacing(),
221+
)
222+
floating_sitk = self._smooth(floating_sitk, sigmas)
201223
resampled = resampler.Execute(floating_sitk)
202224

203225
array, affine = sitk_to_nib(resampled)
204226
image.set_data(torch.as_tensor(array))
205227
image.affine = affine
206228
return subject
207229

230+
@staticmethod
231+
def _smooth(
232+
image: sitk.Image,
233+
sigmas: np.ndarray,
234+
epsilon: float = 1e-9,
235+
) -> sitk.Image:
236+
"""Smooth the image with a Gaussian kernel.
237+
238+
Args:
239+
image: Image to be smoothed.
240+
sigmas: Standard deviations of the Gaussian kernel for each
241+
dimension. If a value is NaN, no smoothing is applied in that
242+
dimension.
243+
epsilon: Small value to replace NaN values in sigmas, to avoid
244+
division-by-zero errors.
245+
"""
246+
247+
sigmas[np.isnan(sigmas)] = epsilon # no smoothing in that dimension
248+
gaussian = sitk.SmoothingRecursiveGaussianImageFilter()
249+
gaussian.SetSigma(sigmas.tolist())
250+
smoothed = gaussian.Execute(image)
251+
return smoothed
252+
253+
@staticmethod
254+
def _get_downsampling_factor(
255+
floating: sitk.Image,
256+
resampler: sitk.ResampleImageFilter,
257+
) -> np.ndarray:
258+
"""Get the downsampling factor for each dimension.
259+
260+
The downsampling factor is the ratio between the output spacing and
261+
the input spacing. If the output spacing is smaller than the input
262+
spacing, the factor is set to NaN, meaning downsampling is not applied
263+
in that dimension.
264+
265+
Args:
266+
floating: The input image to be resampled.
267+
resampler: The resampler that will be used to resample the image.
268+
"""
269+
input_spacing = np.array(floating.GetSpacing())
270+
output_spacing = np.array(resampler.GetOutputSpacing())
271+
factors = output_spacing / input_spacing
272+
no_downsampling = factors <= 1
273+
factors[no_downsampling] = np.nan
274+
return factors
275+
276+
def _get_resampler(
277+
self,
278+
interpolator: int,
279+
floating: sitk.Image,
280+
subject: Subject,
281+
target: TypeTarget,
282+
) -> sitk.ResampleImageFilter:
283+
"""Instantiate a SimpleITK resampler."""
284+
resampler = sitk.ResampleImageFilter()
285+
resampler.SetInterpolator(interpolator)
286+
self._set_resampler_reference(
287+
resampler,
288+
target, # type: ignore[arg-type]
289+
floating,
290+
subject,
291+
)
292+
return resampler
293+
208294
def _set_resampler_reference(
209295
self,
210296
resampler: sitk.ResampleImageFilter,
@@ -216,7 +302,6 @@ def _set_resampler_reference(
216302
# 1) An instance of torchio.Image
217303
# 2) An instance of pathlib.Path
218304
# 3) A string, which could be a path or an image in subject
219-
# 3) A string, which could be a path or an image in subject
220305
# 4) A number or sequence of numbers for spacing
221306
# 5) A tuple of shape, affine
222307
# The fourth case is the different one
@@ -311,11 +396,18 @@ def get_reference_image(
311396
return reference
312397

313398
@staticmethod
314-
def get_sigma(downsampling_factor, spacing):
399+
def _get_sigmas(downsampling_factor: np.ndarray, spacing: np.ndarray) -> np.ndarray:
315400
"""Compute optimal standard deviation for Gaussian kernel.
316401
317-
From Cardoso et al., "Scale factor point spread function
318-
matching: beyond aliasing in image resampling", MICCAI 2015
402+
From Cardoso et al., `Scale factor point spread function matching:
403+
beyond aliasing in image resampling
404+
<https://link.springer.com/chapter/10.1007/978-3-319-24571-3_81>`_,
405+
MICCAI 2015.
406+
407+
Args:
408+
downsampling_factor: Array with the downsampling factor for each
409+
dimension.
410+
spacing: Array with the spacing of the input image in mm.
319411
"""
320412
k = downsampling_factor
321413
variance = (k**2 - 1**2) * (2 * np.sqrt(2 * np.log(2))) ** (-2)

0 commit comments

Comments
 (0)