Repository: Project-MONAI/MONAI

Issue:
Augmentation Transform | InCut & OutCut Dropping (ETA 08/27/21)
Dropping off randomly sized patches from inside the image and out the image where apart from them everything else gets dropped out.



Discussion:

Patch:
diff --git a/monai/transforms/intensity/array.py b/monai/transforms/intensity/array.py
--- a/monai/transforms/intensity/array.py
+++ b/monai/transforms/intensity/array.py
@@ -1633,8 +1633,9 @@ def _set_default_range(self, img: torch.Tensor) -> Sequence[Sequence[float]]:
 class RandCoarseDropout(RandomizableTransform):
     """
     Randomly coarse dropout regions in the image, then fill in the rectangular regions with specified value.
-    Refer to: https://arxiv.org/abs/1708.04552 and:
-    https://albumentations.ai/docs/api_reference/augmentations/transforms/
+    Or keep the rectangular regions and fill in the other areas with specified value.
+    Refer to papers: https://arxiv.org/abs/1708.04552, https://arxiv.org/pdf/1604.07379
+    And other implementation: https://albumentations.ai/docs/api_reference/augmentations/transforms/
     #albumentations.augmentations.transforms.CoarseDropout.
 
     Args:
@@ -1645,6 +1646,8 @@ class RandCoarseDropout(RandomizableTransform):
             if some components of the `spatial_size` are non-positive values, the transform will use the
             corresponding components of input img size. For example, `spatial_size=(32, -1)` will be adapted
             to `(32, 64)` if the second spatial dimension size of img is `64`.
+        dropout_holes: if `True`, dropout the regions of holes and fill value, if `False`, keep the holes and
+            dropout the outside and fill value. default to `True`.
         fill_value: target value to fill the dropout regions, if providing a number, will use it as constant
             value to fill all the regions. if providing a tuple for the `min` and `max`, will randomly select
             value for every pixel / voxel from the range `[min, max)`. if None, will compute the `min` and `max`
@@ -1662,6 +1665,7 @@ def __init__(
         self,
         holes: int,
         spatial_size: Union[Sequence[int], int],
+        dropout_holes: bool = True,
         fill_value: Optional[Union[Tuple[float, float], float]] = None,
         max_holes: Optional[int] = None,
         max_spatial_size: Optional[Union[Sequence[int], int]] = None,
@@ -1672,6 +1676,10 @@ def __init__(
             raise ValueError("number of holes must be greater than 0.")
         self.holes = holes
         self.spatial_size = spatial_size
+        self.dropout_holes = dropout_holes
+        if isinstance(fill_value, (tuple, list)):
+            if len(fill_value) != 2:
+                raise ValueError("fill value should contain 2 numbers if providing the `min` and `max`.")
         self.fill_value = fill_value
         self.max_holes = max_holes
         self.max_spatial_size = max_spatial_size
@@ -1692,16 +1700,23 @@ def randomize(self, img_size: Sequence[int]) -> None:
     def __call__(self, img: np.ndarray):
         self.randomize(img.shape[1:])
         if self._do_transform:
-            for h in self.hole_coords:
-                fill_value = (img.min(), img.max()) if self.fill_value is None else self.fill_value
+            fill_value = (img.min(), img.max()) if self.fill_value is None else self.fill_value
+
+            if self.dropout_holes:
+                for h in self.hole_coords:
+                    if isinstance(fill_value, (tuple, list)):
+                        img[h] = self.R.uniform(fill_value[0], fill_value[1], size=img[h].shape)
+                    else:
+                        img[h] = fill_value
+                return img
+            else:
                 if isinstance(fill_value, (tuple, list)):
-                    if len(fill_value) != 2:
-                        raise ValueError("fill_value should contain 2 numbers if providing the `min` and `max`.")
-                    img[h] = self.R.uniform(fill_value[0], fill_value[1], size=img[h].shape)
+                    ret = self.R.uniform(fill_value[0], fill_value[1], size=img.shape).astype(img.dtype)
                 else:
-                    img[h] = fill_value
-
-        return img
+                    ret = np.full_like(img, fill_value)
+                for h in self.hole_coords:
+                    ret[h] = img[h]
+                return ret
 
 
 class HistogramNormalize(Transform):
diff --git a/monai/transforms/intensity/dictionary.py b/monai/transforms/intensity/dictionary.py
--- a/monai/transforms/intensity/dictionary.py
+++ b/monai/transforms/intensity/dictionary.py
@@ -23,7 +23,6 @@
 
 from monai.config import DtypeLike, KeysCollection, NdarrayTensor
 from monai.config.type_definitions import NdarrayOrTensor
-from monai.data.utils import get_random_patch, get_valid_patch_size
 from monai.transforms.intensity.array import (
     AdjustContrast,
     GaussianSharpen,
@@ -34,6 +33,7 @@
     MaskIntensity,
     NormalizeIntensity,
     RandBiasField,
+    RandCoarseDropout,
     RandGaussianNoise,
     RandKSpaceSpikeNoise,
     RandRicianNoise,
@@ -44,9 +44,9 @@
     StdShiftIntensity,
     ThresholdIntensity,
 )
-from monai.transforms.transform import MapTransform, RandomizableTransform
+from monai.transforms.transform import MapTransform, Randomizable, RandomizableTransform
 from monai.transforms.utils import is_positive
-from monai.utils import convert_to_dst_type, ensure_tuple, ensure_tuple_rep, ensure_tuple_size, fall_back_tuple
+from monai.utils import convert_to_dst_type, ensure_tuple, ensure_tuple_rep, ensure_tuple_size
 
 __all__ = [
     "RandGaussianNoised",
@@ -1423,7 +1423,7 @@ def _to_numpy(self, d: Union[torch.Tensor, np.ndarray]) -> np.ndarray:
         return d_numpy
 
 
-class RandCoarseDropoutd(RandomizableTransform, MapTransform):
+class RandCoarseDropoutd(Randomizable, MapTransform):
     """
     Dictionary-based wrapper of :py:class:`monai.transforms.RandCoarseDropout`.
     Expect all the data specified by `keys` have same spatial shape and will randomly dropout the same regions
