diff --git a/docs/source/transforms.rst b/docs/source/transforms.rst index add6b9b40c..0b0f9f0792 100644 --- a/docs/source/transforms.rst +++ b/docs/source/transforms.rst @@ -733,6 +733,18 @@ Utility :members: :special-members: __call__ +`CuCIM` +""""""" +.. autoclass:: CuCIM + :members: + :special-members: __call__ + +`RandCuCIM` +""""""""""" +.. autoclass:: RandCuCIM + :members: + :special-members: __call__ + Dictionary Transforms --------------------- @@ -1374,6 +1386,17 @@ Utility (Dict) :members: :special-members: __call__ +`CuCIMd` +"""""""" +.. autoclass:: CuCIMd + :members: + :special-members: __call__ + +`RandCuCIMd` +"""""""""""" +.. autoclass:: RandCuCIMd + :members: + :special-members: __call__ Transform Adaptors ------------------ diff --git a/monai/transforms/__init__.py b/monai/transforms/__init__.py index 8f76a4b7a6..e693c06dc2 100644 --- a/monai/transforms/__init__.py +++ b/monai/transforms/__init__.py @@ -359,6 +359,7 @@ CastToType, ClassesToIndices, ConvertToMultiChannelBasedOnBratsClasses, + CuCIM, DataStats, EnsureChannelFirst, EnsureType, @@ -368,6 +369,7 @@ LabelToMask, Lambda, MapLabelValue, + RandCuCIM, RandLambda, RemoveRepeatedChannel, RepeatChannel, @@ -410,6 +412,9 @@ CopyItemsd, CopyItemsD, CopyItemsDict, + CuCIMd, + CuCIMD, + CuCIMDict, DataStatsd, DataStatsD, DataStatsDict, @@ -440,6 +445,9 @@ MapLabelValued, MapLabelValueD, MapLabelValueDict, + RandCuCIMd, + RandCuCIMD, + RandCuCIMDict, RandLambdad, RandLambdaD, RandLambdaDict, diff --git a/monai/transforms/utility/array.py b/monai/transforms/utility/array.py index 848a782533..73fbd6666f 100644 --- a/monai/transforms/utility/array.py +++ b/monai/transforms/utility/array.py @@ -49,7 +49,7 @@ PILImageImage, has_pil = optional_import("PIL.Image", name="Image") pil_image_fromarray, _ = optional_import("PIL.Image", name="fromarray") cp, has_cp = optional_import("cupy") -cp_ndarray, _ = optional_import("cupy", name="ndarray") + __all__ = [ "Identity", @@ -1148,3 +1148,78 @@ def __call__(self, img: torch.Tensor): raise ValueError("img must be PyTorch Tensor, consider converting img by `EnsureType` transform first.") return img.to(self.device, **self.kwargs) + + +class CuCIM(Transform): + """ + Wrap a non-randomized cuCIM transform, defined based on the transform name and args. + For randomized transforms (or randomly applying a transform) use :py:class:`monai.transforms.RandCuCIM`. + + Args: + name: the transform name in CuCIM package + args: parameters for the CuCIM transform + kwargs: parameters for the CuCIM transform + + Note: + CuCIM transform only work with CuPy arrays, so this transform expects input data to be `cupy.ndarray`. + Users can call `ToCuPy` transform to convert a numpy array or torch tensor to cupy array. + """ + + def __init__(self, name: str, *args, **kwargs) -> None: + super().__init__() + self.transform, _ = optional_import("cucim.core.operations.expose.transform", name=name) + self.args = args + self.kwargs = kwargs + + def __call__(self, data): + """ + Args: + data: a CuPy array (`cupy.ndarray`) for the cuCIM transform + + Returns: + `cupy.ndarray` + + """ + return self.transform(data, *self.args, **self.kwargs) + + +class RandCuCIM(CuCIM, RandomizableTransform): + """ + Wrap a randomized cuCIM transform, defined based on the transform name and args, + or randomly apply a non-randomized transform. + For deterministic non-randomized transforms use :py:class:`monai.transforms.CuCIM`. + + Args: + name: the transform name in CuCIM package. + apply_prob: the probability to apply the transform (default=1.0) + args: parameters for the CuCIM transform. + kwargs: parameters for the CuCIM transform. + + Note: + - CuCIM transform only work with CuPy arrays, so this transform expects input data to be `cupy.ndarray`. + Users can call `ToCuPy` transform to convert a numpy array or torch tensor to cupy array. + - If the cuCIM transform is already randomized the `apply_prob` argument has nothing to do with + the randomness of the underlying cuCIM transform. `apply_prob` defines if the transform (either randomized + or non-randomized) being applied randomly, so it can apply non-randomized tranforms randomly but be careful + with setting `apply_prob` to anything than 1.0 when using along with cuCIM's randomized transforms. + - If the random factor of the underlying cuCIM transform is not derived from `self.R`, + the results may not be deterministic. See Also: :py:class:`monai.transforms.Randomizable`. + """ + + def __init__(self, name: str, apply_prob: float = 1.0, *args, **kwargs) -> None: + CuCIM.__init__(self, name, *args, **kwargs) + RandomizableTransform.__init__(self, prob=apply_prob) + + def __call__(self, data): + """ + Args: + data: a CuPy array (`cupy.ndarray`) for the cuCIM transform + + Returns: + `cupy.ndarray` + + """ + self.randomize(data) + if not self._do_transform: + return data + return super().__call__(data) diff --git a/monai/transforms/utility/dictionary.py b/monai/transforms/utility/dictionary.py index a3c51fe3f2..695753a07d 100644 --- a/monai/transforms/utility/dictionary.py +++ b/monai/transforms/utility/dictionary.py @@ -36,6 +36,7 @@ CastToType, ClassesToIndices, ConvertToMultiChannelBasedOnBratsClasses, + CuCIM, DataStats, EnsureChannelFirst, EnsureType, @@ -87,6 +88,9 @@ "CopyItemsD", "CopyItemsDict", "CopyItemsd", + "CuCIMd", + "CuCIMD", + "CuCIMDict", "DataStatsD", "DataStatsDict", "DataStatsd", @@ -117,6 +121,9 @@ "MapLabelValueD", "MapLabelValueDict", "MapLabelValued", + "RandCuCIMd", + "RandCuCIMD", + "RandCuCIMDict", "RandLambdaD", "RandLambdaDict", "RandLambdad", @@ -1481,6 +1488,99 @@ def __call__(self, data: Mapping[Hashable, torch.Tensor]) -> Dict[Hashable, torc return d +class CuCIMd(MapTransform): + """ + Dictionary-based wrapper of :py:class:`monai.transforms.CuCIM` for non-randomized transforms. + For randomized transforms of CuCIM use :py:class:`monai.transforms.RandCuCIMd`. + + Args: + keys: keys of the corresponding items to be transformed. + See also: :py:class:`monai.transforms.compose.MapTransform` + name: The transform name in CuCIM package. + allow_missing_keys: don't raise exception if key is missing. + args: parameters for the CuCIM transform. + kwargs: parameters for the CuCIM transform. + + Note: + CuCIM transforms only work with CuPy arrays, this transform expects input data to be `cupy.ndarray`. + Users can call `ToCuPy` transform to convert a numpy array or torch tensor to cupy array. + """ + + def __init__( + self, + keys: KeysCollection, + name: str, + allow_missing_keys: bool = False, + *args, + **kwargs, + ) -> None: + super().__init__(keys=keys, allow_missing_keys=allow_missing_keys) + self.trans = CuCIM(name, *args, **kwargs) + + def __call__(self, data): + """ + Args: + data: Dict[Hashable, `cupy.ndarray`] + + Returns: + Dict[Hashable, `cupy.ndarray`] + + """ + d = dict(data) + for key in self.key_iterator(d): + d[key] = self.trans(d[key]) + return d + + +class RandCuCIMd(CuCIMd, RandomizableTransform): + """ + Dictionary-based wrapper of :py:class:`monai.transforms.CuCIM` for randomized transforms. + For deterministic non-randomized transforms of CuCIM use :py:class:`monai.transforms.CuCIMd`. + + Args: + keys: keys of the corresponding items to be transformed. + See also: :py:class:`monai.transforms.compose.MapTransform` + name: The transform name in CuCIM package. + apply_prob: the probability to apply the transform (default=1.0) + allow_missing_keys: don't raise exception if key is missing. + args: parameters for the CuCIM transform. + kwargs: parameters for the CuCIM transform. + + Note: + - CuCIM transform only work with CuPy arrays, so this transform expects input data to be `cupy.ndarray`. + Users can call `ToCuPy` transform to convert a numpy array or torch tensor to cupy array. + - If the cuCIM transform is already randomized the `apply_prob` argument has nothing to do with + the randomness of the underlying cuCIM transform. `apply_prob` defines if the transform (either randomized + or non-randomized) being applied randomly, so it can apply non-randomized tranforms randomly but be careful + with setting `apply_prob` to anything than 1.0 when using along with cuCIM's randomized transforms. + - If the random factor of the underlying cuCIM transform is not derived from `self.R`, + the results may not be deterministic. See Also: :py:class:`monai.transforms.Randomizable`. + """ + + def __init__( + self, + apply_prob: float = 1.0, + *args, + **kwargs, + ) -> None: + CuCIMd.__init__(self, *args, **kwargs) + RandomizableTransform.__init__(self, prob=apply_prob) + + def __call__(self, data): + """ + Args: + data: Dict[Hashable, `cupy.ndarray`] + + Returns: + Dict[Hashable, `cupy.ndarray`] + + """ + self.randomize(data) + if not self._do_transform: + return dict(data) + return super().__call__(data) + + IdentityD = IdentityDict = Identityd AsChannelFirstD = AsChannelFirstDict = AsChannelFirstd AsChannelLastD = AsChannelLastDict = AsChannelLastd @@ -1517,3 +1617,5 @@ def __call__(self, data: Mapping[Hashable, torch.Tensor]) -> Dict[Hashable, torc MapLabelValueD = MapLabelValueDict = MapLabelValued IntensityStatsD = IntensityStatsDict = IntensityStatsd ToDeviceD = ToDeviceDict = ToDeviced +CuCIMD = CuCIMDict = CuCIMd +RandCuCIMD = RandCuCIMDict = RandCuCIMd diff --git a/tests/test_cucim_dict_transform.py b/tests/test_cucim_dict_transform.py new file mode 100644 index 0000000000..4936375142 --- /dev/null +++ b/tests/test_cucim_dict_transform.py @@ -0,0 +1,141 @@ +# Copyright 2020 - 2021 MONAI Consortium +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest + +import numpy as np +from parameterized import parameterized + +from monai.transforms import CuCIMd +from monai.utils import optional_import, set_determinism +from tests.utils import skip_if_no_cuda + +_, has_cut = optional_import("cucim.core.operations.expose.transform") +cp, has_cp = optional_import("cupy") + +set_determinism(seed=0) + +TEST_CASE_COLOR_JITTER_1 = [ + {"name": "color_jitter", "brightness": 0.0, "contrast": 0.0, "saturation": 0.0, "hue": 0.0}, + np.array([[[0.0, 1.0], [1.0, 2.0]], [[0.0, 1.0], [1.0, 2.0]], [[0.0, 1.0], [1.0, 2.0]]], dtype=np.float32), + np.array([[[0.0, 1.0], [1.0, 2.0]], [[0.0, 1.0], [1.0, 2.0]], [[0.0, 1.0], [1.0, 2.0]]], dtype=np.float32), +] + +TEST_CASE_COLOR_JITTER_2 = [ + {"name": "color_jitter", "brightness": 0.0, "contrast": 0.0, "saturation": 0.0, "hue": 0.0}, + np.array([[[0, 1], [2, 3]], [[0, 1], [2, 3]], [[0, 1], [2, 3]]], dtype=np.uint8), + np.array([[[0, 1], [2, 3]], [[0, 1], [2, 3]], [[0, 1], [2, 3]]], dtype=np.uint8), +] + +TEST_CASE_FLIP_1 = [ + {"name": "image_flip", "spatial_axis": -1}, + np.array([[[0.0, 1.0], [2.0, 3.0]], [[0.0, 1.0], [2.0, 3.0]], [[0.0, 1.0], [2.0, 3.0]]], dtype=np.float32), + np.array([[[1.0, 0.0], [3.0, 2.0]], [[1.0, 0.0], [3.0, 2.0]], [[1.0, 0.0], [3.0, 2.0]]], dtype=np.float32), +] + + +TEST_CASE_ROTATE_1 = [ + {"name": "image_rotate_90", "k": 1, "spatial_axis": (-2, -1)}, + np.array([[[0.0, 1.0], [2.0, 3.0]], [[0.0, 1.0], [2.0, 3.0]], [[0.0, 1.0], [2.0, 3.0]]], dtype=np.float32), + np.array([[[1.0, 3.0], [0.0, 2.0]], [[1.0, 3.0], [0.0, 2.0]], [[1.0, 3.0], [0.0, 2.0]]], dtype=np.float32), +] + +TEST_CASE_SCALE_INTENSITY_1 = [ + {"name": "scale_intensity_range", "a_min": 0.0, "a_max": 4.0, "b_min": 0.0, "b_max": 1.0, "clip": False}, + np.array([[[0.0, 1.0], [2.0, 3.0]], [[0.0, 1.0], [2.0, 3.0]], [[0.0, 1.0], [2.0, 3.0]]], dtype=np.float32), + np.array([[[0.0, 0.25], [0.5, 0.75]], [[0.0, 0.25], [0.5, 0.75]], [[0.0, 0.25], [0.5, 0.75]]], dtype=np.float32), +] + +TEST_CASE_ZOOM_1 = [ + {"name": "zoom", "zoom_factor": (0.5, 0.5)}, + np.mgrid[:3, 1:4].astype(dtype=np.float32), + np.concatenate([np.ones((1, 3, 3), dtype=np.float32) * 1.0, np.ones((1, 3, 3), dtype=np.float32) * 2.0]), +] + + +@skip_if_no_cuda +@unittest.skipUnless(has_cp, "CuPy is required.") +@unittest.skipUnless(has_cut, "cuCIM transforms are required.") +class TestCuCIMDict(unittest.TestCase): + @parameterized.expand( + [ + TEST_CASE_COLOR_JITTER_1, + TEST_CASE_COLOR_JITTER_2, + TEST_CASE_FLIP_1, + TEST_CASE_ROTATE_1, + TEST_CASE_SCALE_INTENSITY_1, + TEST_CASE_ZOOM_1, + ] + ) + def test_tramsforms_numpy_single(self, params, input, expected): + input = {"image": input} + output = CuCIMd(keys="image", **params)(input)["image"] + self.assertTrue(output.dtype == expected.dtype) + self.assertTrue(isinstance(output, np.ndarray)) + cp.testing.assert_allclose(output, expected) + + @parameterized.expand( + [ + TEST_CASE_COLOR_JITTER_1, + TEST_CASE_COLOR_JITTER_2, + TEST_CASE_FLIP_1, + TEST_CASE_ROTATE_1, + TEST_CASE_SCALE_INTENSITY_1, + TEST_CASE_ZOOM_1, + ] + ) + def test_tramsforms_numpy_batch(self, params, input, expected): + input = {"image": input[cp.newaxis, ...]} + expected = expected[cp.newaxis, ...] + output = CuCIMd(keys="image", **params)(input)["image"] + self.assertTrue(output.dtype == expected.dtype) + self.assertTrue(isinstance(output, np.ndarray)) + cp.testing.assert_allclose(output, expected) + + @parameterized.expand( + [ + TEST_CASE_COLOR_JITTER_1, + TEST_CASE_COLOR_JITTER_2, + TEST_CASE_FLIP_1, + TEST_CASE_ROTATE_1, + TEST_CASE_SCALE_INTENSITY_1, + TEST_CASE_ZOOM_1, + ] + ) + def test_tramsforms_cupy_single(self, params, input, expected): + input = {"image": cp.asarray(input)} + expected = cp.asarray(expected) + output = CuCIMd(keys="image", **params)(input)["image"] + self.assertTrue(output.dtype == expected.dtype) + self.assertTrue(isinstance(output, cp.ndarray)) + cp.testing.assert_allclose(output, expected) + + @parameterized.expand( + [ + TEST_CASE_COLOR_JITTER_1, + TEST_CASE_COLOR_JITTER_2, + TEST_CASE_FLIP_1, + TEST_CASE_ROTATE_1, + TEST_CASE_SCALE_INTENSITY_1, + TEST_CASE_ZOOM_1, + ] + ) + def test_tramsforms_cupy_batch(self, params, input, expected): + input = {"image": cp.asarray(input)[cp.newaxis, ...]} + expected = cp.asarray(expected)[cp.newaxis, ...] + output = CuCIMd(keys="image", **params)(input)["image"] + self.assertTrue(output.dtype == expected.dtype) + self.assertTrue(isinstance(output, cp.ndarray)) + cp.testing.assert_allclose(output, expected) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_cucim_transform.py b/tests/test_cucim_transform.py new file mode 100644 index 0000000000..a6c0084c99 --- /dev/null +++ b/tests/test_cucim_transform.py @@ -0,0 +1,140 @@ +# Copyright 2020 - 2021 MONAI Consortium +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest + +import numpy as np +from parameterized import parameterized + +from monai.transforms import CuCIM +from monai.utils import optional_import, set_determinism +from tests.utils import skip_if_no_cuda + +_, has_cut = optional_import("cucim.core.operations.expose.transform") +cp, has_cp = optional_import("cupy") + +set_determinism(seed=0) + +TEST_CASE_COLOR_JITTER_1 = [ + {"name": "color_jitter", "brightness": 0.0, "contrast": 0.0, "saturation": 0.0, "hue": 0.0}, + np.array([[[0.0, 1.0], [1.0, 2.0]], [[0.0, 1.0], [1.0, 2.0]], [[0.0, 1.0], [1.0, 2.0]]], dtype=np.float32), + np.array([[[0.0, 1.0], [1.0, 2.0]], [[0.0, 1.0], [1.0, 2.0]], [[0.0, 1.0], [1.0, 2.0]]], dtype=np.float32), +] + +TEST_CASE_COLOR_JITTER_2 = [ + {"name": "color_jitter", "brightness": 0.0, "contrast": 0.0, "saturation": 0.0, "hue": 0.0}, + np.array([[[0, 1], [2, 3]], [[0, 1], [2, 3]], [[0, 1], [2, 3]]], dtype=np.uint8), + np.array([[[0, 1], [2, 3]], [[0, 1], [2, 3]], [[0, 1], [2, 3]]], dtype=np.uint8), +] + +TEST_CASE_FLIP_1 = [ + {"name": "image_flip", "spatial_axis": -1}, + np.array([[[0.0, 1.0], [2.0, 3.0]], [[0.0, 1.0], [2.0, 3.0]], [[0.0, 1.0], [2.0, 3.0]]], dtype=np.float32), + np.array([[[1.0, 0.0], [3.0, 2.0]], [[1.0, 0.0], [3.0, 2.0]], [[1.0, 0.0], [3.0, 2.0]]], dtype=np.float32), +] + + +TEST_CASE_ROTATE_1 = [ + {"name": "image_rotate_90", "k": 1, "spatial_axis": (-2, -1)}, + np.array([[[0.0, 1.0], [2.0, 3.0]], [[0.0, 1.0], [2.0, 3.0]], [[0.0, 1.0], [2.0, 3.0]]], dtype=np.float32), + np.array([[[1.0, 3.0], [0.0, 2.0]], [[1.0, 3.0], [0.0, 2.0]], [[1.0, 3.0], [0.0, 2.0]]], dtype=np.float32), +] + +TEST_CASE_SCALE_INTENSITY_1 = [ + {"name": "scale_intensity_range", "a_min": 0.0, "a_max": 4.0, "b_min": 0.0, "b_max": 1.0, "clip": False}, + np.array([[[0.0, 1.0], [2.0, 3.0]], [[0.0, 1.0], [2.0, 3.0]], [[0.0, 1.0], [2.0, 3.0]]], dtype=np.float32), + np.array([[[0.0, 0.25], [0.5, 0.75]], [[0.0, 0.25], [0.5, 0.75]], [[0.0, 0.25], [0.5, 0.75]]], dtype=np.float32), +] + +TEST_CASE_ZOOM_1 = [ + {"name": "zoom", "zoom_factor": (0.5, 0.5)}, + np.mgrid[:3, 1:4].astype(dtype=np.float32), + np.concatenate([np.ones((1, 3, 3), dtype=np.float32) * 1.0, np.ones((1, 3, 3), dtype=np.float32) * 2.0]), +] + + +@skip_if_no_cuda +@unittest.skipUnless(has_cp, "CuPy is required.") +@unittest.skipUnless(has_cut, "cuCIM transforms are required.") +class TestCuCIM(unittest.TestCase): + @parameterized.expand( + [ + TEST_CASE_COLOR_JITTER_1, + TEST_CASE_COLOR_JITTER_2, + TEST_CASE_FLIP_1, + TEST_CASE_ROTATE_1, + TEST_CASE_SCALE_INTENSITY_1, + TEST_CASE_ZOOM_1, + ] + ) + def test_tramsforms_numpy_single(self, params, input, expected): + output = CuCIM(**params)(input) + self.assertTrue(output.dtype == expected.dtype) + self.assertTrue(isinstance(output, np.ndarray)) + cp.testing.assert_allclose(output, expected) + + @parameterized.expand( + [ + TEST_CASE_COLOR_JITTER_1, + TEST_CASE_COLOR_JITTER_2, + TEST_CASE_FLIP_1, + TEST_CASE_ROTATE_1, + TEST_CASE_SCALE_INTENSITY_1, + TEST_CASE_ZOOM_1, + ] + ) + def test_tramsforms_numpy_batch(self, params, input, expected): + input = input[cp.newaxis, ...] + expected = expected[cp.newaxis, ...] + output = CuCIM(**params)(input) + self.assertTrue(output.dtype == expected.dtype) + self.assertTrue(isinstance(output, np.ndarray)) + cp.testing.assert_allclose(output, expected) + + @parameterized.expand( + [ + TEST_CASE_COLOR_JITTER_1, + TEST_CASE_COLOR_JITTER_2, + TEST_CASE_FLIP_1, + TEST_CASE_ROTATE_1, + TEST_CASE_SCALE_INTENSITY_1, + TEST_CASE_ZOOM_1, + ] + ) + def test_tramsforms_cupy_single(self, params, input, expected): + input = cp.asarray(input) + expected = cp.asarray(expected) + output = CuCIM(**params)(input) + self.assertTrue(output.dtype == expected.dtype) + self.assertTrue(isinstance(output, cp.ndarray)) + cp.testing.assert_allclose(output, expected) + + @parameterized.expand( + [ + TEST_CASE_COLOR_JITTER_1, + TEST_CASE_COLOR_JITTER_2, + TEST_CASE_FLIP_1, + TEST_CASE_ROTATE_1, + TEST_CASE_SCALE_INTENSITY_1, + TEST_CASE_ZOOM_1, + ] + ) + def test_tramsforms_cupy_batch(self, params, input, expected): + input = cp.asarray(input)[cp.newaxis, ...] + expected = cp.asarray(expected)[cp.newaxis, ...] + output = CuCIM(**params)(input) + self.assertTrue(output.dtype == expected.dtype) + self.assertTrue(isinstance(output, cp.ndarray)) + cp.testing.assert_allclose(output, expected) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_rand_cucim_dict_transform.py b/tests/test_rand_cucim_dict_transform.py new file mode 100644 index 0000000000..c084331e0e --- /dev/null +++ b/tests/test_rand_cucim_dict_transform.py @@ -0,0 +1,185 @@ +# Copyright 2020 - 2021 MONAI Consortium +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest + +import numpy as np +from parameterized import parameterized + +from monai.transforms import RandCuCIMd +from monai.utils import optional_import, set_determinism +from tests.utils import skip_if_no_cuda + +_, has_cut = optional_import("cucim.core.operations.expose.transform") +cp, has_cp = optional_import("cupy") + +set_determinism(seed=0) + +TEST_CASE_COLOR_JITTER_1 = [ + {"name": "color_jitter", "brightness": 0.0, "contrast": 0.0, "saturation": 0.0, "hue": 0.0}, + np.array([[[0, 1], [2, 3]], [[0, 10], [20, 30]], [[0, 50], [100, 150]]], dtype=np.uint8), + np.array([[[0, 1], [2, 3]], [[0, 10], [20, 30]], [[0, 50], [100, 150]]], dtype=np.uint8), +] + +TEST_CASE_FLIP_1 = [ + {"name": "image_flip", "spatial_axis": -1}, + np.array([[[0.0, 1.0], [2.0, 3.0]], [[0.0, 1.0], [2.0, 3.0]], [[0.0, 1.0], [2.0, 3.0]]], dtype=np.float32), + np.array([[[1.0, 0.0], [3.0, 2.0]], [[1.0, 0.0], [3.0, 2.0]], [[1.0, 0.0], [3.0, 2.0]]], dtype=np.float32), +] + +TEST_CASE_RAND_ROTATE_1 = [ + {"name": "rand_image_rotate_90", "prob": 1.0, "max_k": 1, "spatial_axis": (-2, -1)}, + np.array([[[0.0, 1.0], [2.0, 3.0]], [[0.0, 1.0], [2.0, 3.0]], [[0.0, 1.0], [2.0, 3.0]]], dtype=np.float32), + np.array([[[1.0, 3.0], [0.0, 2.0]], [[1.0, 3.0], [0.0, 2.0]], [[1.0, 3.0], [0.0, 2.0]]], dtype=np.float32), +] + + +TEST_CASE_RAND_ROTATE_2 = [ + {"name": "rand_image_rotate_90", "prob": 0.0, "max_k": 1, "spatial_axis": (-2, -1)}, + np.array([[[0.0, 1.0], [2.0, 3.0]], [[0.0, 1.0], [2.0, 3.0]], [[0.0, 1.0], [2.0, 3.0]]], dtype=np.float32), + np.array([[[0.0, 1.0], [2.0, 3.0]], [[0.0, 1.0], [2.0, 3.0]], [[0.0, 1.0], [2.0, 3.0]]], dtype=np.float32), +] + +TEST_CASE_SCALE_INTENSITY_1 = [ + {"name": "scale_intensity_range", "a_min": 0.0, "a_max": 4.0, "b_min": 0.0, "b_max": 1.0, "clip": False}, + np.array([[[0.0, 1.0], [2.0, 3.0]], [[0.0, 1.0], [2.0, 3.0]], [[0.0, 1.0], [2.0, 3.0]]], dtype=np.float32), + np.array([[[0.0, 0.25], [0.5, 0.75]], [[0.0, 0.25], [0.5, 0.75]], [[0.0, 0.25], [0.5, 0.75]]], dtype=np.float32), +] + +TEST_CASE_ZOOM_1 = [ + {"name": "zoom", "zoom_factor": (0.5, 0.5)}, + np.mgrid[:3, 1:4].astype(dtype=np.float32), + np.concatenate([np.ones((1, 3, 3), dtype=np.float32) * 1.0, np.ones((1, 3, 3), dtype=np.float32) * 2.0]), +] + +TEST_CASE_RAND_ZOOM_1 = [ + {"name": "rand_zoom", "prob": 1.0, "min_zoom": 0.5, "max_zoom": 0.5}, + np.mgrid[:3, 1:4].astype(dtype=np.float32), + np.concatenate([np.ones((1, 3, 3), dtype=np.float32) * 1.0, np.ones((1, 3, 3), dtype=np.float32) * 2.0]), +] + +TEST_CASE_RAND_ZOOM_2 = [ + {"name": "rand_zoom", "prob": 0.0, "min_zoom": 0.5, "max_zoom": 0.5}, + np.mgrid[:3, 1:4].astype(dtype=np.float32), + np.mgrid[:3, 1:4].astype(dtype=np.float32), +] + + +@skip_if_no_cuda +@unittest.skipUnless(has_cp, "CuPy is required.") +@unittest.skipUnless(has_cut, "cuCIM transforms are required.") +class TestRandCuCIMDict(unittest.TestCase): + @parameterized.expand( + [ + TEST_CASE_COLOR_JITTER_1, + TEST_CASE_FLIP_1, + TEST_CASE_RAND_ROTATE_1, + TEST_CASE_RAND_ROTATE_2, + TEST_CASE_SCALE_INTENSITY_1, + TEST_CASE_ZOOM_1, + TEST_CASE_RAND_ZOOM_1, + TEST_CASE_RAND_ZOOM_2, + ] + ) + def test_tramsforms_numpy_single(self, params, input, expected): + input = {"image": input} + # apply_prob=1.0 + output = RandCuCIMd(keys="image", apply_prob=1.0, **params)(input)["image"] + self.assertTrue(output.dtype == expected.dtype) + self.assertTrue(isinstance(output, np.ndarray)) + cp.testing.assert_allclose(output, expected) + # apply_prob=0.0 + output = RandCuCIMd(keys="image", apply_prob=0.0, **params)(input)["image"] + self.assertTrue(output.dtype == input["image"].dtype) + self.assertTrue(isinstance(output, np.ndarray)) + cp.testing.assert_allclose(output, input["image"]) + + @parameterized.expand( + [ + TEST_CASE_COLOR_JITTER_1, + TEST_CASE_FLIP_1, + TEST_CASE_RAND_ROTATE_1, + TEST_CASE_RAND_ROTATE_2, + TEST_CASE_SCALE_INTENSITY_1, + TEST_CASE_ZOOM_1, + TEST_CASE_RAND_ZOOM_1, + TEST_CASE_RAND_ZOOM_2, + ] + ) + def test_tramsforms_numpy_batch(self, params, input, expected): + input = {"image": input[cp.newaxis, ...]} + expected = expected[cp.newaxis, ...] + # apply_prob=1.0 + output = RandCuCIMd(keys="image", apply_prob=1.0, **params)(input)["image"] + self.assertTrue(output.dtype == expected.dtype) + self.assertTrue(isinstance(output, np.ndarray)) + cp.testing.assert_allclose(output, expected) + # apply_prob=0.0 + output = RandCuCIMd(keys="image", apply_prob=0.0, **params)(input)["image"] + self.assertTrue(output.dtype == input["image"].dtype) + self.assertTrue(isinstance(output, np.ndarray)) + cp.testing.assert_allclose(output, input["image"]) + + @parameterized.expand( + [ + TEST_CASE_COLOR_JITTER_1, + TEST_CASE_FLIP_1, + TEST_CASE_RAND_ROTATE_1, + TEST_CASE_RAND_ROTATE_2, + TEST_CASE_SCALE_INTENSITY_1, + TEST_CASE_ZOOM_1, + TEST_CASE_RAND_ZOOM_1, + TEST_CASE_RAND_ZOOM_2, + ] + ) + def test_tramsforms_cupy_single(self, params, input, expected): + input = {"image": cp.asarray(input)} + expected = cp.asarray(expected) + # apply_prob=1.0 + output = RandCuCIMd(keys="image", apply_prob=1.0, **params)(input)["image"] + self.assertTrue(output.dtype == expected.dtype) + self.assertTrue(isinstance(output, cp.ndarray)) + cp.testing.assert_allclose(output, expected) + # apply_prob=0.0 + output = RandCuCIMd(keys="image", apply_prob=0.0, **params)(input)["image"] + self.assertTrue(output.dtype == input["image"].dtype) + self.assertTrue(isinstance(output, cp.ndarray)) + cp.testing.assert_allclose(output, input["image"]) + + @parameterized.expand( + [ + TEST_CASE_COLOR_JITTER_1, + TEST_CASE_FLIP_1, + TEST_CASE_RAND_ROTATE_1, + TEST_CASE_RAND_ROTATE_2, + TEST_CASE_SCALE_INTENSITY_1, + TEST_CASE_ZOOM_1, + TEST_CASE_RAND_ZOOM_1, + TEST_CASE_RAND_ZOOM_2, + ] + ) + def test_tramsforms_cupy_batch(self, params, input, expected): + input = {"image": cp.asarray(input)[cp.newaxis, ...]} + expected = cp.asarray(expected)[cp.newaxis, ...] + # apply_prob=1.0 + output = RandCuCIMd(keys="image", **params)(input)["image"] + self.assertTrue(output.dtype == expected.dtype) + self.assertTrue(isinstance(output, cp.ndarray)) + cp.testing.assert_allclose(output, expected) + # apply_prob=0.0 + output = RandCuCIMd(keys="image", apply_prob=0.0, **params)(input)["image"] + self.assertTrue(output.dtype == input["image"].dtype) + self.assertTrue(isinstance(output, cp.ndarray)) + cp.testing.assert_allclose(output, input["image"]) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_rand_cucim_transform.py b/tests/test_rand_cucim_transform.py new file mode 100644 index 0000000000..907bc35e01 --- /dev/null +++ b/tests/test_rand_cucim_transform.py @@ -0,0 +1,184 @@ +# Copyright 2020 - 2021 MONAI Consortium +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest + +import numpy as np +from parameterized import parameterized + +from monai.transforms import RandCuCIM +from monai.utils import optional_import, set_determinism +from tests.utils import skip_if_no_cuda + +_, has_cut = optional_import("cucim.core.operations.expose.transform") +cp, has_cp = optional_import("cupy") + +set_determinism(seed=0) + +TEST_CASE_COLOR_JITTER_1 = [ + {"name": "color_jitter", "brightness": 0.0, "contrast": 0.0, "saturation": 0.0, "hue": 0.0}, + np.array([[[0, 1], [2, 3]], [[0, 10], [20, 30]], [[0, 50], [100, 150]]], dtype=np.uint8), + np.array([[[0, 1], [2, 3]], [[0, 10], [20, 30]], [[0, 50], [100, 150]]], dtype=np.uint8), +] + +TEST_CASE_FLIP_1 = [ + {"name": "image_flip", "spatial_axis": -1}, + np.array([[[0.0, 1.0], [2.0, 3.0]], [[0.0, 1.0], [2.0, 3.0]], [[0.0, 1.0], [2.0, 3.0]]], dtype=np.float32), + np.array([[[1.0, 0.0], [3.0, 2.0]], [[1.0, 0.0], [3.0, 2.0]], [[1.0, 0.0], [3.0, 2.0]]], dtype=np.float32), +] + +TEST_CASE_RAND_ROTATE_1 = [ + {"name": "rand_image_rotate_90", "prob": 1.0, "max_k": 1, "spatial_axis": (-2, -1)}, + np.array([[[0.0, 1.0], [2.0, 3.0]], [[0.0, 1.0], [2.0, 3.0]], [[0.0, 1.0], [2.0, 3.0]]], dtype=np.float32), + np.array([[[1.0, 3.0], [0.0, 2.0]], [[1.0, 3.0], [0.0, 2.0]], [[1.0, 3.0], [0.0, 2.0]]], dtype=np.float32), +] + + +TEST_CASE_RAND_ROTATE_2 = [ + {"name": "rand_image_rotate_90", "prob": 0.0, "max_k": 1, "spatial_axis": (-2, -1)}, + np.array([[[0.0, 1.0], [2.0, 3.0]], [[0.0, 1.0], [2.0, 3.0]], [[0.0, 1.0], [2.0, 3.0]]], dtype=np.float32), + np.array([[[0.0, 1.0], [2.0, 3.0]], [[0.0, 1.0], [2.0, 3.0]], [[0.0, 1.0], [2.0, 3.0]]], dtype=np.float32), +] + +TEST_CASE_SCALE_INTENSITY_1 = [ + {"name": "scale_intensity_range", "a_min": 0.0, "a_max": 4.0, "b_min": 0.0, "b_max": 1.0, "clip": False}, + np.array([[[0.0, 1.0], [2.0, 3.0]], [[0.0, 1.0], [2.0, 3.0]], [[0.0, 1.0], [2.0, 3.0]]], dtype=np.float32), + np.array([[[0.0, 0.25], [0.5, 0.75]], [[0.0, 0.25], [0.5, 0.75]], [[0.0, 0.25], [0.5, 0.75]]], dtype=np.float32), +] + +TEST_CASE_ZOOM_1 = [ + {"name": "zoom", "zoom_factor": (0.5, 0.5)}, + np.mgrid[:3, 1:4].astype(dtype=np.float32), + np.concatenate([np.ones((1, 3, 3), dtype=np.float32) * 1.0, np.ones((1, 3, 3), dtype=np.float32) * 2.0]), +] + +TEST_CASE_RAND_ZOOM_1 = [ + {"name": "rand_zoom", "prob": 1.0, "min_zoom": 0.5, "max_zoom": 0.5}, + np.mgrid[:3, 1:4].astype(dtype=np.float32), + np.concatenate([np.ones((1, 3, 3), dtype=np.float32) * 1.0, np.ones((1, 3, 3), dtype=np.float32) * 2.0]), +] + +TEST_CASE_RAND_ZOOM_2 = [ + {"name": "rand_zoom", "prob": 0.0, "min_zoom": 0.5, "max_zoom": 0.5}, + np.mgrid[:3, 1:4].astype(dtype=np.float32), + np.mgrid[:3, 1:4].astype(dtype=np.float32), +] + + +@skip_if_no_cuda +@unittest.skipUnless(has_cp, "CuPy is required.") +@unittest.skipUnless(has_cut, "cuCIM transforms are required.") +class TestRandCuCIM(unittest.TestCase): + @parameterized.expand( + [ + TEST_CASE_COLOR_JITTER_1, + TEST_CASE_FLIP_1, + TEST_CASE_RAND_ROTATE_1, + TEST_CASE_RAND_ROTATE_2, + TEST_CASE_SCALE_INTENSITY_1, + TEST_CASE_ZOOM_1, + TEST_CASE_RAND_ZOOM_1, + TEST_CASE_RAND_ZOOM_2, + ] + ) + def test_tramsforms_numpy_single(self, params, input, expected): + # apply_prob=1.0 + output = RandCuCIM(apply_prob=1.0, **params)(input) + self.assertTrue(output.dtype == expected.dtype) + self.assertTrue(isinstance(output, np.ndarray)) + cp.testing.assert_allclose(output, expected) + # apply_prob=0.0 + output = RandCuCIM(apply_prob=0.0, **params)(input) + self.assertTrue(output.dtype == input.dtype) + self.assertTrue(isinstance(output, np.ndarray)) + cp.testing.assert_allclose(output, input) + + @parameterized.expand( + [ + TEST_CASE_COLOR_JITTER_1, + TEST_CASE_FLIP_1, + TEST_CASE_RAND_ROTATE_1, + TEST_CASE_RAND_ROTATE_2, + TEST_CASE_SCALE_INTENSITY_1, + TEST_CASE_ZOOM_1, + TEST_CASE_RAND_ZOOM_1, + TEST_CASE_RAND_ZOOM_2, + ] + ) + def test_tramsforms_numpy_batch(self, params, input, expected): + input = input[cp.newaxis, ...] + expected = expected[cp.newaxis, ...] + # apply_prob=1.0 + output = RandCuCIM(apply_prob=1.0, **params)(input) + self.assertTrue(output.dtype == expected.dtype) + self.assertTrue(isinstance(output, np.ndarray)) + cp.testing.assert_allclose(output, expected) + # apply_prob=0.0 + output = RandCuCIM(apply_prob=0.0, **params)(input) + self.assertTrue(output.dtype == input.dtype) + self.assertTrue(isinstance(output, np.ndarray)) + cp.testing.assert_allclose(output, input) + + @parameterized.expand( + [ + TEST_CASE_COLOR_JITTER_1, + TEST_CASE_FLIP_1, + TEST_CASE_RAND_ROTATE_1, + TEST_CASE_RAND_ROTATE_2, + TEST_CASE_SCALE_INTENSITY_1, + TEST_CASE_ZOOM_1, + TEST_CASE_RAND_ZOOM_1, + TEST_CASE_RAND_ZOOM_2, + ] + ) + def test_tramsforms_cupy_single(self, params, input, expected): + input = cp.asarray(input) + expected = cp.asarray(expected) + # apply_prob=1.0 + output = RandCuCIM(apply_prob=1.0, **params)(input) + self.assertTrue(output.dtype == expected.dtype) + self.assertTrue(isinstance(output, cp.ndarray)) + cp.testing.assert_allclose(output, expected) + # apply_prob=0.0 + output = RandCuCIM(apply_prob=0.0, **params)(input) + self.assertTrue(output.dtype == input.dtype) + self.assertTrue(isinstance(output, cp.ndarray)) + cp.testing.assert_allclose(output, input) + + @parameterized.expand( + [ + TEST_CASE_COLOR_JITTER_1, + TEST_CASE_FLIP_1, + TEST_CASE_RAND_ROTATE_1, + TEST_CASE_RAND_ROTATE_2, + TEST_CASE_SCALE_INTENSITY_1, + TEST_CASE_ZOOM_1, + TEST_CASE_RAND_ZOOM_1, + TEST_CASE_RAND_ZOOM_2, + ] + ) + def test_tramsforms_cupy_batch(self, params, input, expected): + input = cp.asarray(input)[cp.newaxis, ...] + expected = cp.asarray(expected)[cp.newaxis, ...] + # apply_prob=1.0 + output = RandCuCIM(**params)(input) + self.assertTrue(output.dtype == expected.dtype) + self.assertTrue(isinstance(output, cp.ndarray)) + cp.testing.assert_allclose(output, expected) + # apply_prob=0.0 + output = RandCuCIM(apply_prob=0.0, **params)(input) + self.assertTrue(output.dtype == input.dtype) + self.assertTrue(isinstance(output, cp.ndarray)) + cp.testing.assert_allclose(output, input) + + +if __name__ == "__main__": + unittest.main()