From 82c0fb3f8d543d645eb3d8efd123f9297595e03b Mon Sep 17 00:00:00 2001 From: karllandheer Date: Sun, 2 Nov 2025 10:33:34 -0500 Subject: [PATCH 1/8] feat: Add RandNonCentralChiNoise transform Adds RandNonCentralChiNoise and RandNonCentralChiNoised, which generalize Rician noise to k degrees of freedom. Standard brain MRI typically uses 32 (or more) quadrature coils, so accurate noise simulation requires this modification, especially in the low SNR limit Includes array, dictionary, and test files. Signed-off-by: Karl Landheer Signed-off-by: karllandheer --- .gitignore | 2 + CHANGELOG.md | 2 + monai/transforms/__init__.py | 4 + monai/transforms/intensity/array.py | 105 ++++++++++++++++++ monai/transforms/intensity/dictionary.py | 77 +++++++++++++ .../test_rand_noncentralchi_noise.py | 83 ++++++++++++++ .../test_rand_noncentralchi_noised.py | 77 +++++++++++++ 7 files changed, 350 insertions(+) create mode 100644 tests/transforms/test_rand_noncentralchi_noise.py create mode 100644 tests/transforms/test_rand_noncentralchi_noised.py diff --git a/.gitignore b/.gitignore index 76c6ab0d12..05c482ad06 100644 --- a/.gitignore +++ b/.gitignore @@ -165,3 +165,5 @@ runs *.pth *zarr/* + +monai-dev/ \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c6ddd262a..8c741caf0c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ All notable changes to MONAI are documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/). ## [Unreleased] +### Added +* Added `RandNonCentralChiNoise` and `RandNonCentralChiNoised` for generalized Rician noise simulation in MRI. ## [1.5.1] - 2025-09-22 diff --git a/monai/transforms/__init__.py b/monai/transforms/__init__.py index d15042181b..c9cb4ddf41 100644 --- a/monai/transforms/__init__.py +++ b/monai/transforms/__init__.py @@ -117,6 +117,7 @@ RandHistogramShift, RandIntensityRemap, RandKSpaceSpikeNoise, + RandNonCentralChiNoise, RandRicianNoise, RandScaleIntensity, RandScaleIntensityFixedMean, @@ -202,6 +203,9 @@ RandRicianNoised, RandRicianNoiseD, RandRicianNoiseDict, + RandNonCentralChiNoised, + RandNonCentralChiNoiseD, + RandNonCentralChiNoiseDict, RandScaleIntensityd, RandScaleIntensityD, RandScaleIntensityDict, diff --git a/monai/transforms/intensity/array.py b/monai/transforms/intensity/array.py index 0421d34492..18545668a2 100644 --- a/monai/transforms/intensity/array.py +++ b/monai/transforms/intensity/array.py @@ -41,6 +41,7 @@ __all__ = [ "RandGaussianNoise", + "RandNonCentralChiNoise", "RandRicianNoise", "ShiftIntensity", "RandShiftIntensity", @@ -140,6 +141,110 @@ def __call__(self, img: NdarrayOrTensor, mean: float | None = None, randomize: b return img + noise +class RandNonCentralChiNoise(RandomizableTransform): + """ + Add non-central chi noise to an image. + This distribution is the square root of the sum of squares of k independent + Gaussian random variables, where one of the variables has a non-zero mean + (the signal). + This is a generalization of Rician noise. `degrees_of_freedom=2` is Rician noise. + See: https://en.wikipedia.org/wiki/Noncentral_chi_distribution and https://archive.ismrm.org/2024/3123_NZkvJdQat.html + + Args: + prob: Probability to add noise. + mean: Mean or "centre" of the Gaussian noise distributions. + std: Standard deviation (spread) of the Gaussian noise distributions. + degrees_of_freedom: Number of Gaussian distributions (degrees of freedom). + `degrees_of_freedom=2` is 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. + dtype: output data type, if None, same as input image. defaults to float32. + + """ + + backend = [TransformBackends.TORCH, TransformBackends.NUMPY] + + def __init__( + self, + prob: float = 0.1, + mean: Sequence[float] | float = 0.0, + std: Sequence[float] | float = 1.0, + degrees_of_freedom: int = 64, #64 default because typical modern brain MRI is 32 quadrature coils + channel_wise: bool = False, + relative: bool = False, + sample_std: bool = True, + dtype: DtypeLike = np.float32, + ) -> None: + RandomizableTransform.__init__(self, prob) + self.prob = prob + self.mean = mean + self.std = std + if not isinstance(degrees_of_freedom, int) or degrees_of_freedom < 1: + raise ValueError("degrees_of_freedom must be an integer >= 1.") + self.degrees_of_freedom = degrees_of_freedom + self.channel_wise = channel_wise + self.relative = relative + self.sample_std = sample_std + self.dtype = dtype + + def _add_noise(self, img: NdarrayOrTensor, mean: float, std: float, k: int): + dtype_np = get_equivalent_dtype(img.dtype, np.ndarray) + im_shape = img.shape + _std = self.R.uniform(0, std) if self.sample_std else std + + # Create a stack of k noise arrays + noise_shape = (k, *im_shape) + all_noises_np = self.R.normal(mean, _std, size=noise_shape).astype(dtype_np, copy=False) + + if isinstance(img, torch.Tensor): + all_noises = torch.tensor(all_noises_np, device=img.device) + all_noises[0] = all_noises[0] + img + sum_sq = torch.sum(all_noises**2, dim=0) + return torch.sqrt(sum_sq) + + + all_noises_np[0] = all_noises_np[0] + img + sum_sq = np.sum(all_noises_np**2, axis=0) + return np.sqrt(sum_sq) + + def __call__(self, img: NdarrayOrTensor, randomize: bool = True) -> NdarrayOrTensor: + """ + Apply the transform to `img`. + """ + img = convert_to_tensor(img, track_meta=get_track_meta(), dtype=self.dtype) + if randomize: + super().randomize(None) + + if not self._do_transform: + return img + + if self.channel_wise: + _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], + k=self.degrees_of_freedom, + ) + else: + if not isinstance(self.mean, (int, float)): + raise RuntimeError(f"If channel_wise is False, mean must be a float or int, got {type(self.mean)}.") + if not isinstance(self.std, (int, float)): + raise RuntimeError(f"If channel_wise is False, std must be a float or int, got {type(self.std)}.") + std = self.std * img.std().item() if self.relative else self.std + if not isinstance(std, (int, float)): + raise RuntimeError(f"std must be a float or int number, got {type(std)}.") + img = self._add_noise(img, mean=self.mean, std=std, k=self.degrees_of_freedom) + return img + + + class RandRicianNoise(RandomizableTransform): """ Add Rician noise to image. diff --git a/monai/transforms/intensity/dictionary.py b/monai/transforms/intensity/dictionary.py index 3d29b3031d..ee5473dc3f 100644 --- a/monai/transforms/intensity/dictionary.py +++ b/monai/transforms/intensity/dictionary.py @@ -48,6 +48,7 @@ RandGibbsNoise, RandHistogramShift, RandKSpaceSpikeNoise, + RandNonCentralChiNoise, RandRicianNoise, RandScaleIntensity, RandScaleIntensityFixedMean, @@ -69,6 +70,7 @@ __all__ = [ "RandGaussianNoised", "RandRicianNoised", + "RandNonCentralChiNoised", "ShiftIntensityd", "RandShiftIntensityd", "ScaleIntensityd", @@ -234,7 +236,81 @@ def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> dict[Hashable, N for key in self.key_iterator(d): d[key] = self.rand_gaussian_noise(img=d[key], randomize=False) return d + + +class RandNonCentralChiNoised(RandomizableTransform, MapTransform): + """ + Dictionary-based version :py:class:`monai.transforms.RandNonCentralChiNoise`. + Add non-central chi noise to image. This transform assumes all the expected fields have same shape, if want to add + different noise for every field, please use this transform separately. + This is a generalization of Rician noise. `degrees_of_freedom=2` is Rician noise. + + Args: + keys: Keys of the corresponding items to be transformed. + See also: :py:class:`monai.transforms.compose.MapTransform` + prob: Probability to add non-central chi noise to the dictionary. + mean: Mean or "centre" of the Gaussian distributions sampled to make up + the noise. + std: Standard deviation (spread) of the Gaussian distributions sampled + to make up the noise. + degrees_of_freedom: Number of Gaussian distributions (degrees of freedom). + `degrees_of_freedom=2` is 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. + dtype: output data type, if None, same as input image. defaults to float32. + allow_missing_keys: Don't raise exception if key is missing. + """ + + backend = RandNonCentralChiNoise.backend + def __init__( + self, + keys: KeysCollection, + prob: float = 0.1, + mean: Sequence[float] | float = 0.0, + std: Sequence[float] | float = 1.0, + degrees_of_freedom: int = 64, + channel_wise: bool = False, + relative: bool = False, + sample_std: bool = True, + dtype: DtypeLike = np.float32, + allow_missing_keys: bool = False, + ) -> None: + MapTransform.__init__(self, keys, allow_missing_keys) + RandomizableTransform.__init__(self, prob) + self.rand_non_central_chi_noise = RandNonCentralChiNoise( + prob=1.0, + mean=mean, + std=std, + degrees_of_freedom=degrees_of_freedom, + channel_wise=channel_wise, + relative=relative, + sample_std=sample_std, + dtype=dtype, + ) + + def set_random_state( + self, seed: int | None = None, state: np.random.RandomState | None = None + ) -> RandNonCentralChiNoised: + super().set_random_state(seed, state) + self.rand_non_central_chi_noise.set_random_state(seed, state) + return self + + def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> dict[Hashable, NdarrayOrTensor]: + d = dict(data) + self.randomize(None) + if not self._do_transform: + for key in self.key_iterator(d): + d[key] = convert_to_tensor(d[key], track_meta=get_track_meta()) + return d + + for key in self.key_iterator(d): + d[key] = self.rand_non_central_chi_noise(d[key], randomize=True) + return d class RandRicianNoised(RandomizableTransform, MapTransform): """ @@ -1953,6 +2029,7 @@ def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> dict[Hashable, N RandGaussianNoiseD = RandGaussianNoiseDict = RandGaussianNoised RandRicianNoiseD = RandRicianNoiseDict = RandRicianNoised +RandNonCentralChiNoiseD = RandNonCentralChiNoiseDict = RandNonCentralChiNoised ShiftIntensityD = ShiftIntensityDict = ShiftIntensityd RandShiftIntensityD = RandShiftIntensityDict = RandShiftIntensityd StdShiftIntensityD = StdShiftIntensityDict = StdShiftIntensityd diff --git a/tests/transforms/test_rand_noncentralchi_noise.py b/tests/transforms/test_rand_noncentralchi_noise.py new file mode 100644 index 0000000000..654eac32c8 --- /dev/null +++ b/tests/transforms/test_rand_noncentralchi_noise.py @@ -0,0 +1,83 @@ +# Copyright (c) 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. + +from __future__ import annotations + +import unittest + +import numpy as np +import torch +from parameterized import parameterized + +from monai.transforms import RandNonCentralChiNoise +from tests.test_utils import TEST_NDARRAYS, NumpyImageTestCase2D + +TESTS = [] +for p in TEST_NDARRAYS: + TESTS.append(("test_zero_mean", p, 0, 0.1)) + TESTS.append(("test_non_zero_mean", p, 1, 0.5)) + + +class TestRandNonCentralChiNoise(NumpyImageTestCase2D): + @parameterized.expand(TESTS) + def test_correct_results(self, _, in_type, mean, std): + seed = 0 + degrees_of_freedom = 64 #64 is common due to 32 channel head coil + noise_fn = RandNonCentralChiNoise(prob=1.0, mean=mean, std=std, degrees_of_freedom=degrees_of_freedom) + noise_fn.set_random_state(seed) + im = in_type(self.imt) + noised = noise_fn(im) + if isinstance(im, torch.Tensor): + self.assertEqual(im.dtype, noised.dtype) + np.random.seed(seed) + np.random.random() + _std = np.random.uniform(0, std) + + noise_shape = (degrees_of_freedom, *self.imt.shape) + all_noises = np.random.normal(mean, _std, size=noise_shape).astype(np.float32) + all_noises[0] += self.imt + sum_sq = np.sum(all_noises**2, axis=0) + expected = np.sqrt(sum_sq) + + if isinstance(noised, torch.Tensor): + noised = noised.cpu() + np.testing.assert_allclose(expected, noised, atol=1e-5) + + @parameterized.expand(TESTS) + def test_correct_results_dof2(self, _, in_type, mean, std): + """ + Test with k=2 (the Rician case) + """ + seed = 0 + degrees_of_freedom = 2 + noise_fn = RandNonCentralChiNoise(prob=1.0, mean=mean, std=std, degrees_of_freedom=degrees_of_freedom) + noise_fn.set_random_state(seed) + im = in_type(self.imt) + noised = noise_fn(im) + if isinstance(im, torch.Tensor): + self.assertEqual(im.dtype, noised.dtype) + + np.random.seed(seed) + np.random.random() # for prob + _std = np.random.uniform(0, std) # for sample_std + noise_shape = (degrees_of_freedom, *self.imt.shape) + all_noises = np.random.normal(mean, _std, size=noise_shape).astype(np.float32) + all_noises[0] += self.imt + sum_sq = np.sum(all_noises**2, axis=0) + expected = np.sqrt(sum_sq) + + if isinstance(noised, torch.Tensor): + noised = noised.cpu() + np.testing.assert_allclose(expected, noised, atol=1e-5, rtol=1e-5) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/transforms/test_rand_noncentralchi_noised.py b/tests/transforms/test_rand_noncentralchi_noised.py new file mode 100644 index 0000000000..cb6654c8a4 --- /dev/null +++ b/tests/transforms/test_rand_noncentralchi_noised.py @@ -0,0 +1,77 @@ +# Copyright (c) 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. + +from __future__ import annotations + +import unittest + +import numpy as np +import torch +from parameterized import parameterized + +from monai.transforms import RandNonCentralChiNoised +from tests.test_utils import TEST_NDARRAYS, NumpyImageTestCase2D + +TESTS = [] +for p in TEST_NDARRAYS: + TESTS.append(["test_zero_mean", p, ["img1", "img2"], 0, 0.1]) + TESTS.append(["test_non_zero_mean", p, ["img1", "img2"], 1, 0.5]) + +seed = 0 + + +class TestRandNonCentralChiNoised(NumpyImageTestCase2D): + @parameterized.expand(TESTS) + def test_correct_results(self, _, in_type, keys, mean, std): + degrees_of_freedom = 64 + noise_fn = RandNonCentralChiNoised(keys=keys, prob=1.0, mean=mean, std=std, degrees_of_freedom=degrees_of_freedom, dtype=np.float64) + noise_fn.set_random_state(seed) + noised = noise_fn({k: in_type(self.imt) for k in keys}) + np.random.seed(seed) + for k in keys: + # simulate the `randomize` function of transform + np.random.random() + _std = np.random.uniform(0, std) + noise_shape = (degrees_of_freedom, *self.imt.shape) + all_noises = np.random.normal(mean, _std, size=noise_shape).astype(np.float32) + all_noises[0] += self.imt + sum_sq = np.sum(all_noises**2, axis=0) + expected = np.sqrt(sum_sq) + if isinstance(noised[k], torch.Tensor): + noised[k] = noised[k].cpu() + np.testing.assert_allclose(expected, noised[k], atol=1e-5, rtol=1e-5) + + @parameterized.expand(TESTS) + def test_correct_results_k2(self, _, in_type, keys, mean, std): + degrees_of_freedom = 2 + noise_fn = RandNonCentralChiNoised( + keys=keys, prob=1.0, mean=mean, std=std, degrees_of_freedom=degrees_of_freedom, dtype=np.float64 + ) + noise_fn.set_random_state(seed) + noised = noise_fn({k: in_type(self.imt) for k in keys}) + np.random.seed(seed) + for k in keys: + np.random.random() + _std = np.random.uniform(0, std) + + noise_shape = (degrees_of_freedom, *self.imt.shape) + all_noises = np.random.normal(mean, _std, size=noise_shape).astype(np.float32) + all_noises[0] += self.imt + sum_sq = np.sum(all_noises**2, axis=0) + expected = np.sqrt(sum_sq) + + if isinstance(noised[k], torch.Tensor): + noised[k] = noised[k].cpu() + np.testing.assert_allclose(expected, noised[k], atol=1e-5, rtol=1e-5) + + +if __name__ == "__main__": + unittest.main() From 33d8aa22f6ab7b0b4ff8256774524f77dae8d782 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 10 Nov 2025 01:33:42 +0000 Subject: [PATCH 2/8] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .gitignore | 2 +- monai/transforms/intensity/array.py | 8 ++++---- monai/transforms/intensity/dictionary.py | 6 +++--- tests/transforms/test_rand_noncentralchi_noise.py | 6 +++--- tests/transforms/test_rand_noncentralchi_noised.py | 4 ++-- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.gitignore b/.gitignore index 05c482ad06..1470ae8375 100644 --- a/.gitignore +++ b/.gitignore @@ -166,4 +166,4 @@ runs *zarr/* -monai-dev/ \ No newline at end of file +monai-dev/ diff --git a/monai/transforms/intensity/array.py b/monai/transforms/intensity/array.py index 18545668a2..cd39c93606 100644 --- a/monai/transforms/intensity/array.py +++ b/monai/transforms/intensity/array.py @@ -173,7 +173,7 @@ def __init__( prob: float = 0.1, mean: Sequence[float] | float = 0.0, std: Sequence[float] | float = 1.0, - degrees_of_freedom: int = 64, #64 default because typical modern brain MRI is 32 quadrature coils + degrees_of_freedom: int = 64, #64 default because typical modern brain MRI is 32 quadrature coils channel_wise: bool = False, relative: bool = False, sample_std: bool = True, @@ -242,9 +242,9 @@ def __call__(self, img: NdarrayOrTensor, randomize: bool = True) -> NdarrayOrTen raise RuntimeError(f"std must be a float or int number, got {type(std)}.") img = self._add_noise(img, mean=self.mean, std=std, k=self.degrees_of_freedom) return img - - - + + + class RandRicianNoise(RandomizableTransform): """ Add Rician noise to image. diff --git a/monai/transforms/intensity/dictionary.py b/monai/transforms/intensity/dictionary.py index ee5473dc3f..97d2980cc2 100644 --- a/monai/transforms/intensity/dictionary.py +++ b/monai/transforms/intensity/dictionary.py @@ -236,8 +236,8 @@ def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> dict[Hashable, N for key in self.key_iterator(d): d[key] = self.rand_gaussian_noise(img=d[key], randomize=False) return d - - + + class RandNonCentralChiNoised(RandomizableTransform, MapTransform): """ Dictionary-based version :py:class:`monai.transforms.RandNonCentralChiNoise`. @@ -283,7 +283,7 @@ def __init__( MapTransform.__init__(self, keys, allow_missing_keys) RandomizableTransform.__init__(self, prob) self.rand_non_central_chi_noise = RandNonCentralChiNoise( - prob=1.0, + prob=1.0, mean=mean, std=std, degrees_of_freedom=degrees_of_freedom, diff --git a/tests/transforms/test_rand_noncentralchi_noise.py b/tests/transforms/test_rand_noncentralchi_noise.py index 654eac32c8..03d4c1d7d7 100644 --- a/tests/transforms/test_rand_noncentralchi_noise.py +++ b/tests/transforms/test_rand_noncentralchi_noise.py @@ -40,17 +40,17 @@ def test_correct_results(self, _, in_type, mean, std): np.random.seed(seed) np.random.random() _std = np.random.uniform(0, std) - + noise_shape = (degrees_of_freedom, *self.imt.shape) all_noises = np.random.normal(mean, _std, size=noise_shape).astype(np.float32) all_noises[0] += self.imt sum_sq = np.sum(all_noises**2, axis=0) expected = np.sqrt(sum_sq) - + if isinstance(noised, torch.Tensor): noised = noised.cpu() np.testing.assert_allclose(expected, noised, atol=1e-5) - + @parameterized.expand(TESTS) def test_correct_results_dof2(self, _, in_type, mean, std): """ diff --git a/tests/transforms/test_rand_noncentralchi_noised.py b/tests/transforms/test_rand_noncentralchi_noised.py index cb6654c8a4..a24c520e28 100644 --- a/tests/transforms/test_rand_noncentralchi_noised.py +++ b/tests/transforms/test_rand_noncentralchi_noised.py @@ -48,7 +48,7 @@ def test_correct_results(self, _, in_type, keys, mean, std): if isinstance(noised[k], torch.Tensor): noised[k] = noised[k].cpu() np.testing.assert_allclose(expected, noised[k], atol=1e-5, rtol=1e-5) - + @parameterized.expand(TESTS) def test_correct_results_k2(self, _, in_type, keys, mean, std): degrees_of_freedom = 2 @@ -61,7 +61,7 @@ def test_correct_results_k2(self, _, in_type, keys, mean, std): for k in keys: np.random.random() _std = np.random.uniform(0, std) - + noise_shape = (degrees_of_freedom, *self.imt.shape) all_noises = np.random.normal(mean, _std, size=noise_shape).astype(np.float32) all_noises[0] += self.imt From 7eb60caea9c547f3fc420a4286fec56138362bec Mon Sep 17 00:00:00 2001 From: karllandheer Date: Sun, 9 Nov 2025 20:41:30 -0500 Subject: [PATCH 3/8] Fix docstrings and linter errors for NonCentralChiNoise Fixing the checks from the CI pipeline. Signed-off-by: karllandheer --- monai/transforms/intensity/array.py | 39 ++++++++++++++++++- monai/transforms/intensity/dictionary.py | 2 + .../test_rand_noncentralchi_noised.py | 16 +++++++- 3 files changed, 54 insertions(+), 3 deletions(-) diff --git a/monai/transforms/intensity/array.py b/monai/transforms/intensity/array.py index cd39c93606..0a9df9b5ba 100644 --- a/monai/transforms/intensity/array.py +++ b/monai/transforms/intensity/array.py @@ -173,12 +173,32 @@ def __init__( prob: float = 0.1, mean: Sequence[float] | float = 0.0, std: Sequence[float] | float = 1.0, - degrees_of_freedom: int = 64, #64 default because typical modern brain MRI is 32 quadrature coils + degrees_of_freedom: int = 64, # 64 default because typical modern brain MRI is 32 quadrature coils channel_wise: bool = False, relative: bool = False, sample_std: bool = True, dtype: DtypeLike = np.float32, ) -> None: + """ + Initializes the transform. + + Args: + prob: Probability to add noise. + mean: Mean of the Gaussian noise distributions. + std: Standard deviation (spread) of the Gaussian noise distributions. + degrees_of_freedom: Number of Gaussian distributions (degrees of freedom). + `degrees_of_freedom=2` is Rician noise. Defaults to 64 (32 quadrature coils). + 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. + dtype: output data type, if None, same as input image. defaults to float32. + + Raises: + ValueError: If `degrees_of_freedom` is not an integer or is less than 1. + """ RandomizableTransform.__init__(self, prob) self.prob = prob self.mean = mean @@ -192,6 +212,23 @@ def __init__( self.dtype = dtype def _add_noise(self, img: NdarrayOrTensor, mean: float, std: float, k: int): + """ + Applies non-central chi noise to a single image or channel. + + This method generates `k` Gaussian noise arrays, adds the input `img` + to the first one (as the non-centrality component), and then computes + the square root of the sum of squares. + + Args: + img: Input image array. + mean: Mean for the Gaussian noise distributions. + std: Standard deviation for the Gaussian noise distributions. + k: Degrees of freedom (number of noise arrays). + + Returns: + Image with non-central chi noise applied, with the same + backend (Numpy/Torch) as the input. + """ dtype_np = get_equivalent_dtype(img.dtype, np.ndarray) im_shape = img.shape _std = self.R.uniform(0, std) if self.sample_std else std diff --git a/monai/transforms/intensity/dictionary.py b/monai/transforms/intensity/dictionary.py index 97d2980cc2..3de4b63ccf 100644 --- a/monai/transforms/intensity/dictionary.py +++ b/monai/transforms/intensity/dictionary.py @@ -71,6 +71,8 @@ "RandGaussianNoised", "RandRicianNoised", "RandNonCentralChiNoised", + "RandNonCentralChiNoiseD", + "RandNonCentralChiNoiseDict", "ShiftIntensityd", "RandShiftIntensityd", "ScaleIntensityd", diff --git a/tests/transforms/test_rand_noncentralchi_noised.py b/tests/transforms/test_rand_noncentralchi_noised.py index a24c520e28..6bf50dc721 100644 --- a/tests/transforms/test_rand_noncentralchi_noised.py +++ b/tests/transforms/test_rand_noncentralchi_noised.py @@ -32,7 +32,14 @@ class TestRandNonCentralChiNoised(NumpyImageTestCase2D): @parameterized.expand(TESTS) def test_correct_results(self, _, in_type, keys, mean, std): degrees_of_freedom = 64 - noise_fn = RandNonCentralChiNoised(keys=keys, prob=1.0, mean=mean, std=std, degrees_of_freedom=degrees_of_freedom, dtype=np.float64) + noise_fn = RandNonCentralChiNoised( + keys=keys, + prob=1.0, + mean=mean, + std=std, + degrees_of_freedom=degrees_of_freedom, + dtype=np.float64, + ) noise_fn.set_random_state(seed) noised = noise_fn({k: in_type(self.imt) for k in keys}) np.random.seed(seed) @@ -53,7 +60,12 @@ def test_correct_results(self, _, in_type, keys, mean, std): def test_correct_results_k2(self, _, in_type, keys, mean, std): degrees_of_freedom = 2 noise_fn = RandNonCentralChiNoised( - keys=keys, prob=1.0, mean=mean, std=std, degrees_of_freedom=degrees_of_freedom, dtype=np.float64 + keys=keys, + prob=1.0, + mean=mean, + std=std, + degrees_of_freedom=degrees_of_freedom, + dtype=np.float64, ) noise_fn.set_random_state(seed) noised = noise_fn({k: in_type(self.imt) for k in keys}) From 94b40fcd528f790263f30e5de15fe9e9b73fc66b Mon Sep 17 00:00:00 2001 From: karllandheer Date: Sun, 9 Nov 2025 20:57:31 -0500 Subject: [PATCH 4/8] Fixes metadata dropping Fixes metadata-dropping bug by implementing the src/convert_to_dst_type round-trip. Fixes isort error by re-ordering imports in transforms/__init__.py. Signed-off-by: karllandheer --- monai/transforms/__init__.py | 6 +++--- monai/transforms/intensity/array.py | 10 ++++++++++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/monai/transforms/__init__.py b/monai/transforms/__init__.py index c9cb4ddf41..6a12541875 100644 --- a/monai/transforms/__init__.py +++ b/monai/transforms/__init__.py @@ -200,12 +200,12 @@ RandKSpaceSpikeNoised, RandKSpaceSpikeNoiseD, RandKSpaceSpikeNoiseDict, - RandRicianNoised, - RandRicianNoiseD, - RandRicianNoiseDict, RandNonCentralChiNoised, RandNonCentralChiNoiseD, RandNonCentralChiNoiseDict, + RandRicianNoised, + RandRicianNoiseD, + RandRicianNoiseDict, RandScaleIntensityd, RandScaleIntensityD, RandScaleIntensityDict, diff --git a/monai/transforms/intensity/array.py b/monai/transforms/intensity/array.py index 0a9df9b5ba..b1e33fa11f 100644 --- a/monai/transforms/intensity/array.py +++ b/monai/transforms/intensity/array.py @@ -252,11 +252,13 @@ def __call__(self, img: NdarrayOrTensor, randomize: bool = True) -> NdarrayOrTen """ Apply the transform to `img`. """ + src = img img = convert_to_tensor(img, track_meta=get_track_meta(), dtype=self.dtype) if randomize: super().randomize(None) if not self._do_transform: + img, *_ = convert_to_dst_type(img, dst=src, dtype=self.dtype) return img if self.channel_wise: @@ -278,10 +280,18 @@ def __call__(self, img: NdarrayOrTensor, randomize: bool = True) -> NdarrayOrTen if not isinstance(std, (int, float)): raise RuntimeError(f"std must be a float or int number, got {type(std)}.") img = self._add_noise(img, mean=self.mean, std=std, k=self.degrees_of_freedom) + + img, *_ = convert_to_dst_type(img, dst=src, dtype=self.dtype) return img +<<<<<<< HEAD + +======= + + +>>>>>>> 83d8ad52 (Fixes metadata dropping) class RandRicianNoise(RandomizableTransform): """ Add Rician noise to image. From 32290e67ac98f4c76c465acc699a4b527ed42edd Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 10 Nov 2025 02:08:26 +0000 Subject: [PATCH 5/8] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- monai/transforms/intensity/array.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/monai/transforms/intensity/array.py b/monai/transforms/intensity/array.py index b1e33fa11f..8fd40a3051 100644 --- a/monai/transforms/intensity/array.py +++ b/monai/transforms/intensity/array.py @@ -173,7 +173,7 @@ def __init__( prob: float = 0.1, mean: Sequence[float] | float = 0.0, std: Sequence[float] | float = 1.0, - degrees_of_freedom: int = 64, # 64 default because typical modern brain MRI is 32 quadrature coils + degrees_of_freedom: int = 64, # 64 default because typical modern brain MRI is 32 quadrature coils channel_wise: bool = False, relative: bool = False, sample_std: bool = True, @@ -288,9 +288,9 @@ def __call__(self, img: NdarrayOrTensor, randomize: bool = True) -> NdarrayOrTen ======= - - + + >>>>>>> 83d8ad52 (Fixes metadata dropping) class RandRicianNoise(RandomizableTransform): """ From 62f738bc3272a6ffbd0793b5dba33c44525e4dcc Mon Sep 17 00:00:00 2001 From: karllandheer Date: Sun, 9 Nov 2025 20:57:31 -0500 Subject: [PATCH 6/8] Fixes metadata dropping Fixes metadata-dropping bug by implementing the src/convert_to_dst_type round-trip. Fixes isort error by re-ordering imports in transforms/__init__.py. Signed-off-by: karllandheer --- monai/transforms/intensity/array.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/monai/transforms/intensity/array.py b/monai/transforms/intensity/array.py index 8fd40a3051..61e5399ff8 100644 --- a/monai/transforms/intensity/array.py +++ b/monai/transforms/intensity/array.py @@ -283,15 +283,8 @@ def __call__(self, img: NdarrayOrTensor, randomize: bool = True) -> NdarrayOrTen img, *_ = convert_to_dst_type(img, dst=src, dtype=self.dtype) return img -<<<<<<< HEAD - -======= - - - ->>>>>>> 83d8ad52 (Fixes metadata dropping) class RandRicianNoise(RandomizableTransform): """ Add Rician noise to image. From 1a1624774f809c517e8bfe14fd0a799cb550eba7 Mon Sep 17 00:00:00 2001 From: karllandheer Date: Sun, 9 Nov 2025 21:27:59 -0500 Subject: [PATCH 7/8] Style: Run ruff formatter Signed-off-by: karllandheer --- monai/transforms/intensity/array.py | 59 ++++++------------ monai/transforms/intensity/dictionary.py | 61 +++++-------------- .../test_rand_noncentralchi_noise.py | 2 +- 3 files changed, 36 insertions(+), 86 deletions(-) diff --git a/monai/transforms/intensity/array.py b/monai/transforms/intensity/array.py index 61e5399ff8..4979fc637c 100644 --- a/monai/transforms/intensity/array.py +++ b/monai/transforms/intensity/array.py @@ -173,7 +173,7 @@ def __init__( prob: float = 0.1, mean: Sequence[float] | float = 0.0, std: Sequence[float] | float = 1.0, - degrees_of_freedom: int = 64, # 64 default because typical modern brain MRI is 32 quadrature coils + degrees_of_freedom: int = 64, # 64 default because typical modern brain MRI is 32 quadrature coils channel_wise: bool = False, relative: bool = False, sample_std: bool = True, @@ -243,7 +243,6 @@ def _add_noise(self, img: NdarrayOrTensor, mean: float, std: float, k: int): sum_sq = torch.sum(all_noises**2, dim=0) return torch.sqrt(sum_sq) - all_noises_np[0] = all_noises_np[0] + img sum_sq = np.sum(all_noises_np**2, axis=0) return np.sqrt(sum_sq) @@ -489,9 +488,7 @@ class StdShiftIntensity(Transform): backend = [TransformBackends.TORCH, TransformBackends.NUMPY] - def __init__( - self, factor: float, nonzero: bool = False, channel_wise: bool = False, dtype: DtypeLike = np.float32 - ) -> None: + def __init__(self, factor: float, nonzero: bool = False, channel_wise: bool = False, dtype: DtypeLike = np.float32) -> None: self.factor = factor self.nonzero = nonzero self.channel_wise = channel_wise @@ -581,9 +578,7 @@ def __call__(self, img: NdarrayOrTensor, randomize: bool = True) -> NdarrayOrTen if not self._do_transform: return img - shifter = StdShiftIntensity( - factor=self.factor, nonzero=self.nonzero, channel_wise=self.channel_wise, dtype=self.dtype - ) + shifter = StdShiftIntensity(factor=self.factor, nonzero=self.nonzero, channel_wise=self.channel_wise, dtype=self.dtype) return shifter(img=img) @@ -1273,12 +1268,16 @@ def _clip(self, img: NdarrayOrTensor) -> NdarrayOrTensor: ( lower_percentile if lower_percentile is None - else lower_percentile.item() if hasattr(lower_percentile, "item") else lower_percentile + else lower_percentile.item() + if hasattr(lower_percentile, "item") + else lower_percentile ), ( upper_percentile if upper_percentile is None - else upper_percentile.item() if hasattr(upper_percentile, "item") else upper_percentile + else upper_percentile.item() + if hasattr(upper_percentile, "item") + else upper_percentile ), ) ) @@ -1402,9 +1401,7 @@ def __init__( if isinstance(gamma, (int, float)): if gamma <= 0.5: - raise ValueError( - f"if gamma is a number, must greater than 0.5 and value is picked from (0.5, gamma), got {gamma}" - ) + raise ValueError(f"if gamma is a number, must greater than 0.5 and value is picked from (0.5, gamma), got {gamma}") self.gamma = (0.5, gamma) elif len(gamma) != 2: raise ValueError("gamma should be a number or pair of numbers.") @@ -1415,9 +1412,7 @@ def __init__( self.invert_image: bool = invert_image self.retain_stats: bool = retain_stats - self.adjust_contrast = AdjustContrast( - self.gamma_value, invert_image=self.invert_image, retain_stats=self.retain_stats - ) + self.adjust_contrast = AdjustContrast(self.gamma_value, invert_image=self.invert_image, retain_stats=self.retain_stats) def randomize(self, data: Any | None = None) -> None: super().randomize(None) @@ -1543,9 +1538,7 @@ def _normalize(self, img: NdarrayOrTensor) -> NdarrayOrTensor: b_min = ((self.b_max - self.b_min) * (self.lower / 100.0)) + self.b_min b_max = ((self.b_max - self.b_min) * (self.upper / 100.0)) + self.b_min - scalar = ScaleIntensityRange( - a_min=a_min, a_max=a_max, b_min=b_min, b_max=b_max, clip=self.clip, dtype=self.dtype - ) + scalar = ScaleIntensityRange(a_min=a_min, a_max=a_max, b_min=b_min, b_max=b_max, clip=self.clip, dtype=self.dtype) img = scalar(img) img = convert_to_tensor(img, track_meta=False) return img @@ -1868,8 +1861,7 @@ def __call__(self, img: NdarrayTensor) -> NdarrayTensor: img_t, *_ = convert_data_type(img, torch.Tensor, dtype=torch.float32) gf1, gf2 = ( - GaussianFilter(img_t.ndim - 1, sigma, approx=self.approx).to(img_t.device) - for sigma in (self.sigma1, self.sigma2) + GaussianFilter(img_t.ndim - 1, sigma, approx=self.approx).to(img_t.device) for sigma in (self.sigma1, self.sigma2) ) blurred_f = gf1(img_t.unsqueeze(0)) filter_blurred_f = gf2(blurred_f) @@ -2227,9 +2219,7 @@ def __init__(self, loc: tuple | Sequence[tuple], k_intensity: Sequence[float] | # assert one-to-one relationship between factors and locations if isinstance(k_intensity, Sequence): if not isinstance(loc[0], Sequence): - raise ValueError( - "If a sequence is passed to k_intensity, then a sequence of locations must be passed to loc" - ) + raise ValueError("If a sequence is passed to k_intensity, then a sequence of locations must be passed to loc") if len(k_intensity) != len(loc): raise ValueError("There must be one intensity_factor value for each tuple of indices in loc.") if isinstance(self.loc[0], Sequence) and k_intensity is not None and not isinstance(self.k_intensity, Sequence): @@ -2569,9 +2559,7 @@ def __init__( max_spatial_size: Sequence[int] | int | None = None, prob: float = 0.1, ) -> None: - super().__init__( - holes=holes, spatial_size=spatial_size, max_holes=max_holes, max_spatial_size=max_spatial_size, prob=prob - ) + super().__init__(holes=holes, spatial_size=spatial_size, max_holes=max_holes, max_spatial_size=max_spatial_size, prob=prob) self.dropout_holes = dropout_holes if isinstance(fill_value, (tuple, list)): if len(fill_value) != 2: @@ -2784,10 +2772,7 @@ def __call__(self, img: torch.Tensor) -> torch.Tensor: if self._do_transform: if self.channel_wise: img = torch.stack( - [ - IntensityRemap(self.kernel_size, self.R.choice([-self.slope, self.slope]))(img[i]) - for i in range(len(img)) - ] + [IntensityRemap(self.kernel_size, self.R.choice([-self.slope, self.slope]))(img[i]) for i in range(len(img))] ) else: img = IntensityRemap(self.kernel_size, self.R.choice([-self.slope, self.slope]))(img) @@ -2843,9 +2828,7 @@ def __init__( self.thresholds = {k: v for k, v in self.thresholds.items() if v is not None} if self.thresholds.keys().isdisjoint(set("RGBHSV")): - raise ValueError( - f"Threshold for at least one channel of RGB or HSV needs to be set. {self.thresholds} is provided." - ) + raise ValueError(f"Threshold for at least one channel of RGB or HSV needs to be set. {self.thresholds} is provided.") self.invert = invert def _set_threshold(self, threshold, mode): @@ -2856,9 +2839,7 @@ def _set_threshold(self, threshold, mode): elif isinstance(threshold, (float, int)): self.thresholds[mode] = float(threshold) else: - raise ValueError( - f"`threshold` should be either a callable, string, or float number, {type(threshold)} was given." - ) + raise ValueError(f"`threshold` should be either a callable, string, or float number, {type(threshold)} was given.") def _get_threshold(self, image, mode): threshold = self.thresholds.get(mode) @@ -2980,9 +2961,7 @@ def __init__( raise ValueError(f"Unknown mode: {self.mode}. Supported modes are 'B' and 'RF'.") if self.sink_mode not in ["all", "mid", "min", "mask"]: - raise ValueError( - f"Unknown sink mode: {self.sink_mode}. Supported modes are 'all', 'mid', 'min' and 'mask'." - ) + raise ValueError(f"Unknown sink mode: {self.sink_mode}. Supported modes are 'all', 'mid', 'min' and 'mask'.") self._compute_conf_map = UltrasoundConfidenceMap( self.alpha, self.beta, self.gamma, self.mode, self.sink_mode, self.use_cg, self.cg_tol, self.cg_maxiter diff --git a/monai/transforms/intensity/dictionary.py b/monai/transforms/intensity/dictionary.py index 3de4b63ccf..de0eb762ae 100644 --- a/monai/transforms/intensity/dictionary.py +++ b/monai/transforms/intensity/dictionary.py @@ -211,9 +211,7 @@ def __init__( RandomizableTransform.__init__(self, prob) self.rand_gaussian_noise = RandGaussianNoise(mean=mean, std=std, prob=1.0, dtype=dtype, sample_std=sample_std) - def set_random_state( - self, seed: int | None = None, state: np.random.RandomState | None = None - ) -> RandGaussianNoised: + def set_random_state(self, seed: int | None = None, state: np.random.RandomState | None = None) -> RandGaussianNoised: super().set_random_state(seed, state) self.rand_gaussian_noise.set_random_state(seed, state) return self @@ -295,9 +293,7 @@ def __init__( dtype=dtype, ) - def set_random_state( - self, seed: int | None = None, state: np.random.RandomState | None = None - ) -> RandNonCentralChiNoised: + def set_random_state(self, seed: int | None = None, state: np.random.RandomState | None = None) -> RandNonCentralChiNoised: super().set_random_state(seed, state) self.rand_non_central_chi_noise.set_random_state(seed, state) return self @@ -314,6 +310,7 @@ def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> dict[Hashable, N d[key] = self.rand_non_central_chi_noise(d[key], randomize=True) return d + class RandRicianNoised(RandomizableTransform, MapTransform): """ Dictionary-based version :py:class:`monai.transforms.RandRicianNoise`. @@ -499,9 +496,7 @@ def __init__( self.meta_key_postfix = ensure_tuple_rep(meta_key_postfix, len(self.keys)) self.shifter = RandShiftIntensity(offsets=offsets, safe=safe, prob=1.0, channel_wise=channel_wise) - def set_random_state( - self, seed: int | None = None, state: np.random.RandomState | None = None - ) -> RandShiftIntensityd: + def set_random_state(self, seed: int | None = None, state: np.random.RandomState | None = None) -> RandShiftIntensityd: super().set_random_state(seed, state) self.shifter.set_random_state(seed, state) return self @@ -600,13 +595,9 @@ def __init__( """ MapTransform.__init__(self, keys, allow_missing_keys) RandomizableTransform.__init__(self, prob) - self.shifter = RandStdShiftIntensity( - factors=factors, nonzero=nonzero, channel_wise=channel_wise, dtype=dtype, prob=1.0 - ) + self.shifter = RandStdShiftIntensity(factors=factors, nonzero=nonzero, channel_wise=channel_wise, dtype=dtype, prob=1.0) - def set_random_state( - self, seed: int | None = None, state: np.random.RandomState | None = None - ) -> RandStdShiftIntensityd: + def set_random_state(self, seed: int | None = None, state: np.random.RandomState | None = None) -> RandStdShiftIntensityd: super().set_random_state(seed, state) self.shifter.set_random_state(seed, state) return self @@ -703,9 +694,7 @@ def __init__( RandomizableTransform.__init__(self, prob) self.scaler = RandScaleIntensity(factors=factors, dtype=dtype, prob=1.0, channel_wise=channel_wise) - def set_random_state( - self, seed: int | None = None, state: np.random.RandomState | None = None - ) -> RandScaleIntensityd: + def set_random_state(self, seed: int | None = None, state: np.random.RandomState | None = None) -> RandScaleIntensityd: super().set_random_state(seed, state) self.scaler.set_random_state(seed, state) return self @@ -775,9 +764,7 @@ def __init__( factors=factors, fixed_mean=self.fixed_mean, preserve_range=preserve_range, dtype=dtype, prob=1.0 ) - def set_random_state( - self, seed: int | None = None, state: np.random.RandomState | None = None - ) -> RandScaleIntensityFixedMeand: + def set_random_state(self, seed: int | None = None, state: np.random.RandomState | None = None) -> RandScaleIntensityFixedMeand: super().set_random_state(seed, state) self.scaler.set_random_state(seed, state) return self @@ -1087,9 +1074,7 @@ def __init__( self.adjuster = RandAdjustContrast(gamma=gamma, prob=1.0, invert_image=invert_image, retain_stats=retain_stats) self.invert_image = invert_image - def set_random_state( - self, seed: int | None = None, state: np.random.RandomState | None = None - ) -> RandAdjustContrastd: + def set_random_state(self, seed: int | None = None, state: np.random.RandomState | None = None) -> RandAdjustContrastd: super().set_random_state(seed, state) self.adjuster.set_random_state(seed, state) return self @@ -1325,13 +1310,9 @@ def __init__( ) -> None: MapTransform.__init__(self, keys, allow_missing_keys) RandomizableTransform.__init__(self, prob) - self.rand_smooth = RandGaussianSmooth( - sigma_x=sigma_x, sigma_y=sigma_y, sigma_z=sigma_z, approx=approx, prob=1.0 - ) + self.rand_smooth = RandGaussianSmooth(sigma_x=sigma_x, sigma_y=sigma_y, sigma_z=sigma_z, approx=approx, prob=1.0) - def set_random_state( - self, seed: int | None = None, state: np.random.RandomState | None = None - ) -> RandGaussianSmoothd: + def set_random_state(self, seed: int | None = None, state: np.random.RandomState | None = None) -> RandGaussianSmoothd: super().set_random_state(seed, state) self.rand_smooth.set_random_state(seed, state) return self @@ -1446,9 +1427,7 @@ def __init__( prob=1.0, ) - def set_random_state( - self, seed: int | None = None, state: np.random.RandomState | None = None - ) -> RandGaussianSharpend: + def set_random_state(self, seed: int | None = None, state: np.random.RandomState | None = None) -> RandGaussianSharpend: super().set_random_state(seed, state) self.rand_sharpen.set_random_state(seed, state) return self @@ -1496,9 +1475,7 @@ def __init__( RandomizableTransform.__init__(self, prob) self.shifter = RandHistogramShift(num_control_points=num_control_points, prob=1.0) - def set_random_state( - self, seed: int | None = None, state: np.random.RandomState | None = None - ) -> RandHistogramShiftd: + def set_random_state(self, seed: int | None = None, state: np.random.RandomState | None = None) -> RandHistogramShiftd: super().set_random_state(seed, state) self.shifter.set_random_state(seed, state) return self @@ -1725,9 +1702,7 @@ def __init__( RandomizableTransform.__init__(self, prob=prob) self.rand_noise = RandKSpaceSpikeNoise(prob=1.0, intensity_range=intensity_range, channel_wise=channel_wise) - def set_random_state( - self, seed: int | None = None, state: np.random.RandomState | None = None - ) -> RandKSpaceSpikeNoised: + def set_random_state(self, seed: int | None = None, state: np.random.RandomState | None = None) -> RandKSpaceSpikeNoised: super().set_random_state(seed, state) self.rand_noise.set_random_state(seed, state) return self @@ -1803,9 +1778,7 @@ def __init__( prob=1.0, ) - def set_random_state( - self, seed: int | None = None, state: np.random.RandomState | None = None - ) -> RandCoarseDropoutd: + def set_random_state(self, seed: int | None = None, state: np.random.RandomState | None = None) -> RandCoarseDropoutd: super().set_random_state(seed, state) self.dropper.set_random_state(seed, state) return self @@ -1876,9 +1849,7 @@ def __init__( holes=holes, spatial_size=spatial_size, max_holes=max_holes, max_spatial_size=max_spatial_size, prob=1.0 ) - def set_random_state( - self, seed: int | None = None, state: np.random.RandomState | None = None - ) -> RandCoarseShuffled: + def set_random_state(self, seed: int | None = None, state: np.random.RandomState | None = None) -> RandCoarseShuffled: super().set_random_state(seed, state) self.shuffle.set_random_state(seed, state) return self diff --git a/tests/transforms/test_rand_noncentralchi_noise.py b/tests/transforms/test_rand_noncentralchi_noise.py index 03d4c1d7d7..41efb6b8de 100644 --- a/tests/transforms/test_rand_noncentralchi_noise.py +++ b/tests/transforms/test_rand_noncentralchi_noise.py @@ -30,7 +30,7 @@ class TestRandNonCentralChiNoise(NumpyImageTestCase2D): @parameterized.expand(TESTS) def test_correct_results(self, _, in_type, mean, std): seed = 0 - degrees_of_freedom = 64 #64 is common due to 32 channel head coil + degrees_of_freedom = 64 # 64 is common due to 32 channel head coil noise_fn = RandNonCentralChiNoise(prob=1.0, mean=mean, std=std, degrees_of_freedom=degrees_of_freedom) noise_fn.set_random_state(seed) im = in_type(self.imt) From a7f08627bb5d1afcb23dae17453d6146b41417d9 Mon Sep 17 00:00:00 2001 From: karllandheer Date: Mon, 10 Nov 2025 06:27:50 -0500 Subject: [PATCH 8/8] Style: Run black formatter Signed-off-by: karllandheer --- monai/transforms/intensity/array.py | 56 +++++++++++++++------- monai/transforms/intensity/dictionary.py | 60 ++++++++++++++++++------ 2 files changed, 83 insertions(+), 33 deletions(-) diff --git a/monai/transforms/intensity/array.py b/monai/transforms/intensity/array.py index 4979fc637c..4e9e9b80a2 100644 --- a/monai/transforms/intensity/array.py +++ b/monai/transforms/intensity/array.py @@ -488,7 +488,9 @@ class StdShiftIntensity(Transform): backend = [TransformBackends.TORCH, TransformBackends.NUMPY] - def __init__(self, factor: float, nonzero: bool = False, channel_wise: bool = False, dtype: DtypeLike = np.float32) -> None: + def __init__( + self, factor: float, nonzero: bool = False, channel_wise: bool = False, dtype: DtypeLike = np.float32 + ) -> None: self.factor = factor self.nonzero = nonzero self.channel_wise = channel_wise @@ -578,7 +580,9 @@ def __call__(self, img: NdarrayOrTensor, randomize: bool = True) -> NdarrayOrTen if not self._do_transform: return img - shifter = StdShiftIntensity(factor=self.factor, nonzero=self.nonzero, channel_wise=self.channel_wise, dtype=self.dtype) + shifter = StdShiftIntensity( + factor=self.factor, nonzero=self.nonzero, channel_wise=self.channel_wise, dtype=self.dtype + ) return shifter(img=img) @@ -1268,16 +1272,12 @@ def _clip(self, img: NdarrayOrTensor) -> NdarrayOrTensor: ( lower_percentile if lower_percentile is None - else lower_percentile.item() - if hasattr(lower_percentile, "item") - else lower_percentile + else lower_percentile.item() if hasattr(lower_percentile, "item") else lower_percentile ), ( upper_percentile if upper_percentile is None - else upper_percentile.item() - if hasattr(upper_percentile, "item") - else upper_percentile + else upper_percentile.item() if hasattr(upper_percentile, "item") else upper_percentile ), ) ) @@ -1401,7 +1401,9 @@ def __init__( if isinstance(gamma, (int, float)): if gamma <= 0.5: - raise ValueError(f"if gamma is a number, must greater than 0.5 and value is picked from (0.5, gamma), got {gamma}") + raise ValueError( + f"if gamma is a number, must greater than 0.5 and value is picked from (0.5, gamma), got {gamma}" + ) self.gamma = (0.5, gamma) elif len(gamma) != 2: raise ValueError("gamma should be a number or pair of numbers.") @@ -1412,7 +1414,9 @@ def __init__( self.invert_image: bool = invert_image self.retain_stats: bool = retain_stats - self.adjust_contrast = AdjustContrast(self.gamma_value, invert_image=self.invert_image, retain_stats=self.retain_stats) + self.adjust_contrast = AdjustContrast( + self.gamma_value, invert_image=self.invert_image, retain_stats=self.retain_stats + ) def randomize(self, data: Any | None = None) -> None: super().randomize(None) @@ -1538,7 +1542,9 @@ def _normalize(self, img: NdarrayOrTensor) -> NdarrayOrTensor: b_min = ((self.b_max - self.b_min) * (self.lower / 100.0)) + self.b_min b_max = ((self.b_max - self.b_min) * (self.upper / 100.0)) + self.b_min - scalar = ScaleIntensityRange(a_min=a_min, a_max=a_max, b_min=b_min, b_max=b_max, clip=self.clip, dtype=self.dtype) + scalar = ScaleIntensityRange( + a_min=a_min, a_max=a_max, b_min=b_min, b_max=b_max, clip=self.clip, dtype=self.dtype + ) img = scalar(img) img = convert_to_tensor(img, track_meta=False) return img @@ -1861,7 +1867,8 @@ def __call__(self, img: NdarrayTensor) -> NdarrayTensor: img_t, *_ = convert_data_type(img, torch.Tensor, dtype=torch.float32) gf1, gf2 = ( - GaussianFilter(img_t.ndim - 1, sigma, approx=self.approx).to(img_t.device) for sigma in (self.sigma1, self.sigma2) + GaussianFilter(img_t.ndim - 1, sigma, approx=self.approx).to(img_t.device) + for sigma in (self.sigma1, self.sigma2) ) blurred_f = gf1(img_t.unsqueeze(0)) filter_blurred_f = gf2(blurred_f) @@ -2219,7 +2226,9 @@ def __init__(self, loc: tuple | Sequence[tuple], k_intensity: Sequence[float] | # assert one-to-one relationship between factors and locations if isinstance(k_intensity, Sequence): if not isinstance(loc[0], Sequence): - raise ValueError("If a sequence is passed to k_intensity, then a sequence of locations must be passed to loc") + raise ValueError( + "If a sequence is passed to k_intensity, then a sequence of locations must be passed to loc" + ) if len(k_intensity) != len(loc): raise ValueError("There must be one intensity_factor value for each tuple of indices in loc.") if isinstance(self.loc[0], Sequence) and k_intensity is not None and not isinstance(self.k_intensity, Sequence): @@ -2559,7 +2568,9 @@ def __init__( max_spatial_size: Sequence[int] | int | None = None, prob: float = 0.1, ) -> None: - super().__init__(holes=holes, spatial_size=spatial_size, max_holes=max_holes, max_spatial_size=max_spatial_size, prob=prob) + super().__init__( + holes=holes, spatial_size=spatial_size, max_holes=max_holes, max_spatial_size=max_spatial_size, prob=prob + ) self.dropout_holes = dropout_holes if isinstance(fill_value, (tuple, list)): if len(fill_value) != 2: @@ -2772,7 +2783,10 @@ def __call__(self, img: torch.Tensor) -> torch.Tensor: if self._do_transform: if self.channel_wise: img = torch.stack( - [IntensityRemap(self.kernel_size, self.R.choice([-self.slope, self.slope]))(img[i]) for i in range(len(img))] + [ + IntensityRemap(self.kernel_size, self.R.choice([-self.slope, self.slope]))(img[i]) + for i in range(len(img)) + ] ) else: img = IntensityRemap(self.kernel_size, self.R.choice([-self.slope, self.slope]))(img) @@ -2828,7 +2842,9 @@ def __init__( self.thresholds = {k: v for k, v in self.thresholds.items() if v is not None} if self.thresholds.keys().isdisjoint(set("RGBHSV")): - raise ValueError(f"Threshold for at least one channel of RGB or HSV needs to be set. {self.thresholds} is provided.") + raise ValueError( + f"Threshold for at least one channel of RGB or HSV needs to be set. {self.thresholds} is provided." + ) self.invert = invert def _set_threshold(self, threshold, mode): @@ -2839,7 +2855,9 @@ def _set_threshold(self, threshold, mode): elif isinstance(threshold, (float, int)): self.thresholds[mode] = float(threshold) else: - raise ValueError(f"`threshold` should be either a callable, string, or float number, {type(threshold)} was given.") + raise ValueError( + f"`threshold` should be either a callable, string, or float number, {type(threshold)} was given." + ) def _get_threshold(self, image, mode): threshold = self.thresholds.get(mode) @@ -2961,7 +2979,9 @@ def __init__( raise ValueError(f"Unknown mode: {self.mode}. Supported modes are 'B' and 'RF'.") if self.sink_mode not in ["all", "mid", "min", "mask"]: - raise ValueError(f"Unknown sink mode: {self.sink_mode}. Supported modes are 'all', 'mid', 'min' and 'mask'.") + raise ValueError( + f"Unknown sink mode: {self.sink_mode}. Supported modes are 'all', 'mid', 'min' and 'mask'." + ) self._compute_conf_map = UltrasoundConfidenceMap( self.alpha, self.beta, self.gamma, self.mode, self.sink_mode, self.use_cg, self.cg_tol, self.cg_maxiter diff --git a/monai/transforms/intensity/dictionary.py b/monai/transforms/intensity/dictionary.py index de0eb762ae..c2cfaf2707 100644 --- a/monai/transforms/intensity/dictionary.py +++ b/monai/transforms/intensity/dictionary.py @@ -211,7 +211,9 @@ def __init__( RandomizableTransform.__init__(self, prob) self.rand_gaussian_noise = RandGaussianNoise(mean=mean, std=std, prob=1.0, dtype=dtype, sample_std=sample_std) - def set_random_state(self, seed: int | None = None, state: np.random.RandomState | None = None) -> RandGaussianNoised: + def set_random_state( + self, seed: int | None = None, state: np.random.RandomState | None = None + ) -> RandGaussianNoised: super().set_random_state(seed, state) self.rand_gaussian_noise.set_random_state(seed, state) return self @@ -293,7 +295,9 @@ def __init__( dtype=dtype, ) - def set_random_state(self, seed: int | None = None, state: np.random.RandomState | None = None) -> RandNonCentralChiNoised: + def set_random_state( + self, seed: int | None = None, state: np.random.RandomState | None = None + ) -> RandNonCentralChiNoised: super().set_random_state(seed, state) self.rand_non_central_chi_noise.set_random_state(seed, state) return self @@ -496,7 +500,9 @@ def __init__( self.meta_key_postfix = ensure_tuple_rep(meta_key_postfix, len(self.keys)) self.shifter = RandShiftIntensity(offsets=offsets, safe=safe, prob=1.0, channel_wise=channel_wise) - def set_random_state(self, seed: int | None = None, state: np.random.RandomState | None = None) -> RandShiftIntensityd: + def set_random_state( + self, seed: int | None = None, state: np.random.RandomState | None = None + ) -> RandShiftIntensityd: super().set_random_state(seed, state) self.shifter.set_random_state(seed, state) return self @@ -595,9 +601,13 @@ def __init__( """ MapTransform.__init__(self, keys, allow_missing_keys) RandomizableTransform.__init__(self, prob) - self.shifter = RandStdShiftIntensity(factors=factors, nonzero=nonzero, channel_wise=channel_wise, dtype=dtype, prob=1.0) + self.shifter = RandStdShiftIntensity( + factors=factors, nonzero=nonzero, channel_wise=channel_wise, dtype=dtype, prob=1.0 + ) - def set_random_state(self, seed: int | None = None, state: np.random.RandomState | None = None) -> RandStdShiftIntensityd: + def set_random_state( + self, seed: int | None = None, state: np.random.RandomState | None = None + ) -> RandStdShiftIntensityd: super().set_random_state(seed, state) self.shifter.set_random_state(seed, state) return self @@ -694,7 +704,9 @@ def __init__( RandomizableTransform.__init__(self, prob) self.scaler = RandScaleIntensity(factors=factors, dtype=dtype, prob=1.0, channel_wise=channel_wise) - def set_random_state(self, seed: int | None = None, state: np.random.RandomState | None = None) -> RandScaleIntensityd: + def set_random_state( + self, seed: int | None = None, state: np.random.RandomState | None = None + ) -> RandScaleIntensityd: super().set_random_state(seed, state) self.scaler.set_random_state(seed, state) return self @@ -764,7 +776,9 @@ def __init__( factors=factors, fixed_mean=self.fixed_mean, preserve_range=preserve_range, dtype=dtype, prob=1.0 ) - def set_random_state(self, seed: int | None = None, state: np.random.RandomState | None = None) -> RandScaleIntensityFixedMeand: + def set_random_state( + self, seed: int | None = None, state: np.random.RandomState | None = None + ) -> RandScaleIntensityFixedMeand: super().set_random_state(seed, state) self.scaler.set_random_state(seed, state) return self @@ -1074,7 +1088,9 @@ def __init__( self.adjuster = RandAdjustContrast(gamma=gamma, prob=1.0, invert_image=invert_image, retain_stats=retain_stats) self.invert_image = invert_image - def set_random_state(self, seed: int | None = None, state: np.random.RandomState | None = None) -> RandAdjustContrastd: + def set_random_state( + self, seed: int | None = None, state: np.random.RandomState | None = None + ) -> RandAdjustContrastd: super().set_random_state(seed, state) self.adjuster.set_random_state(seed, state) return self @@ -1310,9 +1326,13 @@ def __init__( ) -> None: MapTransform.__init__(self, keys, allow_missing_keys) RandomizableTransform.__init__(self, prob) - self.rand_smooth = RandGaussianSmooth(sigma_x=sigma_x, sigma_y=sigma_y, sigma_z=sigma_z, approx=approx, prob=1.0) + self.rand_smooth = RandGaussianSmooth( + sigma_x=sigma_x, sigma_y=sigma_y, sigma_z=sigma_z, approx=approx, prob=1.0 + ) - def set_random_state(self, seed: int | None = None, state: np.random.RandomState | None = None) -> RandGaussianSmoothd: + def set_random_state( + self, seed: int | None = None, state: np.random.RandomState | None = None + ) -> RandGaussianSmoothd: super().set_random_state(seed, state) self.rand_smooth.set_random_state(seed, state) return self @@ -1427,7 +1447,9 @@ def __init__( prob=1.0, ) - def set_random_state(self, seed: int | None = None, state: np.random.RandomState | None = None) -> RandGaussianSharpend: + def set_random_state( + self, seed: int | None = None, state: np.random.RandomState | None = None + ) -> RandGaussianSharpend: super().set_random_state(seed, state) self.rand_sharpen.set_random_state(seed, state) return self @@ -1475,7 +1497,9 @@ def __init__( RandomizableTransform.__init__(self, prob) self.shifter = RandHistogramShift(num_control_points=num_control_points, prob=1.0) - def set_random_state(self, seed: int | None = None, state: np.random.RandomState | None = None) -> RandHistogramShiftd: + def set_random_state( + self, seed: int | None = None, state: np.random.RandomState | None = None + ) -> RandHistogramShiftd: super().set_random_state(seed, state) self.shifter.set_random_state(seed, state) return self @@ -1702,7 +1726,9 @@ def __init__( RandomizableTransform.__init__(self, prob=prob) self.rand_noise = RandKSpaceSpikeNoise(prob=1.0, intensity_range=intensity_range, channel_wise=channel_wise) - def set_random_state(self, seed: int | None = None, state: np.random.RandomState | None = None) -> RandKSpaceSpikeNoised: + def set_random_state( + self, seed: int | None = None, state: np.random.RandomState | None = None + ) -> RandKSpaceSpikeNoised: super().set_random_state(seed, state) self.rand_noise.set_random_state(seed, state) return self @@ -1778,7 +1804,9 @@ def __init__( prob=1.0, ) - def set_random_state(self, seed: int | None = None, state: np.random.RandomState | None = None) -> RandCoarseDropoutd: + def set_random_state( + self, seed: int | None = None, state: np.random.RandomState | None = None + ) -> RandCoarseDropoutd: super().set_random_state(seed, state) self.dropper.set_random_state(seed, state) return self @@ -1849,7 +1877,9 @@ def __init__( holes=holes, spatial_size=spatial_size, max_holes=max_holes, max_spatial_size=max_spatial_size, prob=1.0 ) - def set_random_state(self, seed: int | None = None, state: np.random.RandomState | None = None) -> RandCoarseShuffled: + def set_random_state( + self, seed: int | None = None, state: np.random.RandomState | None = None + ) -> RandCoarseShuffled: super().set_random_state(seed, state) self.shuffle.set_random_state(seed, state) return self