Repository: Project-MONAI/MONAI

Issue:
adding the antialiasing option for `resized`
**Is your feature request related to a problem? Please describe.**
follow-up of https://github.com/Project-MONAI/MONAI/issues/4020, the antialiasing functions in `Resize`
https://github.com/Project-MONAI/MONAI/blob/0bbeb1e084d1c06a5f14af70ad340c798a1567aa/monai/transforms/spatial/array.py#L626-L633 should be implemented in the `resized`. 

it might be useful to include the default sigma computing logic (based on spatial size) as part of `GaussianSmooth` https://github.com/Project-MONAI/MONAI/blob/0bbeb1e084d1c06a5f14af70ad340c798a1567aa/monai/transforms/intensity/array.py#L1124 


Discussion:

Patch:
diff --git a/monai/transforms/spatial/dictionary.py b/monai/transforms/spatial/dictionary.py
--- a/monai/transforms/spatial/dictionary.py
+++ b/monai/transforms/spatial/dictionary.py
@@ -609,6 +609,15 @@ class Resized(MapTransform, InvertibleTransform):
             'linear', 'bilinear', 'bicubic' or 'trilinear'. Default: None.
             See also: https://pytorch.org/docs/stable/generated/torch.nn.functional.interpolate.html
             It also can be a sequence of bool or None, each element corresponds to a key in ``keys``.
