Skip to content
This repository has been archived by the owner on Mar 27, 2023. It is now read-only.

Commit

Permalink
2707 Fill the outside of holes (Project-MONAI#2848)
Browse files Browse the repository at this point in the history
* [DLMED] add support to fill outside

Signed-off-by: Nic Ma <nma@nvidia.com>

* [DLMED] add paper link for cutout

Signed-off-by: Nic Ma <nma@nvidia.com>

Co-authored-by: Wenqi Li <wenqil@nvidia.com>
  • Loading branch information
Nic-Ma and wyli committed Aug 27, 2021
1 parent 0725bce commit 85b92ae
Show file tree
Hide file tree
Showing 4 changed files with 87 additions and 66 deletions.
35 changes: 25 additions & 10 deletions monai/transforms/intensity/array.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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`
Expand All @@ -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,
Expand All @@ -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
Expand All @@ -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):
Expand Down
52 changes: 20 additions & 32 deletions monai/transforms/intensity/dictionary.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -34,6 +33,7 @@
MaskIntensity,
NormalizeIntensity,
RandBiasField,
RandCoarseDropout,
RandGaussianNoise,
RandKSpaceSpikeNoise,
RandRicianNoise,
Expand All @@ -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",
Expand Down Expand Up @@ -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
Expand All @@ -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`
Expand All @@ -1458,49 +1460,35 @@ 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,
prob: float = 0.1,
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


Expand Down
29 changes: 19 additions & 10 deletions tests/test_rand_coarse_dropout.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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))
Expand Down
37 changes: 23 additions & 14 deletions tests/test_rand_coarse_dropoutd.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand All @@ -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))
Expand Down

0 comments on commit 85b92ae

Please sign in to comment.