From 8d2956e0a8be6ee91e0a59d2eb404a248c85d9bb Mon Sep 17 00:00:00 2001 From: Lyndon Boone Date: Wed, 21 Apr 2021 14:20:40 -0400 Subject: [PATCH 1/8] Added RicianNoise transform Signed-off-by: Lyndon Boone --- monai/transforms/__init__.py | 1 + monai/transforms/intensity/array.py | 89 +++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+) diff --git a/monai/transforms/__init__.py b/monai/transforms/__init__.py index 6d9ddcd94d..093c02e15c 100644 --- a/monai/transforms/__init__.py +++ b/monai/transforms/__init__.py @@ -78,6 +78,7 @@ RandGaussianSharpen, RandGaussianSmooth, RandHistogramShift, + RandRicianNoise, RandScaleIntensity, RandShiftIntensity, RandStdShiftIntensity, diff --git a/monai/transforms/intensity/array.py b/monai/transforms/intensity/array.py index 62350d4ab0..2bd44aae32 100644 --- a/monai/transforms/intensity/array.py +++ b/monai/transforms/intensity/array.py @@ -28,6 +28,7 @@ __all__ = [ "RandGaussianNoise", + "RandRicianNoise", "ShiftIntensity", "RandShiftIntensity", "StdShiftIntensity", @@ -85,6 +86,94 @@ def __call__(self, img: Union[torch.Tensor, np.ndarray]) -> Union[torch.Tensor, return img + self._noise.astype(dtype) +class RandRicianNoise(RandomizableTransform): + """ + Add Rician noise to image. + + Args: + prob: Probability to add Rician noise. + mean: Mean or "centre" of the Gaussian distributions sampled to make up + the Rician noise. + std: Standard deviation (spread) of the Gaussian distributions sampled + to make up the Rician noise. + channel_wise: If True, treats each channel of the image separately. + relative: If True, the spread of the sampled Gaussian distributions will + be std times the standard deviation of the image or channel's intensity + histogram. + sample_std: If True, sample the spread of the Gaussian distributions + uniformly from 0 to std. + """ + def __init__( + self, + prob: float = 0.1, + mean: Union[Sequence[float], float] = 0.0, + std: Union[Sequence[float], float] = 1.0, + channel_wise: bool = False, + relative: bool = False, + sample_std: bool = True, + ) -> None: + RandomizableTransform.__init__(self, prob) + self.prob = prob + self.mean = mean + self.std = std + self.channel_wise = channel_wise + self.relative = relative + self.sample_std = sample_std + self._noise1 = None + self._noise2 = None + + def _add_noise( + self, + img: Union[torch.Tensor, np.ndarray], + mean: float, + std: float + ) -> Union[torch.Tensor, np.ndarray]: + im_shape = img.shape + _std = self.R.uniform(0, std) if self.sample_std else std + self._noise1 = self.R.normal(mean, _std, size=im_shape) + self._noise2 = self.R.normal(mean, _std, size=im_shape) + dtype = dtype_torch_to_numpy(img.dtype) if isinstance(img, torch.Tensor) else img.dtype + return np.sqrt((img + self._noise1.astype(dtype))**2 + self._noise2.astype(dtype)**2) + + def __call__(self, img: Union[torch.Tensor, np.ndarray]) -> Union[torch.Tensor, np.ndarray]: + """ + Apply the transform to `img`. + """ + super().randomize(None) + if not self._do_transform: + return img + if self.channel_wise: + if isinstance(self.mean, Iterable) and len(self.mean) != len(img): + raise ValueError(f"img has {len(img)} channels, but mean has {len(self.mean)} components.") + elif isinstance(self.mean, (int, float)): + _mean = (self.mean,)*len(img) + else: + _mean = self.mean + if isinstance(self.std, Iterable) and len(self.std) != len(img): + raise ValueError(f"img has {len(img)} channels, but std has {len(self.std)} components.") + elif isinstance(self.std, (int, float)): + _std = (self.std,)*len(img) + else: + _std = self.std + for i, d in enumerate(img): + img[i] = self._add_noise( + d, + mean=_mean[i], + std=_std[i]*d.std() if self.relative else _std[i] + ) + else: + if not isinstance(self.mean, (int, float)): + raise AssertionError("If channel_wise is False, mean must be a float or int number.") + if not isinstance(self.std, (int, float)): + raise AssertionError("If channel_wise is False, std must be a float or int number.") + img = self._add_noise( + img, + mean=self.mean, + std=self.std*img.std() if self.relative else self.std + ) + return img + + class ShiftIntensity(Transform): """ Shift intensity uniformly for the entire image with specified `offset`. From 0040e617098d3066acb86638d9c97d879f7b15b2 Mon Sep 17 00:00:00 2001 From: Lyndon Boone Date: Wed, 21 Apr 2021 14:24:59 -0400 Subject: [PATCH 2/8] Use ensure_tuple_rep in channel-wise RandRicianNoise transform Signed-off-by: Lyndon Boone --- monai/transforms/intensity/array.py | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/monai/transforms/intensity/array.py b/monai/transforms/intensity/array.py index 2bd44aae32..fd080913ab 100644 --- a/monai/transforms/intensity/array.py +++ b/monai/transforms/intensity/array.py @@ -24,7 +24,7 @@ from monai.networks.layers import GaussianFilter, HilbertTransform, SavitzkyGolayFilter from monai.transforms.transform import RandomizableTransform, Transform from monai.transforms.utils import rescale_array -from monai.utils import PT_BEFORE_1_7, InvalidPyTorchVersionError, dtype_torch_to_numpy, ensure_tuple_size +from monai.utils import PT_BEFORE_1_7, InvalidPyTorchVersionError, dtype_torch_to_numpy, ensure_tuple_rep, ensure_tuple_size __all__ = [ "RandGaussianNoise", @@ -143,18 +143,8 @@ def __call__(self, img: Union[torch.Tensor, np.ndarray]) -> Union[torch.Tensor, if not self._do_transform: return img if self.channel_wise: - if isinstance(self.mean, Iterable) and len(self.mean) != len(img): - raise ValueError(f"img has {len(img)} channels, but mean has {len(self.mean)} components.") - elif isinstance(self.mean, (int, float)): - _mean = (self.mean,)*len(img) - else: - _mean = self.mean - if isinstance(self.std, Iterable) and len(self.std) != len(img): - raise ValueError(f"img has {len(img)} channels, but std has {len(self.std)} components.") - elif isinstance(self.std, (int, float)): - _std = (self.std,)*len(img) - else: - _std = self.std + _mean = ensure_tuple_rep(self.mean, len(img)) + _std = ensure_tuple_rep(self.std, len(img)) for i, d in enumerate(img): img[i] = self._add_noise( d, From 14c4d24732e3040766e54fee7403e83a29047220 Mon Sep 17 00:00:00 2001 From: Lyndon Boone Date: Wed, 21 Apr 2021 14:52:39 -0400 Subject: [PATCH 3/8] Added RandRicianNoised transform Signed-off-by: Lyndon Boone --- monai/transforms/__init__.py | 3 ++ monai/transforms/intensity/dictionary.py | 59 ++++++++++++++++++++++++ 2 files changed, 62 insertions(+) diff --git a/monai/transforms/__init__.py b/monai/transforms/__init__.py index 093c02e15c..8c0431b4b4 100644 --- a/monai/transforms/__init__.py +++ b/monai/transforms/__init__.py @@ -124,6 +124,9 @@ RandHistogramShiftd, RandHistogramShiftD, RandHistogramShiftDict, + RandRicianNoised, + RandRicianNoiseD, + RandRicianNoiseDict, RandScaleIntensityd, RandScaleIntensityD, RandScaleIntensityDict, diff --git a/monai/transforms/intensity/dictionary.py b/monai/transforms/intensity/dictionary.py index a35e5c8ea6..88593fe14a 100644 --- a/monai/transforms/intensity/dictionary.py +++ b/monai/transforms/intensity/dictionary.py @@ -29,6 +29,7 @@ MaskIntensity, NormalizeIntensity, RandBiasField, + RandRicianNoise, ScaleIntensity, ScaleIntensityRange, ScaleIntensityRangePercentiles, @@ -41,6 +42,7 @@ __all__ = [ "RandGaussianNoised", + "RandRicianNoised", "ShiftIntensityd", "RandShiftIntensityd", "ScaleIntensityd", @@ -152,6 +154,62 @@ def __call__(self, data: Mapping[Hashable, np.ndarray]) -> Dict[Hashable, np.nda return d +class RandRicianNoised(RandomizableTransform, MapTransform): + """ + Dictionary-based version :py:class:`monai.transforms.RandRicianNoise`. + Add Rician noise to image. This transform assumes all the expected fields have same shape. + + Args: + keys: Keys of the corresponding items to be transformed. + See also: :py:class:`monai.transforms.compose.MapTransform` + global_prob: Probability to add Rician noise to the dictionary. + prob: Probability to add Rician noise to each item in the dictionary, + once asserted that noise will be added to the dictionary at all. + mean: Mean or "centre" of the Gaussian distributions sampled to make up + the Rician noise. + std: Standard deviation (spread) of the Gaussian distributions sampled + to make up the Rician noise. + channel_wise: If True, treats each channel of the image separately. + relative: If True, the spread of the sampled Gaussian distributions will + be std times the standard deviation of the image or channel's intensity + histogram. + sample_std: If True, sample the spread of the Gaussian distributions + uniformly from 0 to std. + allow_missing_keys: Don't raise exception if key is missing. + """ + def __init__( + self, + keys: KeysCollection, + global_prob: float = 0.1, + prob: float = 1.0, + mean: Union[Sequence[float], float] = 0.0, + std: Union[Sequence[float], float] = 1.0, + channel_wise: bool = False, + relative: bool = False, + sample_std: bool = True, + allow_missing_keys: bool = False, + ) -> None: + MapTransform.__init__(self, keys, allow_missing_keys) + RandomizableTransform.__init__(self, global_prob) + self.rand_rician_noise = RandRicianNoise( + prob, + mean, + std, + channel_wise, + relative, + sample_std, + ) + + def __call__(self, data: Mapping[Hashable, np.ndarray]) -> Dict[Hashable, np.ndarray]: + d = dict(data) + super().randomize(None) + if not self._do_transform: + return d + for key in self.key_iterator(d): + d[key] = self.rand_rician_noise(d[key]) + return d + + class ShiftIntensityd(MapTransform): """ Dictionary-based wrapper of :py:class:`monai.transforms.ShiftIntensity`. @@ -958,6 +1016,7 @@ def __call__(self, data: Mapping[Hashable, np.ndarray]) -> Dict[Hashable, np.nda RandGaussianNoiseD = RandGaussianNoiseDict = RandGaussianNoised +RandRicianNoiseD = RandRicianNoiseDict = RandRicianNoised ShiftIntensityD = ShiftIntensityDict = ShiftIntensityd RandShiftIntensityD = RandShiftIntensityDict = RandShiftIntensityd StdShiftIntensityD = StdShiftIntensityDict = StdShiftIntensityd From 30350726b8b4b45902ed6eadaf15ddfa0403f435 Mon Sep 17 00:00:00 2001 From: Lyndon Boone Date: Wed, 21 Apr 2021 14:58:44 -0400 Subject: [PATCH 4/8] Autofixed coding style errors Signed-off-by: Lyndon Boone --- monai/transforms/intensity/array.py | 28 ++++++++++-------------- monai/transforms/intensity/dictionary.py | 1 + 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/monai/transforms/intensity/array.py b/monai/transforms/intensity/array.py index fd080913ab..8370890780 100644 --- a/monai/transforms/intensity/array.py +++ b/monai/transforms/intensity/array.py @@ -24,7 +24,13 @@ from monai.networks.layers import GaussianFilter, HilbertTransform, SavitzkyGolayFilter from monai.transforms.transform import RandomizableTransform, Transform from monai.transforms.utils import rescale_array -from monai.utils import PT_BEFORE_1_7, InvalidPyTorchVersionError, dtype_torch_to_numpy, ensure_tuple_rep, ensure_tuple_size +from monai.utils import ( + PT_BEFORE_1_7, + InvalidPyTorchVersionError, + dtype_torch_to_numpy, + ensure_tuple_rep, + ensure_tuple_size, +) __all__ = [ "RandGaussianNoise", @@ -103,6 +109,7 @@ class RandRicianNoise(RandomizableTransform): sample_std: If True, sample the spread of the Gaussian distributions uniformly from 0 to std. """ + def __init__( self, prob: float = 0.1, @@ -123,17 +130,14 @@ def __init__( self._noise2 = None def _add_noise( - self, - img: Union[torch.Tensor, np.ndarray], - mean: float, - std: float + self, img: Union[torch.Tensor, np.ndarray], mean: float, std: float ) -> Union[torch.Tensor, np.ndarray]: im_shape = img.shape _std = self.R.uniform(0, std) if self.sample_std else std self._noise1 = self.R.normal(mean, _std, size=im_shape) self._noise2 = self.R.normal(mean, _std, size=im_shape) dtype = dtype_torch_to_numpy(img.dtype) if isinstance(img, torch.Tensor) else img.dtype - return np.sqrt((img + self._noise1.astype(dtype))**2 + self._noise2.astype(dtype)**2) + return np.sqrt((img + self._noise1.astype(dtype)) ** 2 + self._noise2.astype(dtype) ** 2) def __call__(self, img: Union[torch.Tensor, np.ndarray]) -> Union[torch.Tensor, np.ndarray]: """ @@ -146,21 +150,13 @@ def __call__(self, img: Union[torch.Tensor, np.ndarray]) -> Union[torch.Tensor, _mean = ensure_tuple_rep(self.mean, len(img)) _std = ensure_tuple_rep(self.std, len(img)) for i, d in enumerate(img): - img[i] = self._add_noise( - d, - mean=_mean[i], - std=_std[i]*d.std() if self.relative else _std[i] - ) + img[i] = self._add_noise(d, mean=_mean[i], std=_std[i] * d.std() if self.relative else _std[i]) else: if not isinstance(self.mean, (int, float)): raise AssertionError("If channel_wise is False, mean must be a float or int number.") if not isinstance(self.std, (int, float)): raise AssertionError("If channel_wise is False, std must be a float or int number.") - img = self._add_noise( - img, - mean=self.mean, - std=self.std*img.std() if self.relative else self.std - ) + img = self._add_noise(img, mean=self.mean, std=self.std * img.std() if self.relative else self.std) return img diff --git a/monai/transforms/intensity/dictionary.py b/monai/transforms/intensity/dictionary.py index 88593fe14a..64f650065b 100644 --- a/monai/transforms/intensity/dictionary.py +++ b/monai/transforms/intensity/dictionary.py @@ -177,6 +177,7 @@ class RandRicianNoised(RandomizableTransform, MapTransform): uniformly from 0 to std. allow_missing_keys: Don't raise exception if key is missing. """ + def __init__( self, keys: KeysCollection, From 3d7b74040ae175ec19e2683a0448ac6475343e5d Mon Sep 17 00:00:00 2001 From: Lyndon Boone Date: Fri, 23 Apr 2021 16:45:16 -0400 Subject: [PATCH 5/8] Added paper reference for RandRicianNoise in docstring Signed-off-by: Lyndon Boone --- monai/transforms/intensity/array.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/monai/transforms/intensity/array.py b/monai/transforms/intensity/array.py index 8370890780..9f3e21d1f1 100644 --- a/monai/transforms/intensity/array.py +++ b/monai/transforms/intensity/array.py @@ -95,6 +95,11 @@ def __call__(self, img: Union[torch.Tensor, np.ndarray]) -> Union[torch.Tensor, class RandRicianNoise(RandomizableTransform): """ Add Rician noise to image. + Rician noise in MRI is the result of performing a magnitude operation on complex + data with Gaussian noise of the same variance in both channels, as described in `Noise in Magnitude Magnetic Resonance Images + `_. This transform is adapted from + `DIPY`_. See also: `The rician distribution of noisy mri data + `_. Args: prob: Probability to add Rician noise. From b9b9034179c142433e40907f58768bf82fb4ff56 Mon Sep 17 00:00:00 2001 From: Lyndon Boone Date: Fri, 23 Apr 2021 18:04:03 -0400 Subject: [PATCH 6/8] Added unit test for RandRicianNoise transform Signed-off-by: Lyndon Boone --- tests/test_rand_rician_noise.py | 56 +++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 tests/test_rand_rician_noise.py diff --git a/tests/test_rand_rician_noise.py b/tests/test_rand_rician_noise.py new file mode 100644 index 0000000000..6504fd9069 --- /dev/null +++ b/tests/test_rand_rician_noise.py @@ -0,0 +1,56 @@ +# 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 RandRicianNoise +from tests.utils import NumpyImageTestCase2D, TorchImageTestCase2D + + +class TestRandRicianNoise(NumpyImageTestCase2D): + @parameterized.expand([("test_zero_mean", 0, 0.1), ("test_non_zero_mean", 1, 0.5)]) + def test_correct_results(self, _, mean, std): + seed = 0 + rician_fn = RandRicianNoise(prob=1.0, mean=mean, std=std) + rician_fn.set_random_state(seed) + noised = rician_fn(self.imt) + np.random.seed(seed) + np.random.random() + _std = np.random.uniform(0, std) + expected = np.sqrt( + (self.imt + np.random.normal(mean, _std, size=self.imt.shape)) ** 2 + + np.random.normal(mean, _std, size=self.imt.shape) ** 2 + ) + np.testing.assert_allclose(expected, noised, atol=1e-5) + + +class TestRandRicianNoiseTorch(TorchImageTestCase2D): + @parameterized.expand([("test_zero_mean", 0, 0.1), ("test_non_zero_mean", 1, 0.5)]) + def test_correct_results(self, _, mean, std): + seed = 0 + rician_fn = RandRicianNoise(prob=1.0, mean=mean, std=std) + rician_fn.set_random_state(seed) + noised = rician_fn(self.imt) + np.random.seed(seed) + np.random.random() + _std = np.random.uniform(0, std) + expected = np.sqrt( + (self.imt + np.random.normal(mean, _std, size=self.imt.shape)) ** 2 + + np.random.normal(mean, _std, size=self.imt.shape) ** 2 + ) + np.testing.assert_allclose(expected, noised, atol=1e-5) + + +if __name__ == "__main__": + unittest.main() From 7e2bd40a8c04ee4e9d459ef013ca4e92d90ddf35 Mon Sep 17 00:00:00 2001 From: Lyndon Boone Date: Fri, 23 Apr 2021 19:18:01 -0400 Subject: [PATCH 7/8] Added unit test for RandRicianNoised transform Signed-off-by: Lyndon Boone --- tests/test_rand_rician_noised.py | 60 ++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 tests/test_rand_rician_noised.py diff --git a/tests/test_rand_rician_noised.py b/tests/test_rand_rician_noised.py new file mode 100644 index 0000000000..3dbfce154d --- /dev/null +++ b/tests/test_rand_rician_noised.py @@ -0,0 +1,60 @@ +# 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 RandRicianNoised +from tests.utils import NumpyImageTestCase2D, TorchImageTestCase2D + +TEST_CASE_0 = ["test_zero_mean", ["img1", "img2"], 0, 0.1] +TEST_CASE_1 = ["test_non_zero_mean", ["img1", "img2"], 1, 0.5] +TEST_CASES = [TEST_CASE_0, TEST_CASE_1] + +seed = 0 + + +def test_numpy_or_torch(keys, mean, std, imt): + rician_fn = RandRicianNoised(keys=keys, global_prob=1.0, prob=1.0, mean=mean, std=std) + rician_fn.set_random_state(seed) + rician_fn.rand_rician_noise.set_random_state(seed) + noised = rician_fn({k: imt for k in keys}) + np.random.seed(seed) + np.random.random() + np.random.seed(seed) + for k in keys: + np.random.random() + _std = np.random.uniform(0, std) + expected = np.sqrt( + (imt + np.random.normal(mean, _std, size=imt.shape)) ** 2 + + np.random.normal(mean, _std, size=imt.shape) ** 2 + ) + np.testing.assert_allclose(expected, noised[k], atol=1e-5, rtol=1e-5) + + +# Test with numpy +class TestRandRicianNoisedNumpy(NumpyImageTestCase2D): + @parameterized.expand(TEST_CASES) + def test_correct_results(self, _, keys, mean, std): + test_numpy_or_torch(keys, mean, std, self.imt) + + +# Test with torch +class TestRandRicianNoisedTorch(TorchImageTestCase2D): + @parameterized.expand(TEST_CASES) + def test_correct_results(self, _, keys, mean, std): + test_numpy_or_torch(keys, mean, std, self.imt) + + +if __name__ == "__main__": + unittest.main() From 1148d5466583b086a11cdceb5887ec94b80c7c89 Mon Sep 17 00:00:00 2001 From: Lyndon Boone Date: Wed, 28 Apr 2021 19:32:31 -0400 Subject: [PATCH 8/8] Fixed mypy typing issues Signed-off-by: Lyndon Boone --- monai/transforms/intensity/array.py | 11 +++++++---- monai/transforms/intensity/dictionary.py | 4 +++- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/monai/transforms/intensity/array.py b/monai/transforms/intensity/array.py index 9f3e21d1f1..bde6359efc 100644 --- a/monai/transforms/intensity/array.py +++ b/monai/transforms/intensity/array.py @@ -134,13 +134,13 @@ def __init__( self._noise1 = None self._noise2 = None - def _add_noise( - self, img: Union[torch.Tensor, np.ndarray], mean: float, std: float - ) -> Union[torch.Tensor, np.ndarray]: + def _add_noise(self, img: Union[torch.Tensor, np.ndarray], mean: float, std: float): im_shape = img.shape _std = self.R.uniform(0, std) if self.sample_std else std self._noise1 = self.R.normal(mean, _std, size=im_shape) self._noise2 = self.R.normal(mean, _std, size=im_shape) + if self._noise1 is None or self._noise2 is None: + raise AssertionError dtype = dtype_torch_to_numpy(img.dtype) if isinstance(img, torch.Tensor) else img.dtype return np.sqrt((img + self._noise1.astype(dtype)) ** 2 + self._noise2.astype(dtype) ** 2) @@ -161,7 +161,10 @@ def __call__(self, img: Union[torch.Tensor, np.ndarray]) -> Union[torch.Tensor, raise AssertionError("If channel_wise is False, mean must be a float or int number.") if not isinstance(self.std, (int, float)): raise AssertionError("If channel_wise is False, std must be a float or int number.") - img = self._add_noise(img, mean=self.mean, std=self.std * img.std() if self.relative else self.std) + std = self.std * img.std() if self.relative else self.std + if not isinstance(std, (int, float)): + raise AssertionError + img = self._add_noise(img, mean=self.mean, std=std) return img diff --git a/monai/transforms/intensity/dictionary.py b/monai/transforms/intensity/dictionary.py index 64f650065b..1c393b9650 100644 --- a/monai/transforms/intensity/dictionary.py +++ b/monai/transforms/intensity/dictionary.py @@ -201,7 +201,9 @@ def __init__( sample_std, ) - def __call__(self, data: Mapping[Hashable, np.ndarray]) -> Dict[Hashable, np.ndarray]: + def __call__( + self, data: Mapping[Hashable, Union[torch.Tensor, np.ndarray]] + ) -> Dict[Hashable, Union[torch.Tensor, np.ndarray]]: d = dict(data) super().randomize(None) if not self._do_transform: