From ac702e10b56bab6b1e8d9a47474d73eecb964be1 Mon Sep 17 00:00:00 2001 From: Nic Ma Date: Tue, 3 Aug 2021 10:55:21 +0800 Subject: [PATCH 01/16] [DLMED] add IntensityStats transform Signed-off-by: Nic Ma --- docs/source/transforms.rst | 6 +++++ monai/transforms/__init__.py | 1 + monai/transforms/intensity/array.py | 40 ++++++++++++++++++++++++++++- 3 files changed, 46 insertions(+), 1 deletion(-) diff --git a/docs/source/transforms.rst b/docs/source/transforms.rst index 1c3ee288a1..4712ebf337 100644 --- a/docs/source/transforms.rst +++ b/docs/source/transforms.rst @@ -319,6 +319,12 @@ Intensity :members: :special-members: __call__ +`IntensityStats` +"""""""""""""""" + .. autoclass:: IntensityStats + :members: + :special-members: __call__ + IO ^^ diff --git a/monai/transforms/__init__.py b/monai/transforms/__init__.py index 20e29d5aa9..6e8d27633c 100644 --- a/monai/transforms/__init__.py +++ b/monai/transforms/__init__.py @@ -83,6 +83,7 @@ GaussianSharpen, GaussianSmooth, GibbsNoise, + IntensityStats, KSpaceSpikeNoise, MaskIntensity, NormalizeIntensity, diff --git a/monai/transforms/intensity/array.py b/monai/transforms/intensity/array.py index 4533f333ce..272facbdcd 100644 --- a/monai/transforms/intensity/array.py +++ b/monai/transforms/intensity/array.py @@ -14,7 +14,7 @@ """ from collections.abc import Iterable -from typing import Any, List, Optional, Sequence, Tuple, Union +from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, Union from warnings import warn import numpy as np @@ -64,6 +64,7 @@ "KSpaceSpikeNoise", "RandKSpaceSpikeNoise", "RandCoarseDropout", + "IntensityStats", ] @@ -1618,3 +1619,40 @@ def __call__(self, img: np.ndarray): img[h] = self.fill_value return img + + +class IntensityStats(Transform): + def __init__(self, ops: Sequence[Union[str, Callable]], key_prefix: str, channel_wise: bool = False) -> None: + self.supported_ops = { + "mean": lambda x: np.nanmean(x), + "median": lambda x: np.nanmedian(x), + "max": lambda x: np.nanmax(x), + "min": lambda x: np.nanmin(x), + "std": lambda x: np.nanstd(x), + } + for o in ops: + if isinstance(o, str) and o not in self.supported_ops: + raise ValueError(f"unsupported operation: {o}.") + self.ops = ops + self.key_prefix = key_prefix + self.channel_wise = channel_wise + + def __call__(self, img: np.ndarray, meta_data: Optional[Dict] = None) -> np.ndarray: + if meta_data is None: + meta_data = {} + + def _compute(op: Callable, img: np.ndarray): + if self.channel_wise: + return [op(c) for c in img] + else: + return op(img) + + custom_index = 0 + for o in self.ops: + if isinstance(o, str): + meta_data[self.key_prefix + "_" + o] = _compute(self.supported_ops[o], img) + elif callable(o): + meta_data[self.key_prefix + "_custom_" + custom_index] = _compute(o, img) + custom_index += 1 + + return img, meta_data From 990a99857a588ff957dd21d2c70c72d4b1756532 Mon Sep 17 00:00:00 2001 From: Nic Ma Date: Tue, 3 Aug 2021 15:42:18 +0800 Subject: [PATCH 02/16] [DLMED] add unit tests Signed-off-by: Nic Ma --- monai/transforms/intensity/array.py | 6 +-- tests/test_intensity_stats.py | 58 +++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 3 deletions(-) create mode 100644 tests/test_intensity_stats.py diff --git a/monai/transforms/intensity/array.py b/monai/transforms/intensity/array.py index 272facbdcd..53192bb56b 100644 --- a/monai/transforms/intensity/array.py +++ b/monai/transforms/intensity/array.py @@ -1630,10 +1630,10 @@ def __init__(self, ops: Sequence[Union[str, Callable]], key_prefix: str, channel "min": lambda x: np.nanmin(x), "std": lambda x: np.nanstd(x), } - for o in ops: + self.ops = ensure_tuple(ops) + for o in self.ops: if isinstance(o, str) and o not in self.supported_ops: raise ValueError(f"unsupported operation: {o}.") - self.ops = ops self.key_prefix = key_prefix self.channel_wise = channel_wise @@ -1652,7 +1652,7 @@ def _compute(op: Callable, img: np.ndarray): if isinstance(o, str): meta_data[self.key_prefix + "_" + o] = _compute(self.supported_ops[o], img) elif callable(o): - meta_data[self.key_prefix + "_custom_" + custom_index] = _compute(o, img) + meta_data[self.key_prefix + "_custom_" + str(custom_index)] = _compute(o, img) custom_index += 1 return img, meta_data diff --git a/tests/test_intensity_stats.py b/tests/test_intensity_stats.py new file mode 100644 index 0000000000..ec9b6dfaed --- /dev/null +++ b/tests/test_intensity_stats.py @@ -0,0 +1,58 @@ +# 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 IntensityStats + +TEST_CASE_1 = [ + {"ops": ["max", "mean"], "key_prefix": "orig"}, + np.array([[[0.0, 1.0], [2.0, 3.0]]]), + {"affine": None}, + {"orig_max": 3.0, "orig_mean": 1.5}, +] + +TEST_CASE_2 = [ + {"ops": "std", "key_prefix": "orig"}, + np.array([[[0.0, 1.0], [2.0, 3.0]]]), + None, + {"orig_std": 1.118034}, +] + +TEST_CASE_3 = [ + {"ops": [lambda x: np.mean(x), "max", lambda x: np.min(x)], "key_prefix": "orig"}, + np.array([[[0.0, 1.0], [2.0, 3.0]]]), + None, + {"orig_custom_0": 1.5, "orig_max": 3.0, "orig_custom_1": 0.0}, +] + +TEST_CASE_4 = [ + {"ops": ["max", "mean"], "key_prefix": "orig", "channel_wise": True}, + np.array([[[0.0, 1.0], [2.0, 3.0]], [[4.0, 5.0], [6.0, 7.0]]]), + {"affine": None}, + {"orig_max": [3.0, 7.0], "orig_mean": [1.5, 5.5]}, +] + + +class TestIntensityStats(unittest.TestCase): + @parameterized.expand([TEST_CASE_1, TEST_CASE_2, TEST_CASE_3, TEST_CASE_4]) + def test_value(self, input_param, img, meta_dict, expected): + img, meta_dict = IntensityStats(**input_param)(img, meta_dict) + for k, v in expected.items(): + self.assertTrue(k in meta_dict) + np.testing.assert_allclose(v, meta_dict[k], atol=1e-3) + + +if __name__ == "__main__": + unittest.main() From ebf6cafa861182c99afa69aa1462201d3e626381 Mon Sep 17 00:00:00 2001 From: Nic Ma Date: Tue, 3 Aug 2021 17:25:57 +0800 Subject: [PATCH 03/16] [DLMED] add dict version transform Signed-off-by: Nic Ma --- docs/source/transforms.rst | 6 +++ monai/transforms/__init__.py | 3 ++ monai/transforms/intensity/array.py | 35 +++++++++++-- monai/transforms/intensity/dictionary.py | 63 +++++++++++++++++++++++- tests/test_intensity_statsd.py | 58 ++++++++++++++++++++++ tests/test_rand_shift_intensity.py | 2 +- 6 files changed, 160 insertions(+), 7 deletions(-) create mode 100644 tests/test_intensity_statsd.py diff --git a/docs/source/transforms.rst b/docs/source/transforms.rst index 4712ebf337..3c3e00c146 100644 --- a/docs/source/transforms.rst +++ b/docs/source/transforms.rst @@ -917,6 +917,12 @@ Intensity (Dict) :members: :special-members: __call__ +`IntensityStatsd` +""""""""""""""""" +.. autoclass:: IntensityStatsd + :members: + :special-members: __call__ + IO (Dict) ^^^^^^^^^ diff --git a/monai/transforms/__init__.py b/monai/transforms/__init__.py index 6e8d27633c..97734e9009 100644 --- a/monai/transforms/__init__.py +++ b/monai/transforms/__init__.py @@ -121,6 +121,9 @@ GibbsNoised, GibbsNoiseD, GibbsNoiseDict, + IntensityStatsd, + IntensityStatsD, + IntensityStatsDict, KSpaceSpikeNoised, KSpaceSpikeNoiseD, KSpaceSpikeNoiseDict, diff --git a/monai/transforms/intensity/array.py b/monai/transforms/intensity/array.py index 53192bb56b..8205c7187a 100644 --- a/monai/transforms/intensity/array.py +++ b/monai/transforms/intensity/array.py @@ -188,11 +188,13 @@ class ShiftIntensity(Transform): def __init__(self, offset: float) -> None: self.offset = offset - def __call__(self, img: np.ndarray) -> np.ndarray: + def __call__(self, img: np.ndarray, offset: Optional[float] = None) -> np.ndarray: """ Apply the transform to `img`. """ - return np.asarray((img + self.offset), dtype=img.dtype) + + offset = self.offset if offset is None else offset + return np.asarray((img + offset), dtype=img.dtype) class RandShiftIntensity(RandomizableTransform): @@ -215,20 +217,26 @@ def __init__(self, offsets: Union[Tuple[float, float], float], prob: float = 0.1 raise AssertionError("offsets should be a number or pair of numbers.") self.offsets = (min(offsets), max(offsets)) self._offset = self.offsets[0] + self._shfiter = ShiftIntensity(self._offset) def randomize(self, data: Optional[Any] = None) -> None: self._offset = self.R.uniform(low=self.offsets[0], high=self.offsets[1]) super().randomize(None) - def __call__(self, img: np.ndarray) -> np.ndarray: + def __call__(self, img: np.ndarray, factor: Optional[float] = None) -> np.ndarray: """ Apply the transform to `img`. + + Args: + img: input image to shift intensity. + factor: a factor to multiply the random offset, then shift. + can be some image specific value at runtime, like: max(img), etc. + """ self.randomize() if not self._do_transform: return img - shifter = ShiftIntensity(self._offset) - return shifter(img) + return self._shfiter(img, self._offset if factor is None else self._offset * factor) class StdShiftIntensity(Transform): @@ -1622,6 +1630,23 @@ def __call__(self, img: np.ndarray): class IntensityStats(Transform): + """ + Compute statistics for the intensity values of input image and store into the meta data dictionary. + For example: if `ops=[lambda x: np.mean(x), "max"]` and `key_prefix="orig"`, may generate below stats: + `{"orig_custom_0": 1.5, "orig_max": 3.0}`. + + Args: + ops: expected operations to compute statistics for the intensity. + if a string, will map to the predefined operations, supported: ["mean", "median", "max", "min", "std"] + mapping to `np.nanmean`, `np.nanmedian`, `np.nanmax`, `np.nanmin`, `np.nanstd`. + if a callable function, will execute the function on input image. + key_prefix: the prefix to combine with `ops` name to generate the key to store the results in the + meta data dictionary. if some `ops` are callable functions, will use "{key_prefix}_custom_{index}" + as the key, where index counts from 0. + channel_wise: whether to compute statistics for every channel of input image separately. + if True, return a list of values for every operation, default to False. + + """ def __init__(self, ops: Sequence[Union[str, Callable]], key_prefix: str, channel_wise: bool = False) -> None: self.supported_ops = { "mean": lambda x: np.nanmean(x), diff --git a/monai/transforms/intensity/dictionary.py b/monai/transforms/intensity/dictionary.py index c24f7b67ca..6020660441 100644 --- a/monai/transforms/intensity/dictionary.py +++ b/monai/transforms/intensity/dictionary.py @@ -16,7 +16,7 @@ """ from collections.abc import Iterable -from typing import Any, Dict, Hashable, List, Mapping, Optional, Sequence, Tuple, Union +from typing import Any, Callable, Dict, Hashable, List, Mapping, Optional, Sequence, Tuple, Union import numpy as np import torch @@ -28,6 +28,7 @@ GaussianSharpen, GaussianSmooth, GibbsNoise, + IntensityStats, KSpaceSpikeNoise, MaskIntensity, NormalizeIntensity, @@ -71,6 +72,7 @@ "RandKSpaceSpikeNoised", "RandHistogramShiftd", "RandCoarseDropoutd", + "IntensityStatsd", "RandGaussianNoiseD", "RandGaussianNoiseDict", "ShiftIntensityD", @@ -121,6 +123,8 @@ "RandRicianNoiseDict", "RandCoarseDropoutD", "RandCoarseDropoutDict", + "IntensityStatsD", + "IntensityStatsDict", ] @@ -1405,6 +1409,62 @@ def __call__(self, data): return d +class IntensityStatsd(MapTransform): + """ + Dictionary-based wrapper of :py:class:`monai.transforms.IntensityStats`. + Compute statistics for the intensity values of input image and store into the meta data dictionary. + For example: if `ops=[lambda x: np.mean(x), "max"]` and `key_prefix="orig"`, may generate below stats: + `{"orig_custom_0": 1.5, "orig_max": 3.0}`. + + Args: + keys: keys of the corresponding items to be transformed. + See also: :py:class:`monai.transforms.compose.MapTransform` + ops: expected operations to compute statistics for the intensity. + if a string, will map to the predefined operations, supported: ["mean", "median", "max", "min", "std"] + mapping to `np.nanmean`, `np.nanmedian`, `np.nanmax`, `np.nanmin`, `np.nanstd`. + if a callable function, will execute the function on input image. + key_prefix: the prefix to combine with `ops` name to generate the key to store the results in the + meta data dictionary. if some `ops` are callable functions, will use "{key_prefix}_custom_{index}" + as the key, where index counts from 0. + channel_wise: whether to compute statistics for every channel of input image separately. + if True, return a list of values for every operation, default to False. + meta_keys: explicitly indicate the key of the corresponding meta data dictionary. + used to store the computed statistics to the meta dict. + for example, for data with key `image`, the metadata by default is in `image_meta_dict`. + the meta data is a dictionary object which contains: filename, original_shape, etc. + it can be a sequence of string, map to the `keys`. + if None, will try to construct meta_keys by `key_{meta_key_postfix}`. + meta_key_postfix: if meta_keys is None, use `key_{postfix}` to to fetch the meta data according + to the key data, default is `meta_dict`, the meta data is a dictionary object. + used to store the computed statistics to the meta dict. + allow_missing_keys: don't raise exception if key is missing. + + """ + def __init__( + self, + keys: KeysCollection, + ops: Sequence[Union[str, Callable]], + key_prefix: str, + channel_wise: bool = False, + meta_keys: Optional[KeysCollection] = None, + meta_key_postfix: str = "meta_dict", + allow_missing_keys: bool = False, + ) -> None: + super().__init__(keys, allow_missing_keys) + self.stats = IntensityStats(ops=ops, key_prefix=key_prefix, channel_wise=channel_wise) + self.meta_keys = ensure_tuple_rep(None, len(self.keys)) if meta_keys is None else ensure_tuple(meta_keys) + if len(self.keys) != len(self.meta_keys): + raise ValueError("meta_keys should have the same length as keys.") + self.meta_key_postfix = ensure_tuple_rep(meta_key_postfix, len(self.keys)) + + def __call__(self, data: Mapping[Hashable, np.ndarray]) -> Dict[Hashable, np.ndarray]: + d = dict(data) + for key, meta_key, meta_key_postfix in self.key_iterator(d, self.meta_keys, self.meta_key_postfix): + meta_key = meta_key or f"{key}_{meta_key_postfix}" + d[key], d[meta_key] = self.stats(d[key], d.get(meta_key)) + return d + + RandGaussianNoiseD = RandGaussianNoiseDict = RandGaussianNoised RandRicianNoiseD = RandRicianNoiseDict = RandRicianNoised ShiftIntensityD = ShiftIntensityDict = ShiftIntensityd @@ -1431,3 +1491,4 @@ def __call__(self, data): KSpaceSpikeNoiseD = KSpaceSpikeNoiseDict = KSpaceSpikeNoised RandKSpaceSpikeNoiseD = RandKSpaceSpikeNoiseDict = RandKSpaceSpikeNoised RandCoarseDropoutD = RandCoarseDropoutDict = RandCoarseDropoutd +IntensityStatsD = IntensityStatsDict = IntensityStatsd diff --git a/tests/test_intensity_statsd.py b/tests/test_intensity_statsd.py new file mode 100644 index 0000000000..fb7a8bd59f --- /dev/null +++ b/tests/test_intensity_statsd.py @@ -0,0 +1,58 @@ +# 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 IntensityStatsd + +TEST_CASE_1 = [ + {"keys": "img", "ops": ["max", "mean"], "key_prefix": "orig", "meta_key": "test_meta"}, + {"img": np.array([[[0.0, 1.0], [2.0, 3.0]]]), "test_meta": {"affine": None}}, + "test_meta", + {"orig_max": 3.0, "orig_mean": 1.5}, +] + +TEST_CASE_2 = [ + {"keys": "img", "ops": "std", "key_prefix": "orig"}, + {"img": np.array([[[0.0, 1.0], [2.0, 3.0]]])}, + "img_meta_dict", + {"orig_std": 1.118034}, +] + +TEST_CASE_3 = [ + {"keys": "img", "ops": [lambda x: np.mean(x), "max", lambda x: np.min(x)], "key_prefix": "orig"}, + {"img": np.array([[[0.0, 1.0], [2.0, 3.0]]])}, + "img_meta_dict", + {"orig_custom_0": 1.5, "orig_max": 3.0, "orig_custom_1": 0.0}, +] + +TEST_CASE_4 = [ + {"keys": "img", "ops": ["max", "mean"], "key_prefix": "orig", "channel_wise": True, "meta_key_postfix": "meta"}, + {"img": np.array([[[0.0, 1.0], [2.0, 3.0]], [[4.0, 5.0], [6.0, 7.0]]]), "img_meta": {"affine": None}}, + "img_meta", + {"orig_max": [3.0, 7.0], "orig_mean": [1.5, 5.5]}, +] + + +class TestIntensityStatsd(unittest.TestCase): + @parameterized.expand([TEST_CASE_1, TEST_CASE_2, TEST_CASE_3, TEST_CASE_4]) + def test_value(self, input_param, data, meta_key, expected): + meta = IntensityStatsd(**input_param)(data)[meta_key] + for k, v in expected.items(): + self.assertTrue(k in meta) + np.testing.assert_allclose(v, meta[k], atol=1e-3) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_rand_shift_intensity.py b/tests/test_rand_shift_intensity.py index ba54510bc3..4c4dd87dfe 100644 --- a/tests/test_rand_shift_intensity.py +++ b/tests/test_rand_shift_intensity.py @@ -21,7 +21,7 @@ class TestRandShiftIntensity(NumpyImageTestCase2D): def test_value(self): shifter = RandShiftIntensity(offsets=1.0, prob=1.0) shifter.set_random_state(seed=0) - result = shifter(self.imt) + result = shifter(self.imt, factor=1.0) np.random.seed(0) expected = self.imt + np.random.uniform(low=-1.0, high=1.0) np.testing.assert_allclose(result, expected) From 18f28df19ab728bfec4388a6ce544fbf5ac7b750 Mon Sep 17 00:00:00 2001 From: Nic Ma Date: Tue, 3 Aug 2021 18:16:32 +0800 Subject: [PATCH 04/16] [DLMED] enhance ShiftIntensity transform Signed-off-by: Nic Ma --- monai/transforms/intensity/array.py | 3 +- monai/transforms/intensity/dictionary.py | 80 +++++++++++++++++++++--- tests/test_rand_shift_intensityd.py | 13 +++- tests/test_shift_intensityd.py | 12 +++- 4 files changed, 95 insertions(+), 13 deletions(-) diff --git a/monai/transforms/intensity/array.py b/monai/transforms/intensity/array.py index 8205c7187a..2e3537533d 100644 --- a/monai/transforms/intensity/array.py +++ b/monai/transforms/intensity/array.py @@ -1647,6 +1647,7 @@ class IntensityStats(Transform): if True, return a list of values for every operation, default to False. """ + def __init__(self, ops: Sequence[Union[str, Callable]], key_prefix: str, channel_wise: bool = False) -> None: self.supported_ops = { "mean": lambda x: np.nanmean(x), @@ -1662,7 +1663,7 @@ def __init__(self, ops: Sequence[Union[str, Callable]], key_prefix: str, channel self.key_prefix = key_prefix self.channel_wise = channel_wise - def __call__(self, img: np.ndarray, meta_data: Optional[Dict] = None) -> np.ndarray: + def __call__(self, img: np.ndarray, meta_data: Optional[Dict] = None) -> Tuple[np.ndarray, Dict]: if meta_data is None: meta_data = {} diff --git a/monai/transforms/intensity/dictionary.py b/monai/transforms/intensity/dictionary.py index 6020660441..ff9e2800be 100644 --- a/monai/transforms/intensity/dictionary.py +++ b/monai/transforms/intensity/dictionary.py @@ -43,7 +43,7 @@ ThresholdIntensity, ) from monai.transforms.transform import MapTransform, RandomizableTransform -from monai.utils import dtype_torch_to_numpy, ensure_tuple_rep, ensure_tuple_size, fall_back_tuple +from monai.utils import dtype_torch_to_numpy, ensure_tuple, ensure_tuple_rep, ensure_tuple_size, fall_back_tuple __all__ = [ "RandGaussianNoised", @@ -236,21 +236,53 @@ class ShiftIntensityd(MapTransform): Dictionary-based wrapper of :py:class:`monai.transforms.ShiftIntensity`. """ - def __init__(self, keys: KeysCollection, offset: float, allow_missing_keys: bool = False) -> None: + def __init__( + self, + keys: KeysCollection, + offset: float, + factor_key: Optional[str] = None, + meta_keys: Optional[KeysCollection] = None, + meta_key_postfix: str = "meta_dict", + allow_missing_keys: bool = False, + ) -> None: """ Args: keys: keys of the corresponding items to be transformed. See also: :py:class:`monai.transforms.compose.MapTransform` offset: offset value to shift the intensity of image. + factor_key: if not None, use it as the key to extract a value from the corresponding + meta data dictionary of `key` at runtime, and multiply the `offset` to shift intensity. + Usually, `IntensityStatsd` transform can pre-compute statistics of intensity values + and store in the meta data. + it also can be a sequence of strings, map to `keys`. + meta_keys: explicitly indicate the key of the corresponding meta data dictionary. + used to extract the factor value is `factor_key` is not None. + for example, for data with key `image`, the metadata by default is in `image_meta_dict`. + the meta data is a dictionary object which contains: filename, original_shape, etc. + it can be a sequence of string, map to the `keys`. + if None, will try to construct meta_keys by `key_{meta_key_postfix}`. + meta_key_postfix: if meta_keys is None, use `key_{postfix}` to to fetch the meta data according + to the key data, default is `meta_dict`, the meta data is a dictionary object. + used to extract the factor value is `factor_key` is not None. allow_missing_keys: don't raise exception if key is missing. """ super().__init__(keys, allow_missing_keys) + self.factor_key = ensure_tuple_rep(factor_key, len(self.keys)) + self.meta_keys = ensure_tuple_rep(None, len(self.keys)) if meta_keys is None else ensure_tuple(meta_keys) + if len(self.keys) != len(self.meta_keys): + raise ValueError("meta_keys should have the same length as keys.") + self.meta_key_postfix = ensure_tuple_rep(meta_key_postfix, len(self.keys)) self.shifter = ShiftIntensity(offset) - def __call__(self, data: Mapping[Hashable, np.ndarray]) -> Dict[Hashable, np.ndarray]: + def __call__(self, data) -> Dict[Hashable, np.ndarray]: d = dict(data) - for key in self.key_iterator(d): - d[key] = self.shifter(d[key]) + for key, factor_key, meta_key, meta_key_postfix in self.key_iterator( + d, self.factor_key, self.meta_keys, self.meta_key_postfix + ): + meta_key = meta_key or f"{key}_{meta_key_postfix}" + factor: Optional[float] = d[meta_key].get(factor_key) if meta_key in d else None + offset = None if factor is None else self.shifter.offset * factor + d[key] = self.shifter(d[key], offset=offset) return d @@ -263,6 +295,9 @@ def __init__( self, keys: KeysCollection, offsets: Union[Tuple[float, float], float], + factor_key: Optional[str] = None, + meta_keys: Optional[KeysCollection] = None, + meta_key_postfix: str = "meta_dict", prob: float = 0.1, allow_missing_keys: bool = False, ) -> None: @@ -272,6 +307,20 @@ def __init__( See also: :py:class:`monai.transforms.compose.MapTransform` offsets: offset range to randomly shift. if single number, offset value is picked from (-offsets, offsets). + factor_key: if not None, use it as the key to extract a value from the corresponding + meta data dictionary of `key` at runtime, and multiply the random `offset` to shift intensity. + Usually, `IntensityStatsd` transform can pre-compute statistics of intensity values + and store in the meta data. + it also can be a sequence of strings, map to `keys`. + meta_keys: explicitly indicate the key of the corresponding meta data dictionary. + used to extract the factor value is `factor_key` is not None. + for example, for data with key `image`, the metadata by default is in `image_meta_dict`. + the meta data is a dictionary object which contains: filename, original_shape, etc. + it can be a sequence of string, map to the `keys`. + if None, will try to construct meta_keys by `key_{meta_key_postfix}`. + meta_key_postfix: if meta_keys is None, use `key_{postfix}` to to fetch the meta data according + to the key data, default is `meta_dict`, the meta data is a dictionary object. + used to extract the factor value is `factor_key` is not None. prob: probability of rotating. (Default 0.1, with 10% probability it returns a rotated array.) allow_missing_keys: don't raise exception if key is missing. @@ -286,19 +335,29 @@ def __init__( raise AssertionError("offsets should be a number or pair of numbers.") self.offsets = (min(offsets), max(offsets)) self._offset = self.offsets[0] + self.factor_key = ensure_tuple_rep(factor_key, len(self.keys)) + self.meta_keys = ensure_tuple_rep(None, len(self.keys)) if meta_keys is None else ensure_tuple(meta_keys) + if len(self.keys) != len(self.meta_keys): + raise ValueError("meta_keys should have the same length as keys.") + self.meta_key_postfix = ensure_tuple_rep(meta_key_postfix, len(self.keys)) + self.shifter = ShiftIntensity(self._offset) def randomize(self, data: Optional[Any] = None) -> None: self._offset = self.R.uniform(low=self.offsets[0], high=self.offsets[1]) super().randomize(None) - def __call__(self, data: Mapping[Hashable, np.ndarray]) -> Dict[Hashable, np.ndarray]: + def __call__(self, data) -> Dict[Hashable, np.ndarray]: d = dict(data) self.randomize() if not self._do_transform: return d - shifter = ShiftIntensity(self._offset) - for key in self.key_iterator(d): - d[key] = shifter(d[key]) + for key, factor_key, meta_key, meta_key_postfix in self.key_iterator( + d, self.factor_key, self.meta_keys, self.meta_key_postfix + ): + meta_key = meta_key or f"{key}_{meta_key_postfix}" + factor: Optional[float] = d[meta_key].get(factor_key) if meta_key in d else None + offset = self._offset if factor is None else self._offset * factor + d[key] = self.shifter(d[key], offset=offset) return d @@ -1440,6 +1499,7 @@ class IntensityStatsd(MapTransform): allow_missing_keys: don't raise exception if key is missing. """ + def __init__( self, keys: KeysCollection, @@ -1457,7 +1517,7 @@ def __init__( raise ValueError("meta_keys should have the same length as keys.") self.meta_key_postfix = ensure_tuple_rep(meta_key_postfix, len(self.keys)) - def __call__(self, data: Mapping[Hashable, np.ndarray]) -> Dict[Hashable, np.ndarray]: + def __call__(self, data) -> Dict[Hashable, np.ndarray]: d = dict(data) for key, meta_key, meta_key_postfix in self.key_iterator(d, self.meta_keys, self.meta_key_postfix): meta_key = meta_key or f"{key}_{meta_key_postfix}" diff --git a/tests/test_rand_shift_intensityd.py b/tests/test_rand_shift_intensityd.py index 0c6f25e7b5..71cfd8fc50 100644 --- a/tests/test_rand_shift_intensityd.py +++ b/tests/test_rand_shift_intensityd.py @@ -13,7 +13,7 @@ import numpy as np -from monai.transforms import RandShiftIntensityd +from monai.transforms import IntensityStatsd, RandShiftIntensityd from tests.utils import NumpyImageTestCase2D @@ -27,6 +27,17 @@ def test_value(self): expected = self.imt + np.random.uniform(low=-1.0, high=1.0) np.testing.assert_allclose(result[key], expected) + def test_factor(self): + key = "img" + stats = IntensityStatsd(keys=key, ops="max", key_prefix="orig") + shifter = RandShiftIntensityd(keys=[key], offsets=1.0, factor_key=["orig_max"], prob=1.0) + data = {key: self.imt, key + "_meta_dict": {"affine": None}} + shifter.set_random_state(seed=0) + result = shifter(stats(data)) + np.random.seed(0) + expected = self.imt + np.random.uniform(low=-1.0, high=1.0) * np.nanmax(self.imt) + np.testing.assert_allclose(result[key], expected) + if __name__ == "__main__": unittest.main() diff --git a/tests/test_shift_intensityd.py b/tests/test_shift_intensityd.py index 752cf4b8d2..71cfffc9c5 100644 --- a/tests/test_shift_intensityd.py +++ b/tests/test_shift_intensityd.py @@ -13,7 +13,7 @@ import numpy as np -from monai.transforms import ShiftIntensityd +from monai.transforms import IntensityStatsd, ShiftIntensityd from tests.utils import NumpyImageTestCase2D @@ -25,6 +25,16 @@ def test_value(self): expected = self.imt + 1.0 np.testing.assert_allclose(result[key], expected) + def test_factor(self): + key = "img" + stats = IntensityStatsd(keys=key, ops="max", key_prefix="orig") + shifter = ShiftIntensityd(keys=[key], offset=1.0, factor_key=["orig_max"]) + data = {key: self.imt, key + "_meta_dict": {"affine": None}} + + result = shifter(stats(data)) + expected = self.imt + 1.0 * np.nanmax(self.imt) + np.testing.assert_allclose(result[key], expected) + if __name__ == "__main__": unittest.main() From 0c20766b341676a64654d9c2565276f71540bede Mon Sep 17 00:00:00 2001 From: Nic Ma Date: Tue, 3 Aug 2021 18:28:29 +0800 Subject: [PATCH 05/16] [DLMED] fix typo Signed-off-by: Nic Ma --- tests/test_intensity_statsd.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_intensity_statsd.py b/tests/test_intensity_statsd.py index fb7a8bd59f..8f9841d243 100644 --- a/tests/test_intensity_statsd.py +++ b/tests/test_intensity_statsd.py @@ -17,7 +17,7 @@ from monai.transforms import IntensityStatsd TEST_CASE_1 = [ - {"keys": "img", "ops": ["max", "mean"], "key_prefix": "orig", "meta_key": "test_meta"}, + {"keys": "img", "ops": ["max", "mean"], "key_prefix": "orig", "meta_keys": "test_meta"}, {"img": np.array([[[0.0, 1.0], [2.0, 3.0]]]), "test_meta": {"affine": None}}, "test_meta", {"orig_max": 3.0, "orig_mean": 1.5}, From 0a894703099add871a99df64f7cd12a571c199c6 Mon Sep 17 00:00:00 2001 From: Nic Ma Date: Tue, 3 Aug 2021 21:51:34 +0800 Subject: [PATCH 06/16] [DLMED] change to utility Signed-off-by: Nic Ma --- docs/source/transforms.rst | 25 +++++----- monai/transforms/__init__.py | 8 +-- monai/transforms/intensity/array.py | 58 +--------------------- monai/transforms/intensity/dictionary.py | 61 ----------------------- monai/transforms/utility/array.py | 58 +++++++++++++++++++++- monai/transforms/utility/dictionary.py | 62 ++++++++++++++++++++++++ 6 files changed, 138 insertions(+), 134 deletions(-) diff --git a/docs/source/transforms.rst b/docs/source/transforms.rst index 3c3e00c146..6a25c62c49 100644 --- a/docs/source/transforms.rst +++ b/docs/source/transforms.rst @@ -319,12 +319,6 @@ Intensity :members: :special-members: __call__ -`IntensityStats` -"""""""""""""""" - .. autoclass:: IntensityStats - :members: - :special-members: __call__ - IO ^^ @@ -668,6 +662,13 @@ Utility :members: :special-members: __call__ +`IntensityStats` +"""""""""""""""" + .. autoclass:: IntensityStats + :members: + :special-members: __call__ + + Dictionary Transforms --------------------- @@ -917,11 +918,6 @@ Intensity (Dict) :members: :special-members: __call__ -`IntensityStatsd` -""""""""""""""""" -.. autoclass:: IntensityStatsd - :members: - :special-members: __call__ IO (Dict) ^^^^^^^^^ @@ -1277,6 +1273,13 @@ Utility (Dict) :members: :special-members: __call__ +`IntensityStatsd` +""""""""""""""""" +.. autoclass:: IntensityStatsd + :members: + :special-members: __call__ + + Transform Adaptors ------------------ .. automodule:: monai.transforms.adaptors diff --git a/monai/transforms/__init__.py b/monai/transforms/__init__.py index 97734e9009..cf9198dbf5 100644 --- a/monai/transforms/__init__.py +++ b/monai/transforms/__init__.py @@ -83,7 +83,6 @@ GaussianSharpen, GaussianSmooth, GibbsNoise, - IntensityStats, KSpaceSpikeNoise, MaskIntensity, NormalizeIntensity, @@ -121,9 +120,6 @@ GibbsNoised, GibbsNoiseD, GibbsNoiseDict, - IntensityStatsd, - IntensityStatsD, - IntensityStatsDict, KSpaceSpikeNoised, KSpaceSpikeNoiseD, KSpaceSpikeNoiseDict, @@ -332,6 +328,7 @@ EnsureType, FgBgToIndices, Identity, + IntensityStats, LabelToMask, Lambda, MapLabelValue, @@ -394,6 +391,9 @@ Identityd, IdentityD, IdentityDict, + IntensityStatsd, + IntensityStatsD, + IntensityStatsDict, LabelToMaskd, LabelToMaskD, LabelToMaskDict, diff --git a/monai/transforms/intensity/array.py b/monai/transforms/intensity/array.py index 2e3537533d..5e4ba00889 100644 --- a/monai/transforms/intensity/array.py +++ b/monai/transforms/intensity/array.py @@ -14,7 +14,7 @@ """ from collections.abc import Iterable -from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, Union +from typing import Any, Callable, List, Optional, Sequence, Tuple, Union from warnings import warn import numpy as np @@ -64,7 +64,6 @@ "KSpaceSpikeNoise", "RandKSpaceSpikeNoise", "RandCoarseDropout", - "IntensityStats", ] @@ -1627,58 +1626,3 @@ def __call__(self, img: np.ndarray): img[h] = self.fill_value return img - - -class IntensityStats(Transform): - """ - Compute statistics for the intensity values of input image and store into the meta data dictionary. - For example: if `ops=[lambda x: np.mean(x), "max"]` and `key_prefix="orig"`, may generate below stats: - `{"orig_custom_0": 1.5, "orig_max": 3.0}`. - - Args: - ops: expected operations to compute statistics for the intensity. - if a string, will map to the predefined operations, supported: ["mean", "median", "max", "min", "std"] - mapping to `np.nanmean`, `np.nanmedian`, `np.nanmax`, `np.nanmin`, `np.nanstd`. - if a callable function, will execute the function on input image. - key_prefix: the prefix to combine with `ops` name to generate the key to store the results in the - meta data dictionary. if some `ops` are callable functions, will use "{key_prefix}_custom_{index}" - as the key, where index counts from 0. - channel_wise: whether to compute statistics for every channel of input image separately. - if True, return a list of values for every operation, default to False. - - """ - - def __init__(self, ops: Sequence[Union[str, Callable]], key_prefix: str, channel_wise: bool = False) -> None: - self.supported_ops = { - "mean": lambda x: np.nanmean(x), - "median": lambda x: np.nanmedian(x), - "max": lambda x: np.nanmax(x), - "min": lambda x: np.nanmin(x), - "std": lambda x: np.nanstd(x), - } - self.ops = ensure_tuple(ops) - for o in self.ops: - if isinstance(o, str) and o not in self.supported_ops: - raise ValueError(f"unsupported operation: {o}.") - self.key_prefix = key_prefix - self.channel_wise = channel_wise - - def __call__(self, img: np.ndarray, meta_data: Optional[Dict] = None) -> Tuple[np.ndarray, Dict]: - if meta_data is None: - meta_data = {} - - def _compute(op: Callable, img: np.ndarray): - if self.channel_wise: - return [op(c) for c in img] - else: - return op(img) - - custom_index = 0 - for o in self.ops: - if isinstance(o, str): - meta_data[self.key_prefix + "_" + o] = _compute(self.supported_ops[o], img) - elif callable(o): - meta_data[self.key_prefix + "_custom_" + str(custom_index)] = _compute(o, img) - custom_index += 1 - - return img, meta_data diff --git a/monai/transforms/intensity/dictionary.py b/monai/transforms/intensity/dictionary.py index ff9e2800be..af10fa1e7a 100644 --- a/monai/transforms/intensity/dictionary.py +++ b/monai/transforms/intensity/dictionary.py @@ -28,7 +28,6 @@ GaussianSharpen, GaussianSmooth, GibbsNoise, - IntensityStats, KSpaceSpikeNoise, MaskIntensity, NormalizeIntensity, @@ -72,7 +71,6 @@ "RandKSpaceSpikeNoised", "RandHistogramShiftd", "RandCoarseDropoutd", - "IntensityStatsd", "RandGaussianNoiseD", "RandGaussianNoiseDict", "ShiftIntensityD", @@ -123,8 +121,6 @@ "RandRicianNoiseDict", "RandCoarseDropoutD", "RandCoarseDropoutDict", - "IntensityStatsD", - "IntensityStatsDict", ] @@ -1468,62 +1464,6 @@ def __call__(self, data): return d -class IntensityStatsd(MapTransform): - """ - Dictionary-based wrapper of :py:class:`monai.transforms.IntensityStats`. - Compute statistics for the intensity values of input image and store into the meta data dictionary. - For example: if `ops=[lambda x: np.mean(x), "max"]` and `key_prefix="orig"`, may generate below stats: - `{"orig_custom_0": 1.5, "orig_max": 3.0}`. - - Args: - keys: keys of the corresponding items to be transformed. - See also: :py:class:`monai.transforms.compose.MapTransform` - ops: expected operations to compute statistics for the intensity. - if a string, will map to the predefined operations, supported: ["mean", "median", "max", "min", "std"] - mapping to `np.nanmean`, `np.nanmedian`, `np.nanmax`, `np.nanmin`, `np.nanstd`. - if a callable function, will execute the function on input image. - key_prefix: the prefix to combine with `ops` name to generate the key to store the results in the - meta data dictionary. if some `ops` are callable functions, will use "{key_prefix}_custom_{index}" - as the key, where index counts from 0. - channel_wise: whether to compute statistics for every channel of input image separately. - if True, return a list of values for every operation, default to False. - meta_keys: explicitly indicate the key of the corresponding meta data dictionary. - used to store the computed statistics to the meta dict. - for example, for data with key `image`, the metadata by default is in `image_meta_dict`. - the meta data is a dictionary object which contains: filename, original_shape, etc. - it can be a sequence of string, map to the `keys`. - if None, will try to construct meta_keys by `key_{meta_key_postfix}`. - meta_key_postfix: if meta_keys is None, use `key_{postfix}` to to fetch the meta data according - to the key data, default is `meta_dict`, the meta data is a dictionary object. - used to store the computed statistics to the meta dict. - allow_missing_keys: don't raise exception if key is missing. - - """ - - def __init__( - self, - keys: KeysCollection, - ops: Sequence[Union[str, Callable]], - key_prefix: str, - channel_wise: bool = False, - meta_keys: Optional[KeysCollection] = None, - meta_key_postfix: str = "meta_dict", - allow_missing_keys: bool = False, - ) -> None: - super().__init__(keys, allow_missing_keys) - self.stats = IntensityStats(ops=ops, key_prefix=key_prefix, channel_wise=channel_wise) - self.meta_keys = ensure_tuple_rep(None, len(self.keys)) if meta_keys is None else ensure_tuple(meta_keys) - if len(self.keys) != len(self.meta_keys): - raise ValueError("meta_keys should have the same length as keys.") - self.meta_key_postfix = ensure_tuple_rep(meta_key_postfix, len(self.keys)) - - def __call__(self, data) -> Dict[Hashable, np.ndarray]: - d = dict(data) - for key, meta_key, meta_key_postfix in self.key_iterator(d, self.meta_keys, self.meta_key_postfix): - meta_key = meta_key or f"{key}_{meta_key_postfix}" - d[key], d[meta_key] = self.stats(d[key], d.get(meta_key)) - return d - RandGaussianNoiseD = RandGaussianNoiseDict = RandGaussianNoised RandRicianNoiseD = RandRicianNoiseDict = RandRicianNoised @@ -1551,4 +1491,3 @@ def __call__(self, data) -> Dict[Hashable, np.ndarray]: KSpaceSpikeNoiseD = KSpaceSpikeNoiseDict = KSpaceSpikeNoised RandKSpaceSpikeNoiseD = RandKSpaceSpikeNoiseDict = RandKSpaceSpikeNoised RandCoarseDropoutD = RandCoarseDropoutDict = RandCoarseDropoutd -IntensityStatsD = IntensityStatsDict = IntensityStatsd diff --git a/monai/transforms/utility/array.py b/monai/transforms/utility/array.py index 4e0141652f..ffa9e3d427 100644 --- a/monai/transforms/utility/array.py +++ b/monai/transforms/utility/array.py @@ -17,7 +17,7 @@ import sys import time import warnings -from typing import Callable, List, Mapping, Optional, Sequence, Tuple, Union +from typing import Callable, Dict, List, Mapping, Optional, Sequence, Tuple, Union import numpy as np import torch @@ -66,6 +66,7 @@ "AddExtremePointsChannel", "TorchVision", "MapLabelValue", + "IntensityStats", ] @@ -938,3 +939,58 @@ def __call__(self, img: np.ndarray): np.place(out_flat, img_flat == o, t) return out_flat.reshape(img.shape) + + +class IntensityStats(Transform): + """ + Compute statistics for the intensity values of input image and store into the meta data dictionary. + For example: if `ops=[lambda x: np.mean(x), "max"]` and `key_prefix="orig"`, may generate below stats: + `{"orig_custom_0": 1.5, "orig_max": 3.0}`. + + Args: + ops: expected operations to compute statistics for the intensity. + if a string, will map to the predefined operations, supported: ["mean", "median", "max", "min", "std"] + mapping to `np.nanmean`, `np.nanmedian`, `np.nanmax`, `np.nanmin`, `np.nanstd`. + if a callable function, will execute the function on input image. + key_prefix: the prefix to combine with `ops` name to generate the key to store the results in the + meta data dictionary. if some `ops` are callable functions, will use "{key_prefix}_custom_{index}" + as the key, where index counts from 0. + channel_wise: whether to compute statistics for every channel of input image separately. + if True, return a list of values for every operation, default to False. + + """ + + def __init__(self, ops: Sequence[Union[str, Callable]], key_prefix: str, channel_wise: bool = False) -> None: + self.supported_ops = { + "mean": lambda x: np.nanmean(x), + "median": lambda x: np.nanmedian(x), + "max": lambda x: np.nanmax(x), + "min": lambda x: np.nanmin(x), + "std": lambda x: np.nanstd(x), + } + self.ops = ensure_tuple(ops) + for o in self.ops: + if isinstance(o, str) and o not in self.supported_ops: + raise ValueError(f"unsupported operation: {o}.") + self.key_prefix = key_prefix + self.channel_wise = channel_wise + + def __call__(self, img: np.ndarray, meta_data: Optional[Dict] = None) -> Tuple[np.ndarray, Dict]: + if meta_data is None: + meta_data = {} + + def _compute(op: Callable, img: np.ndarray): + if self.channel_wise: + return [op(c) for c in img] + else: + return op(img) + + custom_index = 0 + for o in self.ops: + if isinstance(o, str): + meta_data[self.key_prefix + "_" + o] = _compute(self.supported_ops[o], img) + elif callable(o): + meta_data[self.key_prefix + "_custom_" + str(custom_index)] = _compute(o, img) + custom_index += 1 + + return img, meta_data diff --git a/monai/transforms/utility/dictionary.py b/monai/transforms/utility/dictionary.py index 75be9685c4..507f1dd3e9 100644 --- a/monai/transforms/utility/dictionary.py +++ b/monai/transforms/utility/dictionary.py @@ -39,6 +39,7 @@ EnsureType, FgBgToIndices, Identity, + IntensityStats, LabelToMask, Lambda, MapLabelValue, @@ -101,6 +102,9 @@ "IdentityD", "IdentityDict", "Identityd", + "IntensityStatsd", + "IntensityStatsD", + "IntensityStatsDict", "LabelToMaskD", "LabelToMaskDict", "LabelToMaskd", @@ -1282,6 +1286,63 @@ def __call__(self, data: Mapping[Hashable, np.ndarray]) -> Dict[Hashable, np.nda return d +class IntensityStatsd(MapTransform): + """ + Dictionary-based wrapper of :py:class:`monai.transforms.IntensityStats`. + Compute statistics for the intensity values of input image and store into the meta data dictionary. + For example: if `ops=[lambda x: np.mean(x), "max"]` and `key_prefix="orig"`, may generate below stats: + `{"orig_custom_0": 1.5, "orig_max": 3.0}`. + + Args: + keys: keys of the corresponding items to be transformed. + See also: :py:class:`monai.transforms.compose.MapTransform` + ops: expected operations to compute statistics for the intensity. + if a string, will map to the predefined operations, supported: ["mean", "median", "max", "min", "std"] + mapping to `np.nanmean`, `np.nanmedian`, `np.nanmax`, `np.nanmin`, `np.nanstd`. + if a callable function, will execute the function on input image. + key_prefix: the prefix to combine with `ops` name to generate the key to store the results in the + meta data dictionary. if some `ops` are callable functions, will use "{key_prefix}_custom_{index}" + as the key, where index counts from 0. + channel_wise: whether to compute statistics for every channel of input image separately. + if True, return a list of values for every operation, default to False. + meta_keys: explicitly indicate the key of the corresponding meta data dictionary. + used to store the computed statistics to the meta dict. + for example, for data with key `image`, the metadata by default is in `image_meta_dict`. + the meta data is a dictionary object which contains: filename, original_shape, etc. + it can be a sequence of string, map to the `keys`. + if None, will try to construct meta_keys by `key_{meta_key_postfix}`. + meta_key_postfix: if meta_keys is None, use `key_{postfix}` to to fetch the meta data according + to the key data, default is `meta_dict`, the meta data is a dictionary object. + used to store the computed statistics to the meta dict. + allow_missing_keys: don't raise exception if key is missing. + + """ + + def __init__( + self, + keys: KeysCollection, + ops: Sequence[Union[str, Callable]], + key_prefix: str, + channel_wise: bool = False, + meta_keys: Optional[KeysCollection] = None, + meta_key_postfix: str = "meta_dict", + allow_missing_keys: bool = False, + ) -> None: + super().__init__(keys, allow_missing_keys) + self.stats = IntensityStats(ops=ops, key_prefix=key_prefix, channel_wise=channel_wise) + self.meta_keys = ensure_tuple_rep(None, len(self.keys)) if meta_keys is None else ensure_tuple(meta_keys) + if len(self.keys) != len(self.meta_keys): + raise ValueError("meta_keys should have the same length as keys.") + self.meta_key_postfix = ensure_tuple_rep(meta_key_postfix, len(self.keys)) + + def __call__(self, data) -> Dict[Hashable, np.ndarray]: + d = dict(data) + for key, meta_key, meta_key_postfix in self.key_iterator(d, self.meta_keys, self.meta_key_postfix): + meta_key = meta_key or f"{key}_{meta_key_postfix}" + d[key], d[meta_key] = self.stats(d[key], d.get(meta_key)) + return d + + IdentityD = IdentityDict = Identityd AsChannelFirstD = AsChannelFirstDict = AsChannelFirstd AsChannelLastD = AsChannelLastDict = AsChannelLastd @@ -1316,3 +1377,4 @@ def __call__(self, data: Mapping[Hashable, np.ndarray]) -> Dict[Hashable, np.nda RandTorchVisionD = RandTorchVisionDict = RandTorchVisiond RandLambdaD = RandLambdaDict = RandLambdad MapLabelValueD = MapLabelValueDict = MapLabelValued +IntensityStatsD = IntensityStatsDict = IntensityStatsd From ec0cca40e78cd6a279a5e449782cad5b571c72e4 Mon Sep 17 00:00:00 2001 From: Nic Ma Date: Tue, 3 Aug 2021 22:18:54 +0800 Subject: [PATCH 07/16] [DLMED] adjust to look_up_option Signed-off-by: Nic Ma --- monai/transforms/intensity/dictionary.py | 1 - monai/transforms/utility/array.py | 7 ++----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/monai/transforms/intensity/dictionary.py b/monai/transforms/intensity/dictionary.py index af10fa1e7a..01d6891816 100644 --- a/monai/transforms/intensity/dictionary.py +++ b/monai/transforms/intensity/dictionary.py @@ -1464,7 +1464,6 @@ def __call__(self, data): return d - RandGaussianNoiseD = RandGaussianNoiseDict = RandGaussianNoised RandRicianNoiseD = RandRicianNoiseDict = RandRicianNoised ShiftIntensityD = ShiftIntensityDict = ShiftIntensityd diff --git a/monai/transforms/utility/array.py b/monai/transforms/utility/array.py index ffa9e3d427..302c967e53 100644 --- a/monai/transforms/utility/array.py +++ b/monai/transforms/utility/array.py @@ -32,7 +32,7 @@ map_binary_to_indices, map_classes_to_indices, ) -from monai.utils import ensure_tuple, issequenceiterable, min_version, optional_import +from monai.utils import ensure_tuple, issequenceiterable, look_up_option, min_version, optional_import PILImageImage, has_pil = optional_import("PIL.Image", name="Image") pil_image_fromarray, _ = optional_import("PIL.Image", name="fromarray") @@ -968,10 +968,7 @@ def __init__(self, ops: Sequence[Union[str, Callable]], key_prefix: str, channel "min": lambda x: np.nanmin(x), "std": lambda x: np.nanstd(x), } - self.ops = ensure_tuple(ops) - for o in self.ops: - if isinstance(o, str) and o not in self.supported_ops: - raise ValueError(f"unsupported operation: {o}.") + self.ops = [o if callable(o) else look_up_option(o, self.supported_ops.keys()) for o in ensure_tuple(ops)] self.key_prefix = key_prefix self.channel_wise = channel_wise From 1d2f634317fd781fb1e68cba0763979c8c76d768 Mon Sep 17 00:00:00 2001 From: Nic Ma Date: Tue, 3 Aug 2021 22:37:30 +0800 Subject: [PATCH 08/16] [DLMED] add multi-processing test Signed-off-by: Nic Ma --- tests/test_intensity_statsd.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/test_intensity_statsd.py b/tests/test_intensity_statsd.py index 8f9841d243..ccb5582f26 100644 --- a/tests/test_intensity_statsd.py +++ b/tests/test_intensity_statsd.py @@ -10,10 +10,12 @@ # limitations under the License. import unittest +import sys import numpy as np from parameterized import parameterized +from monai.data import DataLoader, Dataset from monai.transforms import IntensityStatsd TEST_CASE_1 = [ @@ -53,6 +55,19 @@ def test_value(self, input_param, data, meta_key, expected): self.assertTrue(k in meta) np.testing.assert_allclose(v, meta[k], atol=1e-3) + def test_dataloader(self): + dataset = Dataset( + data=[{"img": np.array([[[0.0, 1.0], [2.0, 3.0]]])}, {"img": np.array([[[0.0, 1.0], [2.0, 3.0]]])}], + transform=IntensityStatsd(keys="img", ops=["max", "mean"], key_prefix="orig") + ) + # set num workers = 0 for mac / win + num_workers = 2 if sys.platform == "linux" else 0 + dataloader = DataLoader(dataset=dataset, num_workers=num_workers, batch_size=2) + for d in dataloader: + meta = d["img_meta_dict"] + np.testing.assert_allclose(meta["orig_max"], [3.0, 3.0], atol=1e-3) + np.testing.assert_allclose(meta["orig_mean"], [1.5, 1.5], atol=1e-3) + if __name__ == "__main__": unittest.main() From f65c3267bef925df71bb6b91c51d054be624a3ab Mon Sep 17 00:00:00 2001 From: Nic Ma Date: Tue, 3 Aug 2021 23:14:34 +0800 Subject: [PATCH 09/16] [DLMED] add mask option Signed-off-by: Nic Ma --- monai/transforms/utility/array.py | 22 +++++++++++++++++++++- monai/transforms/utility/dictionary.py | 15 +++++++++++++-- tests/test_intensity_stats.py | 18 ++++++++++++++++-- tests/test_intensity_statsd.py | 7 +++++++ 4 files changed, 57 insertions(+), 5 deletions(-) diff --git a/monai/transforms/utility/array.py b/monai/transforms/utility/array.py index 302c967e53..d950cc5aec 100644 --- a/monai/transforms/utility/array.py +++ b/monai/transforms/utility/array.py @@ -972,9 +972,29 @@ def __init__(self, ops: Sequence[Union[str, Callable]], key_prefix: str, channel self.key_prefix = key_prefix self.channel_wise = channel_wise - def __call__(self, img: np.ndarray, meta_data: Optional[Dict] = None) -> Tuple[np.ndarray, Dict]: + def __call__( + self, + img: np.ndarray, + meta_data: Optional[Dict] = None, + mask: Optional[np.ndarray] = None, + ) -> Tuple[np.ndarray, Dict]: + """ + Compute statistics for the intensity of input image. + + Args: + img: input image to compute intensity stats. + meta_data: meta data dictionary to store the statistics data, if None, will create an empty dictionary. + mask: if not None, mask the image to extract only the interested area to compute statistics. + mask must have the same shape as input `img`. + + """ if meta_data is None: meta_data = {} + + if mask is not None: + if mask.shape != img.shape or mask.dtype != bool: + raise TypeError("mask must be bool array with the same shape as input `img`.") + img = img[mask] def _compute(op: Callable, img: np.ndarray): if self.channel_wise: diff --git a/monai/transforms/utility/dictionary.py b/monai/transforms/utility/dictionary.py index 507f1dd3e9..fb9963601d 100644 --- a/monai/transforms/utility/dictionary.py +++ b/monai/transforms/utility/dictionary.py @@ -1303,6 +1303,9 @@ class IntensityStatsd(MapTransform): key_prefix: the prefix to combine with `ops` name to generate the key to store the results in the meta data dictionary. if some `ops` are callable functions, will use "{key_prefix}_custom_{index}" as the key, where index counts from 0. + mask_keys: if not None, specify the mask array for the image to extract only the interested area to compute + statistics, mask must have the same shape as the image. + it should be a sequence of strings or None, map to the `keys`. channel_wise: whether to compute statistics for every channel of input image separately. if True, return a list of values for every operation, default to False. meta_keys: explicitly indicate the key of the corresponding meta data dictionary. @@ -1323,6 +1326,7 @@ def __init__( keys: KeysCollection, ops: Sequence[Union[str, Callable]], key_prefix: str, + mask_keys: Optional[KeysCollection] = None, channel_wise: bool = False, meta_keys: Optional[KeysCollection] = None, meta_key_postfix: str = "meta_dict", @@ -1330,6 +1334,7 @@ def __init__( ) -> None: super().__init__(keys, allow_missing_keys) self.stats = IntensityStats(ops=ops, key_prefix=key_prefix, channel_wise=channel_wise) + self.mask_keys = ensure_tuple_rep(None, len(self.keys)) if mask_keys is None else ensure_tuple(mask_keys) self.meta_keys = ensure_tuple_rep(None, len(self.keys)) if meta_keys is None else ensure_tuple(meta_keys) if len(self.keys) != len(self.meta_keys): raise ValueError("meta_keys should have the same length as keys.") @@ -1337,9 +1342,15 @@ def __init__( def __call__(self, data) -> Dict[Hashable, np.ndarray]: d = dict(data) - for key, meta_key, meta_key_postfix in self.key_iterator(d, self.meta_keys, self.meta_key_postfix): + for key, mask_key, meta_key, meta_key_postfix in self.key_iterator( + d, self.mask_keys, self.meta_keys, self.meta_key_postfix + ): meta_key = meta_key or f"{key}_{meta_key_postfix}" - d[key], d[meta_key] = self.stats(d[key], d.get(meta_key)) + d[key], d[meta_key] = self.stats( + img=d[key], + meta_data=d.get(meta_key), + mask=d.get(mask_key) if mask_key is not None else None, + ) return d diff --git a/tests/test_intensity_stats.py b/tests/test_intensity_stats.py index ec9b6dfaed..059271e442 100644 --- a/tests/test_intensity_stats.py +++ b/tests/test_intensity_stats.py @@ -44,15 +44,29 @@ {"orig_max": [3.0, 7.0], "orig_mean": [1.5, 5.5]}, ] +TEST_CASE_5 = [ + {"ops": ["max", "mean"], "key_prefix": "orig"}, + np.array([[[0.0, 1.0], [2.0, 3.0]]]), + {"affine": None}, + {"orig_max": 3.0, "orig_mean": 1.5}, +] + class TestIntensityStats(unittest.TestCase): - @parameterized.expand([TEST_CASE_1, TEST_CASE_2, TEST_CASE_3, TEST_CASE_4]) + @parameterized.expand([TEST_CASE_1, TEST_CASE_2, TEST_CASE_3, TEST_CASE_4, TEST_CASE_5]) def test_value(self, input_param, img, meta_dict, expected): - img, meta_dict = IntensityStats(**input_param)(img, meta_dict) + _, meta_dict = IntensityStats(**input_param)(img, meta_dict) for k, v in expected.items(): self.assertTrue(k in meta_dict) np.testing.assert_allclose(v, meta_dict[k], atol=1e-3) + def test_mask(self): + img = np.array([[[0.0, 1.0], [2.0, 3.0]]]) + mask = np.array([[[1, 0], [1, 0]]], dtype=bool) + img, meta_dict = IntensityStats(ops=["max", "mean"], key_prefix="orig")(img, mask=mask) + np.testing.assert_allclose(meta_dict["orig_max"], 2.0, atol=1e-3) + np.testing.assert_allclose(meta_dict["orig_mean"], 1.0, atol=1e-3) + if __name__ == "__main__": unittest.main() diff --git a/tests/test_intensity_statsd.py b/tests/test_intensity_statsd.py index ccb5582f26..a94394e74b 100644 --- a/tests/test_intensity_statsd.py +++ b/tests/test_intensity_statsd.py @@ -68,6 +68,13 @@ def test_dataloader(self): np.testing.assert_allclose(meta["orig_max"], [3.0, 3.0], atol=1e-3) np.testing.assert_allclose(meta["orig_mean"], [1.5, 1.5], atol=1e-3) + def test_mask(self): + data = {"img": np.array([[[0.0, 1.0], [2.0, 3.0]]]), "img_mask": np.array([[[1, 0], [1, 0]]], dtype=bool)} + stats = IntensityStatsd(keys="img", ops=["max", "mean"], mask_keys="img_mask", key_prefix="orig") + meta = stats(data)["img_meta_dict"] + np.testing.assert_allclose(meta["orig_max"], 2.0, atol=1e-3) + np.testing.assert_allclose(meta["orig_mean"], 1.0, atol=1e-3) + if __name__ == "__main__": unittest.main() From 445057563eacd7e39c499819bc4025b1cc393a71 Mon Sep 17 00:00:00 2001 From: Nic Ma Date: Tue, 3 Aug 2021 23:18:47 +0800 Subject: [PATCH 10/16] [DLMED] fix flake8 Signed-off-by: Nic Ma --- monai/transforms/intensity/array.py | 2 +- monai/transforms/intensity/dictionary.py | 2 +- monai/transforms/utility/array.py | 2 +- tests/test_intensity_statsd.py | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/monai/transforms/intensity/array.py b/monai/transforms/intensity/array.py index 5e4ba00889..15498f7657 100644 --- a/monai/transforms/intensity/array.py +++ b/monai/transforms/intensity/array.py @@ -14,7 +14,7 @@ """ from collections.abc import Iterable -from typing import Any, Callable, List, Optional, Sequence, Tuple, Union +from typing import Any, List, Optional, Sequence, Tuple, Union from warnings import warn import numpy as np diff --git a/monai/transforms/intensity/dictionary.py b/monai/transforms/intensity/dictionary.py index 01d6891816..e43aa1e2b3 100644 --- a/monai/transforms/intensity/dictionary.py +++ b/monai/transforms/intensity/dictionary.py @@ -16,7 +16,7 @@ """ from collections.abc import Iterable -from typing import Any, Callable, Dict, Hashable, List, Mapping, Optional, Sequence, Tuple, Union +from typing import Any, Dict, Hashable, List, Mapping, Optional, Sequence, Tuple, Union import numpy as np import torch diff --git a/monai/transforms/utility/array.py b/monai/transforms/utility/array.py index d950cc5aec..579948cb38 100644 --- a/monai/transforms/utility/array.py +++ b/monai/transforms/utility/array.py @@ -990,7 +990,7 @@ def __call__( """ if meta_data is None: meta_data = {} - + if mask is not None: if mask.shape != img.shape or mask.dtype != bool: raise TypeError("mask must be bool array with the same shape as input `img`.") diff --git a/tests/test_intensity_statsd.py b/tests/test_intensity_statsd.py index a94394e74b..75d3a392bc 100644 --- a/tests/test_intensity_statsd.py +++ b/tests/test_intensity_statsd.py @@ -9,8 +9,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -import unittest import sys +import unittest import numpy as np from parameterized import parameterized @@ -58,7 +58,7 @@ def test_value(self, input_param, data, meta_key, expected): def test_dataloader(self): dataset = Dataset( data=[{"img": np.array([[[0.0, 1.0], [2.0, 3.0]]])}, {"img": np.array([[[0.0, 1.0], [2.0, 3.0]]])}], - transform=IntensityStatsd(keys="img", ops=["max", "mean"], key_prefix="orig") + transform=IntensityStatsd(keys="img", ops=["max", "mean"], key_prefix="orig"), ) # set num workers = 0 for mac / win num_workers = 2 if sys.platform == "linux" else 0 From 341b8195da09aec48fbd2ef1136ec9b20025e31a Mon Sep 17 00:00:00 2001 From: Nic Ma Date: Tue, 3 Aug 2021 23:39:29 +0800 Subject: [PATCH 11/16] [DLMED] fix typo Signed-off-by: Nic Ma --- monai/transforms/utility/array.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/monai/transforms/utility/array.py b/monai/transforms/utility/array.py index 579948cb38..14bb647b4d 100644 --- a/monai/transforms/utility/array.py +++ b/monai/transforms/utility/array.py @@ -991,23 +991,24 @@ def __call__( if meta_data is None: meta_data = {} + img_: np.ndarray = img if mask is not None: if mask.shape != img.shape or mask.dtype != bool: raise TypeError("mask must be bool array with the same shape as input `img`.") - img = img[mask] + img_ = img[mask] - def _compute(op: Callable, img: np.ndarray): + def _compute(op: Callable, data: np.ndarray): if self.channel_wise: - return [op(c) for c in img] + return [op(c) for c in data] else: - return op(img) + return op(data) custom_index = 0 for o in self.ops: if isinstance(o, str): - meta_data[self.key_prefix + "_" + o] = _compute(self.supported_ops[o], img) + meta_data[self.key_prefix + "_" + o] = _compute(self.supported_ops[o], img_) elif callable(o): - meta_data[self.key_prefix + "_custom_" + str(custom_index)] = _compute(o, img) + meta_data[self.key_prefix + "_custom_" + str(custom_index)] = _compute(o, img_) custom_index += 1 return img, meta_data From 9463e08825367ff04dbb8d6c394d2d9ac6271e40 Mon Sep 17 00:00:00 2001 From: Nic Ma Date: Wed, 4 Aug 2021 06:38:37 +0800 Subject: [PATCH 12/16] [DLMED] fix pickle issue Signed-off-by: Nic Ma --- monai/transforms/utility/array.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/monai/transforms/utility/array.py b/monai/transforms/utility/array.py index 14bb647b4d..c4e33a417b 100644 --- a/monai/transforms/utility/array.py +++ b/monai/transforms/utility/array.py @@ -961,14 +961,7 @@ class IntensityStats(Transform): """ def __init__(self, ops: Sequence[Union[str, Callable]], key_prefix: str, channel_wise: bool = False) -> None: - self.supported_ops = { - "mean": lambda x: np.nanmean(x), - "median": lambda x: np.nanmedian(x), - "max": lambda x: np.nanmax(x), - "min": lambda x: np.nanmin(x), - "std": lambda x: np.nanstd(x), - } - self.ops = [o if callable(o) else look_up_option(o, self.supported_ops.keys()) for o in ensure_tuple(ops)] + self.ops = ops self.key_prefix = key_prefix self.channel_wise = channel_wise @@ -997,6 +990,14 @@ def __call__( raise TypeError("mask must be bool array with the same shape as input `img`.") img_ = img[mask] + supported_ops = { + "mean": lambda x: np.nanmean(x), + "median": lambda x: np.nanmedian(x), + "max": lambda x: np.nanmax(x), + "min": lambda x: np.nanmin(x), + "std": lambda x: np.nanstd(x), + } + def _compute(op: Callable, data: np.ndarray): if self.channel_wise: return [op(c) for c in data] @@ -1006,7 +1007,8 @@ def _compute(op: Callable, data: np.ndarray): custom_index = 0 for o in self.ops: if isinstance(o, str): - meta_data[self.key_prefix + "_" + o] = _compute(self.supported_ops[o], img_) + o = look_up_option(o, supported_ops.keys()) + meta_data[self.key_prefix + "_" + o] = _compute(supported_ops[o], img_) elif callable(o): meta_data[self.key_prefix + "_custom_" + str(custom_index)] = _compute(o, img_) custom_index += 1 From 5546dd0e09588a2e5887780d60f4ac281a4fda1c Mon Sep 17 00:00:00 2001 From: Nic Ma Date: Wed, 4 Aug 2021 06:46:16 +0800 Subject: [PATCH 13/16] [DLMED] enhance error message Signed-off-by: Nic Ma --- monai/transforms/utility/array.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/monai/transforms/utility/array.py b/monai/transforms/utility/array.py index c4e33a417b..8e5eba7ce0 100644 --- a/monai/transforms/utility/array.py +++ b/monai/transforms/utility/array.py @@ -1012,5 +1012,7 @@ def _compute(op: Callable, data: np.ndarray): elif callable(o): meta_data[self.key_prefix + "_custom_" + str(custom_index)] = _compute(o, img_) custom_index += 1 + else: + raise ValueError("ops must be key string for predefined operations or callable function.") return img, meta_data From 228dbadbc381fe09cbfd068f022830d33ec790a0 Mon Sep 17 00:00:00 2001 From: Nic Ma Date: Wed, 4 Aug 2021 06:59:00 +0800 Subject: [PATCH 14/16] [DLMED] add pickle test Signed-off-by: Nic Ma --- monai/transforms/utility/array.py | 3 ++- tests/test_intensity_statsd.py | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/monai/transforms/utility/array.py b/monai/transforms/utility/array.py index 8e5eba7ce0..30f3d3583a 100644 --- a/monai/transforms/utility/array.py +++ b/monai/transforms/utility/array.py @@ -961,7 +961,7 @@ class IntensityStats(Transform): """ def __init__(self, ops: Sequence[Union[str, Callable]], key_prefix: str, channel_wise: bool = False) -> None: - self.ops = ops + self.ops = ensure_tuple(ops) self.key_prefix = key_prefix self.channel_wise = channel_wise @@ -976,6 +976,7 @@ def __call__( Args: img: input image to compute intensity stats. + meta_data: meta data dictionary to store the statistics data, if None, will create an empty dictionary. mask: if not None, mask the image to extract only the interested area to compute statistics. mask must have the same shape as input `img`. diff --git a/tests/test_intensity_statsd.py b/tests/test_intensity_statsd.py index 75d3a392bc..80d5f1932c 100644 --- a/tests/test_intensity_statsd.py +++ b/tests/test_intensity_statsd.py @@ -13,6 +13,7 @@ import unittest import numpy as np +import torch.multiprocessing as mp from parameterized import parameterized from monai.data import DataLoader, Dataset @@ -63,6 +64,8 @@ def test_dataloader(self): # set num workers = 0 for mac / win num_workers = 2 if sys.platform == "linux" else 0 dataloader = DataLoader(dataset=dataset, num_workers=num_workers, batch_size=2) + mp.set_start_method('spawn', force=True) + for d in dataloader: meta = d["img_meta_dict"] np.testing.assert_allclose(meta["orig_max"], [3.0, 3.0], atol=1e-3) From 91097dad99d3915430f7f8a591fe18f9f7711095 Mon Sep 17 00:00:00 2001 From: monai-bot Date: Tue, 3 Aug 2021 23:05:49 +0000 Subject: [PATCH 15/16] [MONAI] python code formatting Signed-off-by: monai-bot --- monai/transforms/intensity/array.py | 4 +++- tests/test_intensity_statsd.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/monai/transforms/intensity/array.py b/monai/transforms/intensity/array.py index 15498f7657..f293213927 100644 --- a/monai/transforms/intensity/array.py +++ b/monai/transforms/intensity/array.py @@ -1531,7 +1531,9 @@ def _randomize(self, img: torch.Tensor, intensity_range: Sequence[Sequence[float if isinstance(intensity_range[0], Sequence): self.sampled_k_intensity = [self.R.uniform(p[0], p[1]) for p in intensity_range] else: - self.sampled_k_intensity = [self.R.uniform(intensity_range[0], intensity_range[1])] * len(img) # type: ignore + self.sampled_k_intensity = [self.R.uniform(intensity_range[0], intensity_range[1])] * len( + img + ) # type: ignore def _make_sequence(self, x: torch.Tensor) -> Sequence[Sequence[float]]: """ diff --git a/tests/test_intensity_statsd.py b/tests/test_intensity_statsd.py index 80d5f1932c..4ba990a378 100644 --- a/tests/test_intensity_statsd.py +++ b/tests/test_intensity_statsd.py @@ -64,7 +64,7 @@ def test_dataloader(self): # set num workers = 0 for mac / win num_workers = 2 if sys.platform == "linux" else 0 dataloader = DataLoader(dataset=dataset, num_workers=num_workers, batch_size=2) - mp.set_start_method('spawn', force=True) + mp.set_start_method("spawn", force=True) for d in dataloader: meta = d["img_meta_dict"] From e0743bccb19d80cfa80d6565c12434d939d0736e Mon Sep 17 00:00:00 2001 From: Nic Ma Date: Wed, 4 Aug 2021 07:22:02 +0800 Subject: [PATCH 16/16] [DLMED] enhance pickle test Signed-off-by: Nic Ma --- monai/transforms/intensity/array.py | 6 ++---- monai/transforms/utility/array.py | 1 - tests/test_intensity_statsd.py | 3 +++ 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/monai/transforms/intensity/array.py b/monai/transforms/intensity/array.py index f293213927..14b3e54459 100644 --- a/monai/transforms/intensity/array.py +++ b/monai/transforms/intensity/array.py @@ -1465,7 +1465,7 @@ def __init__( self.intensity_range = intensity_range self.channel_wise = channel_wise self.as_tensor_output = as_tensor_output - self.sampled_k_intensity: List[float] = [] + self.sampled_k_intensity: List = [] self.sampled_locs: List[Tuple] = [] if intensity_range is not None: @@ -1531,9 +1531,7 @@ def _randomize(self, img: torch.Tensor, intensity_range: Sequence[Sequence[float if isinstance(intensity_range[0], Sequence): self.sampled_k_intensity = [self.R.uniform(p[0], p[1]) for p in intensity_range] else: - self.sampled_k_intensity = [self.R.uniform(intensity_range[0], intensity_range[1])] * len( - img - ) # type: ignore + self.sampled_k_intensity = [self.R.uniform(intensity_range[0], intensity_range[1])] * len(img) def _make_sequence(self, x: torch.Tensor) -> Sequence[Sequence[float]]: """ diff --git a/monai/transforms/utility/array.py b/monai/transforms/utility/array.py index 30f3d3583a..3de2408abd 100644 --- a/monai/transforms/utility/array.py +++ b/monai/transforms/utility/array.py @@ -976,7 +976,6 @@ def __call__( Args: img: input image to compute intensity stats. - meta_data: meta data dictionary to store the statistics data, if None, will create an empty dictionary. mask: if not None, mask the image to extract only the interested area to compute statistics. mask must have the same shape as input `img`. diff --git a/tests/test_intensity_statsd.py b/tests/test_intensity_statsd.py index 4ba990a378..8c8bc8795a 100644 --- a/tests/test_intensity_statsd.py +++ b/tests/test_intensity_statsd.py @@ -64,12 +64,15 @@ def test_dataloader(self): # set num workers = 0 for mac / win num_workers = 2 if sys.platform == "linux" else 0 dataloader = DataLoader(dataset=dataset, num_workers=num_workers, batch_size=2) + orig_method = mp.get_start_method() mp.set_start_method("spawn", force=True) for d in dataloader: meta = d["img_meta_dict"] np.testing.assert_allclose(meta["orig_max"], [3.0, 3.0], atol=1e-3) np.testing.assert_allclose(meta["orig_mean"], [1.5, 1.5], atol=1e-3) + # restore the mp method + mp.set_start_method(orig_method, force=True) def test_mask(self): data = {"img": np.array([[[0.0, 1.0], [2.0, 3.0]]]), "img_mask": np.array([[[1, 0], [1, 0]]], dtype=bool)}