Repository: Project-MONAI/MONAI

Issue:
reduce ResizeWithPadOrCrop memory footprint
**Is your feature request related to a problem? Please describe.**
for the ResizeWithPadOrCrop transform, cropping first then padding would have smaller memory footprints than padding then cropping.
(this may require changes of the current cropping transform behaviour, so it's beyond v0.3 after discussions with @Nic-Ma )
reduce ResizeWithPadOrCrop memory footprint
**Is your feature request related to a problem? Please describe.**
for the ResizeWithPadOrCrop transform, cropping first then padding would have smaller memory footprints than padding then cropping.
(this may require changes of the current cropping transform behaviour, so it's beyond v0.3 after discussions with @Nic-Ma )


Discussion:

Resolving patch:
diff --git a/monai/transforms/croppad/array.py b/monai/transforms/croppad/array.py
--- a/monai/transforms/croppad/array.py
+++ b/monai/transforms/croppad/array.py
@@ -199,10 +199,8 @@ class SpatialCrop(Transform):
     """
     General purpose cropper to produce sub-volume region of interest (ROI).
     It can support to crop ND spatial (channel-first) data.
-    Either a spatial center and size must be provided, or alternatively if center and size
-    are not provided, the start and end coordinates of the ROI must be provided.
-    The sub-volume must sit the within original image.
-    Note: This transform will not work if the crop region is larger than the image itself.
+    Either a spatial center and size must be provided, or alternatively,
+    if center and size are not provided, the start and end coordinates of the ROI must be provided.
     """
 
     def __init__(
@@ -220,29 +218,22 @@ def __init__(
             roi_end: voxel coordinates for end of the crop ROI.
         """
         if roi_center is not None and roi_size is not None:
-            roi_center = np.asarray(roi_center, dtype=np.uint16)
-            roi_size = np.asarray(roi_size, dtype=np.uint16)
-            self.roi_start = np.subtract(roi_center, np.floor_divide(roi_size, 2))
-            self.roi_end = np.add(self.roi_start, roi_size)
+            roi_center = np.asarray(roi_center, dtype=np.int16)
+            roi_size = np.asarray(roi_size, dtype=np.int16)
+            self.roi_start = np.maximum(roi_center - np.floor_divide(roi_size, 2), 0)
+            self.roi_end = np.maximum(self.roi_start + roi_size, self.roi_start)
         else:
-            assert roi_start is not None and roi_end is not None, "roi_start and roi_end must be provided."
-            self.roi_start = np.asarray(roi_start, dtype=np.uint16)
-            self.roi_end = np.asarray(roi_end, dtype=np.uint16)
-
-        assert np.all(self.roi_start >= 0), "all elements of roi_start must be greater than or equal to 0."
-        assert np.all(self.roi_end > 0), "all elements of roi_end must be positive."
-        assert np.all(self.roi_end >= self.roi_start), "invalid roi range."
+            if roi_start is None or roi_end is None:
+                raise ValueError("Please specify either roi_center, ori_size or roi_start, roi_end.")
+            self.roi_start = np.maximum(np.asarray(roi_start, dtype=np.int16), 0)
+            self.roi_end = np.maximum(np.asarray(roi_end, dtype=np.int16), self.roi_start)
 
     def __call__(self, img: np.ndarray) -> np.ndarray:
         """
         Apply the transform to `img`, assuming `img` is channel-first and
         slicing doesn't apply to the channel dim.
         """
-        max_end = img.shape[1:]
-        sd = min(len(self.roi_start), len(max_end))
-        assert np.all(max_end[:sd] >= self.roi_start[:sd]), "roi start out of image space."
-        assert np.all(max_end[:sd] >= self.roi_end[:sd]), "roi end out of image space."
-
+        sd = min(len(self.roi_start), len(self.roi_end), len(img.shape[1:]))  # spatial dims
         slices = [slice(None)] + [slice(s, e) for s, e in zip(self.roi_start[:sd], self.roi_end[:sd])]
         return img[tuple(slices)]
 
@@ -410,7 +401,7 @@ def __init__(
         self.margin = margin
         self.return_coords = return_coords
 
-    def __call__(self, img: np.ndarray) -> np.ndarray:
+    def __call__(self, img: np.ndarray):
         """
         Apply the transform to `img`, assuming `img` is channel-first and
         slicing doesn't change the channel dim.
@@ -587,4 +578,4 @@ def __call__(self, img: np.ndarray, mode: Optional[Union[NumpyPadMode, str]] = N
                 If None, defaults to the ``mode`` in construction.
                 See also: https://numpy.org/doc/1.18/reference/generated/numpy.pad.html
         """
-        return self.cropper(self.padder(img, mode=mode))
+        return self.padder(self.cropper(img), mode=mode)


Test patch:
diff --git a/tests/test_resize_with_pad_or_crop.py b/tests/test_resize_with_pad_or_crop.py
--- a/tests/test_resize_with_pad_or_crop.py
+++ b/tests/test_resize_with_pad_or_crop.py
@@ -16,39 +16,43 @@
 
 from monai.transforms import ResizeWithPadOrCrop
 
-TEST_CASE_1 = [
-    {"spatial_size": [15, 8, 8], "mode": "constant"},
-    np.zeros((3, 8, 8, 4)),
-    np.zeros((3, 15, 8, 8)),
-]
-
-TEST_CASE_2 = [
-    {"spatial_size": [15, 4, 8], "mode": "constant"},
-    np.zeros((3, 8, 8, 4)),
-    np.zeros((3, 15, 4, 8)),
-]
-
-TEST_CASE_3 = [
-    {"spatial_size": [15, 4, -1], "mode": "constant"},
-    np.zeros((3, 8, 8, 4)),
-    np.zeros((3, 15, 4, 4)),
-]
-
-TEST_CASE_4 = [
-    {"spatial_size": [15, 4, -1], "mode": "reflect"},
-    np.zeros((3, 8, 8, 4)),
-    np.zeros((3, 15, 4, 4)),
+TEST_CASES = [
+    [
+        {"spatial_size": [15, 8, 8], "mode": "constant"},
+        (3, 8, 8, 4),
+        (3, 15, 8, 8),
+    ],
+    [
+        {"spatial_size": [15, 4, 8], "mode": "constant"},
+        (3, 8, 8, 4),
+        (3, 15, 4, 8),
+    ],
+    [
+        {"spatial_size": [15, 4, -1], "mode": "constant"},
+        (3, 8, 8, 4),
+        (3, 15, 4, 4),
+    ],
+    [
+        {"spatial_size": [15, 4, -1], "mode": "reflect"},
+        (3, 8, 8, 4),
+        (3, 15, 4, 4),
+    ],
+    [
+        {"spatial_size": [-1, -1, -1], "mode": "reflect"},
+        (3, 8, 8, 4),
+        (3, 8, 8, 4),
+    ],
 ]
 
 
 class TestResizeWithPadOrCrop(unittest.TestCase):
-    @parameterized.expand([TEST_CASE_1, TEST_CASE_2, TEST_CASE_3, TEST_CASE_4])
-    def test_pad_shape(self, input_param, input_data, expected_val):
+    @parameterized.expand(TEST_CASES)
+    def test_pad_shape(self, input_param, input_shape, expected_shape):
         paddcroper = ResizeWithPadOrCrop(**input_param)
-        result = paddcroper(input_data)
-        np.testing.assert_allclose(result.shape, expected_val.shape)
-        result = paddcroper(input_data, mode="constant")
-        np.testing.assert_allclose(result.shape, expected_val.shape)
+        result = paddcroper(np.zeros(input_shape))
+        np.testing.assert_allclose(result.shape, expected_shape)
+        result = paddcroper(np.zeros(input_shape), mode="constant")
+        np.testing.assert_allclose(result.shape, expected_shape)
 
 
 if __name__ == "__main__":
diff --git a/tests/test_resize_with_pad_or_cropd.py b/tests/test_resize_with_pad_or_cropd.py
--- a/tests/test_resize_with_pad_or_cropd.py
+++ b/tests/test_resize_with_pad_or_cropd.py
@@ -16,37 +16,41 @@
 
 from monai.transforms import ResizeWithPadOrCropd
 
-TEST_CASE_1 = [
-    {"keys": "img", "spatial_size": [15, 8, 8], "mode": "constant"},
-    {"img": np.zeros((3, 8, 8, 4))},
-    np.zeros((3, 15, 8, 8)),
-]
-
-TEST_CASE_2 = [
-    {"keys": "img", "spatial_size": [15, 4, 8], "mode": "constant"},
-    {"img": np.zeros((3, 8, 8, 4))},
-    np.zeros((3, 15, 4, 8)),
-]
-
-TEST_CASE_3 = [
-    {"keys": "img", "spatial_size": [15, 4, -1], "mode": "constant"},
-    {"img": np.zeros((3, 8, 8, 4))},
-    np.zeros((3, 15, 4, 4)),
-]
-
-TEST_CASE_4 = [
-    {"keys": "img", "spatial_size": [15, 4, -1], "mode": "reflect"},
-    {"img": np.zeros((3, 8, 8, 4))},
-    np.zeros((3, 15, 4, 4)),
+TEST_CASES = [
+    [
+        {"keys": "img", "spatial_size": [15, 8, 8], "mode": "constant"},
+        {"img": np.zeros((3, 8, 8, 4))},
+        (3, 15, 8, 8),
+    ],
+    [
+        {"keys": "img", "spatial_size": [15, 4, 8], "mode": "constant"},
+        {"img": np.zeros((3, 8, 8, 4))},
+        (3, 15, 4, 8),
+    ],
+    [
+        {"keys": "img", "spatial_size": [15, 4, -1], "mode": "constant"},
+        {"img": np.zeros((3, 8, 8, 4))},
+        (3, 15, 4, 4),
+    ],
+    [
+        {"keys": "img", "spatial_size": [15, 4, -1], "mode": "reflect"},
+        {"img": np.zeros((3, 8, 8, 4))},
+        (3, 15, 4, 4),
+    ],
+    [
+        {"keys": "img", "spatial_size": [-1, -1, -1], "mode": "reflect"},
+        {"img": np.zeros((3, 8, 8, 4))},
+        (3, 8, 8, 4),
+    ],
 ]
 
 
 class TestResizeWithPadOrCropd(unittest.TestCase):
-    @parameterized.expand([TEST_CASE_1, TEST_CASE_2, TEST_CASE_3, TEST_CASE_4])
+    @parameterized.expand(TEST_CASES)
     def test_pad_shape(self, input_param, input_data, expected_val):
         paddcroper = ResizeWithPadOrCropd(**input_param)
         result = paddcroper(input_data)
-        np.testing.assert_allclose(result["img"].shape, expected_val.shape)
+        np.testing.assert_allclose(result["img"].shape, expected_val)
 
 
 if __name__ == "__main__":
diff --git a/tests/test_spatial_crop.py b/tests/test_spatial_crop.py
--- a/tests/test_spatial_crop.py
+++ b/tests/test_spatial_crop.py
@@ -16,26 +16,36 @@
 
 from monai.transforms import SpatialCrop
 
-TEST_CASE_1 = [
-    {"roi_center": [1, 1, 1], "roi_size": [2, 2, 2]},
-    np.random.randint(0, 2, size=[3, 3, 3, 3]),
-    (3, 2, 2, 2),
-]
-
-TEST_CASE_2 = [{"roi_start": [0, 0, 0], "roi_end": [2, 2, 2]}, np.random.randint(0, 2, size=[3, 3, 3, 3]), (3, 2, 2, 2)]
-
-TEST_CASE_3 = [{"roi_start": [0, 0], "roi_end": [2, 2]}, np.random.randint(0, 2, size=[3, 3, 3, 3]), (3, 2, 2, 3)]
-
-TEST_CASE_4 = [
-    {"roi_start": [0, 0, 0, 0, 0], "roi_end": [2, 2, 2, 2, 2]},
-    np.random.randint(0, 2, size=[3, 3, 3, 3]),
-    (3, 2, 2, 2),
+TEST_CASES = [
+    [
+        {"roi_center": [1, 1, 1], "roi_size": [2, 2, 2]},
+        (3, 3, 3, 3),
+        (3, 2, 2, 2),
+    ],
+    [{"roi_start": [0, 0, 0], "roi_end": [2, 2, 2]}, (3, 3, 3, 3), (3, 2, 2, 2)],
+    [{"roi_start": [0, 0], "roi_end": [2, 2]}, (3, 3, 3, 3), (3, 2, 2, 3)],
+    [
+        {"roi_start": [0, 0, 0, 0, 0], "roi_end": [2, 2, 2, 2, 2]},
+        (3, 3, 3, 3),
+        (3, 2, 2, 2),
+    ],
+    [
+        {"roi_start": [0, 0, 0, 0, 0], "roi_end": [8, 8, 8, 2, 2]},
+        (3, 3, 3, 3),
+        (3, 3, 3, 3),
+    ],
+    [
+        {"roi_start": [1, 0, 0], "roi_end": [1, 8, 8]},
+        (3, 3, 3, 3),
+        (3, 0, 3, 3),
+    ],
 ]
 
 
 class TestSpatialCrop(unittest.TestCase):
-    @parameterized.expand([TEST_CASE_1, TEST_CASE_2, TEST_CASE_3, TEST_CASE_4])
-    def test_shape(self, input_param, input_data, expected_shape):
+    @parameterized.expand(TEST_CASES)
+    def test_shape(self, input_param, input_shape, expected_shape):
+        input_data = np.random.randint(0, 2, size=input_shape)
         result = SpatialCrop(**input_param)(input_data)
         self.assertTupleEqual(result.shape, expected_shape)