From 898b3e83c70f0a4361530b3ef50b798926daf6fd Mon Sep 17 00:00:00 2001 From: Nic Ma Date: Mon, 20 Dec 2021 19:44:31 +0800 Subject: [PATCH 1/2] [DLMED] change to utility transforms Signed-off-by: Nic Ma --- docs/source/transforms.rst | 25 +++++++-------- monai/transforms/__init__.py | 8 ++--- monai/transforms/spatial/array.py | 44 -------------------------- monai/transforms/spatial/dictionary.py | 33 ------------------- monai/transforms/utility/array.py | 44 ++++++++++++++++++++++++++ monai/transforms/utility/dictionary.py | 33 +++++++++++++++++++ 6 files changed, 93 insertions(+), 94 deletions(-) diff --git a/docs/source/transforms.rst b/docs/source/transforms.rst index 653647a3fa..fc60a2c0f3 100644 --- a/docs/source/transforms.rst +++ b/docs/source/transforms.rst @@ -723,13 +723,6 @@ Spatial :members: :special-members: __call__ -`AddCoordinateChannels` -""""""""""""""""""""""" -.. autoclass:: AddCoordinateChannels - :members: - :special-members: __call__ - - Smooth Field ^^^^^^^^^^^^ @@ -929,6 +922,12 @@ Utility :members: :special-members: __call__ +`AddCoordinateChannels` +""""""""""""""""""""""" +.. autoclass:: AddCoordinateChannels + :members: + :special-members: __call__ + Dictionary Transforms --------------------- @@ -1513,12 +1512,6 @@ Spatial (Dict) :members: :special-members: __call__ -`AddCoordinateChannelsd` -"""""""""""""""""""""""" -.. autoclass:: AddCoordinateChannelsd - :members: - :special-members: __call__ - `GridDistortiond` """"""""""""""""" .. autoclass:: GridDistortiond @@ -1755,6 +1748,12 @@ Utility (Dict) :members: :special-members: __call__ +`AddCoordinateChannelsd` +"""""""""""""""""""""""" +.. autoclass:: AddCoordinateChannelsd + :members: + :special-members: __call__ + Transform Adaptors ------------------ .. automodule:: monai.transforms.adaptors diff --git a/monai/transforms/__init__.py b/monai/transforms/__init__.py index 720c35820e..bdda931a44 100644 --- a/monai/transforms/__init__.py +++ b/monai/transforms/__init__.py @@ -280,7 +280,6 @@ from .smooth_field.array import RandSmoothFieldAdjustContrast, RandSmoothFieldAdjustIntensity, SmoothField from .smooth_field.dictionary import RandSmoothFieldAdjustContrastd, RandSmoothFieldAdjustIntensityd from .spatial.array import ( - AddCoordinateChannels, Affine, AffineGrid, Flip, @@ -305,9 +304,6 @@ Zoom, ) from .spatial.dictionary import ( - AddCoordinateChannelsd, - AddCoordinateChannelsD, - AddCoordinateChannelsDict, Affined, AffineD, AffineDict, @@ -366,6 +362,7 @@ from .transform import MapTransform, Randomizable, RandomizableTransform, ThreadUnsafe, Transform, apply_transform from .utility.array import ( AddChannel, + AddCoordinateChannels, AddExtremePointsChannel, AsChannelFirst, AsChannelLast, @@ -401,6 +398,9 @@ AddChanneld, AddChannelD, AddChannelDict, + AddCoordinateChannelsd, + AddCoordinateChannelsD, + AddCoordinateChannelsDict, AddExtremePointsChanneld, AddExtremePointsChannelD, AddExtremePointsChannelDict, diff --git a/monai/transforms/spatial/array.py b/monai/transforms/spatial/array.py index 8d1558c230..c3697338a7 100644 --- a/monai/transforms/spatial/array.py +++ b/monai/transforms/spatial/array.py @@ -77,7 +77,6 @@ "RandAffine", "Rand2DElastic", "Rand3DElastic", - "AddCoordinateChannels", ] RandRange = Optional[Union[Sequence[Union[Tuple[float, float], float]], float]] @@ -2024,49 +2023,6 @@ def __call__( return out -class AddCoordinateChannels(Transform): - """ - Appends additional channels encoding coordinates of the input. Useful when e.g. training using patch-based sampling, - to allow feeding of the patch's location into the network. - - This can be seen as a input-only version of CoordConv: - - Liu, R. et al. An Intriguing Failing of Convolutional Neural Networks and the CoordConv Solution, NeurIPS 2018. - """ - - backend = [TransformBackends.TORCH, TransformBackends.NUMPY] - - def __init__(self, spatial_channels: Sequence[int]) -> None: - """ - Args: - spatial_channels: the spatial dimensions that are to have their coordinates encoded in a channel and - appended to the input. E.g., `(1,2,3)` will append three channels to the input, encoding the - coordinates of the input's three spatial dimensions (0 is reserved for the channel dimension). - """ - self.spatial_channels = spatial_channels - - def __call__(self, img: NdarrayOrTensor) -> NdarrayOrTensor: - """ - Args: - img: data to be transformed, assuming `img` is channel first. - """ - if max(self.spatial_channels) > img.ndim - 1: - raise ValueError( - f"input has {img.ndim-1} spatial dimensions, cannot add AddCoordinateChannels channel for " - f"dim {max(self.spatial_channels)}." - ) - if 0 in self.spatial_channels: - raise ValueError("cannot add AddCoordinateChannels channel for dimension 0, as 0 is channel dim.") - - spatial_dims = img.shape[1:] - coord_channels = np.array(np.meshgrid(*tuple(np.linspace(-0.5, 0.5, s) for s in spatial_dims), indexing="ij")) - coord_channels, *_ = convert_to_dst_type(coord_channels, img) # type: ignore - # only keep required dimensions. need to subtract 1 since im will be 0-based - # but user input is 1-based (because channel dim is 0) - coord_channels = coord_channels[[s - 1 for s in self.spatial_channels]] - return concatenate((img, coord_channels), axis=0) - - class GridDistortion(Transform): backend = [TransformBackends.TORCH] diff --git a/monai/transforms/spatial/dictionary.py b/monai/transforms/spatial/dictionary.py index e4fc4cf3a9..c2785ea83d 100644 --- a/monai/transforms/spatial/dictionary.py +++ b/monai/transforms/spatial/dictionary.py @@ -29,7 +29,6 @@ from monai.transforms.croppad.array import CenterSpatialCrop, SpatialPad from monai.transforms.inverse import InvertibleTransform from monai.transforms.spatial.array import ( - AddCoordinateChannels, Affine, AffineGrid, Flip, @@ -123,9 +122,6 @@ "ZoomDict", "RandZoomD", "RandZoomDict", - "AddCoordinateChannelsd", - "AddCoordinateChannelsD", - "AddCoordinateChannelsDict", ] GridSampleModeSequence = Union[Sequence[Union[GridSampleMode, str]], GridSampleMode, str] @@ -1756,34 +1752,6 @@ def inverse(self, data: Mapping[Hashable, NdarrayOrTensor]) -> Dict[Hashable, Nd return d -class AddCoordinateChannelsd(MapTransform): - """ - Dictionary-based wrapper of :py:class:`monai.transforms.AddCoordinateChannels`. - """ - - backend = AddCoordinateChannels.backend - - def __init__(self, keys: KeysCollection, spatial_channels: Sequence[int], allow_missing_keys: bool = False) -> None: - """ - Args: - keys: keys of the corresponding items to be transformed. - See also: :py:class:`monai.transforms.compose.MapTransform` - allow_missing_keys: don't raise exception if key is missing. - spatial_channels: the spatial dimensions that are to have their coordinates encoded in a channel and - appended to the input. E.g., `(1,2,3)` will append three channels to the input, encoding the - coordinates of the input's three spatial dimensions. It is assumed dimension 0 is the channel. - - """ - super().__init__(keys, allow_missing_keys) - self.add_coordinate_channels = AddCoordinateChannels(spatial_channels) - - def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> Dict[Hashable, NdarrayOrTensor]: - d = dict(data) - for key in self.key_iterator(d): - d[key] = self.add_coordinate_channels(d[key]) - return d - - class GridDistortiond(MapTransform): """ Dictionary-based wrapper of :py:class:`monai.transforms.GridDistortion`. @@ -1919,4 +1887,3 @@ def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> Dict[Hashable, N RandRotateD = RandRotateDict = RandRotated ZoomD = ZoomDict = Zoomd RandZoomD = RandZoomDict = RandZoomd -AddCoordinateChannelsD = AddCoordinateChannelsDict = AddCoordinateChannelsd diff --git a/monai/transforms/utility/array.py b/monai/transforms/utility/array.py index 3544dd2c41..1e9195fc16 100644 --- a/monai/transforms/utility/array.py +++ b/monai/transforms/utility/array.py @@ -56,6 +56,7 @@ "AsChannelFirst", "AsChannelLast", "AddChannel", + "AddCoordinateChannels", "EnsureChannelFirst", "EnsureType", "RepeatChannel", @@ -1254,3 +1255,46 @@ def __call__(self, data): if not self._do_transform: return data return super().__call__(data) + + +class AddCoordinateChannels(Transform): + """ + Appends additional channels encoding coordinates of the input. Useful when e.g. training using patch-based sampling, + to allow feeding of the patch's location into the network. + + This can be seen as a input-only version of CoordConv: + + Liu, R. et al. An Intriguing Failing of Convolutional Neural Networks and the CoordConv Solution, NeurIPS 2018. + """ + + backend = [TransformBackends.TORCH, TransformBackends.NUMPY] + + def __init__(self, spatial_channels: Sequence[int]) -> None: + """ + Args: + spatial_channels: the spatial dimensions that are to have their coordinates encoded in a channel and + appended to the input. E.g., `(1,2,3)` will append three channels to the input, encoding the + coordinates of the input's three spatial dimensions (0 is reserved for the channel dimension). + """ + self.spatial_channels = spatial_channels + + def __call__(self, img: NdarrayOrTensor) -> NdarrayOrTensor: + """ + Args: + img: data to be transformed, assuming `img` is channel first. + """ + if max(self.spatial_channels) > img.ndim - 1: + raise ValueError( + f"input has {img.ndim-1} spatial dimensions, cannot add AddCoordinateChannels channel for " + f"dim {max(self.spatial_channels)}." + ) + if 0 in self.spatial_channels: + raise ValueError("cannot add AddCoordinateChannels channel for dimension 0, as 0 is channel dim.") + + spatial_dims = img.shape[1:] + coord_channels = np.array(np.meshgrid(*tuple(np.linspace(-0.5, 0.5, s) for s in spatial_dims), indexing="ij")) + coord_channels, *_ = convert_to_dst_type(coord_channels, img) # type: ignore + # only keep required dimensions. need to subtract 1 since im will be 0-based + # but user input is 1-based (because channel dim is 0) + coord_channels = coord_channels[[s - 1 for s in self.spatial_channels]] + return concatenate((img, coord_channels), axis=0) diff --git a/monai/transforms/utility/dictionary.py b/monai/transforms/utility/dictionary.py index 447fdc6f17..296f4aa111 100644 --- a/monai/transforms/utility/dictionary.py +++ b/monai/transforms/utility/dictionary.py @@ -30,6 +30,7 @@ from monai.transforms.transform import MapTransform, Randomizable, RandomizableTransform from monai.transforms.utility.array import ( AddChannel, + AddCoordinateChannels, AddExtremePointsChannel, AsChannelFirst, AsChannelLast, @@ -69,6 +70,9 @@ "AddChannelD", "AddChannelDict", "AddChanneld", + "AddCoordinateChannelsD", + "AddCoordinateChannelsDict", + "AddCoordinateChannelsd", "AddExtremePointsChannelD", "AddExtremePointsChannelDict", "AddExtremePointsChanneld", @@ -1589,6 +1593,34 @@ def __call__(self, data): return super().__call__(data) +class AddCoordinateChannelsd(MapTransform): + """ + Dictionary-based wrapper of :py:class:`monai.transforms.AddCoordinateChannels`. + """ + + backend = AddCoordinateChannels.backend + + def __init__(self, keys: KeysCollection, spatial_channels: Sequence[int], allow_missing_keys: bool = False) -> None: + """ + Args: + keys: keys of the corresponding items to be transformed. + See also: :py:class:`monai.transforms.compose.MapTransform` + allow_missing_keys: don't raise exception if key is missing. + spatial_channels: the spatial dimensions that are to have their coordinates encoded in a channel and + appended to the input. E.g., `(1,2,3)` will append three channels to the input, encoding the + coordinates of the input's three spatial dimensions. It is assumed dimension 0 is the channel. + + """ + super().__init__(keys, allow_missing_keys) + self.add_coordinate_channels = AddCoordinateChannels(spatial_channels) + + def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> Dict[Hashable, NdarrayOrTensor]: + d = dict(data) + for key in self.key_iterator(d): + d[key] = self.add_coordinate_channels(d[key]) + return d + + IdentityD = IdentityDict = Identityd AsChannelFirstD = AsChannelFirstDict = AsChannelFirstd AsChannelLastD = AsChannelLastDict = AsChannelLastd @@ -1627,3 +1659,4 @@ def __call__(self, data): ToDeviceD = ToDeviceDict = ToDeviced CuCIMD = CuCIMDict = CuCIMd RandCuCIMD = RandCuCIMDict = RandCuCIMd +AddCoordinateChannelsD = AddCoordinateChannelsDict = AddCoordinateChannelsd From 1195301bf1771ef2d076e02e574ae17d6de75c6e Mon Sep 17 00:00:00 2001 From: Nic Ma Date: Mon, 20 Dec 2021 20:43:44 +0800 Subject: [PATCH 2/2] [DLMED] enhance args Signed-off-by: Nic Ma --- monai/transforms/spatial/array.py | 1 - monai/transforms/utility/array.py | 40 +++++++++++++------------- monai/transforms/utility/dictionary.py | 31 +++++++++++--------- tests/test_add_coordinate_channels.py | 8 +++--- tests/test_add_coordinate_channelsd.py | 12 +++----- 5 files changed, 46 insertions(+), 46 deletions(-) diff --git a/monai/transforms/spatial/array.py b/monai/transforms/spatial/array.py index c3697338a7..a96a1cc546 100644 --- a/monai/transforms/spatial/array.py +++ b/monai/transforms/spatial/array.py @@ -33,7 +33,6 @@ create_translate, map_spatial_axes, ) -from monai.transforms.utils_pytorch_numpy_unification import concatenate from monai.utils import ( GridSampleMode, GridSamplePadMode, diff --git a/monai/transforms/utility/array.py b/monai/transforms/utility/array.py index 1e9195fc16..e06a517372 100644 --- a/monai/transforms/utility/array.py +++ b/monai/transforms/utility/array.py @@ -37,6 +37,7 @@ convert_to_cupy, convert_to_numpy, convert_to_tensor, + deprecated_arg, ensure_tuple, look_up_option, min_version, @@ -1265,36 +1266,35 @@ class AddCoordinateChannels(Transform): This can be seen as a input-only version of CoordConv: Liu, R. et al. An Intriguing Failing of Convolutional Neural Networks and the CoordConv Solution, NeurIPS 2018. + + Args: + spatial_dims: the spatial dimensions that are to have their coordinates encoded in a channel and + appended to the input image. E.g., `(0, 1, 2)` represents `H, W, D` dims and append three channels + to the input image, encoding the coordinates of the input's three spatial dimensions. + + .. deprecated:: 0.8.0 + ``spatial_channels`` is deprecated, use ``spatial_dims`` instead. + """ backend = [TransformBackends.TORCH, TransformBackends.NUMPY] - def __init__(self, spatial_channels: Sequence[int]) -> None: - """ - Args: - spatial_channels: the spatial dimensions that are to have their coordinates encoded in a channel and - appended to the input. E.g., `(1,2,3)` will append three channels to the input, encoding the - coordinates of the input's three spatial dimensions (0 is reserved for the channel dimension). - """ - self.spatial_channels = spatial_channels + @deprecated_arg( + name="spatial_channels", new_name="spatial_dims", since="0.8", msg_suffix="please use `spatial_dims` instead." + ) + def __init__(self, spatial_dims: Sequence[int]) -> None: + self.spatial_dims = spatial_dims def __call__(self, img: NdarrayOrTensor) -> NdarrayOrTensor: """ Args: img: data to be transformed, assuming `img` is channel first. """ - if max(self.spatial_channels) > img.ndim - 1: - raise ValueError( - f"input has {img.ndim-1} spatial dimensions, cannot add AddCoordinateChannels channel for " - f"dim {max(self.spatial_channels)}." - ) - if 0 in self.spatial_channels: - raise ValueError("cannot add AddCoordinateChannels channel for dimension 0, as 0 is channel dim.") + if max(self.spatial_dims) > img.ndim - 2 or min(self.spatial_dims) < 0: + raise ValueError(f"`spatial_dims` values must be within [0, {img.ndim - 2}]") - spatial_dims = img.shape[1:] - coord_channels = np.array(np.meshgrid(*tuple(np.linspace(-0.5, 0.5, s) for s in spatial_dims), indexing="ij")) + spatial_size = img.shape[1:] + coord_channels = np.array(np.meshgrid(*tuple(np.linspace(-0.5, 0.5, s) for s in spatial_size), indexing="ij")) coord_channels, *_ = convert_to_dst_type(coord_channels, img) # type: ignore - # only keep required dimensions. need to subtract 1 since im will be 0-based - # but user input is 1-based (because channel dim is 0) - coord_channels = coord_channels[[s - 1 for s in self.spatial_channels]] + coord_channels = coord_channels[list(self.spatial_dims)] return concatenate((img, coord_channels), axis=0) diff --git a/monai/transforms/utility/dictionary.py b/monai/transforms/utility/dictionary.py index 296f4aa111..5e48f65a3b 100644 --- a/monai/transforms/utility/dictionary.py +++ b/monai/transforms/utility/dictionary.py @@ -62,7 +62,7 @@ ) from monai.transforms.utils import extreme_points_to_image, get_extreme_points from monai.transforms.utils_pytorch_numpy_unification import concatenate -from monai.utils import convert_to_numpy, ensure_tuple, ensure_tuple_rep +from monai.utils import convert_to_numpy, deprecated_arg, ensure_tuple, ensure_tuple_rep from monai.utils.enums import TraceKeys, TransformBackends from monai.utils.type_conversion import convert_to_dst_type @@ -1596,23 +1596,28 @@ def __call__(self, data): class AddCoordinateChannelsd(MapTransform): """ Dictionary-based wrapper of :py:class:`monai.transforms.AddCoordinateChannels`. + + Args: + keys: keys of the corresponding items to be transformed. + See also: :py:class:`monai.transforms.compose.MapTransform` + spatial_dims: the spatial dimensions that are to have their coordinates encoded in a channel and + appended to the input image. E.g., `(0, 1, 2)` represents `H, W, D` dims and append three channels + to the input image, encoding the coordinates of the input's three spatial dimensions. + allow_missing_keys: don't raise exception if key is missing. + + .. deprecated:: 0.8.0 + ``spatial_channels`` is deprecated, use ``spatial_dims`` instead. + """ backend = AddCoordinateChannels.backend - def __init__(self, keys: KeysCollection, spatial_channels: Sequence[int], allow_missing_keys: bool = False) -> None: - """ - Args: - keys: keys of the corresponding items to be transformed. - See also: :py:class:`monai.transforms.compose.MapTransform` - allow_missing_keys: don't raise exception if key is missing. - spatial_channels: the spatial dimensions that are to have their coordinates encoded in a channel and - appended to the input. E.g., `(1,2,3)` will append three channels to the input, encoding the - coordinates of the input's three spatial dimensions. It is assumed dimension 0 is the channel. - - """ + @deprecated_arg( + name="spatial_channels", new_name="spatial_dims", since="0.8", msg_suffix="please use `spatial_dims` instead." + ) + def __init__(self, keys: KeysCollection, spatial_dims: Sequence[int], allow_missing_keys: bool = False) -> None: super().__init__(keys, allow_missing_keys) - self.add_coordinate_channels = AddCoordinateChannels(spatial_channels) + self.add_coordinate_channels = AddCoordinateChannels(spatial_dims=spatial_dims) def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> Dict[Hashable, NdarrayOrTensor]: d = dict(data) diff --git a/tests/test_add_coordinate_channels.py b/tests/test_add_coordinate_channels.py index 4d779cffff..f07e686dd7 100644 --- a/tests/test_add_coordinate_channels.py +++ b/tests/test_add_coordinate_channels.py @@ -20,10 +20,10 @@ TESTS, TEST_CASES_ERROR_1, TEST_CASES_ERROR_2 = [], [], [] for p in TEST_NDARRAYS: - TESTS.append([{"spatial_channels": (1, 2, 3)}, p(np.random.randint(0, 2, size=(1, 3, 3, 3))), (4, 3, 3, 3)]) - TESTS.append([{"spatial_channels": (1,)}, p(np.random.randint(0, 2, size=(1, 3, 3, 3))), (2, 3, 3, 3)]) - TEST_CASES_ERROR_1.append([{"spatial_channels": (3,)}, p(np.random.randint(0, 2, size=(1, 3, 3)))]) - TEST_CASES_ERROR_2.append([{"spatial_channels": (0, 1, 2)}, p(np.random.randint(0, 2, size=(1, 3, 3)))]) + TESTS.append([{"spatial_dims": (0, 1, 2)}, p(np.random.randint(0, 2, size=(1, 3, 3, 3))), (4, 3, 3, 3)]) + TESTS.append([{"spatial_dims": (0,)}, p(np.random.randint(0, 2, size=(1, 3, 3, 3))), (2, 3, 3, 3)]) + TEST_CASES_ERROR_1.append([{"spatial_dims": (2,)}, p(np.random.randint(0, 2, size=(1, 3, 3)))]) + TEST_CASES_ERROR_2.append([{"spatial_dims": (-1, 0, 1)}, p(np.random.randint(0, 2, size=(1, 3, 3)))]) class TestAddCoordinateChannels(unittest.TestCase): diff --git a/tests/test_add_coordinate_channelsd.py b/tests/test_add_coordinate_channelsd.py index 08d9e62468..56e4841c8a 100644 --- a/tests/test_add_coordinate_channelsd.py +++ b/tests/test_add_coordinate_channelsd.py @@ -22,24 +22,20 @@ for p in TEST_NDARRAYS: TESTS.append( [ - {"spatial_channels": (1, 2, 3), "keys": ["img"]}, + {"spatial_dims": (0, 1, 2), "keys": ["img"]}, {"img": p(np.random.randint(0, 2, size=(1, 3, 3, 3)))}, (4, 3, 3, 3), ] ) TESTS.append( - [ - {"spatial_channels": (1,), "keys": ["img"]}, - {"img": p(np.random.randint(0, 2, size=(1, 3, 3, 3)))}, - (2, 3, 3, 3), - ] + [{"spatial_dims": (0,), "keys": ["img"]}, {"img": p(np.random.randint(0, 2, size=(1, 3, 3, 3)))}, (2, 3, 3, 3)] ) TEST_CASES_ERROR_1.append( - [{"spatial_channels": (3,), "keys": ["img"]}, {"img": p(np.random.randint(0, 2, size=(1, 3, 3)))}] + [{"spatial_dims": (2,), "keys": ["img"]}, {"img": p(np.random.randint(0, 2, size=(1, 3, 3)))}] ) TEST_CASES_ERROR_2.append( - [{"spatial_channels": (0, 1, 2), "keys": ["img"]}, {"img": p(np.random.randint(0, 2, size=(1, 3, 3)))}] + [{"spatial_dims": (-1, 0, 1), "keys": ["img"]}, {"img": p(np.random.randint(0, 2, size=(1, 3, 3)))}] )