+        anti_aliasing: bool
+            Whether to apply a Gaussian filter to smooth the image prior
+            to downsampling. It is crucial to filter when downsampling
+            the image to avoid aliasing artifacts. See also ``skimage.transform.resize``
+        anti_aliasing_sigma: {float, tuple of floats}, optional
+            Standard deviation for Gaussian filtering used when anti-aliasing.
+            By default, this value is chosen as (s - 1) / 2 where s is the
+            downsampling factor, where s > 1. For the up-size case, s < 1, no
+            anti-aliasing is performed prior to rescaling.
         allow_missing_keys: don't raise exception if key is missing.
     """
 
@@ -621,17 +630,29 @@ def __init__(
         size_mode: str = "all",
         mode: SequenceStr = InterpolateMode.AREA,
         align_corners: Union[Sequence[Optional[bool]], Optional[bool]] = None,
+        anti_aliasing: Union[Sequence[bool], bool] = False,
+        anti_aliasing_sigma: Union[Sequence[Union[Sequence[float], float, None]], Sequence[float], float, None] = None,
         allow_missing_keys: bool = False,
     ) -> None:
         super().__init__(keys, allow_missing_keys)
         self.mode = ensure_tuple_rep(mode, len(self.keys))
         self.align_corners = ensure_tuple_rep(align_corners, len(self.keys))
+        self.anti_aliasing = ensure_tuple_rep(anti_aliasing, len(self.keys))
+        self.anti_aliasing_sigma = ensure_tuple_rep(anti_aliasing_sigma, len(self.keys))
         self.resizer = Resize(spatial_size=spatial_size, size_mode=size_mode)
 
     def __call__(self, data: Mapping[Hashable, torch.Tensor]) -> Dict[Hashable, torch.Tensor]:
         d = dict(data)
-        for key, mode, align_corners in self.key_iterator(d, self.mode, self.align_corners):
-            d[key] = self.resizer(d[key], mode=mode, align_corners=align_corners)
+        for key, mode, align_corners, anti_aliasing, anti_aliasing_sigma in self.key_iterator(
+            d, self.mode, self.align_corners, self.anti_aliasing, self.anti_aliasing_sigma
+        ):
+            d[key] = self.resizer(
+                d[key],
+                mode=mode,
+                align_corners=align_corners,
+                anti_aliasing=anti_aliasing,
+                anti_aliasing_sigma=anti_aliasing_sigma,
+            )
         return d
 
     def inverse(self, data: Mapping[Hashable, torch.Tensor]) -> Dict[Hashable, torch.Tensor]:


Test patch:
diff --git a/tests/test_resized.py b/tests/test_resized.py
--- a/tests/test_resized.py
+++ b/tests/test_resized.py
@@ -13,23 +13,44 @@
 
 import numpy as np
 import skimage.transform
+import torch
 from parameterized import parameterized
 
 from monai.data import MetaTensor, set_track_meta
-from monai.transforms import Invertd, Resized
+from monai.transforms import Invertd, Resize, Resized
 from tests.utils import TEST_NDARRAYS_ALL, NumpyImageTestCase2D, assert_allclose, test_local_inversion
 
 TEST_CASE_0 = [{"keys": "img", "spatial_size": 15}, (6, 10, 15)]
 
-TEST_CASE_1 = [{"keys": "img", "spatial_size": 15, "mode": "area"}, (6, 10, 15)]
+TEST_CASE_1 = [
+    {"keys": "img", "spatial_size": 15, "mode": "area", "anti_aliasing": True, "anti_aliasing_sigma": None},
+    (6, 10, 15),
+]
 
-TEST_CASE_2 = [{"keys": "img", "spatial_size": 6, "mode": "trilinear", "align_corners": True}, (2, 4, 6)]
+TEST_CASE_2 = [
+    {"keys": "img", "spatial_size": 6, "mode": "trilinear", "align_corners": True, "anti_aliasing_sigma": 2.0},
+    (2, 4, 6),
+]
 
 TEST_CASE_3 = [
-    {"keys": ["img", "label"], "spatial_size": 6, "mode": ["trilinear", "nearest"], "align_corners": [True, None]},
+    {
+        "keys": ["img", "label"],
+        "spatial_size": 6,
+        "mode": ["trilinear", "nearest"],
+        "align_corners": [True, None],
+        "anti_aliasing": [False, True],
+        "anti_aliasing_sigma": (None, 2.0),
+    },
     (2, 4, 6),
 ]
 
+TEST_CORRECT_CASES = [
+    ((32, -1), "area", False),
+    ((64, 64), "area", True),
+    ((32, 32, 32), "area", True),
+    ((256, 256), "bilinear", False),
+]
+
 
 class TestResized(NumpyImageTestCase2D):
     def test_invalid_inputs(self):
@@ -41,9 +62,9 @@ def test_invalid_inputs(self):
             resize = Resized(keys="img", spatial_size=(128,), mode="order")
             resize({"img": self.imt[0]})
 
-    @parameterized.expand([((32, -1), "area"), ((64, 64), "area"), ((32, 32, 32), "area"), ((256, 256), "bilinear")])
-    def test_correct_results(self, spatial_size, mode):
-        resize = Resized("img", spatial_size, mode=mode)
+    @parameterized.expand(TEST_CORRECT_CASES)
+    def test_correct_results(self, spatial_size, mode, anti_aliasing):
+        resize = Resized("img", spatial_size, mode=mode, anti_aliasing=anti_aliasing)
         _order = 0
         if mode.endswith("linear"):
             _order = 1
@@ -51,7 +72,7 @@ def test_correct_results(self, spatial_size, mode):
             spatial_size = (32, 64)
         expected = [
             skimage.transform.resize(
-                channel, spatial_size, order=_order, clip=False, preserve_range=False, anti_aliasing=False
+                channel, spatial_size, order=_order, clip=False, preserve_range=False, anti_aliasing=anti_aliasing
             )
             for channel in self.imt[0]
         ]
@@ -61,7 +82,7 @@ def test_correct_results(self, spatial_size, mode):
             im = p(self.imt[0])
             out = resize({"img": im})
             test_local_inversion(resize, out, {"img": im}, "img")
-            assert_allclose(out["img"], expected, type_test=False, atol=0.9)
+            assert_allclose(out["img"], expected, type_test=False, atol=1.0)
 
     @parameterized.expand([TEST_CASE_0, TEST_CASE_1, TEST_CASE_2, TEST_CASE_3])
     def test_longest_shape(self, input_param, expected_shape):
@@ -88,6 +109,22 @@ def test_identical_spatial(self):
         transform_inverse = Invertd(keys="Y", transform=xform, orig_keys="X")
         assert_allclose(transform_inverse(out)["Y"].array, np.ones((1, 10, 16, 17)) * 2)
 
+    def test_consistent_resize(self):
+        spatial_size = (16, 16, 16)
+        rescaler_1 = Resize(spatial_size=spatial_size, anti_aliasing=True, anti_aliasing_sigma=(0.5, 1.0, 2.0))
+        rescaler_2 = Resize(spatial_size=spatial_size, anti_aliasing=True, anti_aliasing_sigma=None)
+        rescaler_dict = Resized(
+            keys=["img1", "img2"],
+            spatial_size=spatial_size,
+            anti_aliasing=(True, True),
+            anti_aliasing_sigma=[(0.5, 1.0, 2.0), None],
+        )
+        test_input_1 = torch.randn([3, 32, 32, 32])
+        test_input_2 = torch.randn([3, 32, 32, 32])
+        test_input_dict = {"img1": test_input_1, "img2": test_input_2}
+        assert_allclose(rescaler_1(test_input_1), rescaler_dict(test_input_dict)["img1"])
+        assert_allclose(rescaler_2(test_input_2), rescaler_dict(test_input_dict)["img2"])
+
 
 if __name__ == "__main__":
     unittest.main()