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
@@ -1643,7 +1643,10 @@ 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`.
-        fill_value: target value to fill the dropout regions.
+        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`
+            value of input image then randomly select value to fill, default to None.
         max_holes: if not None, define the maximum number to randomly select the expected number of regions.
         max_spatial_size: if not None, define the maximum spatial size to randomly select size for every region.
             if some components of the `max_spatial_size` are non-positive values, the transform will use the
@@ -1657,7 +1660,7 @@ def __init__(
         self,
         holes: int,
         spatial_size: Union[Sequence[int], int],
-        fill_value: Union[float, int] = 0,
+        fill_value: Optional[Union[Tuple[float, float], float]] = None,
         max_holes: Optional[int] = None,
         max_spatial_size: Optional[Union[Sequence[int], int]] = None,
         prob: float = 0.1,
@@ -1688,7 +1691,13 @@ def __call__(self, img: np.ndarray):
         self.randomize(img.shape[1:])
         if self._do_transform:
             for h in self.hole_coords:
-                img[h] = self.fill_value
+                fill_value = (img.min(), img.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`.")
+                    img[h] = self.R.uniform(fill_value[0], fill_value[1], size=img[h].shape)
+                else:
+                    img[h] = fill_value
 
         return img
 
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
@@ -1435,7 +1435,10 @@ 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`.
-        fill_value: target value to fill the dropout regions.
+        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`
+            value of input image then randomly select value to fill, default to None.
         max_holes: if not None, define the maximum number to randomly select the expected number of regions.
         max_spatial_size: if not None, define the maximum spatial size to randomly select size for every region.
             if some components of the `max_spatial_size` are non-positive values, the transform will use the
@@ -1451,7 +1454,7 @@ def __init__(
         keys: KeysCollection,
         holes: int,
         spatial_size: Union[Sequence[int], int],
-        fill_value: Union[float, int] = 0,
+        fill_value: Optional[Union[Tuple[float, float], float]] = None,
         max_holes: Optional[int] = None,
         max_spatial_size: Optional[Union[Sequence[int], int]] = None,
         prob: float = 0.1,
@@ -1487,7 +1490,13 @@ def __call__(self, data):
         if self._do_transform:
             for key in self.key_iterator(d):
                 for h in self.hole_coords:
-                    d[key][h] = self.fill_value
+                    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
         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
@@ -20,31 +20,37 @@
 TEST_CASE_0 = [
     {"holes": 2, "spatial_size": [2, 2, 2], "fill_value": 5, "prob": 1.0},
     np.random.randint(0, 2, size=[3, 3, 3, 4]),
-    (3, 3, 3, 4),
 ]
 
 TEST_CASE_1 = [
     {"holes": 1, "spatial_size": [1, 2, 3], "fill_value": 5, "max_holes": 5, "prob": 1.0},
     np.random.randint(0, 2, size=[3, 3, 3, 4]),
-    (3, 3, 3, 4),
 ]
 
 TEST_CASE_2 = [
     {"holes": 2, "spatial_size": [2, 2, 2], "fill_value": 5, "max_spatial_size": [4, 4, 3], "prob": 1.0},
     np.random.randint(0, 2, size=[3, 3, 3, 4]),
-    (3, 3, 3, 4),
 ]
 
 TEST_CASE_3 = [
     {"holes": 2, "spatial_size": [2, -1, 2], "fill_value": 5, "max_spatial_size": [4, 4, -1], "prob": 1.0},
     np.random.randint(0, 2, size=[3, 3, 3, 4]),
-    (3, 3, 3, 4),
+]
+
+TEST_CASE_4 = [
+    {"holes": 2, "spatial_size": [2, 2, 2], "fill_value": (3, 6), "prob": 1.0},
+    np.random.randint(0, 2, size=[3, 3, 3, 4]),
+]
+
+TEST_CASE_5 = [
+    {"holes": 2, "spatial_size": [2, 2, 2], "fill_value": None, "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])
-    def test_value(self, input_param, input_data, expected_shape):
+    @parameterized.expand([TEST_CASE_0, TEST_CASE_1, TEST_CASE_2, TEST_CASE_3, TEST_CASE_4, TEST_CASE_5])
+    def test_value(self, input_param, input_data):
         dropout = RandCoarseDropout(**input_param)
         result = dropout(input_data)
         holes = input_param.get("holes")
@@ -60,7 +66,16 @@ def test_value(self, input_param, input_data, expected_shape):
 
         for h in dropout.hole_coords:
             data = result[h]
-            np.testing.assert_allclose(data, input_param.get("fill_value", 0))
+            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])
+
             if max_spatial_size is None:
                 self.assertTupleEqual(data.shape[1:], tuple(spatial_size))
             else:
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
@@ -20,13 +20,11 @@
 TEST_CASE_0 = [
     {"keys": "img", "holes": 2, "spatial_size": [2, 2, 2], "fill_value": 5, "prob": 1.0},
     {"img": np.random.randint(0, 2, size=[3, 3, 3, 4])},
-    (3, 3, 3, 4),
 ]
 
 TEST_CASE_1 = [
     {"keys": "img", "holes": 1, "spatial_size": [1, 2, 3], "fill_value": 5, "max_holes": 5, "prob": 1.0},
     {"img": np.random.randint(0, 2, size=[3, 3, 3, 4])},
-    (3, 3, 3, 4),
 ]
 
 TEST_CASE_2 = [
@@ -39,7 +37,6 @@
         "prob": 1.0,
     },
     {"img": np.random.randint(0, 2, size=[3, 3, 3, 4])},
-    (3, 3, 3, 4),
 ]
 
 TEST_CASE_3 = [
@@ -52,13 +49,22 @@
         "prob": 1.0,
     },
     {"img": np.random.randint(0, 2, size=[3, 3, 3, 4])},
-    (3, 3, 3, 4),
+]
+
+TEST_CASE_4 = [
+    {"keys": "img", "holes": 2, "spatial_size": [2, 2, 2], "fill_value": (0.2, 0.6), "prob": 1.0},
+    {"img": np.random.rand(3, 3, 3, 4)},
+]
+
+TEST_CASE_5 = [
+    {"keys": "img", "holes": 2, "spatial_size": [2, 2, 2], "fill_value": None, "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])
-    def test_value(self, input_param, input_data, expected_shape):
+    @parameterized.expand([TEST_CASE_0, TEST_CASE_1, TEST_CASE_2, TEST_CASE_3, TEST_CASE_4])
+    def test_value(self, input_param, input_data):
         dropout = RandCoarseDropoutd(**input_param)
         result = dropout(input_data)["img"]
         holes = input_param.get("holes")
@@ -74,7 +80,16 @@ def test_value(self, input_param, input_data, expected_shape):
 
         for h in dropout.hole_coords:
             data = result[h]
-            np.testing.assert_allclose(data, input_param.get("fill_value", 0))
+            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])
+
             if max_spatial_size is None:
                 self.assertTupleEqual(data.shape[1:], tuple(spatial_size))
             else: