From b8fe42656ea363aa8a6c0b055c17b45f5d5aaf9e Mon Sep 17 00:00:00 2001 From: Eric Kerfoot Date: Wed, 3 Nov 2021 15:21:56 +0000 Subject: [PATCH 1/9] Adding smooth field transforms Signed-off-by: Eric Kerfoot --- monai/transforms/__init__.py | 2 + monai/transforms/smooth_field/__init__.py | 10 + monai/transforms/smooth_field/array.py | 220 ++++++++++++++++++++ monai/transforms/smooth_field/dictionary.py | 146 +++++++++++++ tests/test_smooth_field.py | 72 +++++++ 5 files changed, 450 insertions(+) create mode 100644 monai/transforms/smooth_field/__init__.py create mode 100644 monai/transforms/smooth_field/array.py create mode 100644 monai/transforms/smooth_field/dictionary.py create mode 100644 tests/test_smooth_field.py diff --git a/monai/transforms/__init__.py b/monai/transforms/__init__.py index 1223254db5..2052e9e97b 100644 --- a/monai/transforms/__init__.py +++ b/monai/transforms/__init__.py @@ -274,6 +274,8 @@ VoteEnsembled, VoteEnsembleDict, ) +from .smooth_field.array import RandSmoothFieldAdjustContrast, RandSmoothFieldAdjustIntensity, SmoothField +from .smooth_field.dictionary import RandSmoothFieldAdjustContrastd, RandSmoothFieldAdjustIntensityd from .spatial.array import ( AddCoordinateChannels, Affine, diff --git a/monai/transforms/smooth_field/__init__.py b/monai/transforms/smooth_field/__init__.py new file mode 100644 index 0000000000..14ae193634 --- /dev/null +++ b/monai/transforms/smooth_field/__init__.py @@ -0,0 +1,10 @@ +# Copyright 2020 - 2021 MONAI Consortium +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/monai/transforms/smooth_field/array.py b/monai/transforms/smooth_field/array.py new file mode 100644 index 0000000000..c45f97b9c3 --- /dev/null +++ b/monai/transforms/smooth_field/array.py @@ -0,0 +1,220 @@ +# Copyright 2020 - 2021 MONAI Consortium +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Transforms using a smooth spatial field generated by interpolating from smaller randomized fields.""" + +from typing import Any, Optional, Sequence, Union + +import numpy as np + +import monai +from monai.transforms.spatial.array import Resize +from monai.transforms.transform import Randomizable, RandomizableTransform, Transform +from monai.transforms.utils import rescale_array +from monai.utils import InterpolateMode, ensure_tuple + +__all__ = [ + "SmoothField", + "SmoothFieldAdjustContrast", + "RandSmoothFieldAdjustContrast", + "RandSmoothFieldAdjustIntensity", +] + + +class SmoothField(Randomizable): + """ + Generate a smooth field array by defining a smaller randomized field and then resizing to the desired size. This + exploits interpolation to create a smoothly varying field used for other applications. + + Args: + spatial_size: final output size of the array + rand_size: size of the randomized field to start from + padder: optional transform to add padding to the randomized field + mode: interpolation mode to use when upsampling + align_corners: if True align the corners when upsampling field + low: low value for randomized field + high: high value for randomized field + channels: number of channels of final output + """ + + def __init__( + self, + spatial_size: Union[Sequence[int], int], + rand_size: Union[Sequence[int], int], + padder: Optional[Transform] = None, + mode: Union[InterpolateMode, str] = InterpolateMode.AREA, + align_corners: Optional[bool] = None, + low: float = -1.0, + high: float = 1.0, + channels: int = 1, + ): + self.resizer = Resize(spatial_size, mode=mode, align_corners=align_corners) + self.rand_size = ensure_tuple(rand_size) + self.padder = padder + self.field = None + self.low = low + self.high = high + self.channels = channels + + def randomize(self, data: Optional[Any] = None) -> None: + self.field = self.R.uniform(self.low, self.high, (self.channels,) + self.rand_size) + if self.padder is not None: + self.field = self.padder(self.field) + + def __call__(self): + resized_field = self.resizer(self.field) + + return rescale_array(resized_field, self.field.min(), self.field.max()) + + +class RandSmoothFieldAdjustContrast(RandomizableTransform): + """ + Randomly adjust the contrast of input images by calculating a randomized smooth field for each invocation. This + uses SmoothFieldAdjustContrast and SmoothField internally. + + Args: + spatial_size: size of input array's spatial dimensions + rand_size: size of the randomized field to start from + padder: optional transform to add padding to the randomized field + mode: interpolation mode to use when upsampling + align_corners: if True align the corners when upsampling field + prob: probability transform is applied + gamma: (min, max) range for exponential field + """ + + def __init__( + self, + spatial_size: Union[Sequence[int], int], + rand_size: Union[Sequence[int], int], + padder: Optional[Transform] = None, + mode: Union[InterpolateMode, str] = InterpolateMode.AREA, + align_corners: Optional[bool] = None, + prob: float = 0.1, + gamma: Union[Sequence[float], float] = (0.5, 4.5), + ): + super().__init__(prob) + + if isinstance(gamma, (int, float)): + self.gamma = (0.5, gamma) + else: + if len(gamma) != 2: + raise ValueError("Argument `gamma` should be a number or pair of numbers.") + + self.gamma = (min(gamma), max(gamma)) + + self.sfield = SmoothField(spatial_size, rand_size, padder, mode, align_corners, self.gamma[0], self.gamma[1]) + + def set_random_state( + self, seed: Optional[int] = None, state: Optional[np.random.RandomState] = None + ) -> "Randomizable": + super().set_random_state(seed, state) + self.sfield.set_random_state(seed, state) + + def randomize(self, data: Optional[Any] = None) -> None: + super().randomize(None) + + if self._do_transform: + self.sfield.randomize() + + def __call__(self, img: np.ndarray, randomize: bool = True): + """ + Apply the transform to `img`, if `randomize` randomizing the smooth field otherwise reusing the previous. + """ + if randomize: + self.randomize() + + if not self._do_transform: + return img + + img_min = img.min() + img_max = img.max() + img_rng = img_max - img_min + + field = self.sfield() + field = np.repeat(field, img.shape[0], 0) + + img = (img - img_min) / max(img_rng, 1e-10) + img = img ** field # contrast is changed by raising image data to a power, in this case the field + + out = (img * img_rng) + img_min + + return out.astype(img.dtype) + + +class RandSmoothFieldAdjustIntensity(RandomizableTransform): + """ + Randomly adjust the intensity of input images by calculating a randomized smooth field for each invocation. This + uses SmoothField internally. + + Args: + spatial_size: size of input array + rand_size: size of the randomized field to start from + padder: optional transform to add padding to the randomized field + mode: interpolation mode to use when upsampling + align_corners: if True align the corners when upsampling field + prob: probability transform is applied + gamma: (min, max) range of intensity multipliers + """ + + def __init__( + self, + spatial_size: Union[Sequence[int], int], + rand_size: Union[Sequence[int], int], + padder: Optional[Transform] = None, + mode: Union[monai.utils.InterpolateMode, str] = monai.utils.InterpolateMode.AREA, + align_corners: Optional[bool] = None, + prob: float = 0.1, + gamma: Union[Sequence[float], float] = (0.1, 1.0), + ): + super().__init__(prob) + + if isinstance(gamma, (int, float)): + self.gamma = (0.5, gamma) + else: + if len(gamma) != 2: + raise ValueError("Argument `gamma` should be a number or pair of numbers.") + + self.gamma = (min(gamma), max(gamma)) + + self.sfield = SmoothField(spatial_size, rand_size, padder, mode, align_corners, self.gamma[0], self.gamma[1]) + + def set_random_state( + self, seed: Optional[int] = None, state: Optional[np.random.RandomState] = None + ) -> "Randomizable": + super().set_random_state(seed, state) + self.sfield.set_random_state(seed, state) + + def randomize(self, data: Optional[Any] = None) -> None: + super().randomize(None) + + if self._do_transform: + self.sfield.randomize() + + def __call__(self, img: np.ndarray, randomize: bool = True): + """ + Apply the transform to `img`, if `randomize` randomizing the smooth field otherwise reusing the previous. + """ + + if randomize: + self.randomize() + + if not self._do_transform: + return img + + img_min = img.min() + img_max = img.max() + + field = self.sfield() + rfield = np.repeat(field, img.shape[0], 0) + + out = img * rfield + + return out.astype(img.dtype) diff --git a/monai/transforms/smooth_field/dictionary.py b/monai/transforms/smooth_field/dictionary.py new file mode 100644 index 0000000000..4212ba5d90 --- /dev/null +++ b/monai/transforms/smooth_field/dictionary.py @@ -0,0 +1,146 @@ +# Copyright 2020 - 2021 MONAI Consortium +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from typing import Any, Dict, Hashable, Mapping, Optional, Sequence, Union + +import numpy as np + +from monai.config import KeysCollection +from monai.transforms.smooth_field.array import RandSmoothFieldAdjustContrast, RandSmoothFieldAdjustIntensity +from monai.transforms.transform import MapTransform, RandomizableTransform, Transform +from monai.utils import InterpolateMode + +__all__ = ["RandSmoothFieldAdjustContrastd", "RandSmoothFieldAdjustIntensityd"] + + +class RandSmoothFieldAdjustContrastd(RandomizableTransform, MapTransform): + """ + Dictionary version of RandSmoothFieldAdjustContrast. The field is randomized once per invocation by default so the + same field is applied to every selected key. + + Args: + keys: key names to apply the augment to + spatial_size: size of input arrays, all arrays stated in `keys` must have same dimensions + rand_size: size of the randomized field to start from + padder: optional transform to add padding to the randomized field + mode: interpolation mode to use when upsampling + align_corners: if True align the corners when upsampling field + prob: probability transform is applied + gamma: (min, max) range for exponential field + apply_same_field: if True, apply the same field to each key, otherwise randomize individually + """ + + def __init__( + self, + keys: KeysCollection, + spatial_size: Union[Sequence[int], int], + rand_size: Union[Sequence[int], int], + padder: Optional[Transform] = None, + mode: Union[InterpolateMode, str] = InterpolateMode.AREA, + align_corners: Optional[bool] = None, + prob: float = 0.1, + gamma: Union[Sequence[float], float] = (0.5, 4.5), + apply_same_field: bool = True, + ): + RandomizableTransform.__init__(self, prob) + MapTransform.__init__(self, keys) + + self.trans = RandSmoothFieldAdjustContrast(spatial_size, rand_size, padder, mode, align_corners, 1.0, gamma) + self.apply_same_field = apply_same_field + + def set_random_state( + self, seed: Optional[int] = None, state: Optional[np.random.RandomState] = None + ) -> "Randomizable": + super().set_random_state(seed, state) + self.trans.set_random_state(seed, state) + + def randomize(self, data: Optional[Any] = None) -> None: + super().randomize(None) + self.trans.randomize() + + def __call__(self, data: Mapping[Hashable, np.ndarray]) -> Dict[Hashable, np.ndarray]: + self.randomize() + + if not self._do_transform: + return data + + d = dict(data) + + for key in self.key_iterator(d): + if not self.apply_same_field: + self.randomize() # new field for every key + + d[key] = self.trans(d[key], False) + + return d + + +class RandSmoothFieldAdjustIntensityd(RandomizableTransform, MapTransform): + """ + Dictionary version of RandSmoothFieldAdjustIntensity. The field is randomized once per invocation by default so + the same field is applied to every selected key. + + Args: + keys: key names to apply the augment to + spatial_size: size of input arrays, all arrays stated in `keys` must have same dimensions + rand_size: size of the randomized field to start from + padder: optional transform to add padding to the randomized field + mode: interpolation mode to use when upsampling + align_corners: if True align the corners when upsampling field + prob: probability transform is applied + gamma: (min, max) range of intensity multipliers + apply_same_field: if True, apply the same field to each key, otherwise randomize individually + """ + + def __init__( + self, + keys: KeysCollection, + spatial_size: Union[Sequence[int], int], + rand_size: Union[Sequence[int], int], + padder: Optional[Transform] = None, + mode: Union[InterpolateMode, str] = InterpolateMode.AREA, + align_corners: Optional[bool] = None, + prob: float = 0.1, + gamma: Union[Sequence[float], float] = (0.1, 1.0), + apply_same_field: bool = True, + ): + RandomizableTransform.__init__(self, prob) + MapTransform.__init__(self, keys) + + self.trans = RandSmoothFieldAdjustIntensity(spatial_size, rand_size, padder, mode, align_corners, 1.0, gamma) + self.apply_same_field = apply_same_field + + def set_random_state( + self, seed: Optional[int] = None, state: Optional[np.random.RandomState] = None + ) -> "Randomizable": + super().set_random_state(seed, state) + self.trans.set_random_state(seed, state) + + def randomize(self, data: Optional[Any] = None) -> None: + super().randomize(None) + self.trans.randomize() + + def __call__(self, data: Mapping[Hashable, np.ndarray]) -> Dict[Hashable, np.ndarray]: + self.randomize() + + if not self._do_transform: + return data + + d = dict(data) + + for key in self.key_iterator(d): + if not self.apply_same_field: + self.randomize() # new field for every key + + d[key] = self.trans(d[key], False) + + return d diff --git a/tests/test_smooth_field.py b/tests/test_smooth_field.py new file mode 100644 index 0000000000..ed81902269 --- /dev/null +++ b/tests/test_smooth_field.py @@ -0,0 +1,72 @@ +# Copyright 2020 - 2021 MONAI Consortium +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest + +import numpy as np +import torch +from parameterized import parameterized + +from monai.transforms import RandSmoothFieldAdjustContrastd, RandSmoothFieldAdjustIntensityd +from tests.utils import assert_allclose, is_tf32_env + +_rtol = 5e-3 if is_tf32_env() else 1e-4 + +INPUT_SHAPE1 = (1, 8, 8) +INPUT_SHAPE2 = (2, 8, 8) + +TESTS_CONTRAST = [ + ( + {"keys": ("test",), "spatial_size": INPUT_SHAPE1[1:], "rand_size": (4, 4), "prob": 1.0}, + {"test": np.ones(INPUT_SHAPE1, np.float32)}, + {"test": np.ones(INPUT_SHAPE1, np.float32)}, + ), + ( + {"keys": ("test",), "spatial_size": INPUT_SHAPE2[1:], "rand_size": (4, 4), "prob": 1.0}, + {"test": np.ones(INPUT_SHAPE2, np.float32)}, + {"test": np.ones(INPUT_SHAPE2, np.float32)}, + ), +] + +TESTS_INTENSITY = [ + ( + {"keys": ("test",), "spatial_size": INPUT_SHAPE1[1:], "rand_size": (4, 4), "prob": 1.0, "gamma": (1, 1)}, + {"test": np.ones(INPUT_SHAPE1, np.float32)}, + {"test": np.ones(INPUT_SHAPE1, np.float32)}, + ), + ( + {"keys": ("test",), "spatial_size": INPUT_SHAPE2[1:], "rand_size": (4, 4), "prob": 1.0, "gamma": (1, 1)}, + {"test": np.ones(INPUT_SHAPE2, np.float32)}, + {"test": np.ones(INPUT_SHAPE2, np.float32)}, + ), +] + + +class TestSmoothField(unittest.TestCase): + @parameterized.expand(TESTS_CONTRAST) + def test_rand_smooth_field_adjust_contrastd(self, input_param, input_data, expected_val): + g = RandSmoothFieldAdjustContrastd(**input_param) + g.set_random_state(123) + + res = g(input_data) + for key, result in res.items(): + expected = expected_val[key] + assert_allclose(result, expected, rtol=_rtol, atol=5e-3) + + @parameterized.expand(TESTS_INTENSITY) + def test_rand_smooth_field_adjust_intensityd(self, input_param, input_data, expected_val): + g = RandSmoothFieldAdjustIntensityd(**input_param) + g.set_random_state(123) + + res = g(input_data) + for key, result in res.items(): + expected = expected_val[key] + assert_allclose(result, expected, rtol=_rtol, atol=5e-3) From 407a324cc5cb0b67b9cbd5df7b0fa091b261dd09 Mon Sep 17 00:00:00 2001 From: Eric Kerfoot Date: Wed, 3 Nov 2021 15:29:37 +0000 Subject: [PATCH 2/9] Adding smooth field transforms Signed-off-by: Eric Kerfoot --- docs/source/transforms.rst | 31 ++++++++++++++++++++++++++ monai/transforms/smooth_field/array.py | 1 - 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/docs/source/transforms.rst b/docs/source/transforms.rst index 804346b290..a2d1a4c077 100644 --- a/docs/source/transforms.rst +++ b/docs/source/transforms.rst @@ -708,6 +708,22 @@ Spatial :members: :special-members: __call__ + +Smooth Field +^^^^^^^^^^^^ + +`RandSmoothFieldAdjustContrast` +""""""""""""""""""""""""""""""" +.. autoclass:: RandSmoothFieldAdjustContrast + :members: + :special-members: __call__ + +`RandSmoothFieldAdjustIntensity` +""""""""""""""""""""""""""""""" +.. autoclass:: RandSmoothFieldAdjustIntensity + :members: + :special-members: __call__ + Utility ^^^^^^^ @@ -1473,6 +1489,21 @@ Spatial (Dict) .. autoclass:: RandGridDistortiond :members: :special-members: __call__ + +Smooth Field (Dict) +^^^^^^^^^^^^^^^^^^^ + +`RandSmoothFieldAdjustContrastd` +"""""""""""""""""""""""""""""""" +.. autoclass:: RandSmoothFieldAdjustContrastd + :members: + :special-members: __call__ + +`RandSmoothFieldAdjustIntensityd` +"""""""""""""""""""""""""""""""" +.. autoclass:: RandSmoothFieldAdjustIntensityd + :members: + :special-members: __call__ Utility (Dict) ^^^^^^^^^^^^^^ diff --git a/monai/transforms/smooth_field/array.py b/monai/transforms/smooth_field/array.py index c45f97b9c3..af0b844f1a 100644 --- a/monai/transforms/smooth_field/array.py +++ b/monai/transforms/smooth_field/array.py @@ -23,7 +23,6 @@ __all__ = [ "SmoothField", - "SmoothFieldAdjustContrast", "RandSmoothFieldAdjustContrast", "RandSmoothFieldAdjustIntensity", ] From 0699a772316e4f909ed44e9a17b5360190921568 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 3 Nov 2021 15:41:25 +0000 Subject: [PATCH 3/9] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- docs/source/transforms.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/transforms.rst b/docs/source/transforms.rst index a2d1a4c077..1e7df094b2 100644 --- a/docs/source/transforms.rst +++ b/docs/source/transforms.rst @@ -1489,7 +1489,7 @@ Spatial (Dict) .. autoclass:: RandGridDistortiond :members: :special-members: __call__ - + Smooth Field (Dict) ^^^^^^^^^^^^^^^^^^^ From 1dcebf4bf67a3dc876dee0e83275e57a78362103 Mon Sep 17 00:00:00 2001 From: Eric Kerfoot Date: Wed, 3 Nov 2021 15:50:05 +0000 Subject: [PATCH 4/9] Adding smooth field transforms Signed-off-by: Eric Kerfoot --- docs/source/transforms.rst | 4 ++-- monai/transforms/smooth_field/dictionary.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/source/transforms.rst b/docs/source/transforms.rst index 1e7df094b2..37a4a4cf44 100644 --- a/docs/source/transforms.rst +++ b/docs/source/transforms.rst @@ -719,7 +719,7 @@ Smooth Field :special-members: __call__ `RandSmoothFieldAdjustIntensity` -""""""""""""""""""""""""""""""" +"""""""""""""""""""""""""""""""" .. autoclass:: RandSmoothFieldAdjustIntensity :members: :special-members: __call__ @@ -1500,7 +1500,7 @@ Smooth Field (Dict) :special-members: __call__ `RandSmoothFieldAdjustIntensityd` -"""""""""""""""""""""""""""""""" +""""""""""""""""""""""""""""""""" .. autoclass:: RandSmoothFieldAdjustIntensityd :members: :special-members: __call__ diff --git a/monai/transforms/smooth_field/dictionary.py b/monai/transforms/smooth_field/dictionary.py index 4212ba5d90..4cd74717aa 100644 --- a/monai/transforms/smooth_field/dictionary.py +++ b/monai/transforms/smooth_field/dictionary.py @@ -16,7 +16,7 @@ from monai.config import KeysCollection from monai.transforms.smooth_field.array import RandSmoothFieldAdjustContrast, RandSmoothFieldAdjustIntensity -from monai.transforms.transform import MapTransform, RandomizableTransform, Transform +from monai.transforms.transform import MapTransform, Randomizable, RandomizableTransform, Transform from monai.utils import InterpolateMode __all__ = ["RandSmoothFieldAdjustContrastd", "RandSmoothFieldAdjustIntensityd"] From 23687be55339798db6e57ab6592dfe6bc5d1c578 Mon Sep 17 00:00:00 2001 From: Eric Kerfoot Date: Wed, 3 Nov 2021 16:05:37 +0000 Subject: [PATCH 5/9] Adding smooth field transforms Signed-off-by: Eric Kerfoot --- monai/transforms/smooth_field/array.py | 12 +++++------- monai/transforms/smooth_field/dictionary.py | 6 ++++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/monai/transforms/smooth_field/array.py b/monai/transforms/smooth_field/array.py index af0b844f1a..74bcd8593e 100644 --- a/monai/transforms/smooth_field/array.py +++ b/monai/transforms/smooth_field/array.py @@ -21,11 +21,7 @@ from monai.transforms.utils import rescale_array from monai.utils import InterpolateMode, ensure_tuple -__all__ = [ - "SmoothField", - "RandSmoothFieldAdjustContrast", - "RandSmoothFieldAdjustIntensity", -] +__all__ = ["SmoothField", "RandSmoothFieldAdjustContrast", "RandSmoothFieldAdjustIntensity"] class SmoothField(Randomizable): @@ -113,9 +109,10 @@ def __init__( def set_random_state( self, seed: Optional[int] = None, state: Optional[np.random.RandomState] = None - ) -> "Randomizable": + ) -> "RandSmoothFieldAdjustContrast": super().set_random_state(seed, state) self.sfield.set_random_state(seed, state) + return self def randomize(self, data: Optional[Any] = None) -> None: super().randomize(None) @@ -187,9 +184,10 @@ def __init__( def set_random_state( self, seed: Optional[int] = None, state: Optional[np.random.RandomState] = None - ) -> "Randomizable": + ) -> "RandSmoothFieldAdjustIntensity": super().set_random_state(seed, state) self.sfield.set_random_state(seed, state) + return self def randomize(self, data: Optional[Any] = None) -> None: super().randomize(None) diff --git a/monai/transforms/smooth_field/dictionary.py b/monai/transforms/smooth_field/dictionary.py index 4cd74717aa..b9dab89f23 100644 --- a/monai/transforms/smooth_field/dictionary.py +++ b/monai/transforms/smooth_field/dictionary.py @@ -59,9 +59,10 @@ def __init__( def set_random_state( self, seed: Optional[int] = None, state: Optional[np.random.RandomState] = None - ) -> "Randomizable": + ) -> "RandSmoothFieldAdjustContrastd": super().set_random_state(seed, state) self.trans.set_random_state(seed, state) + return self def randomize(self, data: Optional[Any] = None) -> None: super().randomize(None) @@ -121,9 +122,10 @@ def __init__( def set_random_state( self, seed: Optional[int] = None, state: Optional[np.random.RandomState] = None - ) -> "Randomizable": + ) -> "RandSmoothFieldAdjustIntensityd": super().set_random_state(seed, state) self.trans.set_random_state(seed, state) + return self def randomize(self, data: Optional[Any] = None) -> None: super().randomize(None) From efa53add37654caddad0843af980b7795a09a23b Mon Sep 17 00:00:00 2001 From: Eric Kerfoot Date: Fri, 19 Nov 2021 12:48:00 +0000 Subject: [PATCH 6/9] Updates to smooth field transforms Signed-off-by: Eric Kerfoot --- monai/transforms/smooth_field/array.py | 16 +++--- monai/transforms/smooth_field/dictionary.py | 26 ++++++++-- tests/test_smooth_field.py | 55 +++++++++++---------- 3 files changed, 61 insertions(+), 36 deletions(-) diff --git a/monai/transforms/smooth_field/array.py b/monai/transforms/smooth_field/array.py index 74bcd8593e..3100009cae 100644 --- a/monai/transforms/smooth_field/array.py +++ b/monai/transforms/smooth_field/array.py @@ -20,6 +20,7 @@ from monai.transforms.transform import Randomizable, RandomizableTransform, Transform from monai.transforms.utils import rescale_array from monai.utils import InterpolateMode, ensure_tuple +from monai.utils.type_conversion import convert_to_dst_type __all__ = ["SmoothField", "RandSmoothFieldAdjustContrast", "RandSmoothFieldAdjustIntensity"] @@ -135,14 +136,16 @@ def __call__(self, img: np.ndarray, randomize: bool = True): img_rng = img_max - img_min field = self.sfield() - field = np.repeat(field, img.shape[0], 0) + field, *_ = convert_to_dst_type(field, img) - img = (img - img_min) / max(img_rng, 1e-10) + img = (img - img_min) / max(img_rng, 1e-10) # rescale to unit values img = img ** field # contrast is changed by raising image data to a power, in this case the field - out = (img * img_rng) + img_min + out = (img * img_rng) + img_min # rescale back to the original image value range - return out.astype(img.dtype) + out, *_ = convert_to_dst_type(out, img, img.dtype) + + return out class RandSmoothFieldAdjustIntensity(RandomizableTransform): @@ -210,8 +213,9 @@ def __call__(self, img: np.ndarray, randomize: bool = True): img_max = img.max() field = self.sfield() - rfield = np.repeat(field, img.shape[0], 0) + rfield, *_ = convert_to_dst_type(field, img) out = img * rfield + out, *_ = convert_to_dst_type(out, img, img.dtype) - return out.astype(img.dtype) + return out diff --git a/monai/transforms/smooth_field/dictionary.py b/monai/transforms/smooth_field/dictionary.py index b9dab89f23..7f6a4bcf86 100644 --- a/monai/transforms/smooth_field/dictionary.py +++ b/monai/transforms/smooth_field/dictionary.py @@ -16,7 +16,7 @@ from monai.config import KeysCollection from monai.transforms.smooth_field.array import RandSmoothFieldAdjustContrast, RandSmoothFieldAdjustIntensity -from monai.transforms.transform import MapTransform, Randomizable, RandomizableTransform, Transform +from monai.transforms.transform import MapTransform, RandomizableTransform, Transform from monai.utils import InterpolateMode __all__ = ["RandSmoothFieldAdjustContrastd", "RandSmoothFieldAdjustIntensityd"] @@ -54,7 +54,15 @@ def __init__( RandomizableTransform.__init__(self, prob) MapTransform.__init__(self, keys) - self.trans = RandSmoothFieldAdjustContrast(spatial_size, rand_size, padder, mode, align_corners, 1.0, gamma) + self.trans = RandSmoothFieldAdjustContrast( + spatial_size=spatial_size, + rand_size=rand_size, + padder=padder, + mode=mode, + align_corners=align_corners, + prob=1.0, + gamma=gamma, + ) self.apply_same_field = apply_same_field def set_random_state( @@ -66,7 +74,9 @@ def set_random_state( def randomize(self, data: Optional[Any] = None) -> None: super().randomize(None) - self.trans.randomize() + + if self._do_transform: + self.trans.randomize() def __call__(self, data: Mapping[Hashable, np.ndarray]) -> Dict[Hashable, np.ndarray]: self.randomize() @@ -117,7 +127,15 @@ def __init__( RandomizableTransform.__init__(self, prob) MapTransform.__init__(self, keys) - self.trans = RandSmoothFieldAdjustIntensity(spatial_size, rand_size, padder, mode, align_corners, 1.0, gamma) + self.trans = RandSmoothFieldAdjustIntensity( + spatial_size=spatial_size, + rand_size=rand_size, + padder=padder, + mode=mode, + align_corners=align_corners, + prob=1.0, + gamma=gamma, + ) self.apply_same_field = apply_same_field def set_random_state( diff --git a/tests/test_smooth_field.py b/tests/test_smooth_field.py index ed81902269..761cc9e5fa 100644 --- a/tests/test_smooth_field.py +++ b/tests/test_smooth_field.py @@ -12,42 +12,45 @@ import unittest import numpy as np -import torch from parameterized import parameterized from monai.transforms import RandSmoothFieldAdjustContrastd, RandSmoothFieldAdjustIntensityd -from tests.utils import assert_allclose, is_tf32_env +from tests.utils import TEST_NDARRAYS, assert_allclose, is_tf32_env _rtol = 5e-3 if is_tf32_env() else 1e-4 INPUT_SHAPE1 = (1, 8, 8) INPUT_SHAPE2 = (2, 8, 8) -TESTS_CONTRAST = [ - ( - {"keys": ("test",), "spatial_size": INPUT_SHAPE1[1:], "rand_size": (4, 4), "prob": 1.0}, - {"test": np.ones(INPUT_SHAPE1, np.float32)}, - {"test": np.ones(INPUT_SHAPE1, np.float32)}, - ), - ( - {"keys": ("test",), "spatial_size": INPUT_SHAPE2[1:], "rand_size": (4, 4), "prob": 1.0}, - {"test": np.ones(INPUT_SHAPE2, np.float32)}, - {"test": np.ones(INPUT_SHAPE2, np.float32)}, - ), -] +TESTS_CONTRAST = [] +TESTS_INTENSITY = [] -TESTS_INTENSITY = [ - ( - {"keys": ("test",), "spatial_size": INPUT_SHAPE1[1:], "rand_size": (4, 4), "prob": 1.0, "gamma": (1, 1)}, - {"test": np.ones(INPUT_SHAPE1, np.float32)}, - {"test": np.ones(INPUT_SHAPE1, np.float32)}, - ), - ( - {"keys": ("test",), "spatial_size": INPUT_SHAPE2[1:], "rand_size": (4, 4), "prob": 1.0, "gamma": (1, 1)}, - {"test": np.ones(INPUT_SHAPE2, np.float32)}, - {"test": np.ones(INPUT_SHAPE2, np.float32)}, - ), -] +for p in TEST_NDARRAYS: + TESTS_CONTRAST += [ + ( + {"keys": ("test",), "spatial_size": INPUT_SHAPE1[1:], "rand_size": (4, 4), "prob": 1.0}, + {"test": p(np.ones(INPUT_SHAPE1, np.float32))}, + {"test": p(np.ones(INPUT_SHAPE1, np.float32))}, + ), + ( + {"keys": ("test",), "spatial_size": INPUT_SHAPE2[1:], "rand_size": (4, 4), "prob": 1.0}, + {"test": p(np.ones(INPUT_SHAPE2, np.float32))}, + {"test": p(np.ones(INPUT_SHAPE2, np.float32))}, + ), + ] + + TESTS_INTENSITY += [ + ( + {"keys": ("test",), "spatial_size": INPUT_SHAPE1[1:], "rand_size": (4, 4), "prob": 1.0, "gamma": (1, 1)}, + {"test": p(np.ones(INPUT_SHAPE1, np.float32))}, + {"test": p(np.ones(INPUT_SHAPE1, np.float32))}, + ), + ( + {"keys": ("test",), "spatial_size": INPUT_SHAPE2[1:], "rand_size": (4, 4), "prob": 1.0, "gamma": (1, 1)}, + {"test": p(np.ones(INPUT_SHAPE2, np.float32))}, + {"test": p(np.ones(INPUT_SHAPE2, np.float32))}, + ), + ] class TestSmoothField(unittest.TestCase): From 6338c383ba09b14f7d05c827639456ebe3588502 Mon Sep 17 00:00:00 2001 From: Eric Kerfoot Date: Fri, 19 Nov 2021 17:22:40 +0000 Subject: [PATCH 7/9] Updates to smooth field transforms Signed-off-by: Eric Kerfoot --- monai/transforms/smooth_field/array.py | 16 ++++++++-------- monai/transforms/smooth_field/dictionary.py | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/monai/transforms/smooth_field/array.py b/monai/transforms/smooth_field/array.py index 3100009cae..269cc0305b 100644 --- a/monai/transforms/smooth_field/array.py +++ b/monai/transforms/smooth_field/array.py @@ -52,16 +52,16 @@ def __init__( high: float = 1.0, channels: int = 1, ): - self.resizer = Resize(spatial_size, mode=mode, align_corners=align_corners) - self.rand_size = ensure_tuple(rand_size) - self.padder = padder - self.field = None - self.low = low - self.high = high - self.channels = channels + self.resizer: Transform = Resize(spatial_size, mode=mode, align_corners=align_corners) + self.rand_size: tuple = ensure_tuple(rand_size) + self.padder: Optional[Transform] = padder + self.field: Optional[np.ndarray] = None + self.low: float = low + self.high: float = high + self.channels: int = channels def randomize(self, data: Optional[Any] = None) -> None: - self.field = self.R.uniform(self.low, self.high, (self.channels,) + self.rand_size) + self.field = self.R.uniform(self.low, self.high, (self.channels,) + self.rand_size) # type: ignore if self.padder is not None: self.field = self.padder(self.field) diff --git a/monai/transforms/smooth_field/dictionary.py b/monai/transforms/smooth_field/dictionary.py index 7f6a4bcf86..cf75e5e8d0 100644 --- a/monai/transforms/smooth_field/dictionary.py +++ b/monai/transforms/smooth_field/dictionary.py @@ -78,7 +78,7 @@ def randomize(self, data: Optional[Any] = None) -> None: if self._do_transform: self.trans.randomize() - def __call__(self, data: Mapping[Hashable, np.ndarray]) -> Dict[Hashable, np.ndarray]: + def __call__(self, data: Mapping[Hashable, np.ndarray]) -> Mapping[Hashable, np.ndarray]: self.randomize() if not self._do_transform: @@ -149,7 +149,7 @@ def randomize(self, data: Optional[Any] = None) -> None: super().randomize(None) self.trans.randomize() - def __call__(self, data: Mapping[Hashable, np.ndarray]) -> Dict[Hashable, np.ndarray]: + def __call__(self, data: Mapping[Hashable, np.ndarray]) -> Mapping[Hashable, np.ndarray]: self.randomize() if not self._do_transform: From 8b2f4b4e25305d325799d0afc91894e8a966b9ba Mon Sep 17 00:00:00 2001 From: Eric Kerfoot Date: Fri, 19 Nov 2021 17:31:32 +0000 Subject: [PATCH 8/9] Updates to smooth field transforms Signed-off-by: Eric Kerfoot --- monai/transforms/smooth_field/dictionary.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monai/transforms/smooth_field/dictionary.py b/monai/transforms/smooth_field/dictionary.py index cf75e5e8d0..d6c9f1ce06 100644 --- a/monai/transforms/smooth_field/dictionary.py +++ b/monai/transforms/smooth_field/dictionary.py @@ -10,7 +10,7 @@ # limitations under the License. -from typing import Any, Dict, Hashable, Mapping, Optional, Sequence, Union +from typing import Any, Hashable, Mapping, Optional, Sequence, Union import numpy as np From eabe85e8f046fae124c63f70dd581a8e6ee99456 Mon Sep 17 00:00:00 2001 From: Eric Kerfoot Date: Fri, 19 Nov 2021 18:22:35 +0000 Subject: [PATCH 9/9] Updates to smooth field transforms Signed-off-by: Eric Kerfoot --- monai/transforms/smooth_field/array.py | 5 +++++ monai/transforms/smooth_field/dictionary.py | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/monai/transforms/smooth_field/array.py b/monai/transforms/smooth_field/array.py index 269cc0305b..b8016dd3fd 100644 --- a/monai/transforms/smooth_field/array.py +++ b/monai/transforms/smooth_field/array.py @@ -20,6 +20,7 @@ from monai.transforms.transform import Randomizable, RandomizableTransform, Transform from monai.transforms.utils import rescale_array from monai.utils import InterpolateMode, ensure_tuple +from monai.utils.enums import TransformBackends from monai.utils.type_conversion import convert_to_dst_type __all__ = ["SmoothField", "RandSmoothFieldAdjustContrast", "RandSmoothFieldAdjustIntensity"] @@ -86,6 +87,8 @@ class RandSmoothFieldAdjustContrast(RandomizableTransform): gamma: (min, max) range for exponential field """ + backend = [TransformBackends.TORCH, TransformBackends.NUMPY] + def __init__( self, spatial_size: Union[Sequence[int], int], @@ -163,6 +166,8 @@ class RandSmoothFieldAdjustIntensity(RandomizableTransform): gamma: (min, max) range of intensity multipliers """ + backend = [TransformBackends.TORCH, TransformBackends.NUMPY] + def __init__( self, spatial_size: Union[Sequence[int], int], diff --git a/monai/transforms/smooth_field/dictionary.py b/monai/transforms/smooth_field/dictionary.py index d6c9f1ce06..43663930ad 100644 --- a/monai/transforms/smooth_field/dictionary.py +++ b/monai/transforms/smooth_field/dictionary.py @@ -18,6 +18,7 @@ from monai.transforms.smooth_field.array import RandSmoothFieldAdjustContrast, RandSmoothFieldAdjustIntensity from monai.transforms.transform import MapTransform, RandomizableTransform, Transform from monai.utils import InterpolateMode +from monai.utils.enums import TransformBackends __all__ = ["RandSmoothFieldAdjustContrastd", "RandSmoothFieldAdjustIntensityd"] @@ -39,6 +40,8 @@ class RandSmoothFieldAdjustContrastd(RandomizableTransform, MapTransform): apply_same_field: if True, apply the same field to each key, otherwise randomize individually """ + backend = [TransformBackends.TORCH, TransformBackends.NUMPY] + def __init__( self, keys: KeysCollection, @@ -112,6 +115,8 @@ class RandSmoothFieldAdjustIntensityd(RandomizableTransform, MapTransform): apply_same_field: if True, apply the same field to each key, otherwise randomize individually """ + backend = [TransformBackends.TORCH, TransformBackends.NUMPY] + def __init__( self, keys: KeysCollection,