@@ -1439,6 +1439,8 @@ class RandCoarseDropoutd(RandomizableTransform, MapTransform):
             if some components of the `spatial_size` are non-positive values, the transform will use the
             corresponding components of input img size. For example, `spatial_size=(32, -1)` will be adapted
             to `(32, 64)` if the second spatial dimension size of img is `64`.
+        dropout_holes: if `True`, dropout the regions of holes and fill value, if `False`, keep the holes and
+            dropout the outside and fill value. default to `True`.
         fill_value: target value to fill the dropout regions, if providing a number, will use it as constant
             value to fill all the regions. if providing a tuple for the `min` and `max`, will randomly select
             value for every pixel / voxel from the range `[min, max)`. if None, will compute the `min` and `max`
@@ -1458,6 +1460,7 @@ def __init__(
         keys: KeysCollection,
         holes: int,
         spatial_size: Union[Sequence[int], int],
+        dropout_holes: bool = True,
         fill_value: Optional[Union[Tuple[float, float], float]] = None,
         max_holes: Optional[int] = None,
         max_spatial_size: Optional[Union[Sequence[int], int]] = None,
@@ -1465,42 +1468,27 @@ def __init__(
         allow_missing_keys: bool = False,
     ):
         MapTransform.__init__(self, keys, allow_missing_keys)
-        RandomizableTransform.__init__(self, prob)
-        if holes < 1:
-            raise ValueError("number of holes must be greater than 0.")
-        self.holes = holes
-        self.spatial_size = spatial_size
-        self.fill_value = fill_value
-        self.max_holes = max_holes
-        self.max_spatial_size = max_spatial_size
-        self.hole_coords: List = []
+        self.dropper = RandCoarseDropout(
+            holes=holes,
+            spatial_size=spatial_size,
+            dropout_holes=dropout_holes,
+            fill_value=fill_value,
+            max_holes=max_holes,
+            max_spatial_size=max_spatial_size,
+            prob=prob,
+        )
 
     def randomize(self, img_size: Sequence[int]) -> None:
-        super().randomize(None)
-        size = fall_back_tuple(self.spatial_size, img_size)
-        self.hole_coords = []  # clear previously computed coords
-        num_holes = self.holes if self.max_holes is None else self.R.randint(self.holes, self.max_holes + 1)
-        for _ in range(num_holes):
-            if self.max_spatial_size is not None:
-                max_size = fall_back_tuple(self.max_spatial_size, img_size)
-                size = tuple(self.R.randint(low=size[i], high=max_size[i] + 1) for i in range(len(img_size)))
-            valid_size = get_valid_patch_size(img_size, size)
-            self.hole_coords.append((slice(None),) + get_random_patch(img_size, valid_size, self.R))
+        self.dropper.randomize(img_size=img_size)
 
     def __call__(self, data):
         d = dict(data)
         # expect all the specified keys have same spatial shape
         self.randomize(d[self.keys[0]].shape[1:])
-        if self._do_transform:
+        if self.dropper._do_transform:
             for key in self.key_iterator(d):
-                for h in self.hole_coords:
-                    fill_value = (d[key].min(), d[key].max()) if self.fill_value is None else self.fill_value
-                    if isinstance(fill_value, (tuple, list)):
-                        if len(fill_value) != 2:
-                            raise ValueError("fill_value should contain 2 numbers if providing the `min` and `max`.")
-                        d[key][h] = self.R.uniform(fill_value[0], fill_value[1], size=d[key][h].shape)
-                    else:
-                        d[key][h] = fill_value
+                d[key] = self.dropper(img=d[key])
+
         return d
 
 


Test patch:
diff --git a/tests/test_rand_coarse_dropout.py b/tests/test_rand_coarse_dropout.py
--- a/tests/test_rand_coarse_dropout.py
+++ b/tests/test_rand_coarse_dropout.py
@@ -47,9 +47,14 @@
     np.random.randint(0, 2, size=[3, 3, 3, 4]),
 ]
 
