Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

2707 Fill the outside of holes #2848

Merged
merged 7 commits into from
Aug 27, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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