From 733bf13297bc0c10ac8c5e6a74f0348f2f2436d7 Mon Sep 17 00:00:00 2001 From: Richard Brown <33289025+rijobro@users.noreply.github.com> Date: Tue, 24 Aug 2021 15:21:23 +0100 Subject: [PATCH 1/3] all close Signed-off-by: Richard Brown <33289025+rijobro@users.noreply.github.com> --- tests/test_fill_holes.py | 3 +-- tests/test_flip.py | 7 ++----- tests/test_flipd.py | 7 ++----- tests/test_keep_largest_connected_component.py | 4 ++-- tests/test_label_filter.py | 3 +-- tests/test_rand_axis_flip.py | 8 ++------ tests/test_rand_axis_flipd.py | 7 ++----- tests/test_rand_flip.py | 7 ++----- tests/test_rand_flipd.py | 7 ++----- tests/utils.py | 17 +++++++---------- 10 files changed, 23 insertions(+), 47 deletions(-) diff --git a/tests/test_fill_holes.py b/tests/test_fill_holes.py index 294bbd8c87..dfc6fb5154 100644 --- a/tests/test_fill_holes.py +++ b/tests/test_fill_holes.py @@ -278,10 +278,9 @@ def test_correct_results(self, _, args, input_image, expected): converter = FillHoles(**args) if isinstance(input_image, torch.Tensor) and torch.cuda.is_available(): result = converter(clone(input_image).cuda()) - assert allclose(result, expected.cuda()) else: result = converter(clone(input_image)) - assert allclose(result, expected) + self.assertTrue(allclose(result, expected)) @parameterized.expand(INVALID_CASES) def test_raise_exception(self, _, args, input_image, expected_error): diff --git a/tests/test_flip.py b/tests/test_flip.py index bd0162fb8b..f9dbb5368e 100644 --- a/tests/test_flip.py +++ b/tests/test_flip.py @@ -12,11 +12,10 @@ import unittest import numpy as np -import torch from parameterized import parameterized from monai.transforms import Flip -from tests.utils import TEST_NDARRAYS, NumpyImageTestCase2D +from tests.utils import TEST_NDARRAYS, NumpyImageTestCase2D, allclose INVALID_CASES = [("wrong_axis", ["s", 1], TypeError), ("not_numbers", "s", TypeError)] @@ -40,9 +39,7 @@ def test_correct_results(self, _, spatial_axis): expected.append(np.flip(channel, spatial_axis)) expected = np.stack(expected) result = flip(im) - if isinstance(result, torch.Tensor): - result = result.cpu() - self.assertTrue(np.allclose(expected, result)) + self.assertTrue(allclose(expected, result)) if __name__ == "__main__": diff --git a/tests/test_flipd.py b/tests/test_flipd.py index cec4a99cbf..5266e36955 100644 --- a/tests/test_flipd.py +++ b/tests/test_flipd.py @@ -12,11 +12,10 @@ import unittest import numpy as np -import torch from parameterized import parameterized from monai.transforms import Flipd -from tests.utils import TEST_NDARRAYS, NumpyImageTestCase2D +from tests.utils import TEST_NDARRAYS, NumpyImageTestCase2D, allclose INVALID_CASES = [("wrong_axis", ["s", 1], TypeError), ("not_numbers", "s", TypeError)] @@ -39,9 +38,7 @@ def test_correct_results(self, _, spatial_axis): expected.append(np.flip(channel, spatial_axis)) expected = np.stack(expected) result = flip({"img": p(self.imt[0])})["img"] - if isinstance(result, torch.Tensor): - result = result.cpu() - assert np.allclose(expected, result) + self.assertTrue(allclose(expected, result)) if __name__ == "__main__": diff --git a/tests/test_keep_largest_connected_component.py b/tests/test_keep_largest_connected_component.py index 670dd2d2ee..91a2d79718 100644 --- a/tests/test_keep_largest_connected_component.py +++ b/tests/test_keep_largest_connected_component.py @@ -327,10 +327,10 @@ def test_correct_results(self, _, args, input_image, expected): converter = KeepLargestConnectedComponent(**args) if isinstance(input_image, torch.Tensor) and torch.cuda.is_available(): result = converter(clone(input_image).cuda()) - assert allclose(result, expected.cuda()) + else: result = converter(clone(input_image)) - assert allclose(result, expected) + self.assertTrue(allclose(result, expected.cuda())) @parameterized.expand(INVALID_CASES) def test_raise_exception(self, _, args, input_image, expected_error): diff --git a/tests/test_label_filter.py b/tests/test_label_filter.py index 9165fddc40..87da61acc9 100644 --- a/tests/test_label_filter.py +++ b/tests/test_label_filter.py @@ -108,10 +108,9 @@ def test_correct_results(self, _, args, input_image, expected): converter = LabelFilter(**args) if isinstance(input_image, torch.Tensor) and torch.cuda.is_available(): result = converter(clone(input_image).cuda()) - assert allclose(result, expected.cuda()) else: result = converter(clone(input_image)) - assert allclose(result, expected) + self.assertTrue(allclose(result, expected)) @parameterized.expand(INVALID_CASES) def test_raise_exception(self, _, args, input_image, expected_error): diff --git a/tests/test_rand_axis_flip.py b/tests/test_rand_axis_flip.py index bd53fa1fb0..9acd7be74c 100644 --- a/tests/test_rand_axis_flip.py +++ b/tests/test_rand_axis_flip.py @@ -12,10 +12,9 @@ import unittest import numpy as np -import torch from monai.transforms import RandAxisFlip -from tests.utils import TEST_NDARRAYS, NumpyImageTestCase2D +from tests.utils import TEST_NDARRAYS, NumpyImageTestCase2D, allclose class TestRandAxisFlip(NumpyImageTestCase2D): @@ -23,13 +22,10 @@ def test_correct_results(self): for p in TEST_NDARRAYS: flip = RandAxisFlip(prob=1.0) result = flip(p(self.imt[0])) - if isinstance(result, torch.Tensor): - result = result.cpu() - expected = [] for channel in self.imt[0]: expected.append(np.flip(channel, flip._axis)) - self.assertTrue(np.allclose(np.stack(expected), result)) + self.assertTrue(allclose(np.stack(expected), result)) if __name__ == "__main__": diff --git a/tests/test_rand_axis_flipd.py b/tests/test_rand_axis_flipd.py index 518d78dd29..bba7eb632c 100644 --- a/tests/test_rand_axis_flipd.py +++ b/tests/test_rand_axis_flipd.py @@ -12,10 +12,9 @@ import unittest import numpy as np -import torch from monai.transforms import RandAxisFlipd -from tests.utils import TEST_NDARRAYS, NumpyImageTestCase3D +from tests.utils import TEST_NDARRAYS, NumpyImageTestCase3D, allclose class TestRandAxisFlip(NumpyImageTestCase3D): @@ -23,13 +22,11 @@ def test_correct_results(self): for p in TEST_NDARRAYS: flip = RandAxisFlipd(keys="img", prob=1.0) result = flip({"img": p(self.imt[0])})["img"] - if isinstance(result, torch.Tensor): - result = result.cpu() expected = [] for channel in self.imt[0]: expected.append(np.flip(channel, flip._axis)) - self.assertTrue(np.allclose(np.stack(expected), result)) + self.assertTrue(allclose(np.stack(expected), result)) if __name__ == "__main__": diff --git a/tests/test_rand_flip.py b/tests/test_rand_flip.py index c20c13fec5..1d5cc34316 100644 --- a/tests/test_rand_flip.py +++ b/tests/test_rand_flip.py @@ -12,11 +12,10 @@ import unittest import numpy as np -import torch from parameterized import parameterized from monai.transforms import RandFlip -from tests.utils import TEST_NDARRAYS, NumpyImageTestCase2D +from tests.utils import TEST_NDARRAYS, NumpyImageTestCase2D, allclose INVALID_CASES = [("wrong_axis", ["s", 1], TypeError), ("not_numbers", "s", TypeError)] @@ -40,9 +39,7 @@ def test_correct_results(self, _, spatial_axis): expected.append(np.flip(channel, spatial_axis)) expected = np.stack(expected) result = flip(im) - if isinstance(result, torch.Tensor): - result = result.cpu() - self.assertTrue(np.allclose(expected, result)) + self.assertTrue(allclose(expected, result)) if __name__ == "__main__": diff --git a/tests/test_rand_flipd.py b/tests/test_rand_flipd.py index 42c7dfe4b5..743c5a49e8 100644 --- a/tests/test_rand_flipd.py +++ b/tests/test_rand_flipd.py @@ -12,11 +12,10 @@ import unittest import numpy as np -import torch from parameterized import parameterized from monai.transforms import RandFlipd -from tests.utils import TEST_NDARRAYS, NumpyImageTestCase2D +from tests.utils import TEST_NDARRAYS, NumpyImageTestCase2D, allclose VALID_CASES = [("no_axis", None), ("one_axis", 1), ("many_axis", [0, 1])] @@ -27,13 +26,11 @@ def test_correct_results(self, _, spatial_axis): for p in TEST_NDARRAYS: flip = RandFlipd(keys="img", prob=1.0, spatial_axis=spatial_axis) result = flip({"img": p(self.imt[0])})["img"] - if isinstance(result, torch.Tensor): - result = result.cpu() expected = [] for channel in self.imt[0]: expected.append(np.flip(channel, spatial_axis)) expected = np.stack(expected) - self.assertTrue(np.allclose(expected, result)) + self.assertTrue(allclose(expected, result)) if __name__ == "__main__": diff --git a/tests/utils.py b/tests/utils.py index 1148af7551..1b2b7dea9b 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -33,6 +33,7 @@ from monai.config import NdarrayTensor from monai.config.deviceconfig import USE_COMPILED +from monai.config.type_definitions import NdarrayOrTensor from monai.data import create_test_image_2d, create_test_image_3d from monai.utils import ensure_tuple, optional_import, set_determinism from monai.utils.module import version_leq @@ -55,7 +56,7 @@ def clone(data: NdarrayTensor) -> NdarrayTensor: return copy.deepcopy(data) -def allclose(a: NdarrayTensor, b: NdarrayTensor) -> bool: +def allclose(a: NdarrayOrTensor, b: NdarrayOrTensor, rtol=1.0e-5, atol=1.0e-8, equal_nan=False) -> bool: """ Check if all values of two data objects are close. @@ -63,19 +64,15 @@ def allclose(a: NdarrayTensor, b: NdarrayTensor) -> bool: This method also checks that both data objects are either Pytorch Tensors or numpy arrays. Args: - a (NdarrayTensor): Pytorch Tensor or numpy array for comparison - b (NdarrayTensor): Pytorch Tensor or numpy array to compare against + a (NdarrayOrTensor): Pytorch Tensor or numpy array for comparison + b (NdarrayOrTensor): Pytorch Tensor or numpy array to compare against Returns: bool: If both data objects are close. """ - if isinstance(a, np.ndarray) and isinstance(b, np.ndarray): - return np.allclose(a, b) - - if isinstance(a, torch.Tensor) and isinstance(b, torch.Tensor): - return torch.allclose(a, b) - - return False + a = a.cpu() if isinstance(a, torch.Tensor) else a + b = b.cpu() if isinstance(b, torch.Tensor) else b + return np.allclose(a, b, rtol, atol, equal_nan) def test_pretrained_networks(network, input_param, device): From 00e9f22da43cc7e667d929b3fa87968b774887e0 Mon Sep 17 00:00:00 2001 From: Richard Brown <33289025+rijobro@users.noreply.github.com> Date: Tue, 24 Aug 2021 15:48:55 +0100 Subject: [PATCH 2/3] assert_allclose Signed-off-by: Richard Brown <33289025+rijobro@users.noreply.github.com> --- tests/test_fill_holes.py | 4 ++-- tests/test_flip.py | 4 ++-- tests/test_flipd.py | 4 ++-- tests/test_keep_largest_connected_component.py | 4 ++-- tests/test_label_filter.py | 4 ++-- tests/test_rand_axis_flip.py | 4 ++-- tests/test_rand_axis_flipd.py | 4 ++-- tests/test_rand_flip.py | 4 ++-- tests/test_rand_flipd.py | 4 ++-- tests/utils.py | 12 +++--------- 10 files changed, 21 insertions(+), 27 deletions(-) diff --git a/tests/test_fill_holes.py b/tests/test_fill_holes.py index dfc6fb5154..6ea83c239b 100644 --- a/tests/test_fill_holes.py +++ b/tests/test_fill_holes.py @@ -16,7 +16,7 @@ from parameterized import parameterized from monai.transforms import FillHoles -from tests.utils import allclose, clone +from tests.utils import assert_allclose, clone grid_1_raw = [ [1, 1, 1], @@ -280,7 +280,7 @@ def test_correct_results(self, _, args, input_image, expected): result = converter(clone(input_image).cuda()) else: result = converter(clone(input_image)) - self.assertTrue(allclose(result, expected)) + assert_allclose(result, expected) @parameterized.expand(INVALID_CASES) def test_raise_exception(self, _, args, input_image, expected_error): diff --git a/tests/test_flip.py b/tests/test_flip.py index f9dbb5368e..404a3def7d 100644 --- a/tests/test_flip.py +++ b/tests/test_flip.py @@ -15,7 +15,7 @@ from parameterized import parameterized from monai.transforms import Flip -from tests.utils import TEST_NDARRAYS, NumpyImageTestCase2D, allclose +from tests.utils import TEST_NDARRAYS, NumpyImageTestCase2D, assert_allclose INVALID_CASES = [("wrong_axis", ["s", 1], TypeError), ("not_numbers", "s", TypeError)] @@ -39,7 +39,7 @@ def test_correct_results(self, _, spatial_axis): expected.append(np.flip(channel, spatial_axis)) expected = np.stack(expected) result = flip(im) - self.assertTrue(allclose(expected, result)) + assert_allclose(expected, result) if __name__ == "__main__": diff --git a/tests/test_flipd.py b/tests/test_flipd.py index 5266e36955..1676723800 100644 --- a/tests/test_flipd.py +++ b/tests/test_flipd.py @@ -15,7 +15,7 @@ from parameterized import parameterized from monai.transforms import Flipd -from tests.utils import TEST_NDARRAYS, NumpyImageTestCase2D, allclose +from tests.utils import TEST_NDARRAYS, NumpyImageTestCase2D, assert_allclose INVALID_CASES = [("wrong_axis", ["s", 1], TypeError), ("not_numbers", "s", TypeError)] @@ -38,7 +38,7 @@ def test_correct_results(self, _, spatial_axis): expected.append(np.flip(channel, spatial_axis)) expected = np.stack(expected) result = flip({"img": p(self.imt[0])})["img"] - self.assertTrue(allclose(expected, result)) + assert_allclose(expected, result) if __name__ == "__main__": diff --git a/tests/test_keep_largest_connected_component.py b/tests/test_keep_largest_connected_component.py index 91a2d79718..527d986614 100644 --- a/tests/test_keep_largest_connected_component.py +++ b/tests/test_keep_largest_connected_component.py @@ -15,7 +15,7 @@ from parameterized import parameterized from monai.transforms import KeepLargestConnectedComponent -from tests.utils import allclose, clone +from tests.utils import assert_allclose, clone grid_1 = torch.tensor([[[0, 0, 1, 0, 0], [0, 2, 1, 1, 1], [1, 2, 1, 0, 0], [1, 2, 0, 1, 0], [2, 2, 0, 0, 2]]]) grid_2 = torch.tensor([[[0, 0, 0, 0, 1], [0, 0, 1, 1, 1], [1, 0, 1, 1, 2], [1, 0, 1, 2, 2], [0, 0, 0, 0, 1]]]) @@ -330,7 +330,7 @@ def test_correct_results(self, _, args, input_image, expected): else: result = converter(clone(input_image)) - self.assertTrue(allclose(result, expected.cuda())) + assert_allclose(result, expected) @parameterized.expand(INVALID_CASES) def test_raise_exception(self, _, args, input_image, expected_error): diff --git a/tests/test_label_filter.py b/tests/test_label_filter.py index 87da61acc9..c699fb31fd 100644 --- a/tests/test_label_filter.py +++ b/tests/test_label_filter.py @@ -16,7 +16,7 @@ from parameterized import parameterized from monai.transforms import LabelFilter -from tests.utils import allclose, clone +from tests.utils import assert_allclose, clone grid_1 = torch.tensor( [ @@ -110,7 +110,7 @@ def test_correct_results(self, _, args, input_image, expected): result = converter(clone(input_image).cuda()) else: result = converter(clone(input_image)) - self.assertTrue(allclose(result, expected)) + assert_allclose(result, expected) @parameterized.expand(INVALID_CASES) def test_raise_exception(self, _, args, input_image, expected_error): diff --git a/tests/test_rand_axis_flip.py b/tests/test_rand_axis_flip.py index 9acd7be74c..c05c3a1e0d 100644 --- a/tests/test_rand_axis_flip.py +++ b/tests/test_rand_axis_flip.py @@ -14,7 +14,7 @@ import numpy as np from monai.transforms import RandAxisFlip -from tests.utils import TEST_NDARRAYS, NumpyImageTestCase2D, allclose +from tests.utils import TEST_NDARRAYS, NumpyImageTestCase2D, assert_allclose class TestRandAxisFlip(NumpyImageTestCase2D): @@ -25,7 +25,7 @@ def test_correct_results(self): expected = [] for channel in self.imt[0]: expected.append(np.flip(channel, flip._axis)) - self.assertTrue(allclose(np.stack(expected), result)) + assert_allclose(np.stack(expected), result) if __name__ == "__main__": diff --git a/tests/test_rand_axis_flipd.py b/tests/test_rand_axis_flipd.py index bba7eb632c..7bef0baa63 100644 --- a/tests/test_rand_axis_flipd.py +++ b/tests/test_rand_axis_flipd.py @@ -14,7 +14,7 @@ import numpy as np from monai.transforms import RandAxisFlipd -from tests.utils import TEST_NDARRAYS, NumpyImageTestCase3D, allclose +from tests.utils import TEST_NDARRAYS, NumpyImageTestCase3D, assert_allclose class TestRandAxisFlip(NumpyImageTestCase3D): @@ -26,7 +26,7 @@ def test_correct_results(self): expected = [] for channel in self.imt[0]: expected.append(np.flip(channel, flip._axis)) - self.assertTrue(allclose(np.stack(expected), result)) + assert_allclose(np.stack(expected), result) if __name__ == "__main__": diff --git a/tests/test_rand_flip.py b/tests/test_rand_flip.py index 1d5cc34316..b3c514cb1f 100644 --- a/tests/test_rand_flip.py +++ b/tests/test_rand_flip.py @@ -15,7 +15,7 @@ from parameterized import parameterized from monai.transforms import RandFlip -from tests.utils import TEST_NDARRAYS, NumpyImageTestCase2D, allclose +from tests.utils import TEST_NDARRAYS, NumpyImageTestCase2D, assert_allclose INVALID_CASES = [("wrong_axis", ["s", 1], TypeError), ("not_numbers", "s", TypeError)] @@ -39,7 +39,7 @@ def test_correct_results(self, _, spatial_axis): expected.append(np.flip(channel, spatial_axis)) expected = np.stack(expected) result = flip(im) - self.assertTrue(allclose(expected, result)) + assert_allclose(expected, result) if __name__ == "__main__": diff --git a/tests/test_rand_flipd.py b/tests/test_rand_flipd.py index 743c5a49e8..8972024fd8 100644 --- a/tests/test_rand_flipd.py +++ b/tests/test_rand_flipd.py @@ -15,7 +15,7 @@ from parameterized import parameterized from monai.transforms import RandFlipd -from tests.utils import TEST_NDARRAYS, NumpyImageTestCase2D, allclose +from tests.utils import TEST_NDARRAYS, NumpyImageTestCase2D, assert_allclose VALID_CASES = [("no_axis", None), ("one_axis", 1), ("many_axis", [0, 1])] @@ -30,7 +30,7 @@ def test_correct_results(self, _, spatial_axis): for channel in self.imt[0]: expected.append(np.flip(channel, spatial_axis)) expected = np.stack(expected) - self.assertTrue(allclose(expected, result)) + assert_allclose(expected, result) if __name__ == "__main__": diff --git a/tests/utils.py b/tests/utils.py index 1b2b7dea9b..22720849f1 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -56,23 +56,17 @@ def clone(data: NdarrayTensor) -> NdarrayTensor: return copy.deepcopy(data) -def allclose(a: NdarrayOrTensor, b: NdarrayOrTensor, rtol=1.0e-5, atol=1.0e-8, equal_nan=False) -> bool: +def assert_allclose(a: NdarrayOrTensor, b: NdarrayOrTensor, *args, **kwargs): """ - Check if all values of two data objects are close. - - Note: - This method also checks that both data objects are either Pytorch Tensors or numpy arrays. + Assert that all values of two data objects are close. Args: a (NdarrayOrTensor): Pytorch Tensor or numpy array for comparison b (NdarrayOrTensor): Pytorch Tensor or numpy array to compare against - - Returns: - bool: If both data objects are close. """ a = a.cpu() if isinstance(a, torch.Tensor) else a b = b.cpu() if isinstance(b, torch.Tensor) else b - return np.allclose(a, b, rtol, atol, equal_nan) + np.testing.assert_allclose(a, b, *args, **kwargs) def test_pretrained_networks(network, input_param, device): From 586737936976a225a3eb5eaf20f360d2165d0f68 Mon Sep 17 00:00:00 2001 From: Richard Brown <33289025+rijobro@users.noreply.github.com> Date: Tue, 24 Aug 2021 17:03:29 +0100 Subject: [PATCH 3/3] ScaleIntensity Signed-off-by: Richard Brown <33289025+rijobro@users.noreply.github.com> --- monai/data/synthetic.py | 4 ++-- monai/transforms/intensity/array.py | 14 +++++++---- monai/transforms/intensity/dictionary.py | 8 +++++-- monai/transforms/utils.py | 12 ++++++---- monai/visualize/img2tensorboard.py | 2 +- tests/test_rand_scale_intensity.py | 15 ++++++------ tests/test_rand_scale_intensityd.py | 17 +++++++------- tests/test_scale_intensity.py | 26 ++++++++++---------- tests/test_scale_intensityd.py | 30 +++++++++++++----------- 9 files changed, 74 insertions(+), 54 deletions(-) diff --git a/monai/data/synthetic.py b/monai/data/synthetic.py index 20a7829cab..6eec9fd277 100644 --- a/monai/data/synthetic.py +++ b/monai/data/synthetic.py @@ -76,7 +76,7 @@ def create_test_image_2d( labels = np.ceil(image).astype(np.int32) norm = rs.uniform(0, num_seg_classes * noise_max, size=image.shape) - noisyimage = rescale_array(np.maximum(image, norm)) + noisyimage: np.ndarray = rescale_array(np.maximum(image, norm)) # type: ignore if channel_dim is not None: if not (isinstance(channel_dim, int) and channel_dim in (-1, 0, 2)): @@ -151,7 +151,7 @@ def create_test_image_3d( labels = np.ceil(image).astype(np.int32) norm = rs.uniform(0, num_seg_classes * noise_max, size=image.shape) - noisyimage = rescale_array(np.maximum(image, norm)) + noisyimage: np.ndarray = rescale_array(np.maximum(image, norm)) # type: ignore if channel_dim is not None: if not (isinstance(channel_dim, int) and channel_dim in (-1, 0, 3)): diff --git a/monai/transforms/intensity/array.py b/monai/transforms/intensity/array.py index 4b7c8d6997..c2c4b3c38b 100644 --- a/monai/transforms/intensity/array.py +++ b/monai/transforms/intensity/array.py @@ -373,6 +373,8 @@ class ScaleIntensity(Transform): If `minv` and `maxv` not provided, use `factor` to scale image by ``v = v * (1 + factor)``. """ + backend = [TransformBackends.TORCH, TransformBackends.NUMPY] + def __init__( self, minv: Optional[float] = 0.0, maxv: Optional[float] = 1.0, factor: Optional[float] = None ) -> None: @@ -387,7 +389,7 @@ def __init__( self.maxv = maxv self.factor = factor - def __call__(self, img: np.ndarray) -> np.ndarray: + def __call__(self, img: NdarrayOrTensor) -> NdarrayOrTensor: """ Apply the transform to `img`. @@ -396,9 +398,11 @@ def __call__(self, img: np.ndarray) -> np.ndarray: """ if self.minv is not None and self.maxv is not None: - return np.asarray(rescale_array(img, self.minv, self.maxv, img.dtype)) + return rescale_array(img, self.minv, self.maxv, img.dtype) if self.factor is not None: - return np.asarray(img * (1 + self.factor), dtype=img.dtype) + out = img * (1 + self.factor) + out, *_ = convert_data_type(out, dtype=img.dtype) + return out raise ValueError("Incompatible values: minv=None or maxv=None and factor=None.") @@ -408,6 +412,8 @@ class RandScaleIntensity(RandomizableTransform): is randomly picked. """ + backend = ScaleIntensity.backend + def __init__(self, factors: Union[Tuple[float, float], float], prob: float = 0.1) -> None: """ Args: @@ -429,7 +435,7 @@ def randomize(self, data: Optional[Any] = None) -> None: self.factor = self.R.uniform(low=self.factors[0], high=self.factors[1]) super().randomize(None) - def __call__(self, img: np.ndarray) -> np.ndarray: + def __call__(self, img: NdarrayOrTensor) -> NdarrayOrTensor: """ Apply the transform to `img`. """ diff --git a/monai/transforms/intensity/dictionary.py b/monai/transforms/intensity/dictionary.py index 522007df29..ad57353d07 100644 --- a/monai/transforms/intensity/dictionary.py +++ b/monai/transforms/intensity/dictionary.py @@ -472,6 +472,8 @@ class ScaleIntensityd(MapTransform): If `minv` and `maxv` not provided, use `factor` to scale image by ``v = v * (1 + factor)``. """ + backend = ScaleIntensity.backend + def __init__( self, keys: KeysCollection, @@ -494,7 +496,7 @@ def __init__( super().__init__(keys, allow_missing_keys) self.scaler = ScaleIntensity(minv, maxv, factor) - def __call__(self, data: Mapping[Hashable, np.ndarray]) -> Dict[Hashable, np.ndarray]: + def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> Dict[Hashable, NdarrayOrTensor]: d = dict(data) for key in self.key_iterator(d): d[key] = self.scaler(d[key]) @@ -506,6 +508,8 @@ class RandScaleIntensityd(RandomizableTransform, MapTransform): Dictionary-based version :py:class:`monai.transforms.RandScaleIntensity`. """ + backend = ScaleIntensity.backend + def __init__( self, keys: KeysCollection, @@ -539,7 +543,7 @@ def randomize(self, data: Optional[Any] = None) -> None: self.factor = self.R.uniform(low=self.factors[0], high=self.factors[1]) super().randomize(None) - def __call__(self, data: Mapping[Hashable, np.ndarray]) -> Dict[Hashable, np.ndarray]: + def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> Dict[Hashable, NdarrayOrTensor]: d = dict(data) self.randomize() if not self._do_transform: diff --git a/monai/transforms/utils.py b/monai/transforms/utils.py index e81cb7ca17..e3e61b6c97 100644 --- a/monai/transforms/utils.py +++ b/monai/transforms/utils.py @@ -22,6 +22,7 @@ import monai import monai.transforms.transform from monai.config import DtypeLike, IndexSelection +from monai.config.type_definitions import NdarrayOrTensor from monai.networks.layers import GaussianFilter from monai.transforms.compose import Compose, OneOf from monai.transforms.transform import MapTransform, Transform @@ -37,6 +38,7 @@ min_version, optional_import, ) +from monai.utils.type_conversion import convert_data_type measure, _ = optional_import("skimage.measure", "0.14.2", min_version) ndimage, _ = optional_import("scipy.ndimage") @@ -130,15 +132,17 @@ def zero_margins(img: np.ndarray, margin: int) -> bool: return not np.any(img[:, :margin, :]) and not np.any(img[:, -margin:, :]) -def rescale_array(arr: np.ndarray, minv: float = 0.0, maxv: float = 1.0, dtype: DtypeLike = np.float32): +def rescale_array( + arr: NdarrayOrTensor, minv: float = 0.0, maxv: float = 1.0, dtype: Union[DtypeLike, torch.dtype] = np.float32 +) -> NdarrayOrTensor: """ Rescale the values of numpy array `arr` to be from `minv` to `maxv`. """ if dtype is not None: - arr = arr.astype(dtype) + arr, *_ = convert_data_type(arr, dtype=dtype) - mina = np.min(arr) - maxa = np.max(arr) + mina = arr.min() + maxa = arr.max() if mina == maxa: return arr * minv diff --git a/monai/visualize/img2tensorboard.py b/monai/visualize/img2tensorboard.py index 4a17607320..ccdbdc2396 100644 --- a/monai/visualize/img2tensorboard.py +++ b/monai/visualize/img2tensorboard.py @@ -188,7 +188,7 @@ def plot_2d_or_3d_image( d: np.ndarray = data_index.detach().cpu().numpy() if isinstance(data_index, torch.Tensor) else data_index if d.ndim == 2: - d = rescale_array(d, 0, 1) + d = rescale_array(d, 0, 1) # type: ignore dataformats = "HW" writer.add_image(f"{tag}_{dataformats}", d, step, dataformats=dataformats) return diff --git a/tests/test_rand_scale_intensity.py b/tests/test_rand_scale_intensity.py index 2126301758..750d88bfad 100644 --- a/tests/test_rand_scale_intensity.py +++ b/tests/test_rand_scale_intensity.py @@ -14,17 +14,18 @@ import numpy as np from monai.transforms import RandScaleIntensity -from tests.utils import NumpyImageTestCase2D +from tests.utils import TEST_NDARRAYS, NumpyImageTestCase2D, assert_allclose class TestRandScaleIntensity(NumpyImageTestCase2D): def test_value(self): - scaler = RandScaleIntensity(factors=0.5, prob=1.0) - scaler.set_random_state(seed=0) - result = scaler(self.imt) - np.random.seed(0) - expected = (self.imt * (1 + np.random.uniform(low=-0.5, high=0.5))).astype(np.float32) - np.testing.assert_allclose(result, expected) + for p in TEST_NDARRAYS: + scaler = RandScaleIntensity(factors=0.5, prob=1.0) + scaler.set_random_state(seed=0) + result = scaler(p(self.imt)) + np.random.seed(0) + expected = p((self.imt * (1 + np.random.uniform(low=-0.5, high=0.5))).astype(np.float32)) + assert_allclose(result, expected, rtol=1e-7, atol=0) if __name__ == "__main__": diff --git a/tests/test_rand_scale_intensityd.py b/tests/test_rand_scale_intensityd.py index 6e207e3cc2..a8d2e63f65 100644 --- a/tests/test_rand_scale_intensityd.py +++ b/tests/test_rand_scale_intensityd.py @@ -14,18 +14,19 @@ import numpy as np from monai.transforms import RandScaleIntensityd -from tests.utils import NumpyImageTestCase2D +from tests.utils import TEST_NDARRAYS, NumpyImageTestCase2D, assert_allclose class TestRandScaleIntensityd(NumpyImageTestCase2D): def test_value(self): - key = "img" - scaler = RandScaleIntensityd(keys=[key], factors=0.5, prob=1.0) - scaler.set_random_state(seed=0) - result = scaler({key: self.imt}) - np.random.seed(0) - expected = (self.imt * (1 + np.random.uniform(low=-0.5, high=0.5))).astype(np.float32) - np.testing.assert_allclose(result[key], expected) + for p in TEST_NDARRAYS: + key = "img" + scaler = RandScaleIntensityd(keys=[key], factors=0.5, prob=1.0) + scaler.set_random_state(seed=0) + result = scaler({key: p(self.imt)}) + np.random.seed(0) + expected = (self.imt * (1 + np.random.uniform(low=-0.5, high=0.5))).astype(np.float32) + assert_allclose(result[key], expected) if __name__ == "__main__": diff --git a/tests/test_scale_intensity.py b/tests/test_scale_intensity.py index 61e89191fd..c2485af616 100644 --- a/tests/test_scale_intensity.py +++ b/tests/test_scale_intensity.py @@ -14,24 +14,26 @@ import numpy as np from monai.transforms import ScaleIntensity -from tests.utils import NumpyImageTestCase2D +from tests.utils import TEST_NDARRAYS, NumpyImageTestCase2D, assert_allclose class TestScaleIntensity(NumpyImageTestCase2D): def test_range_scale(self): - scaler = ScaleIntensity(minv=1.0, maxv=2.0) - result = scaler(self.imt) - mina = np.min(self.imt) - maxa = np.max(self.imt) - norm = (self.imt - mina) / (maxa - mina) - expected = (norm * (2.0 - 1.0)) + 1.0 - np.testing.assert_allclose(result, expected) + for p in TEST_NDARRAYS: + scaler = ScaleIntensity(minv=1.0, maxv=2.0) + result = scaler(p(self.imt)) + mina = self.imt.min() + maxa = self.imt.max() + norm = (self.imt - mina) / (maxa - mina) + expected = p((norm * (2.0 - 1.0)) + 1.0) + assert_allclose(result, expected, rtol=1e-7, atol=0) def test_factor_scale(self): - scaler = ScaleIntensity(minv=None, maxv=None, factor=0.1) - result = scaler(self.imt) - expected = (self.imt * (1 + 0.1)).astype(np.float32) - np.testing.assert_allclose(result, expected) + for p in TEST_NDARRAYS: + scaler = ScaleIntensity(minv=None, maxv=None, factor=0.1) + result = scaler(p(self.imt)) + expected = p((self.imt * (1 + 0.1)).astype(np.float32)) + assert_allclose(result, expected, rtol=1e-7, atol=0) if __name__ == "__main__": diff --git a/tests/test_scale_intensityd.py b/tests/test_scale_intensityd.py index 688c99c6af..6e13dbc272 100644 --- a/tests/test_scale_intensityd.py +++ b/tests/test_scale_intensityd.py @@ -14,26 +14,28 @@ import numpy as np from monai.transforms import ScaleIntensityd -from tests.utils import NumpyImageTestCase2D +from tests.utils import TEST_NDARRAYS, NumpyImageTestCase2D, assert_allclose class TestScaleIntensityd(NumpyImageTestCase2D): def test_range_scale(self): - key = "img" - scaler = ScaleIntensityd(keys=[key], minv=1.0, maxv=2.0) - result = scaler({key: self.imt}) - mina = np.min(self.imt) - maxa = np.max(self.imt) - norm = (self.imt - mina) / (maxa - mina) - expected = (norm * (2.0 - 1.0)) + 1.0 - np.testing.assert_allclose(result[key], expected) + for p in TEST_NDARRAYS: + key = "img" + scaler = ScaleIntensityd(keys=[key], minv=1.0, maxv=2.0) + result = scaler({key: p(self.imt)}) + mina = np.min(self.imt) + maxa = np.max(self.imt) + norm = (self.imt - mina) / (maxa - mina) + expected = (norm * (2.0 - 1.0)) + 1.0 + assert_allclose(result[key], expected) def test_factor_scale(self): - key = "img" - scaler = ScaleIntensityd(keys=[key], minv=None, maxv=None, factor=0.1) - result = scaler({key: self.imt}) - expected = (self.imt * (1 + 0.1)).astype(np.float32) - np.testing.assert_allclose(result[key], expected) + for p in TEST_NDARRAYS: + key = "img" + scaler = ScaleIntensityd(keys=[key], minv=None, maxv=None, factor=0.1) + result = scaler({key: p(self.imt)}) + expected = (self.imt * (1 + 0.1)).astype(np.float32) + assert_allclose(result[key], expected) if __name__ == "__main__":