2020from ...spatial_transform import SpatialTransform
2121
2222TypeSpacing = Union [float , tuple [float , float , float ]]
23+ TypeTarget = Union [TypeSpacing , str , Path , Image , None ]
24+ ONE_MILLIMITER_ISOTROPIC = 1
2325
2426
2527class 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