From f9cd2ce7a814e0eb03dc3d18164dd5eb674a12dc Mon Sep 17 00:00:00 2001 From: Eric Kerfoot Date: Sat, 25 Dec 2021 05:17:11 +0000 Subject: [PATCH 01/16] Adding smooth deform transform Signed-off-by: Eric Kerfoot --- monai/transforms/__init__.py | 9 +- monai/transforms/smooth_field/array.py | 254 +++++++++++++++++--- monai/transforms/smooth_field/dictionary.py | 113 ++++++++- tests/test_smooth_field.py | 122 +++++++--- 4 files changed, 427 insertions(+), 71 deletions(-) diff --git a/monai/transforms/__init__.py b/monai/transforms/__init__.py index 3f7e53f514..4a2e5fd00c 100644 --- a/monai/transforms/__init__.py +++ b/monai/transforms/__init__.py @@ -274,8 +274,13 @@ VoteEnsembled, VoteEnsembleDict, ) -from .smooth_field.array import RandSmoothFieldAdjustContrast, RandSmoothFieldAdjustIntensity, SmoothField -from .smooth_field.dictionary import RandSmoothFieldAdjustContrastd, RandSmoothFieldAdjustIntensityd +from .smooth_field.array import ( + RandSmoothDeform, + RandSmoothFieldAdjustContrast, + RandSmoothFieldAdjustIntensity, + SmoothField, +) +from .smooth_field.dictionary import RandSmoothDeformd, RandSmoothFieldAdjustContrastd, RandSmoothFieldAdjustIntensityd from .spatial.array import ( AddCoordinateChannels, Affine, diff --git a/monai/transforms/smooth_field/array.py b/monai/transforms/smooth_field/array.py index b8016dd3fd..41e8a54816 100644 --- a/monai/transforms/smooth_field/array.py +++ b/monai/transforms/smooth_field/array.py @@ -14,6 +14,8 @@ from typing import Any, Optional, Sequence, Union import numpy as np +import torch +from torch.nn.functional import grid_sample, interpolate import monai from monai.transforms.spatial.array import Resize @@ -21,55 +23,116 @@ 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 +from monai.utils.module import look_up_option +from monai.utils.type_conversion import convert_to_dst_type, convert_to_tensor -__all__ = ["SmoothField", "RandSmoothFieldAdjustContrast", "RandSmoothFieldAdjustIntensity"] +__all__ = ["SmoothField", "RandSmoothFieldAdjustContrast", "RandSmoothFieldAdjustIntensity", "RandSmoothDeform"] 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. + Generate a smooth field array by defining a smaller randomized field and then reinterpolating to the desired size. + + This exploits interpolation to create a smoothly varying field used for other applications. An initial randomized + field is defined with `rand_size` dimensions with `pad` number of values padding it along each dimension using + `pad_val` as the value. If `spatial_size` is given this is interpolated to that size, otherwise if None the random + array is produced uninterpolated. The output is always a Pytorch tensor allocated on the specified device. 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 + pad: number of pixels/voxels along the edges of the field to pad with `pad_val` + pad_val: value with which to pad field edges low: low value for randomized field high: high value for randomized field channels: number of channels of final output + spatial_size: final output size of the array, None to produce original uninterpolated field + mode: interpolation mode for resizing the field + align_corners: if True align the corners when upsampling field + device: Pytorch device to define field on """ 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, + pad: int = 0, + pad_val: float = 0, low: float = -1.0, high: float = 1.0, channels: int = 1, + spatial_size: Optional[Union[Sequence[int], int]] = None, + mode: Union[monai.utils.InterpolateMode, str] = monai.utils.InterpolateMode.AREA, + align_corners: Optional[bool] = None, + device: Optional[torch.device] = None, ): - 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 + self.rand_size = ensure_tuple(rand_size) + self.pad = pad + self.low = low + self.high = high + self.channels = channels + self.mode = mode + self.align_corners = align_corners + self.device = device + + self.spatial_size = None + self.spatial_zoom = None + + if low >= high: + raise ValueError("Value for `low` must be less than `high` otherwise field will be zeros") + + self.total_rand_size = tuple(rs + self.pad * 2 for rs in self.rand_size) + + self.field = torch.ones((1, self.channels) + self.total_rand_size, device=self.device) * pad_val + + self.crand_size = (self.channels,) + self.rand_size + + pad_slice = slice(None) if self.pad == 0 else slice(self.pad, -self.pad) + self.rand_slices = (0, slice(None)) + (pad_slice,) * len(self.rand_size) + + self.set_spatial_size(spatial_size) def randomize(self, data: Optional[Any] = None) -> None: - 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) + self.field[self.rand_slices] = torch.from_numpy(self.R.uniform(self.low, self.high, self.crand_size)) - def __call__(self): - resized_field = self.resizer(self.field) + def set_spatial_size(self, spatial_size: Optional[Union[Sequence[int], int]]) -> None: + """ + Set the `spatial_size` and `spatial_zoom` attributes used for interpolating the field to the given + dimension, or not interpolate at all if None. + + Args: + spatial_size: new size to interpolate to, or None to not interpolate + """ + if spatial_size is None: + self.spatial_size = None + self.spatial_zoom = None + else: + self.spatial_size = tuple(spatial_size) + self.spatial_zoom = tuple(s / f for s, f in zip(self.spatial_size, self.total_rand_size)) - return rescale_array(resized_field, self.field.min(), self.field.max()) + def __call__(self, randomize=True): + if randomize: + self.randomize() + + field = self.field.to(self.device).clone() + + if self.spatial_zoom is not None: + resized_field = interpolate( # type: ignore + input=field, # type: ignore + scale_factor=self.spatial_zoom, + mode=look_up_option(self.mode, monai.utils.InterpolateMode).value, + align_corners=self.align_corners, + recompute_scale_factor=False, + ) + + mina = resized_field.min() + maxa = resized_field.max() + minv = self.field.min() + maxv = self.field.max() + + # faster than rescale_array (?) + norm_field = (resized_field.squeeze(0) - mina).div_(maxa - mina) + field = norm_field.mul_(maxv - minv).add_(minv) + + return field class RandSmoothFieldAdjustContrast(RandomizableTransform): @@ -80,11 +143,12 @@ class RandSmoothFieldAdjustContrast(RandomizableTransform): 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 + pad: number of pixels/voxels along the edges of the field to pad with 1 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 + device: Pytorch device to define field on """ backend = [TransformBackends.TORCH, TransformBackends.NUMPY] @@ -93,11 +157,12 @@ def __init__( self, spatial_size: Union[Sequence[int], int], rand_size: Union[Sequence[int], int], - padder: Optional[Transform] = None, + pad: int = 0, mode: Union[InterpolateMode, str] = InterpolateMode.AREA, align_corners: Optional[bool] = None, prob: float = 0.1, gamma: Union[Sequence[float], float] = (0.5, 4.5), + device: Optional[torch.device] = None, ): super().__init__(prob) @@ -109,7 +174,18 @@ def __init__( self.gamma = (min(gamma), max(gamma)) - self.sfield = SmoothField(spatial_size, rand_size, padder, mode, align_corners, self.gamma[0], self.gamma[1]) + self.sfield = SmoothField( + rand_size=rand_size, + pad=pad, + pad_val=1, + low=self.gamma[0], + high=self.gamma[1], + channels=1, + spatial_size=spatial_size, + mode=mode, + align_corners=align_corners, + device=device, + ) def set_random_state( self, seed: Optional[int] = None, state: Optional[np.random.RandomState] = None @@ -146,7 +222,7 @@ def __call__(self, img: np.ndarray, randomize: bool = True): out = (img * img_rng) + img_min # rescale back to the original image value range - out, *_ = convert_to_dst_type(out, img, img.dtype) + out, *_ = convert_to_dst_type(out, img) return out @@ -159,11 +235,12 @@ class RandSmoothFieldAdjustIntensity(RandomizableTransform): 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 + pad: number of pixels/voxels along the edges of the field to pad with 1 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 + device: Pytorch device to define field on """ backend = [TransformBackends.TORCH, TransformBackends.NUMPY] @@ -172,11 +249,12 @@ def __init__( self, spatial_size: Union[Sequence[int], int], rand_size: Union[Sequence[int], int], - padder: Optional[Transform] = None, + pad: int = 0, 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), + device: Optional[torch.device] = None, ): super().__init__(prob) @@ -188,7 +266,18 @@ def __init__( self.gamma = (min(gamma), max(gamma)) - self.sfield = SmoothField(spatial_size, rand_size, padder, mode, align_corners, self.gamma[0], self.gamma[1]) + self.sfield = SmoothField( + rand_size=rand_size, + pad=pad, + pad_val=1, + low=self.gamma[0], + high=self.gamma[1], + channels=1, + spatial_size=spatial_size, + mode=mode, + align_corners=align_corners, + device=device, + ) def set_random_state( self, seed: Optional[int] = None, state: Optional[np.random.RandomState] = None @@ -221,6 +310,105 @@ def __call__(self, img: np.ndarray, randomize: bool = True): rfield, *_ = convert_to_dst_type(field, img) out = img * rfield - out, *_ = convert_to_dst_type(out, img, img.dtype) + out, *_ = convert_to_dst_type(out, img) + + return out + + +class RandSmoothDeform(RandomizableTransform): + """ + Deform an image using a random smooth field and scipy.ndimage.map_coordinates. + + Args: + spatial_size: input array size to which deformation grid is interpolated + rand_size: size of the randomized field to start from + pad: number of pixels/voxels along the edges of the field to pad with 0 + field_mode: interpolation mode to use when upsampling the deformation field + align_corners: if True align the corners when upsampling field + prob: probability transform is applied + def_range: (min, max) value of the deformation range in pixel/voxel units + grid_dtype: type for the deformation grid calculated from the field + grid_mode: interpolation mode used for sampling input using deformation grid + grid_align_corners: if True align the corners when sampling the deformation grid + device: Pytorch device to define field on + """ + + def __init__( + self, + spatial_size: Union[Sequence[int], int], + rand_size: Union[Sequence[int], int], + pad: int = 0, + field_mode: Union[monai.utils.InterpolateMode, str] = monai.utils.InterpolateMode.AREA, + align_corners: Optional[bool] = None, + prob: float = 0.1, + def_range: float = 1.0, + grid_dtype=torch.float32, + grid_mode: Union[monai.utils.GridSampleMode, str] = monai.utils.GridSampleMode.NEAREST, + grid_align_corners: Optional[bool] = False, + device: Optional[torch.device] = None, + ): + super().__init__(prob) + + self.grid_dtype = grid_dtype + self.grid_mode = grid_mode + self.def_range = def_range + self.device = device + self.grid_align_corners = grid_align_corners + self.grid_padding_mode = "border" + + self.sfield = SmoothField( + spatial_size=spatial_size, + rand_size=rand_size, + pad=pad, + low=-def_range, + high=def_range, + channels=len(rand_size), + mode=field_mode, + align_corners=align_corners, + device=device, + ) + + grid_space = spatial_size if spatial_size is not None else self.sfield.field.shape[2:] + grid_ranges = [torch.linspace(-1, 1, d) for d in grid_space] + self.grid = torch.stack(torch.meshgrid(*grid_ranges, indexing="ij")).unsqueeze(0).to(self.device, self.grid_dtype) + + 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) + return self + + 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, device: Optional[torch.device] = None): + if randomize: + self.randomize() + + if not self._do_transform: + return img + + device = device if device is not None else self.device + + field = self.sfield() + + dgrid = self.grid + field.to(self.grid_dtype) + dgrid = dgrid.moveaxis(1, -1) + + img_t = convert_to_tensor(img[None], torch.float32, device) + + out = grid_sample( + input=img_t, + grid=dgrid, + mode=self.grid_mode.value, + align_corners=self.grid_align_corners, + padding_mode=self.grid_padding_mode, + ) + + out, *_ = convert_to_dst_type(out.squeeze(0), img) return out diff --git a/monai/transforms/smooth_field/dictionary.py b/monai/transforms/smooth_field/dictionary.py index 43663930ad..9d94a54040 100644 --- a/monai/transforms/smooth_field/dictionary.py +++ b/monai/transforms/smooth_field/dictionary.py @@ -13,14 +13,19 @@ from typing import Any, Hashable, Mapping, Optional, Sequence, Union import numpy as np +import torch from monai.config import KeysCollection -from monai.transforms.smooth_field.array import RandSmoothFieldAdjustContrast, RandSmoothFieldAdjustIntensity +from monai.transforms.smooth_field.array import ( + RandSmoothDeform, + RandSmoothFieldAdjustContrast, + RandSmoothFieldAdjustIntensity, +) from monai.transforms.transform import MapTransform, RandomizableTransform, Transform -from monai.utils import InterpolateMode +from monai.utils import GridSampleMode, InterpolateMode from monai.utils.enums import TransformBackends -__all__ = ["RandSmoothFieldAdjustContrastd", "RandSmoothFieldAdjustIntensityd"] +__all__ = ["RandSmoothFieldAdjustContrastd", "RandSmoothFieldAdjustIntensityd", "RandSmoothDeformd"] class RandSmoothFieldAdjustContrastd(RandomizableTransform, MapTransform): @@ -32,12 +37,13 @@ class RandSmoothFieldAdjustContrastd(RandomizableTransform, MapTransform): 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 + pad: number of pixels/voxels along the edges of the field to pad with 0 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 + device: Pytorch device to define field on """ backend = [TransformBackends.TORCH, TransformBackends.NUMPY] @@ -47,12 +53,13 @@ def __init__( keys: KeysCollection, spatial_size: Union[Sequence[int], int], rand_size: Union[Sequence[int], int], - padder: Optional[Transform] = None, + pad: int = 0, 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, + device: Optional[torch.device] = None ): RandomizableTransform.__init__(self, prob) MapTransform.__init__(self, keys) @@ -60,11 +67,12 @@ def __init__( self.trans = RandSmoothFieldAdjustContrast( spatial_size=spatial_size, rand_size=rand_size, - padder=padder, + pad=pad, mode=mode, align_corners=align_corners, prob=1.0, gamma=gamma, + device=device ) self.apply_same_field = apply_same_field @@ -107,12 +115,13 @@ class RandSmoothFieldAdjustIntensityd(RandomizableTransform, MapTransform): 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 + pad: number of pixels/voxels along the edges of the field to pad with 0 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 + device: Pytorch device to define field on """ backend = [TransformBackends.TORCH, TransformBackends.NUMPY] @@ -122,12 +131,13 @@ def __init__( keys: KeysCollection, spatial_size: Union[Sequence[int], int], rand_size: Union[Sequence[int], int], - padder: Optional[Transform] = None, + pad: int = 0, 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, + device: Optional[torch.device] = None ): RandomizableTransform.__init__(self, prob) MapTransform.__init__(self, keys) @@ -135,11 +145,12 @@ def __init__( self.trans = RandSmoothFieldAdjustIntensity( spatial_size=spatial_size, rand_size=rand_size, - padder=padder, + pad=pad, mode=mode, align_corners=align_corners, prob=1.0, gamma=gamma, + device=device ) self.apply_same_field = apply_same_field @@ -169,3 +180,87 @@ def __call__(self, data: Mapping[Hashable, np.ndarray]) -> Mapping[Hashable, np. d[key] = self.trans(d[key], False) return d + + +class RandSmoothDeformd(RandomizableTransform, MapTransform): + """ + Dictionary version of RandSmoothDeform. + + Args: + keys: key names to apply the augment to + spatial_size: input array size to which deformation grid is interpolated + rand_size: size of the randomized field to start from + pad: number of pixels/voxels along the edges of the field to pad with 0 + field_mode: interpolation mode to use when upsampling the deformation field + align_corners: if True align the corners when upsampling field + prob: probability transform is applied + def_range: (min, max) value of the deformation range in pixel/voxel units + grid_dtype: type for the deformation grid calculated from the field + grid_mode: interpolation mode used for sampling input using deformation grid + grid_align_corners: if True align the corners when sampling the deformation grid + apply_same_field: if True, apply the same field to each key, otherwise randomize individually + device: Pytorch device to define field on + """ + backend = [TransformBackends.TORCH, TransformBackends.NUMPY] + + def __init__( + self, + keys: KeysCollection, + spatial_size: Union[Sequence[int], int], + rand_size: Union[Sequence[int], int], + pad: int = 0, + field_mode: Union[InterpolateMode, str] = InterpolateMode.AREA, + align_corners: Optional[bool] = None, + prob: float = 0.1, + def_range: float = 1.0, + grid_dtype=torch.float32, + grid_mode: Union[GridSampleMode, str] = GridSampleMode.NEAREST, + grid_align_corners: Optional[bool] = False, + apply_same_field: bool = True, + device: Optional[torch.device] = None, + ): + RandomizableTransform.__init__(self, prob) + MapTransform.__init__(self, keys) + + self.trans=RandSmoothDeform( + rand_size=rand_size, + spatial_size=spatial_size, + pad=pad, + field_mode=field_mode, + align_corners=align_corners, + prob=1.0, + def_range=def_range, + grid_dtype=grid_dtype, + grid_mode=grid_mode, + grid_align_corners=grid_align_corners, + device=device + ) + + self.apply_same_field=apply_same_field + + def set_random_state( + self, seed: Optional[int] = None, state: Optional[np.random.RandomState] = None + ) -> "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) + self.trans.randomize() + + def __call__(self, data: Mapping[Hashable, np.ndarray]) -> Mapping[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 \ No newline at end of file diff --git a/tests/test_smooth_field.py b/tests/test_smooth_field.py index 761cc9e5fa..e62735b604 100644 --- a/tests/test_smooth_field.py +++ b/tests/test_smooth_field.py @@ -10,47 +10,72 @@ # limitations under the License. import unittest +from itertools import product import numpy as np +import torch from parameterized import parameterized -from monai.transforms import RandSmoothFieldAdjustContrastd, RandSmoothFieldAdjustIntensityd +from monai.transforms import RandSmoothDeformd, RandSmoothFieldAdjustContrastd, RandSmoothFieldAdjustIntensityd 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) +INPUT_SHAPES = ((1, 8, 8), (2, 8, 8), (1, 8, 8, 8)) TESTS_CONTRAST = [] TESTS_INTENSITY = [] +TESTS_DEFORM = [] -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))}, - ), +KEY = "test" + +for arr_type, shape in product(TEST_NDARRAYS, INPUT_SHAPES): + in_arr = arr_type(np.ones(shape, np.float32)) + exp_arr = arr_type(np.ones(shape, np.float32)) + rand_size = (4,) * (len(shape) - 1) + + device = torch.device("cpu") + + if isinstance(in_arr, torch.Tensor) and in_arr.get_device() >= 0: + device = in_arr.get_device() + + TESTS_CONTRAST.append( ( - {"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))}, - ), - ] + {"keys": (KEY,), "spatial_size": shape[1:], "rand_size": rand_size, "prob": 1.0, "device": device,}, + {KEY: in_arr}, + {KEY: exp_arr}, + ) + ) - TESTS_INTENSITY += [ + TESTS_INTENSITY.append( ( - {"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": (KEY,), + "spatial_size": shape[1:], + "rand_size": rand_size, + "prob": 1.0, + "device": device, + "gamma": (0.9, 1), + }, + {KEY: in_arr}, + {KEY: exp_arr}, + ) + ) + + TESTS_DEFORM.append( ( - {"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))}, - ), - ] + { + "keys": (KEY,), + "spatial_size": shape[1:], + "rand_size": rand_size, + "prob": 1.0, + "device": device, + "def_range": 0.1, + }, + {KEY: in_arr}, + {KEY: exp_arr}, + ) + ) class TestSmoothField(unittest.TestCase): @@ -62,7 +87,18 @@ def test_rand_smooth_field_adjust_contrastd(self, input_param, input_data, expec res = g(input_data) for key, result in res.items(): expected = expected_val[key] - assert_allclose(result, expected, rtol=_rtol, atol=5e-3) + assert_allclose(result, expected, rtol=_rtol, atol=1e-1) + + def test_rand_smooth_field_adjust_contrastd_pad(self): + input_param, input_data, expected_val = TESTS_CONTRAST[0] + + g = RandSmoothFieldAdjustContrastd(pad=1, **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=1e-1) @parameterized.expand(TESTS_INTENSITY) def test_rand_smooth_field_adjust_intensityd(self, input_param, input_data, expected_val): @@ -72,4 +108,36 @@ def test_rand_smooth_field_adjust_intensityd(self, input_param, input_data, expe res = g(input_data) for key, result in res.items(): expected = expected_val[key] - assert_allclose(result, expected, rtol=_rtol, atol=5e-3) + assert_allclose(result, expected, rtol=_rtol, atol=1e-1) + + def test_rand_smooth_field_adjust_intensityd_pad(self): + input_param, input_data, expected_val = TESTS_INTENSITY[0] + + g = RandSmoothFieldAdjustIntensityd(pad=1, **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=1e-1) + + @parameterized.expand(TESTS_DEFORM) + def test_rand_smooth_deformd(self, input_param, input_data, expected_val): + g = RandSmoothDeformd(**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=1e-1) + + def test_rand_smooth_deformd_pad(self): + input_param, input_data, expected_val = TESTS_DEFORM[0] + + g = RandSmoothDeformd(pad=1, **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=1e-1) From b580da2058e17b974ffe6037b4d625d263124b15 Mon Sep 17 00:00:00 2001 From: Eric Kerfoot Date: Mon, 27 Dec 2021 03:45:40 +0000 Subject: [PATCH 02/16] Update Signed-off-by: Eric Kerfoot --- monai/transforms/smooth_field/array.py | 19 +++++- monai/transforms/smooth_field/dictionary.py | 74 +++++++++++++-------- 2 files changed, 62 insertions(+), 31 deletions(-) diff --git a/monai/transforms/smooth_field/array.py b/monai/transforms/smooth_field/array.py index 41e8a54816..d7033768c9 100644 --- a/monai/transforms/smooth_field/array.py +++ b/monai/transforms/smooth_field/array.py @@ -108,7 +108,10 @@ def set_spatial_size(self, spatial_size: Optional[Union[Sequence[int], int]]) -> self.spatial_size = tuple(spatial_size) self.spatial_zoom = tuple(s / f for s, f in zip(self.spatial_size, self.total_rand_size)) - def __call__(self, randomize=True): + def set_mode(self, mode: Union[monai.utils.InterpolateMode, str]) -> None: + self.mode = mode + + def __call__(self, randomize=False): if randomize: self.randomize() @@ -200,6 +203,9 @@ def randomize(self, data: Optional[Any] = None) -> None: if self._do_transform: self.sfield.randomize() + def set_mode(self, mode: Union[monai.utils.InterpolateMode, str]) -> None: + self.sfield.set_mode(mode) + def __call__(self, img: np.ndarray, randomize: bool = True): """ Apply the transform to `img`, if `randomize` randomizing the smooth field otherwise reusing the previous. @@ -292,6 +298,9 @@ def randomize(self, data: Optional[Any] = None) -> None: if self._do_transform: self.sfield.randomize() + def set_mode(self, mode: Union[monai.utils.InterpolateMode, str]) -> None: + self.sfield.set_mode(mode) + def __call__(self, img: np.ndarray, randomize: bool = True): """ Apply the transform to `img`, if `randomize` randomizing the smooth field otherwise reusing the previous. @@ -385,6 +394,12 @@ def randomize(self, data: Optional[Any] = None) -> None: if self._do_transform: self.sfield.randomize() + def set_field_mode(self, mode: Union[monai.utils.InterpolateMode, str]) -> None: + self.sfield.set_mode(mode) + + def set_grid_mode(self, mode: Union[monai.utils.GridSampleMode, str]) -> None: + self.grid_mode = mode + def __call__(self, img: np.ndarray, randomize: bool = True, device: Optional[torch.device] = None): if randomize: self.randomize() @@ -404,7 +419,7 @@ def __call__(self, img: np.ndarray, randomize: bool = True, device: Optional[tor out = grid_sample( input=img_t, grid=dgrid, - mode=self.grid_mode.value, + mode=look_up_option(self.grid_mode, monai.utils.GridSampleMode).value, align_corners=self.grid_align_corners, padding_mode=self.grid_padding_mode, ) diff --git a/monai/transforms/smooth_field/dictionary.py b/monai/transforms/smooth_field/dictionary.py index 9d94a54040..7d9ea1973b 100644 --- a/monai/transforms/smooth_field/dictionary.py +++ b/monai/transforms/smooth_field/dictionary.py @@ -22,12 +22,16 @@ RandSmoothFieldAdjustIntensity, ) from monai.transforms.transform import MapTransform, RandomizableTransform, Transform -from monai.utils import GridSampleMode, InterpolateMode +from monai.utils import ensure_tuple_rep, GridSampleMode, InterpolateMode from monai.utils.enums import TransformBackends __all__ = ["RandSmoothFieldAdjustContrastd", "RandSmoothFieldAdjustIntensityd", "RandSmoothDeformd"] +InterpolateModeType = Union[InterpolateMode, str] +GridSampleModeType = Union[GridSampleMode, str] + + class RandSmoothFieldAdjustContrastd(RandomizableTransform, MapTransform): """ Dictionary version of RandSmoothFieldAdjustContrast. The field is randomized once per invocation by default so the @@ -54,27 +58,29 @@ def __init__( spatial_size: Union[Sequence[int], int], rand_size: Union[Sequence[int], int], pad: int = 0, - mode: Union[InterpolateMode, str] = InterpolateMode.AREA, + mode: Union[InterpolateModeType, Sequence[InterpolateModeType]] = 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, - device: Optional[torch.device] = None + device: Optional[torch.device] = None, ): RandomizableTransform.__init__(self, prob) MapTransform.__init__(self, keys) + self.apply_same_field = apply_same_field + self.mode = ensure_tuple_rep(mode, len(self.keys)) + self.trans = RandSmoothFieldAdjustContrast( spatial_size=spatial_size, rand_size=rand_size, pad=pad, - mode=mode, + mode=self.mode[0], align_corners=align_corners, prob=1.0, gamma=gamma, - device=device + device=device, ) - self.apply_same_field = apply_same_field def set_random_state( self, seed: Optional[int] = None, state: Optional[np.random.RandomState] = None @@ -97,10 +103,11 @@ def __call__(self, data: Mapping[Hashable, np.ndarray]) -> Mapping[Hashable, np. d = dict(data) - for key in self.key_iterator(d): + for idx, key in enumerate(self.key_iterator(d)): if not self.apply_same_field: self.randomize() # new field for every key + self.trans.set_mode(self.mode[idx % len(self.mode)]) d[key] = self.trans(d[key], False) return d @@ -132,27 +139,29 @@ def __init__( spatial_size: Union[Sequence[int], int], rand_size: Union[Sequence[int], int], pad: int = 0, - mode: Union[InterpolateMode, str] = InterpolateMode.AREA, + mode: Union[InterpolateModeType, Sequence[InterpolateModeType]] = 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, - device: Optional[torch.device] = None + device: Optional[torch.device] = None, ): RandomizableTransform.__init__(self, prob) MapTransform.__init__(self, keys) + self.apply_same_field = apply_same_field + self.mode = ensure_tuple_rep(mode,len(self.keys)) + self.trans = RandSmoothFieldAdjustIntensity( spatial_size=spatial_size, rand_size=rand_size, pad=pad, - mode=mode, + mode=self.mode[0], align_corners=align_corners, prob=1.0, gamma=gamma, - device=device + device=device, ) - self.apply_same_field = apply_same_field def set_random_state( self, seed: Optional[int] = None, state: Optional[np.random.RandomState] = None @@ -173,15 +182,16 @@ def __call__(self, data: Mapping[Hashable, np.ndarray]) -> Mapping[Hashable, np. d = dict(data) - for key in self.key_iterator(d): + for idx, key in enumerate(self.key_iterator(d)): if not self.apply_same_field: self.randomize() # new field for every key + self.trans.set_mode(self.mode[idx % len(self.mode)]) d[key] = self.trans(d[key], False) return d - + class RandSmoothDeformd(RandomizableTransform, MapTransform): """ Dictionary version of RandSmoothDeform. @@ -201,6 +211,7 @@ class RandSmoothDeformd(RandomizableTransform, MapTransform): apply_same_field: if True, apply the same field to each key, otherwise randomize individually device: Pytorch device to define field on """ + backend = [TransformBackends.TORCH, TransformBackends.NUMPY] def __init__( @@ -209,35 +220,37 @@ def __init__( spatial_size: Union[Sequence[int], int], rand_size: Union[Sequence[int], int], pad: int = 0, - field_mode: Union[InterpolateMode, str] = InterpolateMode.AREA, + field_mode: Union[InterpolateModeType, Sequence[InterpolateModeType]] = InterpolateMode.AREA, align_corners: Optional[bool] = None, prob: float = 0.1, def_range: float = 1.0, grid_dtype=torch.float32, - grid_mode: Union[GridSampleMode, str] = GridSampleMode.NEAREST, + grid_mode: Union[GridSampleModeType, Sequence[GridSampleModeType]] = GridSampleMode.NEAREST, grid_align_corners: Optional[bool] = False, apply_same_field: bool = True, - device: Optional[torch.device] = None, + device: Optional[torch.device] = None, ): RandomizableTransform.__init__(self, prob) MapTransform.__init__(self, keys) - - self.trans=RandSmoothDeform( + + self.field_mode = ensure_tuple_rep(field_mode,len(self.keys)) + self.grid_mode = ensure_tuple_rep(grid_mode,len(self.keys)) + self.apply_same_field = apply_same_field + + self.trans = RandSmoothDeform( rand_size=rand_size, spatial_size=spatial_size, pad=pad, - field_mode=field_mode, + field_mode=self.field_mode[0], align_corners=align_corners, prob=1.0, def_range=def_range, grid_dtype=grid_dtype, - grid_mode=grid_mode, + grid_mode=self.grid_mode[0], grid_align_corners=grid_align_corners, - device=device + device=device, ) - - self.apply_same_field=apply_same_field - + def set_random_state( self, seed: Optional[int] = None, state: Optional[np.random.RandomState] = None ) -> "RandSmoothFieldAdjustIntensityd": @@ -248,7 +261,7 @@ def set_random_state( def randomize(self, data: Optional[Any] = None) -> None: super().randomize(None) self.trans.randomize() - + def __call__(self, data: Mapping[Hashable, np.ndarray]) -> Mapping[Hashable, np.ndarray]: self.randomize() @@ -257,10 +270,13 @@ def __call__(self, data: Mapping[Hashable, np.ndarray]) -> Mapping[Hashable, np. d = dict(data) - for key in self.key_iterator(d): + for idx, key in enumerate(self.key_iterator(d)): if not self.apply_same_field: self.randomize() # new field for every key - d[key] = self.trans(d[key], False) + self.trans.set_field_mode(self.field_mode[idx % len(self.field_mode)]) + self.trans.set_grid_mode(self.grid_mode[idx % len(self.grid_mode)]) - return d \ No newline at end of file + d[key] = self.trans(d[key], False, self.trans.device) + + return d From 4012cdaa41d0097633059e868df4f45934bcb78a Mon Sep 17 00:00:00 2001 From: Eric Kerfoot Date: Tue, 28 Dec 2021 19:01:16 +0000 Subject: [PATCH 03/16] Updates Signed-off-by: Eric Kerfoot --- monai/transforms/smooth_field/array.py | 99 ++++++++++++++------- monai/transforms/smooth_field/dictionary.py | 35 +++++--- tests/test_smooth_field.py | 2 +- 3 files changed, 90 insertions(+), 46 deletions(-) diff --git a/monai/transforms/smooth_field/array.py b/monai/transforms/smooth_field/array.py index 7c618f7261..a81d790058 100644 --- a/monai/transforms/smooth_field/array.py +++ b/monai/transforms/smooth_field/array.py @@ -18,10 +18,11 @@ from torch.nn.functional import grid_sample, interpolate import monai +from monai.config.type_definitions import NdarrayOrTensor 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 +from monai.utils import GridSampleMode, GridSamplePadMode, InterpolateMode, ensure_tuple from monai.utils.enums import TransformBackends from monai.utils.module import look_up_option from monai.utils.type_conversion import convert_to_dst_type, convert_to_tensor @@ -31,11 +32,11 @@ class SmoothField(Randomizable): """ - Generate a smooth field array by defining a smaller randomized field and then reinterpolating to the desired size. - - This exploits interpolation to create a smoothly varying field used for other applications. An initial randomized - field is defined with `rand_size` dimensions with `pad` number of values padding it along each dimension using - `pad_val` as the value. If `spatial_size` is given this is interpolated to that size, otherwise if None the random + Generate a smooth field array by defining a smaller randomized field and then reinterpolating to the desired size. + + This exploits interpolation to create a smoothly varying field used for other applications. An initial randomized + field is defined with `rand_size` dimensions with `pad` number of values padding it along each dimension using + `pad_val` as the value. If `spatial_size` is given this is interpolated to that size, otherwise if None the random array is produced uninterpolated. The output is always a Pytorch tensor allocated on the specified device. Args: @@ -60,7 +61,7 @@ def __init__( high: float = 1.0, channels: int = 1, spatial_size: Optional[Union[Sequence[int], int]] = None, - mode: Union[monai.utils.InterpolateMode, str] = monai.utils.InterpolateMode.AREA, + mode: Union[InterpolateMode, str] = InterpolateMode.AREA, align_corners: Optional[bool] = None, device: Optional[torch.device] = None, ): @@ -97,7 +98,7 @@ def set_spatial_size(self, spatial_size: Optional[Union[Sequence[int], int]]) -> """ Set the `spatial_size` and `spatial_zoom` attributes used for interpolating the field to the given dimension, or not interpolate at all if None. - + Args: spatial_size: new size to interpolate to, or None to not interpolate """ @@ -111,7 +112,7 @@ def set_spatial_size(self, spatial_size: Optional[Union[Sequence[int], int]]) -> def set_mode(self, mode: Union[monai.utils.InterpolateMode, str]) -> None: self.mode = mode - def __call__(self, randomize=False): + def __call__(self, randomize=False) -> torch.Tensor: if randomize: self.randomize() @@ -121,7 +122,7 @@ def __call__(self, randomize=False): resized_field = interpolate( # type: ignore input=field, # type: ignore scale_factor=self.spatial_zoom, - mode=look_up_option(self.mode, monai.utils.InterpolateMode).value, + mode=look_up_option(self.mode, InterpolateMode).value, align_corners=self.align_corners, recompute_scale_factor=False, ) @@ -140,8 +141,13 @@ def __call__(self, randomize=False): 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. + Randomly adjust the contrast of input images by calculating a randomized smooth field for each invocation. + + This uses SmoothField internally to define the adjustment over the image. If `pad` is greater than 0 the + edges of the input volume of that width will be mostly unchanged. Contrast is changed by raising input + values by the power of the smooth field so the range of values given by `gamma` should be chosen with this + in mind. For example, a minimum value of 0 in `gamma` will produce white areas so this should be avoided. + Afte the contrast is adjusted the values of the result are rescaled to the range of the original input. Args: spatial_size: size of input array's spatial dimensions @@ -206,7 +212,7 @@ def randomize(self, data: Optional[Any] = None) -> None: def set_mode(self, mode: Union[monai.utils.InterpolateMode, str]) -> None: self.sfield.set_mode(mode) - def __call__(self, img: np.ndarray, randomize: bool = True): + def __call__(self, img: np.ndarray, randomize: bool = True) -> NdarrayOrTensor: """ Apply the transform to `img`, if `randomize` randomizing the smooth field otherwise reusing the previous. """ @@ -223,20 +229,25 @@ def __call__(self, img: np.ndarray, randomize: bool = True): field = self.sfield() field, *_ = convert_to_dst_type(field, img) + # everything below here is to be computed using the destination type (numpy, tensor, etc.) + 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 # rescale back to the original image value range - out, *_ = convert_to_dst_type(out, img) - return out class RandSmoothFieldAdjustIntensity(RandomizableTransform): """ - Randomly adjust the intensity of input images by calculating a randomized smooth field for each invocation. This - uses SmoothField internally. + Randomly adjust the intensity of input images by calculating a randomized smooth field for each invocation. + + This uses SmoothField internally to define the adjustment over the image. If `pad` is greater than 0 the + edges of the input volume of that width will be mostly unchanged. Intensity is changed by multiplying the + inputs by the smooth field, so the values of `gamma` should be chosen with this in mind. The default values + of `(0.1, 1.0)` are sensible in that values will not be zeroed out by the field nor multiplied greater than + the original value range. Args: spatial_size: size of input array @@ -256,7 +267,7 @@ def __init__( spatial_size: Union[Sequence[int], int], rand_size: Union[Sequence[int], int], pad: int = 0, - mode: Union[monai.utils.InterpolateMode, str] = monai.utils.InterpolateMode.AREA, + mode: Union[InterpolateMode, str] = InterpolateMode.AREA, align_corners: Optional[bool] = None, prob: float = 0.1, gamma: Union[Sequence[float], float] = (0.1, 1.0), @@ -298,10 +309,10 @@ def randomize(self, data: Optional[Any] = None) -> None: if self._do_transform: self.sfield.randomize() - def set_mode(self, mode: Union[monai.utils.InterpolateMode, str]) -> None: + def set_mode(self, mode: Union[InterpolateMode, str]) -> None: self.sfield.set_mode(mode) - def __call__(self, img: np.ndarray, randomize: bool = True): + def __call__(self, img: np.ndarray, randomize: bool = True) -> NdarrayOrTensor: """ Apply the transform to `img`, if `randomize` randomizing the smooth field otherwise reusing the previous. """ @@ -315,16 +326,21 @@ def __call__(self, img: np.ndarray, randomize: bool = True): field = self.sfield() rfield, *_ = convert_to_dst_type(field, img) + # everything below here is to be computed using the destination type (numpy, tensor, etc.) + out = img * rfield - out, *_ = convert_to_dst_type(out, img) return out class RandSmoothDeform(RandomizableTransform): """ - Deform an image using a random smooth field and scipy.ndimage.map_coordinates. - + Deform an image using a random smooth field and Pytorch's grid_sample. + + The amount of deformation is given by `def_range` in fractions of the size of the image. The size of each dimension + of the input image is always defined as 2 regardless of actual image voxel dimensions, that is the coordinates in + every dimension range from -1 to 1. A value of 0.1 means pixels/voxels can be moved by up to 5% of the image's size. + Args: spatial_size: input array size to which deformation grid is interpolated rand_size: size of the randomized field to start from @@ -332,24 +348,28 @@ class RandSmoothDeform(RandomizableTransform): field_mode: interpolation mode to use when upsampling the deformation field align_corners: if True align the corners when upsampling field prob: probability transform is applied - def_range: (min, max) value of the deformation range in pixel/voxel units + def_range: value of the deformation range in image size fractions, single min/max value or min/max pair grid_dtype: type for the deformation grid calculated from the field grid_mode: interpolation mode used for sampling input using deformation grid + grid_padding_mode: padding mode used for sampling input using deformation grid grid_align_corners: if True align the corners when sampling the deformation grid device: Pytorch device to define field on """ + backend = [TransformBackends.TORCH, TransformBackends.NUMPY] + def __init__( self, spatial_size: Union[Sequence[int], int], rand_size: Union[Sequence[int], int], pad: int = 0, - field_mode: Union[monai.utils.InterpolateMode, str] = monai.utils.InterpolateMode.AREA, + field_mode: Union[InterpolateMode, str] = InterpolateMode.AREA, align_corners: Optional[bool] = None, prob: float = 0.1, - def_range: float = 1.0, + def_range: Union[Sequence[float], float] = 1.0, grid_dtype=torch.float32, - grid_mode: Union[monai.utils.GridSampleMode, str] = monai.utils.GridSampleMode.NEAREST, + grid_mode: Union[GridSampleMode, str] = GridSampleMode.NEAREST, + grid_padding_mode: Union[GridSamplePadMode, str] = GridSamplePadMode.BORDER, grid_align_corners: Optional[bool] = False, device: Optional[torch.device] = None, ): @@ -360,14 +380,22 @@ def __init__( self.def_range = def_range self.device = device self.grid_align_corners = grid_align_corners - self.grid_padding_mode = "border" + self.grid_padding_mode = grid_padding_mode + + if isinstance(def_range, (int, float)): + self.def_range = (-def_range, def_range) + else: + if len(def_range) != 2: + raise ValueError("Argument `def_range` should be a number or pair of numbers.") + + self.def_range = (min(def_range), max(def_range)) self.sfield = SmoothField( spatial_size=spatial_size, rand_size=rand_size, pad=pad, - low=-def_range, - high=def_range, + low=self.def_range[0], + high=self.def_range[1], channels=len(rand_size), mode=field_mode, align_corners=align_corners, @@ -376,7 +404,8 @@ def __init__( grid_space = spatial_size if spatial_size is not None else self.sfield.field.shape[2:] grid_ranges = [torch.linspace(-1, 1, d) for d in grid_space] - self.grid = torch.stack(torch.meshgrid(*grid_ranges, indexing="ij")).unsqueeze(0).to(self.device, self.grid_dtype) + grid = torch.meshgrid(*grid_ranges, indexing="ij") + self.grid = torch.stack(grid).unsqueeze(0).to(self.device, self.grid_dtype) def set_random_state( self, seed: Optional[int] = None, state: Optional[np.random.RandomState] = None @@ -397,7 +426,9 @@ def set_field_mode(self, mode: Union[monai.utils.InterpolateMode, str]) -> None: def set_grid_mode(self, mode: Union[monai.utils.GridSampleMode, str]) -> None: self.grid_mode = mode - def __call__(self, img: np.ndarray, randomize: bool = True, device: Optional[torch.device] = None): + def __call__( + self, img: np.ndarray, randomize: bool = True, device: Optional[torch.device] = None + ) -> NdarrayOrTensor: if randomize: self.randomize() @@ -416,9 +447,9 @@ def __call__(self, img: np.ndarray, randomize: bool = True, device: Optional[tor out = grid_sample( input=img_t, grid=dgrid, - mode=look_up_option(self.grid_mode, monai.utils.GridSampleMode).value, + mode=look_up_option(self.grid_mode, GridSampleMode).value, align_corners=self.grid_align_corners, - padding_mode=self.grid_padding_mode, + padding_mode=look_up_option(self.grid_padding_mode, GridSamplePadMode).value, ) out, *_ = convert_to_dst_type(out.squeeze(0), img) diff --git a/monai/transforms/smooth_field/dictionary.py b/monai/transforms/smooth_field/dictionary.py index 044df5e3da..263311203c 100644 --- a/monai/transforms/smooth_field/dictionary.py +++ b/monai/transforms/smooth_field/dictionary.py @@ -22,7 +22,7 @@ RandSmoothFieldAdjustIntensity, ) from monai.transforms.transform import MapTransform, RandomizableTransform, Transform -from monai.utils import ensure_tuple_rep, GridSampleMode, InterpolateMode +from monai.utils import GridSampleMode, GridSamplePadMode, InterpolateMode, ensure_tuple_rep from monai.utils.enums import TransformBackends __all__ = ["RandSmoothFieldAdjustContrastd", "RandSmoothFieldAdjustIntensityd", "RandSmoothDeformd"] @@ -34,8 +34,11 @@ 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. + Dictionary version of RandSmoothFieldAdjustContrast. + + The field is randomized once per invocation by default so the same field is applied to every selected key. The + `mode` parameter specifying interpolation mode for the field can be a single value or a sequence of values with + one for each key in `keys`. Args: keys: key names to apply the augment to @@ -115,8 +118,11 @@ def __call__(self, data: Mapping[Hashable, np.ndarray]) -> Mapping[Hashable, np. 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. + Dictionary version of RandSmoothFieldAdjustIntensity. + + The field is randomized once per invocation by default so the same field is applied to every selected key. The + `mode` parameter specifying interpolation mode for the field can be a single value or a sequence of values with + one for each key in `keys`. Args: keys: key names to apply the augment to @@ -150,7 +156,7 @@ def __init__( MapTransform.__init__(self, keys) self.apply_same_field = apply_same_field - self.mode = ensure_tuple_rep(mode,len(self.keys)) + self.mode = ensure_tuple_rep(mode, len(self.keys)) self.trans = RandSmoothFieldAdjustIntensity( spatial_size=spatial_size, @@ -195,7 +201,11 @@ def __call__(self, data: Mapping[Hashable, np.ndarray]) -> Mapping[Hashable, np. class RandSmoothDeformd(RandomizableTransform, MapTransform): """ Dictionary version of RandSmoothDeform. - + + The field is randomized once per invocation by default so the same field is applied to every selected key. The + `field_mode` parameter specifying interpolation mode for the field can be a single value or a sequence of values + with one for each key in `keys`. Similarly the `grid_mode` parameter can be one value or one per key. + Args: keys: key names to apply the augment to spatial_size: input array size to which deformation grid is interpolated @@ -204,9 +214,10 @@ class RandSmoothDeformd(RandomizableTransform, MapTransform): field_mode: interpolation mode to use when upsampling the deformation field align_corners: if True align the corners when upsampling field prob: probability transform is applied - def_range: (min, max) value of the deformation range in pixel/voxel units + def_range: value of the deformation range in image size fractions grid_dtype: type for the deformation grid calculated from the field grid_mode: interpolation mode used for sampling input using deformation grid + grid_padding_mode: padding mode used for sampling input using deformation grid grid_align_corners: if True align the corners when sampling the deformation grid apply_same_field: if True, apply the same field to each key, otherwise randomize individually device: Pytorch device to define field on @@ -223,9 +234,10 @@ def __init__( field_mode: Union[InterpolateModeType, Sequence[InterpolateModeType]] = InterpolateMode.AREA, align_corners: Optional[bool] = None, prob: float = 0.1, - def_range: float = 1.0, + def_range: Union[Sequence[float], float] = 1.0, grid_dtype=torch.float32, grid_mode: Union[GridSampleModeType, Sequence[GridSampleModeType]] = GridSampleMode.NEAREST, + grid_padding_mode: Union[GridSamplePadMode, str] = GridSamplePadMode.BORDER, grid_align_corners: Optional[bool] = False, apply_same_field: bool = True, device: Optional[torch.device] = None, @@ -233,8 +245,8 @@ def __init__( RandomizableTransform.__init__(self, prob) MapTransform.__init__(self, keys) - self.field_mode = ensure_tuple_rep(field_mode,len(self.keys)) - self.grid_mode = ensure_tuple_rep(grid_mode,len(self.keys)) + self.field_mode = ensure_tuple_rep(field_mode, len(self.keys)) + self.grid_mode = ensure_tuple_rep(grid_mode, len(self.keys)) self.apply_same_field = apply_same_field self.trans = RandSmoothDeform( @@ -247,6 +259,7 @@ def __init__( def_range=def_range, grid_dtype=grid_dtype, grid_mode=self.grid_mode[0], + grid_padding_mode=grid_padding_mode, grid_align_corners=grid_align_corners, device=device, ) diff --git a/tests/test_smooth_field.py b/tests/test_smooth_field.py index 9a666a1c76..426b37911a 100644 --- a/tests/test_smooth_field.py +++ b/tests/test_smooth_field.py @@ -41,7 +41,7 @@ TESTS_CONTRAST.append( ( - {"keys": (KEY,), "spatial_size": shape[1:], "rand_size": rand_size, "prob": 1.0, "device": device,}, + {"keys": (KEY,), "spatial_size": shape[1:], "rand_size": rand_size, "prob": 1.0, "device": device}, {KEY: in_arr}, {KEY: exp_arr}, ) From a99f0b631fe1a606530a9feebc5dd7deb1f9d1b1 Mon Sep 17 00:00:00 2001 From: Eric Kerfoot Date: Tue, 28 Dec 2021 19:23:21 +0000 Subject: [PATCH 04/16] Docs update Signed-off-by: Eric Kerfoot --- docs/source/transforms.rst | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/source/transforms.rst b/docs/source/transforms.rst index 3256eacfe6..bd9440a138 100644 --- a/docs/source/transforms.rst +++ b/docs/source/transforms.rst @@ -744,6 +744,12 @@ Smooth Field :members: :special-members: __call__ +`RandSmoothDeform` +"""""""""""""""""" +.. autoclass:: RandSmoothDeform + :members: + :special-members: __call__ + Utility ^^^^^^^ @@ -1553,6 +1559,12 @@ Smooth Field (Dict) :members: :special-members: __call__ +`RandSmoothDeformd` +""""""""""""""""""" +.. autoclass:: RandSmoothDeformd + :members: + :special-members: __call__ + Utility (Dict) ^^^^^^^^^^^^^^ From 5efba23f47c706703897546d6fa2c2d5b1947abf Mon Sep 17 00:00:00 2001 From: Eric Kerfoot Date: Wed, 29 Dec 2021 02:36:39 +0000 Subject: [PATCH 05/16] Type fixing Signed-off-by: Eric Kerfoot --- monai/transforms/smooth_field/array.py | 44 ++++++++++----------- monai/transforms/smooth_field/dictionary.py | 23 +++++------ tests/test_smooth_field.py | 2 +- 3 files changed, 34 insertions(+), 35 deletions(-) diff --git a/monai/transforms/smooth_field/array.py b/monai/transforms/smooth_field/array.py index a81d790058..1349eb270a 100644 --- a/monai/transforms/smooth_field/array.py +++ b/monai/transforms/smooth_field/array.py @@ -19,9 +19,7 @@ import monai from monai.config.type_definitions import NdarrayOrTensor -from monai.transforms.spatial.array import Resize -from monai.transforms.transform import Randomizable, RandomizableTransform, Transform -from monai.transforms.utils import rescale_array +from monai.transforms.transform import Randomizable, RandomizableTransform from monai.utils import GridSampleMode, GridSamplePadMode, InterpolateMode, ensure_tuple from monai.utils.enums import TransformBackends from monai.utils.module import look_up_option @@ -54,18 +52,18 @@ class SmoothField(Randomizable): def __init__( self, - rand_size: Union[Sequence[int], int], + rand_size: Sequence[int], pad: int = 0, pad_val: float = 0, low: float = -1.0, high: float = 1.0, channels: int = 1, - spatial_size: Optional[Union[Sequence[int], int]] = None, + spatial_size: Optional[Sequence[int]] = None, mode: Union[InterpolateMode, str] = InterpolateMode.AREA, align_corners: Optional[bool] = None, device: Optional[torch.device] = None, ): - self.rand_size = ensure_tuple(rand_size) + self.rand_size = tuple(rand_size) self.pad = pad self.low = low self.high = high @@ -74,8 +72,8 @@ def __init__( self.align_corners = align_corners self.device = device - self.spatial_size = None - self.spatial_zoom = None + self.spatial_size: Optional[Sequence[int]] = None + self.spatial_zoom: Optional[Sequence[float]] = None if low >= high: raise ValueError("Value for `low` must be less than `high` otherwise field will be zeros") @@ -94,7 +92,7 @@ def __init__( def randomize(self, data: Optional[Any] = None) -> None: self.field[self.rand_slices] = torch.from_numpy(self.R.uniform(self.low, self.high, self.crand_size)) - def set_spatial_size(self, spatial_size: Optional[Union[Sequence[int], int]]) -> None: + def set_spatial_size(self, spatial_size: Optional[Sequence[int]]) -> None: """ Set the `spatial_size` and `spatial_zoom` attributes used for interpolating the field to the given dimension, or not interpolate at all if None. @@ -164,8 +162,8 @@ class RandSmoothFieldAdjustContrast(RandomizableTransform): def __init__( self, - spatial_size: Union[Sequence[int], int], - rand_size: Union[Sequence[int], int], + spatial_size: Sequence[int], + rand_size: Sequence[int], pad: int = 0, mode: Union[InterpolateMode, str] = InterpolateMode.AREA, align_corners: Optional[bool] = None, @@ -212,7 +210,7 @@ def randomize(self, data: Optional[Any] = None) -> None: def set_mode(self, mode: Union[monai.utils.InterpolateMode, str]) -> None: self.sfield.set_mode(mode) - def __call__(self, img: np.ndarray, randomize: bool = True) -> NdarrayOrTensor: + def __call__(self, img: NdarrayOrTensor, randomize: bool = True) -> NdarrayOrTensor: """ Apply the transform to `img`, if `randomize` randomizing the smooth field otherwise reusing the previous. """ @@ -227,12 +225,12 @@ def __call__(self, img: np.ndarray, randomize: bool = True) -> NdarrayOrTensor: img_rng = img_max - img_min field = self.sfield() - field, *_ = convert_to_dst_type(field, img) + rfield, *_ = convert_to_dst_type(field, img) # everything below here is to be computed using the destination type (numpy, tensor, etc.) - 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 + img = (img - img_min) / (img_rng + 1e-10) # rescale to unit values + img = img ** rfield # contrast is changed by raising image data to a power, in this case the field out = (img * img_rng) + img_min # rescale back to the original image value range @@ -264,8 +262,8 @@ class RandSmoothFieldAdjustIntensity(RandomizableTransform): def __init__( self, - spatial_size: Union[Sequence[int], int], - rand_size: Union[Sequence[int], int], + spatial_size: Sequence[int], + rand_size: Sequence[int], pad: int = 0, mode: Union[InterpolateMode, str] = InterpolateMode.AREA, align_corners: Optional[bool] = None, @@ -312,7 +310,7 @@ def randomize(self, data: Optional[Any] = None) -> None: def set_mode(self, mode: Union[InterpolateMode, str]) -> None: self.sfield.set_mode(mode) - def __call__(self, img: np.ndarray, randomize: bool = True) -> NdarrayOrTensor: + def __call__(self, img: NdarrayOrTensor, randomize: bool = True) -> NdarrayOrTensor: """ Apply the transform to `img`, if `randomize` randomizing the smooth field otherwise reusing the previous. """ @@ -360,8 +358,8 @@ class RandSmoothDeform(RandomizableTransform): def __init__( self, - spatial_size: Union[Sequence[int], int], - rand_size: Union[Sequence[int], int], + spatial_size: Sequence[int], + rand_size: Sequence[int], pad: int = 0, field_mode: Union[InterpolateMode, str] = InterpolateMode.AREA, align_corners: Optional[bool] = None, @@ -427,7 +425,7 @@ def set_grid_mode(self, mode: Union[monai.utils.GridSampleMode, str]) -> None: self.grid_mode = mode def __call__( - self, img: np.ndarray, randomize: bool = True, device: Optional[torch.device] = None + self, img: NdarrayOrTensor, randomize: bool = True, device: Optional[torch.device] = None ) -> NdarrayOrTensor: if randomize: self.randomize() @@ -452,6 +450,6 @@ def __call__( padding_mode=look_up_option(self.grid_padding_mode, GridSamplePadMode).value, ) - out, *_ = convert_to_dst_type(out.squeeze(0), img) + out_t, *_ = convert_to_dst_type(out.squeeze(0), img) - return out + return out_t diff --git a/monai/transforms/smooth_field/dictionary.py b/monai/transforms/smooth_field/dictionary.py index 263311203c..b8aeb35308 100644 --- a/monai/transforms/smooth_field/dictionary.py +++ b/monai/transforms/smooth_field/dictionary.py @@ -15,13 +15,14 @@ import numpy as np import torch +from monai.config.type_definitions import NdarrayOrTensor from monai.config import KeysCollection from monai.transforms.smooth_field.array import ( RandSmoothDeform, RandSmoothFieldAdjustContrast, RandSmoothFieldAdjustIntensity, ) -from monai.transforms.transform import MapTransform, RandomizableTransform, Transform +from monai.transforms.transform import MapTransform, RandomizableTransform from monai.utils import GridSampleMode, GridSamplePadMode, InterpolateMode, ensure_tuple_rep from monai.utils.enums import TransformBackends @@ -58,8 +59,8 @@ class RandSmoothFieldAdjustContrastd(RandomizableTransform, MapTransform): def __init__( self, keys: KeysCollection, - spatial_size: Union[Sequence[int], int], - rand_size: Union[Sequence[int], int], + spatial_size: Sequence[int], + rand_size: Sequence[int], pad: int = 0, mode: Union[InterpolateModeType, Sequence[InterpolateModeType]] = InterpolateMode.AREA, align_corners: Optional[bool] = None, @@ -98,7 +99,7 @@ def randomize(self, data: Optional[Any] = None) -> None: if self._do_transform: self.trans.randomize() - def __call__(self, data: Mapping[Hashable, np.ndarray]) -> Mapping[Hashable, np.ndarray]: + def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> Mapping[Hashable, NdarrayOrTensor]: self.randomize() if not self._do_transform: @@ -142,8 +143,8 @@ class RandSmoothFieldAdjustIntensityd(RandomizableTransform, MapTransform): def __init__( self, keys: KeysCollection, - spatial_size: Union[Sequence[int], int], - rand_size: Union[Sequence[int], int], + spatial_size: Sequence[int], + rand_size: Sequence[int], pad: int = 0, mode: Union[InterpolateModeType, Sequence[InterpolateModeType]] = InterpolateMode.AREA, align_corners: Optional[bool] = None, @@ -180,7 +181,7 @@ def randomize(self, data: Optional[Any] = None) -> None: super().randomize(None) self.trans.randomize() - def __call__(self, data: Mapping[Hashable, np.ndarray]) -> Mapping[Hashable, np.ndarray]: + def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> Mapping[Hashable, NdarrayOrTensor]: self.randomize() if not self._do_transform: @@ -228,8 +229,8 @@ class RandSmoothDeformd(RandomizableTransform, MapTransform): def __init__( self, keys: KeysCollection, - spatial_size: Union[Sequence[int], int], - rand_size: Union[Sequence[int], int], + spatial_size: Sequence[int], + rand_size: Sequence[int], pad: int = 0, field_mode: Union[InterpolateModeType, Sequence[InterpolateModeType]] = InterpolateMode.AREA, align_corners: Optional[bool] = None, @@ -266,7 +267,7 @@ def __init__( def set_random_state( self, seed: Optional[int] = None, state: Optional[np.random.RandomState] = None - ) -> "RandSmoothFieldAdjustIntensityd": + ) -> "RandSmoothDeformd": super().set_random_state(seed, state) self.trans.set_random_state(seed, state) return self @@ -275,7 +276,7 @@ def randomize(self, data: Optional[Any] = None) -> None: super().randomize(None) self.trans.randomize() - def __call__(self, data: Mapping[Hashable, np.ndarray]) -> Mapping[Hashable, np.ndarray]: + def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> Mapping[Hashable, NdarrayOrTensor]: self.randomize() if not self._do_transform: diff --git a/tests/test_smooth_field.py b/tests/test_smooth_field.py index 426b37911a..5849b96167 100644 --- a/tests/test_smooth_field.py +++ b/tests/test_smooth_field.py @@ -37,7 +37,7 @@ device = torch.device("cpu") if isinstance(in_arr, torch.Tensor) and in_arr.get_device() >= 0: - device = in_arr.get_device() + device = torch.device(in_arr.get_device()) TESTS_CONTRAST.append( ( From fabb95b54de3aa027a57b4f7b3985578fe28933f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 29 Dec 2021 02:56:41 +0000 Subject: [PATCH 06/16] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- monai/transforms/smooth_field/array.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monai/transforms/smooth_field/array.py b/monai/transforms/smooth_field/array.py index 1349eb270a..e9c683e5df 100644 --- a/monai/transforms/smooth_field/array.py +++ b/monai/transforms/smooth_field/array.py @@ -225,7 +225,7 @@ def __call__(self, img: NdarrayOrTensor, randomize: bool = True) -> NdarrayOrTen img_rng = img_max - img_min field = self.sfield() - rfield, *_ = convert_to_dst_type(field, img) + rfield, *_ = convert_to_dst_type(field, img) # everything below here is to be computed using the destination type (numpy, tensor, etc.) From 2dd92975fdf5ec4fad55f8ad5822dce781defe80 Mon Sep 17 00:00:00 2001 From: monai-bot Date: Wed, 29 Dec 2021 03:01:39 +0000 Subject: [PATCH 07/16] [MONAI] python code formatting Signed-off-by: monai-bot --- 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 b8aeb35308..4eca541fcc 100644 --- a/monai/transforms/smooth_field/dictionary.py +++ b/monai/transforms/smooth_field/dictionary.py @@ -15,8 +15,8 @@ import numpy as np import torch -from monai.config.type_definitions import NdarrayOrTensor from monai.config import KeysCollection +from monai.config.type_definitions import NdarrayOrTensor from monai.transforms.smooth_field.array import ( RandSmoothDeform, RandSmoothFieldAdjustContrast, From 83f8eaa91553b808917f0fb3e01d43c3f15b8d51 Mon Sep 17 00:00:00 2001 From: Eric Kerfoot Date: Wed, 29 Dec 2021 03:12:24 +0000 Subject: [PATCH 08/16] Fix Signed-off-by: Eric Kerfoot --- monai/transforms/smooth_field/array.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monai/transforms/smooth_field/array.py b/monai/transforms/smooth_field/array.py index e9c683e5df..71660cc6be 100644 --- a/monai/transforms/smooth_field/array.py +++ b/monai/transforms/smooth_field/array.py @@ -20,7 +20,7 @@ import monai from monai.config.type_definitions import NdarrayOrTensor from monai.transforms.transform import Randomizable, RandomizableTransform -from monai.utils import GridSampleMode, GridSamplePadMode, InterpolateMode, ensure_tuple +from monai.utils import GridSampleMode, GridSamplePadMode, InterpolateMode from monai.utils.enums import TransformBackends from monai.utils.module import look_up_option from monai.utils.type_conversion import convert_to_dst_type, convert_to_tensor From e99232e425904d60bed7b0c3b363e086e7a9eb15 Mon Sep 17 00:00:00 2001 From: Eric Kerfoot Date: Wed, 29 Dec 2021 03:27:32 +0000 Subject: [PATCH 09/16] Fix Signed-off-by: Eric Kerfoot --- monai/transforms/smooth_field/array.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/monai/transforms/smooth_field/array.py b/monai/transforms/smooth_field/array.py index 71660cc6be..ed6e8df305 100644 --- a/monai/transforms/smooth_field/array.py +++ b/monai/transforms/smooth_field/array.py @@ -22,7 +22,7 @@ from monai.transforms.transform import Randomizable, RandomizableTransform from monai.utils import GridSampleMode, GridSamplePadMode, InterpolateMode from monai.utils.enums import TransformBackends -from monai.utils.module import look_up_option +from monai.utils.module import look_up_option, pytorch_after from monai.utils.type_conversion import convert_to_dst_type, convert_to_tensor __all__ = ["SmoothField", "RandSmoothFieldAdjustContrast", "RandSmoothFieldAdjustIntensity", "RandSmoothDeform"] @@ -402,7 +402,12 @@ def __init__( grid_space = spatial_size if spatial_size is not None else self.sfield.field.shape[2:] grid_ranges = [torch.linspace(-1, 1, d) for d in grid_space] - grid = torch.meshgrid(*grid_ranges, indexing="ij") + + if pytorch_after(1, 10): + grid = torch.meshgrid(*grid_ranges, indexing="ij") + else: + grid = torch.meshgrid(*grid_ranges) + self.grid = torch.stack(grid).unsqueeze(0).to(self.device, self.grid_dtype) def set_random_state( From 3c6fb2fac56326f0d56de71dab0b082ad4aafeee Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 29 Dec 2021 03:27:57 +0000 Subject: [PATCH 10/16] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- monai/transforms/smooth_field/array.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monai/transforms/smooth_field/array.py b/monai/transforms/smooth_field/array.py index ed6e8df305..4f17d2f84c 100644 --- a/monai/transforms/smooth_field/array.py +++ b/monai/transforms/smooth_field/array.py @@ -402,12 +402,12 @@ def __init__( grid_space = spatial_size if spatial_size is not None else self.sfield.field.shape[2:] grid_ranges = [torch.linspace(-1, 1, d) for d in grid_space] - + if pytorch_after(1, 10): grid = torch.meshgrid(*grid_ranges, indexing="ij") else: grid = torch.meshgrid(*grid_ranges) - + self.grid = torch.stack(grid).unsqueeze(0).to(self.device, self.grid_dtype) def set_random_state( From 9bafd9fee1f59b26b2a8827c1ab10e4613147bc0 Mon Sep 17 00:00:00 2001 From: Eric Kerfoot Date: Wed, 29 Dec 2021 05:25:42 +0000 Subject: [PATCH 11/16] Fix for moveaxis Signed-off-by: Eric Kerfoot --- monai/transforms/smooth_field/array.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/monai/transforms/smooth_field/array.py b/monai/transforms/smooth_field/array.py index 4f17d2f84c..aa2d0194e5 100644 --- a/monai/transforms/smooth_field/array.py +++ b/monai/transforms/smooth_field/array.py @@ -20,6 +20,7 @@ import monai from monai.config.type_definitions import NdarrayOrTensor from monai.transforms.transform import Randomizable, RandomizableTransform +from monai.transforms.utils_pytorch_numpy_unification import moveaxis from monai.utils import GridSampleMode, GridSamplePadMode, InterpolateMode from monai.utils.enums import TransformBackends from monai.utils.module import look_up_option, pytorch_after @@ -443,7 +444,7 @@ def __call__( field = self.sfield() dgrid = self.grid + field.to(self.grid_dtype) - dgrid = dgrid.moveaxis(1, -1) + dgrid = moveaxis(dgrid, 1, -1) img_t = convert_to_tensor(img[None], torch.float32, device) From 114299aae337368ce05bbe81013a2526154d77d7 Mon Sep 17 00:00:00 2001 From: Eric Kerfoot Date: Wed, 29 Dec 2021 05:50:03 +0000 Subject: [PATCH 12/16] Fix for moveaxis Signed-off-by: Eric Kerfoot --- monai/transforms/smooth_field/array.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monai/transforms/smooth_field/array.py b/monai/transforms/smooth_field/array.py index aa2d0194e5..cbaef2f0a0 100644 --- a/monai/transforms/smooth_field/array.py +++ b/monai/transforms/smooth_field/array.py @@ -444,7 +444,7 @@ def __call__( field = self.sfield() dgrid = self.grid + field.to(self.grid_dtype) - dgrid = moveaxis(dgrid, 1, -1) + dgrid = moveaxis(dgrid, 1, -1) # type: ignore img_t = convert_to_tensor(img[None], torch.float32, device) From c042beaa2638bb2d6c8ce8f9e91eea464dd90597 Mon Sep 17 00:00:00 2001 From: Eric Kerfoot Date: Wed, 29 Dec 2021 17:59:55 +0000 Subject: [PATCH 13/16] Adding example images, random field sized reduced to (10,10,10) Signed-off-by: Eric Kerfoot --- docs/source/transforms.rst | 4 ++ .../transforms/utils_create_transform_ims.py | 55 +++++++++++-------- 2 files changed, 35 insertions(+), 24 deletions(-) diff --git a/docs/source/transforms.rst b/docs/source/transforms.rst index bd9440a138..49ed4c9e6c 100644 --- a/docs/source/transforms.rst +++ b/docs/source/transforms.rst @@ -746,6 +746,8 @@ Smooth Field `RandSmoothDeform` """""""""""""""""" +.. image:: https://github.com/Project-MONAI/DocImages/raw/main/transforms/RandSmoothDeform.png + :alt: example of RandSmoothDeform .. autoclass:: RandSmoothDeform :members: :special-members: __call__ @@ -1561,6 +1563,8 @@ Smooth Field (Dict) `RandSmoothDeformd` """"""""""""""""""" +.. image:: https://github.com/Project-MONAI/DocImages/raw/main/transforms/RandSmoothDeformd.png + :alt: example of RandSmoothDeformd .. autoclass:: RandSmoothDeformd :members: :special-members: __call__ diff --git a/monai/transforms/utils_create_transform_ims.py b/monai/transforms/utils_create_transform_ims.py index 64d4e345c1..59f18b93a8 100644 --- a/monai/transforms/utils_create_transform_ims.py +++ b/monai/transforms/utils_create_transform_ims.py @@ -145,8 +145,16 @@ ) from monai.transforms.post.array import KeepLargestConnectedComponent, LabelFilter, LabelToContour from monai.transforms.post.dictionary import AsDiscreted, KeepLargestConnectedComponentd, LabelFilterd, LabelToContourd -from monai.transforms.smooth_field.array import RandSmoothFieldAdjustContrast, RandSmoothFieldAdjustIntensity -from monai.transforms.smooth_field.dictionary import RandSmoothFieldAdjustContrastd, RandSmoothFieldAdjustIntensityd +from monai.transforms.smooth_field.array import ( + RandSmoothDeform, + RandSmoothFieldAdjustContrast, + RandSmoothFieldAdjustIntensity, +) +from monai.transforms.smooth_field.dictionary import ( + RandSmoothDeformd, + RandSmoothFieldAdjustContrastd, + RandSmoothFieldAdjustIntensityd, +) from monai.transforms.spatial.array import ( GridDistortion, Rand2DElastic, @@ -388,9 +396,7 @@ def get_images(data, is_label=False): return out, shape_str -def create_transform_im( - transform, transform_args, data, ndim=3, colorbar=False, update_doc=True, seed=0, is_post=False -): +def create_transform_im(transform, transform_args, data, ndim=3, colorbar=False, update_doc=True, seed=0, is_post=False): """Create an image with the before and after of the transform. Also update the transform's documentation to point to this image.""" @@ -511,9 +517,7 @@ def create_transform_im( create_transform_im(ScaleIntensity, dict(minv=0, maxv=10), data, colorbar=True) create_transform_im(ScaleIntensityd, dict(keys=CommonKeys.IMAGE, minv=0, maxv=10), data, colorbar=True) create_transform_im(RandScaleIntensity, dict(prob=1.0, factors=(5, 10)), data, colorbar=True) - create_transform_im( - RandScaleIntensityd, dict(keys=CommonKeys.IMAGE, prob=1.0, factors=(5, 10)), data, colorbar=True - ) + create_transform_im(RandScaleIntensityd, dict(keys=CommonKeys.IMAGE, prob=1.0, factors=(5, 10)), data, colorbar=True) create_transform_im(DivisiblePad, dict(k=64), data) create_transform_im(DivisiblePadd, dict(keys=keys, k=64), data) create_transform_im(CropForeground, dict(), data) @@ -539,9 +543,7 @@ def create_transform_im( create_transform_im(ShiftIntensity, dict(offset=1), data, colorbar=True) create_transform_im(ShiftIntensityd, dict(keys=CommonKeys.IMAGE, offset=1), data, colorbar=True) create_transform_im(RandShiftIntensity, dict(prob=1.0, offsets=(10, 20)), data, colorbar=True) - create_transform_im( - RandShiftIntensityd, dict(keys=CommonKeys.IMAGE, prob=1.0, offsets=(10, 20)), data, colorbar=True - ) + create_transform_im(RandShiftIntensityd, dict(keys=CommonKeys.IMAGE, prob=1.0, offsets=(10, 20)), data, colorbar=True) create_transform_im(StdShiftIntensity, dict(factor=10), data, colorbar=True) create_transform_im(StdShiftIntensityd, dict(keys=CommonKeys.IMAGE, factor=10), data, colorbar=True) create_transform_im(RandStdShiftIntensity, dict(prob=1.0, factors=(5, 10)), data, colorbar=True) @@ -629,9 +631,7 @@ def create_transform_im( ) create_transform_im( RandCropByLabelClasses, - dict( - spatial_size=(100, 100, 100), label=data[CommonKeys.LABEL] > 0, num_classes=2, ratios=[0, 1], num_samples=4 - ), + dict(spatial_size=(100, 100, 100), label=data[CommonKeys.LABEL] > 0, num_classes=2, ratios=[0, 1], num_samples=4), data, ) create_transform_im( @@ -655,9 +655,7 @@ def create_transform_im( create_transform_im(AsDiscrete, dict(to_onehot=None, threshold=10), data, is_post=True, colorbar=True) create_transform_im(AsDiscreted, dict(keys=CommonKeys.LABEL, to_onehot=None, threshold=10), data, is_post=True) create_transform_im(LabelFilter, dict(applied_labels=(1, 2, 3, 4, 5, 6)), data, is_post=True) - create_transform_im( - LabelFilterd, dict(keys=CommonKeys.LABEL, applied_labels=(1, 2, 3, 4, 5, 6)), data, is_post=True - ) + create_transform_im(LabelFilterd, dict(keys=CommonKeys.LABEL, applied_labels=(1, 2, 3, 4, 5, 6)), data, is_post=True) create_transform_im(LabelToContour, dict(), data, is_post=True) create_transform_im(LabelToContourd, dict(keys=CommonKeys.LABEL), data, is_post=True) create_transform_im(Spacing, dict(pixdim=(5, 5, 5), image_only=True), data) @@ -677,9 +675,7 @@ def create_transform_im( ) create_transform_im( GridDistortiond, - dict( - keys=keys, num_cells=3, distort_steps=[(1.5,) * 4] * 3, mode=["bilinear", "nearest"], padding_mode="zeros" - ), + dict(keys=keys, num_cells=3, distort_steps=[(1.5,) * 4] * 3, mode=["bilinear", "nearest"], padding_mode="zeros"), data, ) create_transform_im(RandGridDistortion, dict(num_cells=3, prob=1.0, distort_limit=(-0.1, 0.1)), data) @@ -689,20 +685,31 @@ def create_transform_im( data, ) create_transform_im( - RandSmoothFieldAdjustContrast, dict(spatial_size=(217, 217, 217), rand_size=(100, 100, 100), prob=1.0), data + RandSmoothFieldAdjustContrast, dict(spatial_size=(217, 217, 217), rand_size=(10, 10, 10), prob=1.0), data ) create_transform_im( RandSmoothFieldAdjustContrastd, - dict(keys=keys, spatial_size=(217, 217, 217), rand_size=(100, 100, 100), prob=1.0), + dict(keys=keys, spatial_size=(217, 217, 217), rand_size=(10, 10, 10), prob=1.0), data, ) create_transform_im( RandSmoothFieldAdjustIntensity, - dict(spatial_size=(217, 217, 217), rand_size=(100, 100, 100), prob=1.0, gamma=(0.5, 4.5)), + dict(spatial_size=(217, 217, 217), rand_size=(10, 10, 10), prob=1.0, gamma=(0.5, 4.5)), data, ) create_transform_im( RandSmoothFieldAdjustIntensityd, - dict(keys=keys, spatial_size=(217, 217, 217), rand_size=(100, 100, 100), prob=1.0, gamma=(0.5, 4.5)), + dict(keys=keys, spatial_size=(217, 217, 217), rand_size=(10, 10, 10), prob=1.0, gamma=(0.5, 4.5)), + data, + ) + + create_transform_im( + RandSmoothDeform, + dict(spatial_size=(217, 217, 217), rand_size=(10, 10, 10), prob=1.0, def_range=0.05,grid_mode="blinear"), + data, + ) + create_transform_im( + RandSmoothDeformd, + dict(keys=keys, spatial_size=(217, 217, 217), rand_size=(10, 10, 10), prob=1.0, def_range=0.05,grid_mode="blinear"), data, ) From 8f0cca3659536f234ee15fb6be1a29091ff4a421 Mon Sep 17 00:00:00 2001 From: Eric Kerfoot Date: Wed, 29 Dec 2021 18:01:43 +0000 Subject: [PATCH 14/16] Changed backend Signed-off-by: Eric Kerfoot --- monai/transforms/smooth_field/array.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monai/transforms/smooth_field/array.py b/monai/transforms/smooth_field/array.py index cbaef2f0a0..172d98e29e 100644 --- a/monai/transforms/smooth_field/array.py +++ b/monai/transforms/smooth_field/array.py @@ -355,7 +355,7 @@ class RandSmoothDeform(RandomizableTransform): device: Pytorch device to define field on """ - backend = [TransformBackends.TORCH, TransformBackends.NUMPY] + backend = [TransformBackends.TORCH] def __init__( self, From d6f10528d620c174103e7eb73d0467538cf7c380 Mon Sep 17 00:00:00 2001 From: monai-bot Date: Wed, 29 Dec 2021 18:15:29 +0000 Subject: [PATCH 15/16] [MONAI] python code formatting Signed-off-by: monai-bot --- .../transforms/utils_create_transform_ims.py | 35 ++++++++++++++----- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/monai/transforms/utils_create_transform_ims.py b/monai/transforms/utils_create_transform_ims.py index 59f18b93a8..ab282d5332 100644 --- a/monai/transforms/utils_create_transform_ims.py +++ b/monai/transforms/utils_create_transform_ims.py @@ -396,7 +396,9 @@ def get_images(data, is_label=False): return out, shape_str -def create_transform_im(transform, transform_args, data, ndim=3, colorbar=False, update_doc=True, seed=0, is_post=False): +def create_transform_im( + transform, transform_args, data, ndim=3, colorbar=False, update_doc=True, seed=0, is_post=False +): """Create an image with the before and after of the transform. Also update the transform's documentation to point to this image.""" @@ -517,7 +519,9 @@ def create_transform_im(transform, transform_args, data, ndim=3, colorbar=False, create_transform_im(ScaleIntensity, dict(minv=0, maxv=10), data, colorbar=True) create_transform_im(ScaleIntensityd, dict(keys=CommonKeys.IMAGE, minv=0, maxv=10), data, colorbar=True) create_transform_im(RandScaleIntensity, dict(prob=1.0, factors=(5, 10)), data, colorbar=True) - create_transform_im(RandScaleIntensityd, dict(keys=CommonKeys.IMAGE, prob=1.0, factors=(5, 10)), data, colorbar=True) + create_transform_im( + RandScaleIntensityd, dict(keys=CommonKeys.IMAGE, prob=1.0, factors=(5, 10)), data, colorbar=True + ) create_transform_im(DivisiblePad, dict(k=64), data) create_transform_im(DivisiblePadd, dict(keys=keys, k=64), data) create_transform_im(CropForeground, dict(), data) @@ -543,7 +547,9 @@ def create_transform_im(transform, transform_args, data, ndim=3, colorbar=False, create_transform_im(ShiftIntensity, dict(offset=1), data, colorbar=True) create_transform_im(ShiftIntensityd, dict(keys=CommonKeys.IMAGE, offset=1), data, colorbar=True) create_transform_im(RandShiftIntensity, dict(prob=1.0, offsets=(10, 20)), data, colorbar=True) - create_transform_im(RandShiftIntensityd, dict(keys=CommonKeys.IMAGE, prob=1.0, offsets=(10, 20)), data, colorbar=True) + create_transform_im( + RandShiftIntensityd, dict(keys=CommonKeys.IMAGE, prob=1.0, offsets=(10, 20)), data, colorbar=True + ) create_transform_im(StdShiftIntensity, dict(factor=10), data, colorbar=True) create_transform_im(StdShiftIntensityd, dict(keys=CommonKeys.IMAGE, factor=10), data, colorbar=True) create_transform_im(RandStdShiftIntensity, dict(prob=1.0, factors=(5, 10)), data, colorbar=True) @@ -631,7 +637,9 @@ def create_transform_im(transform, transform_args, data, ndim=3, colorbar=False, ) create_transform_im( RandCropByLabelClasses, - dict(spatial_size=(100, 100, 100), label=data[CommonKeys.LABEL] > 0, num_classes=2, ratios=[0, 1], num_samples=4), + dict( + spatial_size=(100, 100, 100), label=data[CommonKeys.LABEL] > 0, num_classes=2, ratios=[0, 1], num_samples=4 + ), data, ) create_transform_im( @@ -655,7 +663,9 @@ def create_transform_im(transform, transform_args, data, ndim=3, colorbar=False, create_transform_im(AsDiscrete, dict(to_onehot=None, threshold=10), data, is_post=True, colorbar=True) create_transform_im(AsDiscreted, dict(keys=CommonKeys.LABEL, to_onehot=None, threshold=10), data, is_post=True) create_transform_im(LabelFilter, dict(applied_labels=(1, 2, 3, 4, 5, 6)), data, is_post=True) - create_transform_im(LabelFilterd, dict(keys=CommonKeys.LABEL, applied_labels=(1, 2, 3, 4, 5, 6)), data, is_post=True) + create_transform_im( + LabelFilterd, dict(keys=CommonKeys.LABEL, applied_labels=(1, 2, 3, 4, 5, 6)), data, is_post=True + ) create_transform_im(LabelToContour, dict(), data, is_post=True) create_transform_im(LabelToContourd, dict(keys=CommonKeys.LABEL), data, is_post=True) create_transform_im(Spacing, dict(pixdim=(5, 5, 5), image_only=True), data) @@ -675,7 +685,9 @@ def create_transform_im(transform, transform_args, data, ndim=3, colorbar=False, ) create_transform_im( GridDistortiond, - dict(keys=keys, num_cells=3, distort_steps=[(1.5,) * 4] * 3, mode=["bilinear", "nearest"], padding_mode="zeros"), + dict( + keys=keys, num_cells=3, distort_steps=[(1.5,) * 4] * 3, mode=["bilinear", "nearest"], padding_mode="zeros" + ), data, ) create_transform_im(RandGridDistortion, dict(num_cells=3, prob=1.0, distort_limit=(-0.1, 0.1)), data) @@ -705,11 +717,18 @@ def create_transform_im(transform, transform_args, data, ndim=3, colorbar=False, create_transform_im( RandSmoothDeform, - dict(spatial_size=(217, 217, 217), rand_size=(10, 10, 10), prob=1.0, def_range=0.05,grid_mode="blinear"), + dict(spatial_size=(217, 217, 217), rand_size=(10, 10, 10), prob=1.0, def_range=0.05, grid_mode="blinear"), data, ) create_transform_im( RandSmoothDeformd, - dict(keys=keys, spatial_size=(217, 217, 217), rand_size=(10, 10, 10), prob=1.0, def_range=0.05,grid_mode="blinear"), + dict( + keys=keys, + spatial_size=(217, 217, 217), + rand_size=(10, 10, 10), + prob=1.0, + def_range=0.05, + grid_mode="blinear", + ), data, ) From 1ebb7a195dc2f684233822707ff7089467ed9d4a Mon Sep 17 00:00:00 2001 From: Eric Kerfoot Date: Thu, 30 Dec 2021 00:49:40 +0000 Subject: [PATCH 16/16] Tweak Signed-off-by: Eric Kerfoot --- monai/transforms/smooth_field/array.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monai/transforms/smooth_field/array.py b/monai/transforms/smooth_field/array.py index 172d98e29e..31ce76e5b5 100644 --- a/monai/transforms/smooth_field/array.py +++ b/monai/transforms/smooth_field/array.py @@ -115,7 +115,7 @@ def __call__(self, randomize=False) -> torch.Tensor: if randomize: self.randomize() - field = self.field.to(self.device).clone() + field = self.field.clone() if self.spatial_zoom is not None: resized_field = interpolate( # type: ignore @@ -131,7 +131,7 @@ def __call__(self, randomize=False) -> torch.Tensor: minv = self.field.min() maxv = self.field.max() - # faster than rescale_array (?) + # faster than rescale_array, this uses in-place operations and doesn't perform unneeded range checks norm_field = (resized_field.squeeze(0) - mina).div_(maxa - mina) field = norm_field.mul_(maxv - minv).add_(minv)