+TEST_CASE_6 = [
+    {"holes": 2, "spatial_size": [2, 2, 2], "dropout_holes": False, "fill_value": (3, 6), "prob": 1.0},
+    np.random.randint(0, 2, size=[3, 3, 3, 4]),
+]
+
 
 class TestRandCoarseDropout(unittest.TestCase):
-    @parameterized.expand([TEST_CASE_0, TEST_CASE_1, TEST_CASE_2, TEST_CASE_3, TEST_CASE_4, TEST_CASE_5])
+    @parameterized.expand([TEST_CASE_0, TEST_CASE_1, TEST_CASE_2, TEST_CASE_3, TEST_CASE_4, TEST_CASE_5, TEST_CASE_6])
     def test_value(self, input_param, input_data):
         dropout = RandCoarseDropout(**input_param)
         result = dropout(input_data)
@@ -66,15 +71,19 @@ def test_value(self, input_param, input_data):
 
         for h in dropout.hole_coords:
             data = result[h]
-            fill_value = input_param.get("fill_value", None)
-            if isinstance(fill_value, (int, float)):
-                np.testing.assert_allclose(data, fill_value)
-            elif fill_value is not None:
-                min_value = data.min()
-                max_value = data.max()
-                self.assertGreaterEqual(max_value, min_value)
-                self.assertGreaterEqual(min_value, fill_value[0])
-                self.assertLess(max_value, fill_value[1])
+            # test hole value
+            if input_param.get("dropout_holes", True):
+                fill_value = input_param.get("fill_value", None)
+                if isinstance(fill_value, (int, float)):
+                    np.testing.assert_allclose(data, fill_value)
+                elif fill_value is not None:
+                    min_value = data.min()
+                    max_value = data.max()
+                    self.assertGreaterEqual(max_value, min_value)
+                    self.assertGreaterEqual(min_value, fill_value[0])
+                    self.assertLess(max_value, fill_value[1])
+            else:
+                np.testing.assert_allclose(data, input_data[h])
 
             if max_spatial_size is None:
                 self.assertTupleEqual(data.shape[1:], tuple(spatial_size))
diff --git a/tests/test_rand_coarse_dropoutd.py b/tests/test_rand_coarse_dropoutd.py
--- a/tests/test_rand_coarse_dropoutd.py
+++ b/tests/test_rand_coarse_dropoutd.py
@@ -61,9 +61,14 @@
     {"img": np.random.rand(3, 3, 3, 4)},
 ]
 
+TEST_CASE_6 = [
+    {"keys": "img", "holes": 2, "spatial_size": [2, 2, 2], "dropout_holes": False, "fill_value": 0.5, "prob": 1.0},
+    {"img": np.random.rand(3, 3, 3, 4)},
+]
+
 
 class TestRandCoarseDropoutd(unittest.TestCase):
-    @parameterized.expand([TEST_CASE_0, TEST_CASE_1, TEST_CASE_2, TEST_CASE_3, TEST_CASE_4])
+    @parameterized.expand([TEST_CASE_0, TEST_CASE_1, TEST_CASE_2, TEST_CASE_3, TEST_CASE_4, TEST_CASE_5, TEST_CASE_6])
     def test_value(self, input_param, input_data):
         dropout = RandCoarseDropoutd(**input_param)
         result = dropout(input_data)["img"]
@@ -73,22 +78,26 @@ def test_value(self, input_param, input_data):
         max_spatial_size = fall_back_tuple(input_param.get("max_spatial_size"), input_data["img"].shape[1:])
 
         if max_holes is None:
-            self.assertEqual(len(dropout.hole_coords), holes)
+            self.assertEqual(len(dropout.dropper.hole_coords), holes)
         else:
-            self.assertGreaterEqual(len(dropout.hole_coords), holes)
-            self.assertLessEqual(len(dropout.hole_coords), max_holes)
+            self.assertGreaterEqual(len(dropout.dropper.hole_coords), holes)
+            self.assertLessEqual(len(dropout.dropper.hole_coords), max_holes)
 
-        for h in dropout.hole_coords:
+        for h in dropout.dropper.hole_coords:
             data = result[h]
-            fill_value = input_param.get("fill_value", 0)
-            if isinstance(fill_value, (int, float)):
-                np.testing.assert_allclose(data, fill_value)
-            elif fill_value is not None:
-                min_value = data.min()
-                max_value = data.max()
-                self.assertGreaterEqual(max_value, min_value)
-                self.assertGreaterEqual(min_value, fill_value[0])
-                self.assertLess(max_value, fill_value[1])
+            # test hole value
+            if input_param.get("dropout_holes", True):
+                fill_value = input_param.get("fill_value", 0)
+                if isinstance(fill_value, (int, float)):
+                    np.testing.assert_allclose(data, fill_value)
+                elif fill_value is not None:
+                    min_value = data.min()
+                    max_value = data.max()
+                    self.assertGreaterEqual(max_value, min_value)
+                    self.assertGreaterEqual(min_value, fill_value[0])
+                    self.assertLess(max_value, fill_value[1])
+            else:
+                np.testing.assert_allclose(data, input_data["img"][h])
 
             if max_spatial_size is None:
                 self.assertTupleEqual(data.shape[1:], tuple(spatial_size))