Repository: Project-MONAI/MONAI

Issue:
align_corner should support multiple values
**Is your feature request related to a problem? Please describe.**
Currently, for `Resized`, `Zoomd` and `RandZoomd` transforms, if set different `mode` for "image" and "label", can't set different `align_corner` for them.



Discussion:

Resolving patch:
diff --git a/monai/transforms/spatial/array.py b/monai/transforms/spatial/array.py
--- a/monai/transforms/spatial/array.py
+++ b/monai/transforms/spatial/array.py
@@ -290,13 +290,18 @@ def __init__(
         self.mode: InterpolateMode = InterpolateMode(mode)
         self.align_corners = align_corners
 
-    def __call__(self, img, mode: Optional[Union[InterpolateMode, str]] = None):
+    def __call__(
+        self, img, mode: Optional[Union[InterpolateMode, str]] = None, align_corners: Optional[bool] = None,
+    ):
         """
         Args:
-            img (ndarray): channel first array, must have shape: (num_channels, H[, W, ..., ]),
+            img (ndarray): channel first array, must have shape: (num_channels, H[, W, ..., ]).
             mode: {``"nearest"``, ``"linear"``, ``"bilinear"``, ``"bicubic"``, ``"trilinear"``, ``"area"``}
                 The interpolation mode. Defaults to ``self.mode``.
                 See also: https://pytorch.org/docs/stable/nn.functional.html#interpolate
+            align_corners: This only has an effect when mode is
+                'linear', 'bilinear', 'bicubic' or 'trilinear'. Defaults to ``self.align_corners``.
+                See also: https://pytorch.org/docs/stable/nn.functional.html#interpolate
 
         Raises:
             ValueError: len(spatial_size) cannot be smaller than the image spatial dimensions,
@@ -317,7 +322,7 @@ def __call__(self, img, mode: Optional[Union[InterpolateMode, str]] = None):
             input=torch.as_tensor(img[None], dtype=torch.float),
             size=self.spatial_size,
             mode=self.mode.value if mode is None else InterpolateMode(mode).value,
-            align_corners=self.align_corners,
+            align_corners=self.align_corners if align_corners is None else align_corners,
         )
         resized = resized.squeeze(0).detach().cpu().numpy()
         return resized
@@ -446,18 +451,25 @@ def __init__(
         self.keep_size = keep_size
 
     def __call__(  # type: ignore # see issue #495
-        self, img, mode: Optional[Union[InterpolateMode, str]] = None
+        self, img, mode: Optional[Union[InterpolateMode, str]] = None, align_corners: Optional[bool] = None,
     ):
         """
         Args:
-            img (ndarray): channel first array, must have shape: (num_channels, H[, W, ..., ]),
+            img (ndarray): channel first array, must have shape: (num_channels, H[, W, ..., ]).
+            mode: {``"nearest"``, ``"linear"``, ``"bilinear"``, ``"bicubic"``, ``"trilinear"``, ``"area"``}
+                The interpolation mode. Defaults to ``self.mode``.
+                See also: https://pytorch.org/docs/stable/nn.functional.html#interpolate
+            align_corners: This only has an effect when mode is
+                'linear', 'bilinear', 'bicubic' or 'trilinear'. Defaults to ``self.align_corners``.
+                See also: https://pytorch.org/docs/stable/nn.functional.html#interpolate
+
         """
         self.zoom = ensure_tuple_rep(self.zoom, img.ndim - 1)  # match the spatial image dim
         zoomed = _torch_interp(
             input=torch.as_tensor(img[None], dtype=torch.float),
             scale_factor=list(self.zoom),
             mode=self.mode.value if mode is None else InterpolateMode(mode).value,
-            align_corners=self.align_corners,
+            align_corners=self.align_corners if align_corners is None else align_corners,
         )
         zoomed = zoomed.squeeze(0).detach().cpu().numpy()
         if not self.keep_size or np.allclose(img.shape, zoomed.shape):
@@ -715,20 +727,27 @@ def randomize(self) -> None:  # type: ignore # see issue #495
         else:
             self._zoom = self.R.uniform(self.min_zoom, self.max_zoom)
 
-    def __call__(self, img, mode: Optional[Union[InterpolateMode, str]] = None):
+    def __call__(
+        self, img, mode: Optional[Union[InterpolateMode, str]] = None, align_corners: Optional[bool] = None,
+    ):
         """
         Args:
             img (ndarray): channel first array, must have shape 2D: (nchannels, H, W), or 3D: (nchannels, H, W, D).
             mode: {``"nearest"``, ``"linear"``, ``"bilinear"``, ``"bicubic"``, ``"trilinear"``, ``"area"``}
                 The interpolation mode. Defaults to ``self.mode``.
                 See also: https://pytorch.org/docs/stable/nn.functional.html#interpolate
+            align_corners: This only has an effect when mode is
+                'linear', 'bilinear', 'bicubic' or 'trilinear'. Defaults to ``self.align_corners``.
+                See also: https://pytorch.org/docs/stable/nn.functional.html#interpolate
         """
         self.randomize()
         _dtype = np.float32
         if not self._do_transform:
             return img.astype(_dtype)
-        zoomer = Zoom(self._zoom, align_corners=self.align_corners, keep_size=self.keep_size)
-        return zoomer(img, mode=mode or self.mode).astype(_dtype)
+        zoomer = Zoom(self._zoom, keep_size=self.keep_size)
+        return zoomer(
+            img, mode=mode or self.mode, align_corners=self.align_corners if align_corners is None else align_corners,
+        ).astype(_dtype)
 
 
 class AffineGrid(Transform):
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
@@ -269,6 +269,7 @@ class Resized(MapTransform):
         align_corners (optional bool): This only has an effect when mode is
             'linear', 'bilinear', 'bicubic' or 'trilinear'. Default: None.
             See also: https://pytorch.org/docs/stable/nn.functional.html#interpolate
+            It also can be a sequence of bool or None, each element corresponds to a key in ``keys``.
     """
 
     def __init__(
@@ -280,12 +281,13 @@ def __init__(
     ):
         super().__init__(keys)
         self.mode = ensure_tuple_rep(mode, len(self.keys))
-        self.resizer = Resize(spatial_size=spatial_size, align_corners=align_corners)
+        self.align_corners = ensure_tuple_rep(align_corners, len(self.keys))
+        self.resizer = Resize(spatial_size=spatial_size)
 
     def __call__(self, data):
         d = dict(data)
         for idx, key in enumerate(self.keys):
-            d[key] = self.resizer(d[key], mode=self.mode[idx])
+            d[key] = self.resizer(d[key], mode=self.mode[idx], align_corners=self.align_corners[idx])
         return d
 
 
@@ -766,6 +768,7 @@ class Zoomd(MapTransform):
         align_corners (optional bool): This only has an effect when mode is
             'linear', 'bilinear', 'bicubic' or 'trilinear'. Default: None.
             See also: https://pytorch.org/docs/stable/nn.functional.html#interpolate
+            It also can be a sequence of bool or None, each element corresponds to a key in ``keys``.
         keep_size (bool): Should keep original size (pad if needed), default is True.
     """
 
@@ -778,13 +781,14 @@ def __init__(
         keep_size: bool = True,
     ):
         super().__init__(keys)
-        self.zoomer = Zoom(zoom=zoom, align_corners=align_corners, keep_size=keep_size)
         self.mode = ensure_tuple_rep(mode, len(self.keys))
+        self.align_corners = ensure_tuple_rep(align_corners, len(self.keys))
+        self.zoomer = Zoom(zoom=zoom, keep_size=keep_size)
 
     def __call__(self, data):
         d = dict(data)
         for idx, key in enumerate(self.keys):
-            d[key] = self.zoomer(d[key], mode=self.mode[idx])
+            d[key] = self.zoomer(d[key], mode=self.mode[idx], align_corners=self.align_corners[idx])
         return d
 
 
@@ -808,6 +812,7 @@ class RandZoomd(Randomizable, MapTransform):
         align_corners (optional bool): This only has an effect when mode is
             'linear', 'bilinear', 'bicubic' or 'trilinear'. Default: None.
             See also: https://pytorch.org/docs/stable/nn.functional.html#interpolate
+            It also can be a sequence of bool or None, each element corresponds to a key in ``keys``.
         keep_size: Should keep original size (pad if needed), default is True.
     """
 
@@ -829,7 +834,7 @@ def __init__(
         self.prob = prob
 
         self.mode = ensure_tuple_rep(mode, len(self.keys))
-        self.align_corners = align_corners
+        self.align_corners = ensure_tuple_rep(align_corners, len(self.keys))
         self.keep_size = keep_size
 
         self._do_transform = False
@@ -847,9 +852,9 @@ def __call__(self, data):
         d = dict(data)
         if not self._do_transform:
             return d
-        zoomer = Zoom(self._zoom, align_corners=self.align_corners, keep_size=self.keep_size)
+        zoomer = Zoom(self._zoom, keep_size=self.keep_size)
         for idx, key in enumerate(self.keys):
-            d[key] = zoomer(d[key], mode=self.mode[idx])
+            d[key] = zoomer(d[key], mode=self.mode[idx], align_corners=self.align_corners[idx])
         return d
 
 


Test patch: