From 85b92aeac93d3354e05ea35a36d01ba3b8add580 Mon Sep 17 00:00:00 2001 From: Nic Ma Date: Fri, 27 Aug 2021 08:42:16 +0800 Subject: [PATCH] 2707 Fill the outside of holes (#2848) * [DLMED] add support to fill outside Signed-off-by: Nic Ma * [DLMED] add paper link for cutout Signed-off-by: Nic Ma Co-authored-by: Wenqi Li --- monai/transforms/intensity/array.py | 35 +++++++++++----- monai/transforms/intensity/dictionary.py | 52 +++++++++--------------- tests/test_rand_coarse_dropout.py | 29 ++++++++----- tests/test_rand_coarse_dropoutd.py | 37 ++++++++++------- 4 files changed, 87 insertions(+), 66 deletions(-) diff --git a/monai/transforms/intensity/array.py b/monai/transforms/intensity/array.py index ddf79bdbd69..5268794c7de 100644 --- 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 index 23f72c56771..bc53fb6b7b7 100644 --- 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 diff --git a/tests/test_rand_coarse_dropout.py b/tests/test_rand_coarse_dropout.py index 18d026e5735..830832c2a5e 100644 --- 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 index 932e65c8cfc..fc898a9fcad 100644 --- 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))