Repository: Project-MONAI/MONAI

Issue:
Add BorderPad transform
**Is your feature request related to a problem? Please describe.**
No matter what the input data size is, pad an expected border to it.



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
@@ -24,9 +24,10 @@
 
 
 class SpatialPad(Transform):
-    """Performs padding to the data, symmetric for all sides or all on one side for each dimension.
-     Uses np.pad so in practice, a mode needs to be provided. See numpy.lib.arraypad.pad
-     for additional details.
+    """
+    Performs padding to the data, symmetric for all sides or all on one side for each dimension.
+    Uses np.pad so in practice, a mode needs to be provided. See numpy.lib.arraypad.pad
+    for additional details.
 
     Args:
         spatial_size (sequence of int): the spatial size of output data after padding.
@@ -64,6 +65,49 @@ def __call__(self, img, mode: Optional[str] = None):
             return img
 
 
+class BorderPad(Transform):
+    """
+    Pad the input data by adding specified borders to every dimension.
+
+    Args:
+        spatial_border (int or sequence of int): specified size for every spatial border. it can be 3 shapes:
+            - single int number, pad all the borders with the same size.
+            - length equals the length of image shape, pad every spatial dimension separately.
+                for example, image shape(CHW) is [1, 4, 4], spatial_border is [2, 1],
+                pad every border of H dim with 2, pad every border of W dim with 1, result shape is [1, 8, 6].
+            - length equals 2 x (length of image shape), pad every border of every dimension separately.
+                for example, image shape(CHW) is [1, 4, 4], spatial_border is [1, 2, 3, 4], pad top of H dim with 1,
+                pad bottom of H dim with 2, pad left of W dim with 3, pad right of W dim with 4.
+                the result shape is [1, 7, 11].
+        mode: one of the following string values or a user supplied function: {'constant', 'edge', 'linear_ramp',
+            'maximum', 'mean', 'median', 'minimum', 'reflect', 'symmetric', 'wrap', 'empty', <function>}
+            for more details, please check: https://docs.scipy.org/doc/numpy/reference/generated/numpy.pad.html
+
+    """
+
+    def __init__(self, spatial_border, mode: str = "constant"):
+        self.spatial_border = spatial_border
+        self.mode = mode
+
+    def __call__(self, img, mode: Optional[str] = None):
+        spatial_shape = img.shape[1:]
+        spatial_border = ensure_tuple(self.spatial_border)
+        for b in spatial_border:
+            if b < 0 or not isinstance(b, int):
+                raise ValueError("spatial_border must be int number and can not be less than 0.")
+
+        if len(spatial_border) == 1:
+            data_pad_width = [(spatial_border[0], spatial_border[0]) for _ in range(len(spatial_shape))]
+        elif len(spatial_border) == len(spatial_shape):
+            data_pad_width = [(spatial_border[i], spatial_border[i]) for i in range(len(spatial_shape))]
+        elif len(spatial_border) == len(spatial_shape) * 2:
+            data_pad_width = [(spatial_border[2 * i], spatial_border[2 * i + 1]) for i in range(len(spatial_shape))]
+        else:
+            raise ValueError("unsupported length of spatial_border definition.")
+
+        return np.pad(img, [(0, 0)] + data_pad_width, mode=mode or self.mode)
+
+
 class DivisiblePad(Transform):
     """
     Pad the input data, so that the spatial sizes are divisible by `k`.
diff --git a/monai/transforms/croppad/dictionary.py b/monai/transforms/croppad/dictionary.py
--- a/monai/transforms/croppad/dictionary.py
+++ b/monai/transforms/croppad/dictionary.py
@@ -20,7 +20,7 @@
 from monai.config.type_definitions import IndexSelection, KeysCollection
 from monai.data.utils import get_random_patch, get_valid_patch_size
 from monai.transforms.compose import MapTransform, Randomizable
-from monai.transforms.croppad.array import CenterSpatialCrop, DivisiblePad, SpatialCrop, SpatialPad
+from monai.transforms.croppad.array import CenterSpatialCrop, DivisiblePad, SpatialCrop, SpatialPad, BorderPad
 from monai.transforms.utils import generate_pos_neg_label_crop_centers, generate_spatial_bounding_box
 from monai.utils.misc import ensure_tuple, ensure_tuple_rep
 
@@ -54,10 +54,45 @@ def __call__(self, data):
         return d
 
 
+class BorderPadd(MapTransform):
+    """
+    Pad the input data by adding specified borders to every dimension.
+    Dictionary-based wrapper of :py:class:`monai.transforms.BorderPad`.
+    """
+
+    def __init__(self, keys: KeysCollection, spatial_border, mode="constant"):
+        """
+        Args:
+            spatial_border (int or sequence of int): specified size for every spatial border. it can be 3 shapes:
+                - single int number, pad all the borders with the same size.
+                - length equals the length of image shape, pad every spatial dimension separately.
+                    for example, image shape(CHW) is [1, 4, 4], spatial_border is [2, 1],
+                    pad every border of H dim with 2, pad every border of W dim with 1, result shape is [1, 8, 6].
+                - length equals 2 x (length of image shape), pad every border of every dimension separately.
+                    for example, image shape(CHW) is [1, 4, 4], spatial_border is [1, 2, 3, 4], pad top of H dim with 1,
+                    pad bottom of H dim with 2, pad left of W dim with 3, pad right of W dim with 4.
+                    the result shape is [1, 7, 11].
+            mode (str or sequence of str): one of the following string values or a user supplied function:
+                {'constant', 'edge', 'linear_ramp', 'maximum', 'mean', 'median', 'minimum', 'reflect',
+                'symmetric', 'wrap', 'empty', <function>}
+                user can set a sequence of mode values corresponding to every item specified by `keys`.
+                for more details, please check: https://docs.scipy.org/doc/numpy/reference/generated/numpy.pad.html
+
+        """
+        super().__init__(keys)
+        self.mode = ensure_tuple_rep(mode, len(self.keys))
+        self.padder = BorderPad(spatial_border=spatial_border)
+
+    def __call__(self, data):
+        d = dict(data)
+        for key, m in zip(self.keys, self.mode):
+            d[key] = self.padder(d[key], mode=m)
+        return d
+
+
 class DivisiblePadd(MapTransform):
     """
     Pad the input data, so that the spatial sizes are divisible by `k`.
-
     Dictionary-based wrapper of :py:class: `monai.transforms.DivisiblePad`.
     """
 
@@ -296,6 +331,7 @@ def __call__(self, data):
 
 
 SpatialPadD = SpatialPadDict = SpatialPadd
+BorderPadD = BorderPadDict = BorderPadd
 DivisiblePadD = DivisiblePadDict = DivisiblePadd
 SpatialCropD = SpatialCropDict = SpatialCropd
 CenterSpatialCropD = CenterSpatialCropDict = CenterSpatialCropd


Test patch:
diff --git a/tests/test_border_pad.py b/tests/test_border_pad.py
new file mode 100644
--- /dev/null
+++ b/tests/test_border_pad.py
@@ -0,0 +1,47 @@
+# Copyright 2020 MONAI Consortium
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#     http://www.apache.org/licenses/LICENSE-2.0
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import unittest
+import numpy as np
+from parameterized import parameterized
+from monai.transforms import BorderPad
+
+TEST_CASE_1 = [
+    {"spatial_border": 2, "mode": "constant"},
+    np.zeros((3, 8, 8, 4)),
+    np.zeros((3, 12, 12, 8)),
+]
+
+TEST_CASE_2 = [
+    {"spatial_border": [1, 2, 3], "mode": "constant"},
+    np.zeros((3, 8, 8, 4)),
+    np.zeros((3, 10, 12, 10)),
+]
+
+TEST_CASE_3 = [
+    {"spatial_border": [1, 2, 3, 4, 5, 6], "mode": "constant"},
+    np.zeros((3, 8, 8, 4)),
+    np.zeros((3, 11, 15, 15)),
+]
+
+
+class TestBorderPad(unittest.TestCase):
+    @parameterized.expand([TEST_CASE_1, TEST_CASE_2, TEST_CASE_3])
+    def test_pad_shape(self, input_param, input_data, expected_val):
+        padder = BorderPad(**input_param)
+        result = padder(input_data)
+        self.assertAlmostEqual(result.shape, expected_val.shape)
+        result = padder(input_data, mode=input_param["mode"])
+        self.assertAlmostEqual(result.shape, expected_val.shape)
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/tests/test_border_padd.py b/tests/test_border_padd.py
new file mode 100644
--- /dev/null
+++ b/tests/test_border_padd.py
@@ -0,0 +1,45 @@
+# Copyright 2020 MONAI Consortium
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#     http://www.apache.org/licenses/LICENSE-2.0
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import unittest
+import numpy as np
+from parameterized import parameterized
+from monai.transforms import BorderPadd
+
+TEST_CASE_1 = [
+    {"keys": ["img", "seg"], "spatial_border": 2, "mode": ["constant", "edge"]},
+    {"img": np.zeros((3, 8, 8, 4)), "seg": np.zeros((3, 8, 8, 4))},
+    np.zeros((3, 12, 12, 8)),
+]
+
+TEST_CASE_2 = [
+    {"keys": "img", "spatial_border": [1, 2, 3], "mode": "constant"},
+    {"img": np.zeros((3, 8, 8, 4))},
+    np.zeros((3, 10, 12, 10)),
+]
+
+TEST_CASE_3 = [
+    {"keys": "img", "spatial_border": [1, 2, 3, 4, 5, 6], "mode": "constant"},
+    {"img": np.zeros((3, 8, 8, 4))},
+    np.zeros((3, 11, 15, 15)),
+]
+
+
+class TestBorderPadd(unittest.TestCase):
+    @parameterized.expand([TEST_CASE_1, TEST_CASE_2, TEST_CASE_3])
+    def test_pad_shape(self, input_param, input_data, expected_val):
+        padder = BorderPadd(**input_param)
+        result = padder(input_data)
+        self.assertAlmostEqual(result["img"].shape, expected_val.shape)
+
+
+if __name__ == "__main__":
+    unittest.main()