From 2b2a31e3c82486ecee449041060fc62abf258ed2 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Thu, 26 May 2022 01:34:17 +0000 Subject: [PATCH 01/23] Implement foreground mask Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- monai/transforms/intensity/array.py | 78 +++++++++++++++++++++++++---- 1 file changed, 68 insertions(+), 10 deletions(-) diff --git a/monai/transforms/intensity/array.py b/monai/transforms/intensity/array.py index 06b8cfa108..f5d35a226e 100644 --- a/monai/transforms/intensity/array.py +++ b/monai/transforms/intensity/array.py @@ -16,10 +16,11 @@ from abc import abstractmethod from collections.abc import Iterable from functools import partial -from typing import Any, Callable, List, Optional, Sequence, Tuple, Union +from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, Union from warnings import warn import numpy as np +import skimage import torch from monai.config import DtypeLike @@ -29,17 +30,10 @@ from monai.transforms.transform import RandomizableTransform, Transform from monai.transforms.utils import Fourier, equalize_hist, is_positive, rescale_array from monai.transforms.utils_pytorch_numpy_unification import clip, percentile, where -from monai.utils import ( - convert_data_type, - convert_to_dst_type, - ensure_tuple, - ensure_tuple_rep, - ensure_tuple_size, - fall_back_tuple, -) from monai.utils.deprecate_utils import deprecated_arg from monai.utils.enums import TransformBackends -from monai.utils.type_conversion import convert_to_tensor, get_equivalent_dtype +from monai.utils.misc import ensure_tuple, ensure_tuple_rep, ensure_tuple_size, fall_back_tuple +from monai.utils.type_conversion import convert_data_type, convert_to_dst_type, convert_to_tensor, get_equivalent_dtype __all__ = [ "RandGaussianNoise", @@ -2161,3 +2155,67 @@ def __call__(self, img: torch.Tensor) -> torch.Tensor: img = IntensityRemap(self.kernel_size, self.R.choice([-self.slope, self.slope]))(img) return img + + +class ForegroundMask(Transform): + def __init__( + self, + threshold: Union[Dict, Callable, str, float] = "otsu", + hsv_threshold: Optional[Union[Dict, Callable, str, float]] = None, + high_value_foreground: bool = False, + ) -> None: + self.thresholds = {} + if isinstance(threshold, dict): + for mode, th in threshold.items(): + self._set_threshold(th, mode) + else: + self._set_threshold(threshold, "R") + self._set_threshold(threshold, "G") + self._set_threshold(threshold, "B") + if hsv_threshold is not None: + if isinstance(hsv_threshold, dict): + for mode, th in threshold.items(): + self._set_threshold(th, mode) + else: + self._set_threshold(hsv_threshold, "H") + self._set_threshold(hsv_threshold, "S") + self._set_threshold(hsv_threshold, "V") + + self.high_value_foreground = high_value_foreground + + def _set_threshold(self, threshold, mode): + if callable(threshold): + self.thresholds[mode] = threshold + elif isinstance(threshold, str): + self.thresholds[mode] = getattr(skimage.filters, "threshold_" + threshold.lower()) + elif isinstance(threshold, float): + self.thresholds[mode] = threshold + else: + 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) + if callable(threshold): + return threshold(image) + return threshold + + def __call__(self, img_rgb: NdarrayOrTensor): + if self.high_value_foreground: + img_rgb = skimage.util.invert(img_rgb.invert) + + foreground = np.zeros_like(img_rgb[:1]) + for img, mode in zip(img_rgb, "RGB"): + threshold = self._get_threshold(img, mode) + if threshold: + foreground |= img < threshold + + if set(list("HSV")) & set(self.thresholds.keys()): + img_hsv = skimage.color.rgb2hsv(img_rgb, channel_axis=0) + for img, mode in zip(img_hsv, "HSV"): + threshold = self._get_threshold(img, mode) + if threshold: + foreground |= img < threshold + + return foreground From 381ac738b24a1b5a436af95e7af240eaec260f2b Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Thu, 26 May 2022 01:34:36 +0000 Subject: [PATCH 02/23] Add unittests for foreground mask Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- tests/test_foreground_mask.py | 43 +++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 tests/test_foreground_mask.py diff --git a/tests/test_foreground_mask.py b/tests/test_foreground_mask.py new file mode 100644 index 0000000000..6d1b543098 --- /dev/null +++ b/tests/test_foreground_mask.py @@ -0,0 +1,43 @@ +# 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. + +import unittest + +import numpy as np +from parameterized import parameterized + +from monai.transforms.intensity.array import ForegroundMask +from monai.utils import set_determinism + +set_determinism(1234) + +A = np.random.randint(51, 128, (3, 3, 2)) +B = np.ones_like(A[:1]) +IMAGE1 = np.pad(A, ((0, 0), (2, 2), (2, 2)), constant_values=255) +IMAGE2 = np.copy(IMAGE1) +IMAGE2[0] = 0 +IMAGE3 = np.copy(IMAGE2) +IMAGE3[1] = 0 +MASK = np.pad(B, ((0, 0), (2, 2), (2, 2)), constant_values=0) +TEST_CASE_1 = [{"threshold": "otsu"}, IMAGE1, MASK] +TEST_CASE_2 = [{"threshold": "otsu"}, IMAGE2, MASK] +TEST_CASE_3 = [{"threshold": "otsu"}, IMAGE3, MASK] + + +class TestForegroundMask(unittest.TestCase): + @parameterized.expand([TEST_CASE_1, TEST_CASE_3]) + def test_foreground_mask(self, arguments, image, mask): + result = ForegroundMask(**arguments)(image) + np.testing.assert_allclose(result, mask) + + +if __name__ == "__main__": + unittest.main() From ba45b6283ffa05176ac87bd19830fd64bcbcdc92 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Thu, 26 May 2022 13:40:10 +0000 Subject: [PATCH 03/23] Add several test cases Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- monai/transforms/intensity/array.py | 39 +++++++++++++------------ tests/test_foreground_mask.py | 44 ++++++++++++++++++++++++----- 2 files changed, 58 insertions(+), 25 deletions(-) diff --git a/monai/transforms/intensity/array.py b/monai/transforms/intensity/array.py index f5d35a226e..ca35163c10 100644 --- a/monai/transforms/intensity/array.py +++ b/monai/transforms/intensity/array.py @@ -20,7 +20,6 @@ from warnings import warn import numpy as np -import skimage import torch from monai.config import DtypeLike @@ -33,8 +32,11 @@ from monai.utils.deprecate_utils import deprecated_arg from monai.utils.enums import TransformBackends from monai.utils.misc import ensure_tuple, ensure_tuple_rep, ensure_tuple_size, fall_back_tuple +from monai.utils.module import optional_import from monai.utils.type_conversion import convert_data_type, convert_to_dst_type, convert_to_tensor, get_equivalent_dtype +skimage, _ = optional_import("skimage") + __all__ = [ "RandGaussianNoise", "RandRicianNoise", @@ -2161,37 +2163,38 @@ class ForegroundMask(Transform): def __init__( self, threshold: Union[Dict, Callable, str, float] = "otsu", - hsv_threshold: Optional[Union[Dict, Callable, str, float]] = None, - high_value_foreground: bool = False, + hsv_threshold: Optional[Union[Dict, Callable, str, float, int]] = None, + invert: bool = False, ) -> None: self.thresholds = {} - if isinstance(threshold, dict): - for mode, th in threshold.items(): - self._set_threshold(th, mode) - else: - self._set_threshold(threshold, "R") - self._set_threshold(threshold, "G") - self._set_threshold(threshold, "B") + if threshold is not None: + if isinstance(threshold, dict): + for mode, th in threshold.items(): + self._set_threshold(th, mode) + else: + self._set_threshold(threshold, "R") + self._set_threshold(threshold, "G") + self._set_threshold(threshold, "B") if hsv_threshold is not None: if isinstance(hsv_threshold, dict): - for mode, th in threshold.items(): + for mode, th in hsv_threshold.items(): self._set_threshold(th, mode) else: self._set_threshold(hsv_threshold, "H") self._set_threshold(hsv_threshold, "S") self._set_threshold(hsv_threshold, "V") - self.high_value_foreground = high_value_foreground + self.invert = invert def _set_threshold(self, threshold, mode): if callable(threshold): self.thresholds[mode] = threshold elif isinstance(threshold, str): self.thresholds[mode] = getattr(skimage.filters, "threshold_" + threshold.lower()) - elif isinstance(threshold, float): - self.thresholds[mode] = threshold + elif isinstance(threshold, (float, int)): + self.thresholds[mode] = float(threshold) else: - ValueError( + raise ValueError( f"`threshold` should be either a callable, string, or float number, {type(threshold)} was given." ) @@ -2202,8 +2205,8 @@ def _get_threshold(self, image, mode): return threshold def __call__(self, img_rgb: NdarrayOrTensor): - if self.high_value_foreground: - img_rgb = skimage.util.invert(img_rgb.invert) + if self.invert: + img_rgb = skimage.util.invert(img_rgb) foreground = np.zeros_like(img_rgb[:1]) for img, mode in zip(img_rgb, "RGB"): @@ -2216,6 +2219,6 @@ def __call__(self, img_rgb: NdarrayOrTensor): for img, mode in zip(img_hsv, "HSV"): threshold = self._get_threshold(img, mode) if threshold: - foreground |= img < threshold + foreground |= img > threshold return foreground diff --git a/tests/test_foreground_mask.py b/tests/test_foreground_mask.py index 6d1b543098..66c87249f9 100644 --- a/tests/test_foreground_mask.py +++ b/tests/test_foreground_mask.py @@ -15,25 +15,55 @@ from parameterized import parameterized from monai.transforms.intensity.array import ForegroundMask -from monai.utils import set_determinism +from monai.utils import optional_import, set_determinism +skimage, has_skimage = optional_import("skimage") set_determinism(1234) -A = np.random.randint(51, 128, (3, 3, 2)) +A = np.random.randint(64, 128, (3, 3, 2)).astype(np.uint8) B = np.ones_like(A[:1]) +MASK = np.pad(B, ((0, 0), (2, 2), (2, 2)), constant_values=0) IMAGE1 = np.pad(A, ((0, 0), (2, 2), (2, 2)), constant_values=255) IMAGE2 = np.copy(IMAGE1) IMAGE2[0] = 0 -IMAGE3 = np.copy(IMAGE2) -IMAGE3[1] = 0 -MASK = np.pad(B, ((0, 0), (2, 2), (2, 2)), constant_values=0) +IMAGE3 = np.pad(A, ((0, 0), (2, 2), (2, 2)), constant_values=0) +TEST_CASE_0 = [{}, IMAGE1, MASK] TEST_CASE_1 = [{"threshold": "otsu"}, IMAGE1, MASK] TEST_CASE_2 = [{"threshold": "otsu"}, IMAGE2, MASK] -TEST_CASE_3 = [{"threshold": "otsu"}, IMAGE3, MASK] +TEST_CASE_3 = [{"threshold": 140}, IMAGE1, MASK] +TEST_CASE_4 = [{"threshold": "otsu", "invert": True}, IMAGE3, MASK] +TEST_CASE_5 = [{"threshold": 0.5}, MASK, np.logical_not(MASK)] +TEST_CASE_6 = [{"threshold": 140}, IMAGE2, np.ones_like(MASK)] +TEST_CASE_7 = [{"threshold": {"R": "otsu", "G": "otsu", "B": "otsu"}}, IMAGE2, MASK] +TEST_CASE_8 = [{"threshold": {"R": 140, "G": "otsu", "B": "otsu"}}, IMAGE2, np.ones_like(MASK)] +TEST_CASE_9 = [{"threshold": {"R": 140, "G": skimage.filters.threshold_otsu, "B": "otsu"}}, IMAGE2, np.ones_like(MASK)] +TEST_CASE_10 = [{"threshold": skimage.filters.threshold_mean}, IMAGE1, MASK] +TEST_CASE_11 = [{"threshold": None}, IMAGE1, np.zeros_like(MASK)] +TEST_CASE_12 = [{"threshold": None, "hsv_threshold": "otsu"}, IMAGE1, np.ones_like(MASK)] +TEST_CASE_13 = [{"threshold": None, "hsv_threshold": {"S": "otsu"}}, IMAGE1, MASK] +TEST_CASE_14 = [{"threshold": 100, "invert": True}, IMAGE1, np.logical_not(MASK)] class TestForegroundMask(unittest.TestCase): - @parameterized.expand([TEST_CASE_1, TEST_CASE_3]) + @parameterized.expand( + [ + TEST_CASE_0, + TEST_CASE_1, + TEST_CASE_2, + TEST_CASE_3, + TEST_CASE_4, + TEST_CASE_5, + TEST_CASE_6, + TEST_CASE_7, + TEST_CASE_8, + TEST_CASE_9, + TEST_CASE_10, + TEST_CASE_11, + TEST_CASE_12, + TEST_CASE_13, + TEST_CASE_14, + ] + ) def test_foreground_mask(self, arguments, image, mask): result = ForegroundMask(**arguments)(image) np.testing.assert_allclose(result, mask) From 7294c87db9610e12f873e00bdd71f6308e5c3e77 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Thu, 26 May 2022 14:13:22 +0000 Subject: [PATCH 04/23] Add ForegroundMaskd Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- monai/transforms/intensity/array.py | 20 +++++++++++++++-- monai/transforms/intensity/dictionary.py | 28 ++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/monai/transforms/intensity/array.py b/monai/transforms/intensity/array.py index ca35163c10..663a8a2ea4 100644 --- a/monai/transforms/intensity/array.py +++ b/monai/transforms/intensity/array.py @@ -2160,6 +2160,22 @@ def __call__(self, img: torch.Tensor) -> torch.Tensor: class ForegroundMask(Transform): + """ + Creates a binary mask that defines the foreground. + This transform receives an RGB (or grayscale) image where by default it is assumed that the foreground has + low values (dark) while the background is white. + + Args: + threshold: an int or a float number that defines the threshold for the input image. Or a callable that receives + each dimension of the image and calculate the threshold, or a string the defines such callable from + skimage.filter.threshold_xxxx. Also a dictionary can be passed that defines such thresholds for each channel. + like {"R": 100, "G": "otsu", "B": skimage.filter.threshold_mean } + hsv_threshold: similar to threshold but for HSV color space ("H", "S", and "V"). + invert: invert the intensity range of the input image, so that the dtype maximum is now the dtype minimum, + and vice-versa. + + """ + def __init__( self, threshold: Union[Dict, Callable, str, float] = "otsu", @@ -2170,7 +2186,7 @@ def __init__( if threshold is not None: if isinstance(threshold, dict): for mode, th in threshold.items(): - self._set_threshold(th, mode) + self._set_threshold(th, mode.upper()) else: self._set_threshold(threshold, "R") self._set_threshold(threshold, "G") @@ -2178,7 +2194,7 @@ def __init__( if hsv_threshold is not None: if isinstance(hsv_threshold, dict): for mode, th in hsv_threshold.items(): - self._set_threshold(th, mode) + self._set_threshold(th, mode.upper()) else: self._set_threshold(hsv_threshold, "H") self._set_threshold(hsv_threshold, "S") diff --git a/monai/transforms/intensity/dictionary.py b/monai/transforms/intensity/dictionary.py index 67dc73f93e..7ac95df273 100644 --- a/monai/transforms/intensity/dictionary.py +++ b/monai/transforms/intensity/dictionary.py @@ -23,6 +23,7 @@ from monai.config.type_definitions import NdarrayOrTensor from monai.transforms.intensity.array import ( AdjustContrast, + ForegroundMask, GaussianSharpen, GaussianSmooth, GibbsNoise, @@ -88,6 +89,7 @@ "RandCoarseDropoutd", "RandCoarseShuffled", "HistogramNormalized", + "ForegroundMaskd", "RandGaussianNoiseD", "RandGaussianNoiseDict", "ShiftIntensityD", @@ -146,6 +148,8 @@ "HistogramNormalizeDict", "RandKSpaceSpikeNoiseD", "RandKSpaceSpikeNoiseDict", + "ForegroundMaskD", + "ForegroundMaskDict", ] DEFAULT_POST_FIX = PostFix.meta() @@ -1654,6 +1658,29 @@ def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> Dict[Hashable, N return d +class ForegroundMaskd(MapTransform): + def __init__( + self, + keys: KeysCollection, + threshold: Union[Dict, Callable, str, float] = "otsu", + hsv_threshold: Optional[Union[Dict, Callable, str, float, int]] = None, + invert: bool = False, + new_key_prefix: Optional[str] = None, + allow_missing_keys: bool = False, + ) -> None: + super().__init__(keys, allow_missing_keys) + self.transform = ForegroundMask(threshold=threshold, hsv_threshold=hsv_threshold, invert=invert) + self.new_key_prefix = new_key_prefix + + def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> Dict[Hashable, NdarrayOrTensor]: + d = dict(data) + for key in self.key_iterator(d): + new_key = key if self.new_key_prefix is None else self.new_key_prefix + key + d[new_key] = self.transform(d[key]) + + return d + + RandGaussianNoiseD = RandGaussianNoiseDict = RandGaussianNoised RandRicianNoiseD = RandRicianNoiseDict = RandRicianNoised ShiftIntensityD = ShiftIntensityDict = ShiftIntensityd @@ -1683,3 +1710,4 @@ def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> Dict[Hashable, N RandCoarseDropoutD = RandCoarseDropoutDict = RandCoarseDropoutd HistogramNormalizeD = HistogramNormalizeDict = HistogramNormalized RandCoarseShuffleD = RandCoarseShuffleDict = RandCoarseShuffled +ForegroundMaskD = ForegroundMaskDict = ForegroundMask From cb86da5191bd4dffb1739110bd6be7d41dfb58b5 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Thu, 26 May 2022 14:13:50 +0000 Subject: [PATCH 05/23] Add unittests for ForegroundMaskd Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- tests/test_foreground_maskd.py | 81 ++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 tests/test_foreground_maskd.py diff --git a/tests/test_foreground_maskd.py b/tests/test_foreground_maskd.py new file mode 100644 index 0000000000..c3a87581bd --- /dev/null +++ b/tests/test_foreground_maskd.py @@ -0,0 +1,81 @@ +# 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. + +import unittest + +import numpy as np +from parameterized import parameterized + +from monai.transforms.intensity.dictionary import ForegroundMaskd +from monai.utils import optional_import, set_determinism + +skimage, has_skimage = optional_import("skimage") +set_determinism(1234) + +A = np.random.randint(64, 128, (3, 3, 2)).astype(np.uint8) +B = np.ones_like(A[:1]) +MASK = np.pad(B, ((0, 0), (2, 2), (2, 2)), constant_values=0) +IMAGE1 = np.pad(A, ((0, 0), (2, 2), (2, 2)), constant_values=255) +IMAGE2 = np.copy(IMAGE1) +IMAGE2[0] = 0 +IMAGE3 = np.pad(A, ((0, 0), (2, 2), (2, 2)), constant_values=0) +TEST_CASE_0 = [{"keys": "image"}, {"image": IMAGE1}, MASK] +TEST_CASE_1 = [{"keys": "image", "threshold": "otsu"}, {"image": IMAGE1}, MASK] +TEST_CASE_2 = [{"keys": "image", "threshold": "otsu"}, {"image": IMAGE2}, MASK] +TEST_CASE_3 = [{"keys": "image", "threshold": 140}, {"image": IMAGE1}, MASK] +TEST_CASE_4 = [{"keys": "image", "threshold": "otsu", "invert": True}, {"image": IMAGE3}, MASK] +TEST_CASE_5 = [{"keys": "image", "threshold": 0.5}, {"image": MASK}, np.logical_not(MASK)] +TEST_CASE_6 = [{"keys": "image", "threshold": 140}, {"image": IMAGE2}, np.ones_like(MASK)] +TEST_CASE_7 = [{"keys": "image", "threshold": {"R": "otsu", "G": "otsu", "B": "otsu"}}, {"image": IMAGE2}, MASK] +TEST_CASE_8 = [ + {"keys": "image", "threshold": {"R": 140, "G": "otsu", "B": "otsu"}}, + {"image": IMAGE2}, + np.ones_like(MASK), +] +TEST_CASE_9 = [ + {"keys": "image", "threshold": {"R": 140, "G": skimage.filters.threshold_otsu, "B": "otsu"}}, + {"image": IMAGE2}, + np.ones_like(MASK), +] +TEST_CASE_10 = [{"keys": "image", "threshold": skimage.filters.threshold_mean}, {"image": IMAGE1}, MASK] +TEST_CASE_11 = [{"keys": "image", "threshold": None}, {"image": IMAGE1}, np.zeros_like(MASK)] +TEST_CASE_12 = [{"keys": "image", "threshold": None, "hsv_threshold": "otsu"}, {"image": IMAGE1}, np.ones_like(MASK)] +TEST_CASE_13 = [{"keys": "image", "threshold": None, "hsv_threshold": {"S": "otsu"}}, {"image": IMAGE1}, MASK] +TEST_CASE_14 = [{"keys": "image", "threshold": 100, "invert": True}, {"image": IMAGE1}, np.logical_not(MASK)] + + +class TestForegroundMaskd(unittest.TestCase): + @parameterized.expand( + [ + TEST_CASE_0, + TEST_CASE_1, + TEST_CASE_2, + TEST_CASE_3, + TEST_CASE_4, + TEST_CASE_5, + TEST_CASE_6, + TEST_CASE_7, + TEST_CASE_8, + TEST_CASE_9, + TEST_CASE_10, + TEST_CASE_11, + TEST_CASE_12, + TEST_CASE_13, + TEST_CASE_14, + ] + ) + def test_foreground_mask(self, arguments, image, mask): + result = ForegroundMaskd(**arguments)(image)[arguments["keys"]] + np.testing.assert_allclose(result, mask) + + +if __name__ == "__main__": + unittest.main() From a2feb794cb736779174a695b8ba2fde6f5f6bbe2 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Thu, 26 May 2022 14:15:44 +0000 Subject: [PATCH 06/23] Update init Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- monai/transforms/__init__.py | 4 ++++ tests/test_foreground_maskd.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/monai/transforms/__init__.py b/monai/transforms/__init__.py index c2385499b3..fa2c64f38e 100644 --- a/monai/transforms/__init__.py +++ b/monai/transforms/__init__.py @@ -81,6 +81,7 @@ from .intensity.array import ( AdjustContrast, DetectEnvelope, + ForegroundMask, GaussianSharpen, GaussianSmooth, GibbsNoise, @@ -117,6 +118,9 @@ AdjustContrastd, AdjustContrastD, AdjustContrastDict, + ForegroundMaskd, + ForegroundMaskD, + ForegroundMaskDict, GaussianSharpend, GaussianSharpenD, GaussianSharpenDict, diff --git a/tests/test_foreground_maskd.py b/tests/test_foreground_maskd.py index c3a87581bd..01256d7423 100644 --- a/tests/test_foreground_maskd.py +++ b/tests/test_foreground_maskd.py @@ -14,7 +14,7 @@ import numpy as np from parameterized import parameterized -from monai.transforms.intensity.dictionary import ForegroundMaskd +from monai.transforms import ForegroundMaskd from monai.utils import optional_import, set_determinism skimage, has_skimage = optional_import("skimage") From d271d98d0b712e3fa7ed4ee965b33a9c1dcd3680 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Thu, 26 May 2022 14:20:03 +0000 Subject: [PATCH 07/23] Update docstring Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- monai/transforms/intensity/array.py | 6 +++--- monai/transforms/intensity/dictionary.py | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/monai/transforms/intensity/array.py b/monai/transforms/intensity/array.py index 663a8a2ea4..e5b567739e 100644 --- a/monai/transforms/intensity/array.py +++ b/monai/transforms/intensity/array.py @@ -2178,11 +2178,11 @@ class ForegroundMask(Transform): def __init__( self, - threshold: Union[Dict, Callable, str, float] = "otsu", + threshold: Union[Dict, Callable, str, float, int] = "otsu", hsv_threshold: Optional[Union[Dict, Callable, str, float, int]] = None, invert: bool = False, ) -> None: - self.thresholds = {} + self.thresholds: Dict[str, Union[Callable, float]] = {} if threshold is not None: if isinstance(threshold, dict): for mode, th in threshold.items(): @@ -2230,7 +2230,7 @@ def __call__(self, img_rgb: NdarrayOrTensor): if threshold: foreground |= img < threshold - if set(list("HSV")) & set(self.thresholds.keys()): + if set("HSV") & set(self.thresholds.keys()): img_hsv = skimage.color.rgb2hsv(img_rgb, channel_axis=0) for img, mode in zip(img_hsv, "HSV"): threshold = self._get_threshold(img, mode) diff --git a/monai/transforms/intensity/dictionary.py b/monai/transforms/intensity/dictionary.py index 7ac95df273..ee23544dd9 100644 --- a/monai/transforms/intensity/dictionary.py +++ b/monai/transforms/intensity/dictionary.py @@ -1659,6 +1659,24 @@ def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> Dict[Hashable, N class ForegroundMaskd(MapTransform): + """ + Creates a binary mask that defines the foreground. + This transform receives an RGB (or grayscale) image where by default it is assumed that the foreground has + low values (dark) while the background is white. + + Args: + keys: keys of the corresponding items to be transformed. + threshold: an int or a float number that defines the threshold for the input image. Or a callable that receives + each dimension of the image and calculate the threshold, or a string the defines such callable from + skimage.filter.threshold_xxxx. Also a dictionary can be passed that defines such thresholds for each channel. + like {"R": 100, "G": "otsu", "B": skimage.filter.threshold_mean } + hsv_threshold: similar to threshold but for HSV color space ("H", "S", and "V"). + invert: invert the intensity range of the input image, so that the dtype maximum is now the dtype minimum, + and vice-versa. + allow_missing_keys: do not raise exception if key is missing. + + """ + def __init__( self, keys: KeysCollection, From 32313ecb9503e458350d45b3fd47d7c174265205 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Sat, 28 May 2022 19:18:47 +0000 Subject: [PATCH 08/23] Update to less or equal for RGB threshold Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- monai/transforms/intensity/array.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/monai/transforms/intensity/array.py b/monai/transforms/intensity/array.py index e5b567739e..f461bb6295 100644 --- a/monai/transforms/intensity/array.py +++ b/monai/transforms/intensity/array.py @@ -2228,13 +2228,15 @@ def __call__(self, img_rgb: NdarrayOrTensor): for img, mode in zip(img_rgb, "RGB"): threshold = self._get_threshold(img, mode) if threshold: - foreground |= img < threshold + foreground |= img <= threshold if set("HSV") & set(self.thresholds.keys()): img_hsv = skimage.color.rgb2hsv(img_rgb, channel_axis=0) + hsv_foreground = np.zeros_like(img_rgb[:1]) for img, mode in zip(img_hsv, "HSV"): threshold = self._get_threshold(img, mode) if threshold: - foreground |= img > threshold + hsv_foreground |= img > threshold + foreground &= hsv_foreground return foreground From 4771712abfe88a3fd0ffa8f222c030938967ee81 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Sat, 28 May 2022 20:11:22 +0000 Subject: [PATCH 09/23] Update RGB and HSV mask combination Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- monai/transforms/intensity/array.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/monai/transforms/intensity/array.py b/monai/transforms/intensity/array.py index f461bb6295..cae447c1ba 100644 --- a/monai/transforms/intensity/array.py +++ b/monai/transforms/intensity/array.py @@ -2161,9 +2161,9 @@ def __call__(self, img: torch.Tensor) -> torch.Tensor: class ForegroundMask(Transform): """ - Creates a binary mask that defines the foreground. + Creates a binary mask that defines the foreground based on thresholds in RGB or HSV color sapce. This transform receives an RGB (or grayscale) image where by default it is assumed that the foreground has - low values (dark) while the background is white. + low values (dark) while the background has high values (white). Otherwise, set `invert` argument to `True`. Args: threshold: an int or a float number that defines the threshold for the input image. Or a callable that receives @@ -2224,12 +2224,14 @@ def __call__(self, img_rgb: NdarrayOrTensor): if self.invert: img_rgb = skimage.util.invert(img_rgb) - foreground = np.zeros_like(img_rgb[:1]) - for img, mode in zip(img_rgb, "RGB"): - threshold = self._get_threshold(img, mode) - if threshold: - foreground |= img <= threshold - + foregrounds = [] + if set("RGB") & set(self.thresholds.keys()): + rgb_foreground = np.zeros_like(img_rgb[:1]) + for img, mode in zip(img_rgb, "RGB"): + threshold = self._get_threshold(img, mode) + if threshold: + rgb_foreground |= img <= threshold + foregrounds.append(rgb_foreground) if set("HSV") & set(self.thresholds.keys()): img_hsv = skimage.color.rgb2hsv(img_rgb, channel_axis=0) hsv_foreground = np.zeros_like(img_rgb[:1]) @@ -2237,6 +2239,8 @@ def __call__(self, img_rgb: NdarrayOrTensor): threshold = self._get_threshold(img, mode) if threshold: hsv_foreground |= img > threshold - foreground &= hsv_foreground + foregrounds.append(hsv_foreground) - return foreground + if foregrounds: + return np.stack(foregrounds).all(axis=0) + return np.zeros_like(img_rgb[:1]) From 7a72eb9344b8e88e1af7b91be1956f3c2a3cb10f Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Sat, 28 May 2022 20:43:17 +0000 Subject: [PATCH 10/23] Add support for torch.Tensor Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- monai/transforms/intensity/array.py | 11 ++++++-- tests/test_foreground_mask.py | 44 ++++++++++++++--------------- 2 files changed, 30 insertions(+), 25 deletions(-) diff --git a/monai/transforms/intensity/array.py b/monai/transforms/intensity/array.py index cae447c1ba..e97b017bbe 100644 --- a/monai/transforms/intensity/array.py +++ b/monai/transforms/intensity/array.py @@ -2176,6 +2176,8 @@ class ForegroundMask(Transform): """ + backend = [TransformBackends.TORCH, TransformBackends.NUMPY] + def __init__( self, threshold: Union[Dict, Callable, str, float, int] = "otsu", @@ -2220,7 +2222,8 @@ def _get_threshold(self, image, mode): return threshold(image) return threshold - def __call__(self, img_rgb: NdarrayOrTensor): + def __call__(self, image: NdarrayOrTensor): + img_rgb, *_ = convert_data_type(image, np.ndarray) if self.invert: img_rgb = skimage.util.invert(img_rgb) @@ -2242,5 +2245,7 @@ def __call__(self, img_rgb: NdarrayOrTensor): foregrounds.append(hsv_foreground) if foregrounds: - return np.stack(foregrounds).all(axis=0) - return np.zeros_like(img_rgb[:1]) + mask = np.stack(foregrounds).all(axis=0) + else: + mask = np.zeros_like(img_rgb[:1]) + return convert_to_dst_type(src=mask, dst=image)[0] diff --git a/tests/test_foreground_mask.py b/tests/test_foreground_mask.py index 66c87249f9..9d7602759e 100644 --- a/tests/test_foreground_mask.py +++ b/tests/test_foreground_mask.py @@ -16,6 +16,7 @@ from monai.transforms.intensity.array import ForegroundMask from monai.utils import optional_import, set_determinism +from tests.utils import TEST_NDARRAYS, assert_allclose skimage, has_skimage = optional_import("skimage") set_determinism(1234) @@ -43,30 +44,29 @@ TEST_CASE_13 = [{"threshold": None, "hsv_threshold": {"S": "otsu"}}, IMAGE1, MASK] TEST_CASE_14 = [{"threshold": 100, "invert": True}, IMAGE1, np.logical_not(MASK)] +TESTS = [] +for p in TEST_NDARRAYS: + TESTS.append([p, *TEST_CASE_0]) + TESTS.append([p, *TEST_CASE_1]) + TESTS.append([p, *TEST_CASE_2]) + TESTS.append([p, *TEST_CASE_3]) + TESTS.append([p, *TEST_CASE_4]) + TESTS.append([p, *TEST_CASE_5]) + TESTS.append([p, *TEST_CASE_6]) + TESTS.append([p, *TEST_CASE_7]) + TESTS.append([p, *TEST_CASE_8]) + TESTS.append([p, *TEST_CASE_9]) + TESTS.append([p, *TEST_CASE_10]) + TESTS.append([p, *TEST_CASE_11]) + TESTS.append([p, *TEST_CASE_12]) + class TestForegroundMask(unittest.TestCase): - @parameterized.expand( - [ - TEST_CASE_0, - TEST_CASE_1, - TEST_CASE_2, - TEST_CASE_3, - TEST_CASE_4, - TEST_CASE_5, - TEST_CASE_6, - TEST_CASE_7, - TEST_CASE_8, - TEST_CASE_9, - TEST_CASE_10, - TEST_CASE_11, - TEST_CASE_12, - TEST_CASE_13, - TEST_CASE_14, - ] - ) - def test_foreground_mask(self, arguments, image, mask): - result = ForegroundMask(**arguments)(image) - np.testing.assert_allclose(result, mask) + @parameterized.expand(TESTS) + def test_foreground_mask(self, in_type, arguments, image, mask): + input_image = in_type(image) + result = ForegroundMask(**arguments)(input_image) + assert_allclose(result, mask, type_test=False) if __name__ == "__main__": From d393e3540f3c48ffe6073032301c8e773f511add Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Sat, 28 May 2022 20:57:28 +0000 Subject: [PATCH 11/23] Update docsting and dict tests Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- monai/transforms/intensity/array.py | 14 ++++---- monai/transforms/intensity/dictionary.py | 14 ++++---- tests/test_foreground_maskd.py | 46 ++++++++++++------------ 3 files changed, 40 insertions(+), 34 deletions(-) diff --git a/monai/transforms/intensity/array.py b/monai/transforms/intensity/array.py index e97b017bbe..7c1e353cf3 100644 --- a/monai/transforms/intensity/array.py +++ b/monai/transforms/intensity/array.py @@ -2161,16 +2161,18 @@ def __call__(self, img: torch.Tensor) -> torch.Tensor: class ForegroundMask(Transform): """ - Creates a binary mask that defines the foreground based on thresholds in RGB or HSV color sapce. + Creates a binary mask that defines the foreground based on thresholds in RGB or HSV color space. This transform receives an RGB (or grayscale) image where by default it is assumed that the foreground has low values (dark) while the background has high values (white). Otherwise, set `invert` argument to `True`. Args: - threshold: an int or a float number that defines the threshold for the input image. Or a callable that receives - each dimension of the image and calculate the threshold, or a string the defines such callable from - skimage.filter.threshold_xxxx. Also a dictionary can be passed that defines such thresholds for each channel. - like {"R": 100, "G": "otsu", "B": skimage.filter.threshold_mean } - hsv_threshold: similar to threshold but for HSV color space ("H", "S", and "V"). + threshold: an int or a float number that defines the threshold that values less than that are foreground. + It also can be a callable that receives each dimension of the image and calculate the threshold, + or a string the defines such callable from `skimage.filter.threshold_xxxx`. + Moreover, a dictionary can be passed that defines such thresholds for each channel, like + {"R": 100, "G": "otsu", "B": skimage.filter.threshold_mean} + hsv_threshold: similar to threshold but HSV color space ("H", "S", and "V"). + Unlike RBG, in HSV, value greater than `hsv_threshold` are considered foreground. invert: invert the intensity range of the input image, so that the dtype maximum is now the dtype minimum, and vice-versa. diff --git a/monai/transforms/intensity/dictionary.py b/monai/transforms/intensity/dictionary.py index ee23544dd9..af685ab33f 100644 --- a/monai/transforms/intensity/dictionary.py +++ b/monai/transforms/intensity/dictionary.py @@ -1660,17 +1660,19 @@ def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> Dict[Hashable, N class ForegroundMaskd(MapTransform): """ - Creates a binary mask that defines the foreground. + Creates a binary mask that defines the foreground based on thresholds in RGB or HSV color space. This transform receives an RGB (or grayscale) image where by default it is assumed that the foreground has low values (dark) while the background is white. Args: keys: keys of the corresponding items to be transformed. - threshold: an int or a float number that defines the threshold for the input image. Or a callable that receives - each dimension of the image and calculate the threshold, or a string the defines such callable from - skimage.filter.threshold_xxxx. Also a dictionary can be passed that defines such thresholds for each channel. - like {"R": 100, "G": "otsu", "B": skimage.filter.threshold_mean } - hsv_threshold: similar to threshold but for HSV color space ("H", "S", and "V"). + threshold: an int or a float number that defines the threshold that values less than that are foreground. + It also can be a callable that receives each dimension of the image and calculate the threshold, + or a string the defines such callable from `skimage.filter.threshold_xxxx`. + Moreover, a dictionary can be passed that defines such thresholds for each channel, like + {"R": 100, "G": "otsu", "B": skimage.filter.threshold_mean} + hsv_threshold: similar to threshold but HSV color space ("H", "S", and "V"). + Unlike RBG, in HSV, value greater than `hsv_threshold` are considered foreground. invert: invert the intensity range of the input image, so that the dtype maximum is now the dtype minimum, and vice-versa. allow_missing_keys: do not raise exception if key is missing. diff --git a/tests/test_foreground_maskd.py b/tests/test_foreground_maskd.py index 01256d7423..fb296a9866 100644 --- a/tests/test_foreground_maskd.py +++ b/tests/test_foreground_maskd.py @@ -16,6 +16,7 @@ from monai.transforms import ForegroundMaskd from monai.utils import optional_import, set_determinism +from tests.utils import TEST_NDARRAYS, assert_allclose skimage, has_skimage = optional_import("skimage") set_determinism(1234) @@ -51,30 +52,31 @@ TEST_CASE_13 = [{"keys": "image", "threshold": None, "hsv_threshold": {"S": "otsu"}}, {"image": IMAGE1}, MASK] TEST_CASE_14 = [{"keys": "image", "threshold": 100, "invert": True}, {"image": IMAGE1}, np.logical_not(MASK)] +TESTS = [] +for p in TEST_NDARRAYS: + TESTS.append([p, *TEST_CASE_0]) + TESTS.append([p, *TEST_CASE_1]) + TESTS.append([p, *TEST_CASE_2]) + TESTS.append([p, *TEST_CASE_3]) + TESTS.append([p, *TEST_CASE_4]) + TESTS.append([p, *TEST_CASE_5]) + TESTS.append([p, *TEST_CASE_6]) + TESTS.append([p, *TEST_CASE_7]) + TESTS.append([p, *TEST_CASE_8]) + TESTS.append([p, *TEST_CASE_9]) + TESTS.append([p, *TEST_CASE_10]) + TESTS.append([p, *TEST_CASE_11]) + TESTS.append([p, *TEST_CASE_12]) + TESTS.append([p, *TEST_CASE_13]) + TESTS.append([p, *TEST_CASE_14]) + class TestForegroundMaskd(unittest.TestCase): - @parameterized.expand( - [ - TEST_CASE_0, - TEST_CASE_1, - TEST_CASE_2, - TEST_CASE_3, - TEST_CASE_4, - TEST_CASE_5, - TEST_CASE_6, - TEST_CASE_7, - TEST_CASE_8, - TEST_CASE_9, - TEST_CASE_10, - TEST_CASE_11, - TEST_CASE_12, - TEST_CASE_13, - TEST_CASE_14, - ] - ) - def test_foreground_mask(self, arguments, image, mask): - result = ForegroundMaskd(**arguments)(image)[arguments["keys"]] - np.testing.assert_allclose(result, mask) + @parameterized.expand(TESTS) + def test_foreground_mask(self, in_type, arguments, data_dict, mask): + data_dict[arguments["keys"]] = in_type(data_dict[arguments["keys"]]) + result = ForegroundMaskd(**arguments)(data_dict)[arguments["keys"]] + assert_allclose(result, mask, type_test=False) if __name__ == "__main__": From 8902a8aa94ff9502639a0b122bd96e7e139dc41b Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Sat, 28 May 2022 21:03:12 +0000 Subject: [PATCH 12/23] Add docs Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- docs/source/transforms.rst | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/source/transforms.rst b/docs/source/transforms.rst index a93c48984c..854fd4dbb4 100644 --- a/docs/source/transforms.rst +++ b/docs/source/transforms.rst @@ -446,6 +446,13 @@ Intensity :members: :special-members: __call__ + +`ForegroundMask` +"""""""""""""""" +.. autoclass:: ForegroundMask + :members: + :special-members: __call__ + IO ^^ @@ -1327,6 +1334,11 @@ Intensity (Dict) :members: :special-members: __call__ +`ForegroundMaskd` +""""""""""""""""" +.. autoclass:: ForegroundMaskd + :members: + :special-members: __call__ IO (Dict) ^^^^^^^^^ From 4a8aa142a5057da39c59a5d65b46835f83d586d7 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Sun, 29 May 2022 04:02:50 +0000 Subject: [PATCH 13/23] Add skipunless for skimage Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- tests/test_foreground_mask.py | 1 + tests/test_foreground_maskd.py | 1 + 2 files changed, 2 insertions(+) diff --git a/tests/test_foreground_mask.py b/tests/test_foreground_mask.py index 9d7602759e..fca55d55ef 100644 --- a/tests/test_foreground_mask.py +++ b/tests/test_foreground_mask.py @@ -61,6 +61,7 @@ TESTS.append([p, *TEST_CASE_12]) +@unittest.skipUnless(has_skimage, "Requires sci-kit image") class TestForegroundMask(unittest.TestCase): @parameterized.expand(TESTS) def test_foreground_mask(self, in_type, arguments, image, mask): diff --git a/tests/test_foreground_maskd.py b/tests/test_foreground_maskd.py index fb296a9866..336ee9d574 100644 --- a/tests/test_foreground_maskd.py +++ b/tests/test_foreground_maskd.py @@ -71,6 +71,7 @@ TESTS.append([p, *TEST_CASE_14]) +@unittest.skipUnless(has_skimage, "Requires sci-kit image") class TestForegroundMaskd(unittest.TestCase): @parameterized.expand(TESTS) def test_foreground_mask(self, in_type, arguments, data_dict, mask): From 33fc743661ce9b56a3c08e6ee067d42d21f60c92 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Sun, 29 May 2022 13:07:52 +0000 Subject: [PATCH 14/23] Exclude form min tests Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- tests/min_tests.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/min_tests.py b/tests/min_tests.py index b52dc2a73d..cc35cf687f 100644 --- a/tests/min_tests.py +++ b/tests/min_tests.py @@ -53,6 +53,8 @@ def run_testsuit(): "test_ensure_channel_firstd", "test_fill_holes", "test_fill_holesd", + "test_foreground_mask", + "test_foreground_maskd", "test_global_mutual_information_loss", "test_handler_checkpoint_loader", "test_handler_checkpoint_saver", From aa1170676771581e77f353d80ee39032b51176a3 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Sun, 29 May 2022 15:48:42 +0000 Subject: [PATCH 15/23] fix a typo Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- monai/transforms/intensity/dictionary.py | 2 +- tests/test_foreground_maskd.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/monai/transforms/intensity/dictionary.py b/monai/transforms/intensity/dictionary.py index af685ab33f..5cc407ea01 100644 --- a/monai/transforms/intensity/dictionary.py +++ b/monai/transforms/intensity/dictionary.py @@ -1730,4 +1730,4 @@ def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> Dict[Hashable, N RandCoarseDropoutD = RandCoarseDropoutDict = RandCoarseDropoutd HistogramNormalizeD = HistogramNormalizeDict = HistogramNormalized RandCoarseShuffleD = RandCoarseShuffleDict = RandCoarseShuffled -ForegroundMaskD = ForegroundMaskDict = ForegroundMask +ForegroundMaskD = ForegroundMaskDict = ForegroundMaskd diff --git a/tests/test_foreground_maskd.py b/tests/test_foreground_maskd.py index 336ee9d574..282789dd44 100644 --- a/tests/test_foreground_maskd.py +++ b/tests/test_foreground_maskd.py @@ -14,7 +14,7 @@ import numpy as np from parameterized import parameterized -from monai.transforms import ForegroundMaskd +from monai.transforms.intensity.dictionary import ForegroundMaskd from monai.utils import optional_import, set_determinism from tests.utils import TEST_NDARRAYS, assert_allclose From de94b006d4508438cd8f96a0951183dddddb50a1 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Mon, 30 May 2022 19:14:47 +0000 Subject: [PATCH 16/23] Update no or wrong threshold cases Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- monai/transforms/intensity/array.py | 16 +++++++++------- tests/test_foreground_mask.py | 21 +++++++++++++++++---- tests/test_foreground_maskd.py | 21 ++++++++++++++++----- 3 files changed, 42 insertions(+), 16 deletions(-) diff --git a/monai/transforms/intensity/array.py b/monai/transforms/intensity/array.py index 7c1e353cf3..c4fb09a64b 100644 --- a/monai/transforms/intensity/array.py +++ b/monai/transforms/intensity/array.py @@ -2168,7 +2168,8 @@ class ForegroundMask(Transform): Args: threshold: an int or a float number that defines the threshold that values less than that are foreground. It also can be a callable that receives each dimension of the image and calculate the threshold, - or a string the defines such callable from `skimage.filter.threshold_xxxx`. + or a string that defines such callable from `skimage.filter.threshold_...`. For the list of available + threshold functions, please refer to https://scikit-image.org/docs/stable/api/skimage.filters.html Moreover, a dictionary can be passed that defines such thresholds for each channel, like {"R": 100, "G": "otsu", "B": skimage.filter.threshold_mean} hsv_threshold: similar to threshold but HSV color space ("H", "S", and "V"). @@ -2205,6 +2206,10 @@ def __init__( self._set_threshold(hsv_threshold, "V") self.invert = invert + 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." + ) def _set_threshold(self, threshold, mode): if callable(threshold): @@ -2230,14 +2235,14 @@ def __call__(self, image: NdarrayOrTensor): img_rgb = skimage.util.invert(img_rgb) foregrounds = [] - if set("RGB") & set(self.thresholds.keys()): + if not self.thresholds.keys().isdisjoint(set("RGB")): rgb_foreground = np.zeros_like(img_rgb[:1]) for img, mode in zip(img_rgb, "RGB"): threshold = self._get_threshold(img, mode) if threshold: rgb_foreground |= img <= threshold foregrounds.append(rgb_foreground) - if set("HSV") & set(self.thresholds.keys()): + if not self.thresholds.keys().isdisjoint(set("HSV")): img_hsv = skimage.color.rgb2hsv(img_rgb, channel_axis=0) hsv_foreground = np.zeros_like(img_rgb[:1]) for img, mode in zip(img_hsv, "HSV"): @@ -2246,8 +2251,5 @@ def __call__(self, image: NdarrayOrTensor): hsv_foreground |= img > threshold foregrounds.append(hsv_foreground) - if foregrounds: - mask = np.stack(foregrounds).all(axis=0) - else: - mask = np.zeros_like(img_rgb[:1]) + mask = np.stack(foregrounds).all(axis=0) return convert_to_dst_type(src=mask, dst=image)[0] diff --git a/tests/test_foreground_mask.py b/tests/test_foreground_mask.py index fca55d55ef..0a511fe5b0 100644 --- a/tests/test_foreground_mask.py +++ b/tests/test_foreground_mask.py @@ -39,10 +39,11 @@ TEST_CASE_8 = [{"threshold": {"R": 140, "G": "otsu", "B": "otsu"}}, IMAGE2, np.ones_like(MASK)] TEST_CASE_9 = [{"threshold": {"R": 140, "G": skimage.filters.threshold_otsu, "B": "otsu"}}, IMAGE2, np.ones_like(MASK)] TEST_CASE_10 = [{"threshold": skimage.filters.threshold_mean}, IMAGE1, MASK] -TEST_CASE_11 = [{"threshold": None}, IMAGE1, np.zeros_like(MASK)] -TEST_CASE_12 = [{"threshold": None, "hsv_threshold": "otsu"}, IMAGE1, np.ones_like(MASK)] -TEST_CASE_13 = [{"threshold": None, "hsv_threshold": {"S": "otsu"}}, IMAGE1, MASK] -TEST_CASE_14 = [{"threshold": 100, "invert": True}, IMAGE1, np.logical_not(MASK)] +TEST_CASE_11 = [{"threshold": None, "hsv_threshold": "otsu"}, IMAGE1, np.ones_like(MASK)] +TEST_CASE_12 = [{"threshold": None, "hsv_threshold": {"S": "otsu"}}, IMAGE1, MASK] +TEST_CASE_13 = [{"threshold": 100, "invert": True}, IMAGE1, np.logical_not(MASK)] +TEST_CASE_ERROR_1 = [{"threshold": None}, IMAGE1] +TEST_CASE_ERROR_2 = [{"threshold": {"K": 1}}, IMAGE1] TESTS = [] for p in TEST_NDARRAYS: @@ -59,6 +60,12 @@ TESTS.append([p, *TEST_CASE_10]) TESTS.append([p, *TEST_CASE_11]) TESTS.append([p, *TEST_CASE_12]) + TESTS.append([p, *TEST_CASE_13]) + +TESTS_ERROR = [] +for p in TEST_NDARRAYS: + TESTS_ERROR.append([p, *TEST_CASE_ERROR_1]) + TESTS_ERROR.append([p, *TEST_CASE_ERROR_2]) @unittest.skipUnless(has_skimage, "Requires sci-kit image") @@ -69,6 +76,12 @@ def test_foreground_mask(self, in_type, arguments, image, mask): result = ForegroundMask(**arguments)(input_image) assert_allclose(result, mask, type_test=False) + @parameterized.expand(TESTS_ERROR) + def test_foreground_mask_error(self, in_type, arguments, image): + input_image = in_type(image) + with self.assertRaises(ValueError): + ForegroundMask(**arguments)(input_image) + if __name__ == "__main__": unittest.main() diff --git a/tests/test_foreground_maskd.py b/tests/test_foreground_maskd.py index 282789dd44..bf1df76af4 100644 --- a/tests/test_foreground_maskd.py +++ b/tests/test_foreground_maskd.py @@ -47,10 +47,11 @@ np.ones_like(MASK), ] TEST_CASE_10 = [{"keys": "image", "threshold": skimage.filters.threshold_mean}, {"image": IMAGE1}, MASK] -TEST_CASE_11 = [{"keys": "image", "threshold": None}, {"image": IMAGE1}, np.zeros_like(MASK)] -TEST_CASE_12 = [{"keys": "image", "threshold": None, "hsv_threshold": "otsu"}, {"image": IMAGE1}, np.ones_like(MASK)] -TEST_CASE_13 = [{"keys": "image", "threshold": None, "hsv_threshold": {"S": "otsu"}}, {"image": IMAGE1}, MASK] -TEST_CASE_14 = [{"keys": "image", "threshold": 100, "invert": True}, {"image": IMAGE1}, np.logical_not(MASK)] +TEST_CASE_11 = [{"keys": "image", "threshold": None, "hsv_threshold": "otsu"}, {"image": IMAGE1}, np.ones_like(MASK)] +TEST_CASE_12 = [{"keys": "image", "threshold": None, "hsv_threshold": {"S": "otsu"}}, {"image": IMAGE1}, MASK] +TEST_CASE_13 = [{"keys": "image", "threshold": 100, "invert": True}, {"image": IMAGE1}, np.logical_not(MASK)] +TEST_CASE_ERROR_1 = [{"keys": "image", "threshold": None}, {"image": IMAGE1}] +TEST_CASE_ERROR_2 = [{"keys": "image", "threshold": {"K": 1}}, {"image": IMAGE1}] TESTS = [] for p in TEST_NDARRAYS: @@ -68,7 +69,11 @@ TESTS.append([p, *TEST_CASE_11]) TESTS.append([p, *TEST_CASE_12]) TESTS.append([p, *TEST_CASE_13]) - TESTS.append([p, *TEST_CASE_14]) + +TESTS_ERROR = [] +for p in TEST_NDARRAYS: + TESTS_ERROR.append([p, *TEST_CASE_ERROR_1]) + TESTS_ERROR.append([p, *TEST_CASE_ERROR_2]) @unittest.skipUnless(has_skimage, "Requires sci-kit image") @@ -79,6 +84,12 @@ def test_foreground_mask(self, in_type, arguments, data_dict, mask): result = ForegroundMaskd(**arguments)(data_dict)[arguments["keys"]] assert_allclose(result, mask, type_test=False) + @parameterized.expand(TESTS_ERROR) + def test_foreground_mask_error(self, in_type, arguments, data_dict): + data_dict[arguments["keys"]] = in_type(data_dict[arguments["keys"]]) + with self.assertRaises(ValueError): + ForegroundMaskd(**arguments)(data_dict)[arguments["keys"]] + if __name__ == "__main__": unittest.main() From ebde248121f160b323237d68df890abca956618f Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Mon, 30 May 2022 19:20:03 +0000 Subject: [PATCH 17/23] Update docstring Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- monai/transforms/intensity/dictionary.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/monai/transforms/intensity/dictionary.py b/monai/transforms/intensity/dictionary.py index 5cc407ea01..eae603df4a 100644 --- a/monai/transforms/intensity/dictionary.py +++ b/monai/transforms/intensity/dictionary.py @@ -1675,6 +1675,8 @@ class ForegroundMaskd(MapTransform): Unlike RBG, in HSV, value greater than `hsv_threshold` are considered foreground. invert: invert the intensity range of the input image, so that the dtype maximum is now the dtype minimum, and vice-versa. + new_key_prefix: this prefix be prepended to the key to create a new key for the output and keep the value of + key intact. By default not prefix is set and the corresponding array to the key will be replaced. allow_missing_keys: do not raise exception if key is missing. """ From 641137314eb9b33d452f3cdb5c43f40d97aed05a Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Tue, 31 May 2022 02:53:15 +0000 Subject: [PATCH 18/23] Update the min verison for skimage to 0.19.0 Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- monai/transforms/intensity/array.py | 4 ++-- monai/transforms/intensity/dictionary.py | 3 ++- requirements-dev.txt | 2 +- tests/test_foreground_mask.py | 4 ++-- tests/test_foreground_maskd.py | 4 ++-- 5 files changed, 9 insertions(+), 8 deletions(-) diff --git a/monai/transforms/intensity/array.py b/monai/transforms/intensity/array.py index c4fb09a64b..e80f5bcb97 100644 --- a/monai/transforms/intensity/array.py +++ b/monai/transforms/intensity/array.py @@ -32,10 +32,10 @@ from monai.utils.deprecate_utils import deprecated_arg from monai.utils.enums import TransformBackends from monai.utils.misc import ensure_tuple, ensure_tuple_rep, ensure_tuple_size, fall_back_tuple -from monai.utils.module import optional_import +from monai.utils.module import min_version, optional_import from monai.utils.type_conversion import convert_data_type, convert_to_dst_type, convert_to_tensor, get_equivalent_dtype -skimage, _ = optional_import("skimage") +skimage, _ = optional_import("skimage", "0.19.0", min_version) __all__ = [ "RandGaussianNoise", diff --git a/monai/transforms/intensity/dictionary.py b/monai/transforms/intensity/dictionary.py index eae603df4a..25cf261fe1 100644 --- a/monai/transforms/intensity/dictionary.py +++ b/monai/transforms/intensity/dictionary.py @@ -1668,7 +1668,8 @@ class ForegroundMaskd(MapTransform): keys: keys of the corresponding items to be transformed. threshold: an int or a float number that defines the threshold that values less than that are foreground. It also can be a callable that receives each dimension of the image and calculate the threshold, - or a string the defines such callable from `skimage.filter.threshold_xxxx`. + or a string that defines such callable from `skimage.filter.threshold_...`. For the list of available + threshold functions, please refer to https://scikit-image.org/docs/stable/api/skimage.filters.html Moreover, a dictionary can be passed that defines such thresholds for each channel, like {"R": 100, "G": "otsu", "B": skimage.filter.threshold_mean} hsv_threshold: similar to threshold but HSV color space ("H", "S", and "V"). diff --git a/requirements-dev.txt b/requirements-dev.txt index ac8b3730d8..7bc06b8039 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -7,7 +7,7 @@ itk>=5.2 nibabel pillow!=8.3.0 # https://github.com/python-pillow/Pillow/issues/5571 tensorboard -scikit-image>=0.14.2 +scikit-image>=0.19.0 tqdm>=4.47.0 lmdb flake8>=3.8.1 diff --git a/tests/test_foreground_mask.py b/tests/test_foreground_mask.py index 0a511fe5b0..60038cd1b1 100644 --- a/tests/test_foreground_mask.py +++ b/tests/test_foreground_mask.py @@ -15,10 +15,10 @@ from parameterized import parameterized from monai.transforms.intensity.array import ForegroundMask -from monai.utils import optional_import, set_determinism +from monai.utils import min_version, optional_import, set_determinism from tests.utils import TEST_NDARRAYS, assert_allclose -skimage, has_skimage = optional_import("skimage") +skimage, has_skimage = optional_import("skimage", "0.19.0", min_version) set_determinism(1234) A = np.random.randint(64, 128, (3, 3, 2)).astype(np.uint8) diff --git a/tests/test_foreground_maskd.py b/tests/test_foreground_maskd.py index bf1df76af4..c6b2f00e37 100644 --- a/tests/test_foreground_maskd.py +++ b/tests/test_foreground_maskd.py @@ -15,10 +15,10 @@ from parameterized import parameterized from monai.transforms.intensity.dictionary import ForegroundMaskd -from monai.utils import optional_import, set_determinism +from monai.utils import min_version, optional_import, set_determinism from tests.utils import TEST_NDARRAYS, assert_allclose -skimage, has_skimage = optional_import("skimage") +skimage, has_skimage = optional_import("skimage", "0.19.0", min_version) set_determinism(1234) A = np.random.randint(64, 128, (3, 3, 2)).astype(np.uint8) From 1b0b8c44553f45aa80209c98e88684a837444f3b Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Tue, 31 May 2022 12:50:06 +0000 Subject: [PATCH 19/23] Added 3D image test case Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- tests/test_foreground_mask.py | 6 ++++++ tests/test_foreground_maskd.py | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/tests/test_foreground_mask.py b/tests/test_foreground_mask.py index 60038cd1b1..90145d67c9 100644 --- a/tests/test_foreground_mask.py +++ b/tests/test_foreground_mask.py @@ -22,9 +22,13 @@ set_determinism(1234) A = np.random.randint(64, 128, (3, 3, 2)).astype(np.uint8) +A3D = np.random.randint(64, 128, (3, 3, 2, 2)).astype(np.uint8) B = np.ones_like(A[:1]) +B3D = np.ones_like(A3D[:1]) MASK = np.pad(B, ((0, 0), (2, 2), (2, 2)), constant_values=0) +MASK3D = np.pad(B3D, ((0, 0), (2, 2), (2, 2), (2, 2)), constant_values=0) IMAGE1 = np.pad(A, ((0, 0), (2, 2), (2, 2)), constant_values=255) +IMAGE3D = np.pad(A3D, ((0, 0), (2, 2), (2, 2), (2, 2)), constant_values=255) IMAGE2 = np.copy(IMAGE1) IMAGE2[0] = 0 IMAGE3 = np.pad(A, ((0, 0), (2, 2), (2, 2)), constant_values=0) @@ -42,6 +46,7 @@ TEST_CASE_11 = [{"threshold": None, "hsv_threshold": "otsu"}, IMAGE1, np.ones_like(MASK)] TEST_CASE_12 = [{"threshold": None, "hsv_threshold": {"S": "otsu"}}, IMAGE1, MASK] TEST_CASE_13 = [{"threshold": 100, "invert": True}, IMAGE1, np.logical_not(MASK)] +TEST_CASE_14 = [{}, IMAGE3D, MASK3D] TEST_CASE_ERROR_1 = [{"threshold": None}, IMAGE1] TEST_CASE_ERROR_2 = [{"threshold": {"K": 1}}, IMAGE1] @@ -61,6 +66,7 @@ TESTS.append([p, *TEST_CASE_11]) TESTS.append([p, *TEST_CASE_12]) TESTS.append([p, *TEST_CASE_13]) + TESTS.append([p, *TEST_CASE_14]) TESTS_ERROR = [] for p in TEST_NDARRAYS: diff --git a/tests/test_foreground_maskd.py b/tests/test_foreground_maskd.py index c6b2f00e37..c17d8eeca2 100644 --- a/tests/test_foreground_maskd.py +++ b/tests/test_foreground_maskd.py @@ -22,9 +22,13 @@ set_determinism(1234) A = np.random.randint(64, 128, (3, 3, 2)).astype(np.uint8) +A3D = np.random.randint(64, 128, (3, 3, 2, 2)).astype(np.uint8) B = np.ones_like(A[:1]) +B3D = np.ones_like(A3D[:1]) MASK = np.pad(B, ((0, 0), (2, 2), (2, 2)), constant_values=0) +MASK3D = np.pad(B3D, ((0, 0), (2, 2), (2, 2), (2, 2)), constant_values=0) IMAGE1 = np.pad(A, ((0, 0), (2, 2), (2, 2)), constant_values=255) +IMAGE3D = np.pad(A3D, ((0, 0), (2, 2), (2, 2), (2, 2)), constant_values=255) IMAGE2 = np.copy(IMAGE1) IMAGE2[0] = 0 IMAGE3 = np.pad(A, ((0, 0), (2, 2), (2, 2)), constant_values=0) @@ -50,6 +54,7 @@ TEST_CASE_11 = [{"keys": "image", "threshold": None, "hsv_threshold": "otsu"}, {"image": IMAGE1}, np.ones_like(MASK)] TEST_CASE_12 = [{"keys": "image", "threshold": None, "hsv_threshold": {"S": "otsu"}}, {"image": IMAGE1}, MASK] TEST_CASE_13 = [{"keys": "image", "threshold": 100, "invert": True}, {"image": IMAGE1}, np.logical_not(MASK)] +TEST_CASE_14 = [{"keys": "image"}, {"image": IMAGE3D}, MASK3D] TEST_CASE_ERROR_1 = [{"keys": "image", "threshold": None}, {"image": IMAGE1}] TEST_CASE_ERROR_2 = [{"keys": "image", "threshold": {"K": 1}}, {"image": IMAGE1}] @@ -69,6 +74,7 @@ TESTS.append([p, *TEST_CASE_11]) TESTS.append([p, *TEST_CASE_12]) TESTS.append([p, *TEST_CASE_13]) + TESTS.append([p, *TEST_CASE_14]) TESTS_ERROR = [] for p in TEST_NDARRAYS: From 2520c60f4bcb0a211867879607b446c3c050fb25 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Tue, 31 May 2022 12:55:48 +0000 Subject: [PATCH 20/23] Add another 3D test case Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- tests/test_foreground_mask.py | 3 +++ tests/test_foreground_maskd.py | 3 +++ 2 files changed, 6 insertions(+) diff --git a/tests/test_foreground_mask.py b/tests/test_foreground_mask.py index 90145d67c9..c18e87fe53 100644 --- a/tests/test_foreground_mask.py +++ b/tests/test_foreground_mask.py @@ -47,6 +47,8 @@ TEST_CASE_12 = [{"threshold": None, "hsv_threshold": {"S": "otsu"}}, IMAGE1, MASK] TEST_CASE_13 = [{"threshold": 100, "invert": True}, IMAGE1, np.logical_not(MASK)] TEST_CASE_14 = [{}, IMAGE3D, MASK3D] +TEST_CASE_15 = [{"hsv_threshold": {"S": 0.1}}, IMAGE3D, MASK3D] + TEST_CASE_ERROR_1 = [{"threshold": None}, IMAGE1] TEST_CASE_ERROR_2 = [{"threshold": {"K": 1}}, IMAGE1] @@ -67,6 +69,7 @@ TESTS.append([p, *TEST_CASE_12]) TESTS.append([p, *TEST_CASE_13]) TESTS.append([p, *TEST_CASE_14]) + TESTS.append([p, *TEST_CASE_15]) TESTS_ERROR = [] for p in TEST_NDARRAYS: diff --git a/tests/test_foreground_maskd.py b/tests/test_foreground_maskd.py index c17d8eeca2..3c8aa08d7f 100644 --- a/tests/test_foreground_maskd.py +++ b/tests/test_foreground_maskd.py @@ -55,6 +55,8 @@ TEST_CASE_12 = [{"keys": "image", "threshold": None, "hsv_threshold": {"S": "otsu"}}, {"image": IMAGE1}, MASK] TEST_CASE_13 = [{"keys": "image", "threshold": 100, "invert": True}, {"image": IMAGE1}, np.logical_not(MASK)] TEST_CASE_14 = [{"keys": "image"}, {"image": IMAGE3D}, MASK3D] +TEST_CASE_15 = [{"keys": "image", "hsv_threshold": {"S": 0.1}}, {"image": IMAGE3D}, MASK3D] + TEST_CASE_ERROR_1 = [{"keys": "image", "threshold": None}, {"image": IMAGE1}] TEST_CASE_ERROR_2 = [{"keys": "image", "threshold": {"K": 1}}, {"image": IMAGE1}] @@ -75,6 +77,7 @@ TESTS.append([p, *TEST_CASE_12]) TESTS.append([p, *TEST_CASE_13]) TESTS.append([p, *TEST_CASE_14]) + TESTS.append([p, *TEST_CASE_15]) TESTS_ERROR = [] for p in TEST_NDARRAYS: From 2c1b868a74262b30aa28187afa1457441235ca34 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Tue, 31 May 2022 14:06:11 +0000 Subject: [PATCH 21/23] Add enums Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- monai/utils/__init__.py | 1 + monai/utils/enums.py | 12 ++++++++++++ 2 files changed, 13 insertions(+) diff --git a/monai/utils/__init__.py b/monai/utils/__init__.py index e7ecab077d..2c51f9f1a1 100644 --- a/monai/utils/__init__.py +++ b/monai/utils/__init__.py @@ -32,6 +32,7 @@ MetricReduction, NumpyPadMode, PostFix, + ProbMapKeys, PytorchPadMode, SkipMode, TraceKeys, diff --git a/monai/utils/enums.py b/monai/utils/enums.py index af044f30fe..d75546cb0e 100644 --- a/monai/utils/enums.py +++ b/monai/utils/enums.py @@ -329,3 +329,15 @@ class BoxModeName(Enum): XYZWHD = "xyzwhd" # [xmin, ymin, zmin, xsize, ysize, zsize] CCWH = "ccwh" # [xcenter, ycenter, xsize, ysize] CCCWHD = "cccwhd" # [xcenter, ycenter, zcenter, xsize, ysize, zsize] + + +class ProbMapKeys(Enum): + """ + The keys to be used for generating the probability maps from patches + """ + + LOCATION = "mask_location" + SIZE = "mask_size" + COUNT = "num_patches" + PATH = "path" + PRE_PATH = "image" From 18f66841debba5ea156c84dc2b8b44cef9fbe5fc Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Tue, 31 May 2022 14:43:57 +0000 Subject: [PATCH 22/23] Remove a threshold if it explicitly set to None Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- monai/transforms/intensity/array.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/monai/transforms/intensity/array.py b/monai/transforms/intensity/array.py index e80f5bcb97..43ed2df62a 100644 --- a/monai/transforms/intensity/array.py +++ b/monai/transforms/intensity/array.py @@ -2205,12 +2205,14 @@ def __init__( self._set_threshold(hsv_threshold, "S") self._set_threshold(hsv_threshold, "V") - self.invert = invert + 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." ) + self.invert = invert + def _set_threshold(self, threshold, mode): if callable(threshold): self.thresholds[mode] = threshold @@ -2233,14 +2235,13 @@ def __call__(self, image: NdarrayOrTensor): img_rgb, *_ = convert_data_type(image, np.ndarray) if self.invert: img_rgb = skimage.util.invert(img_rgb) - foregrounds = [] if not self.thresholds.keys().isdisjoint(set("RGB")): rgb_foreground = np.zeros_like(img_rgb[:1]) for img, mode in zip(img_rgb, "RGB"): threshold = self._get_threshold(img, mode) if threshold: - rgb_foreground |= img <= threshold + rgb_foreground = np.logical_or(rgb_foreground, img <= threshold) foregrounds.append(rgb_foreground) if not self.thresholds.keys().isdisjoint(set("HSV")): img_hsv = skimage.color.rgb2hsv(img_rgb, channel_axis=0) @@ -2248,7 +2249,7 @@ def __call__(self, image: NdarrayOrTensor): for img, mode in zip(img_hsv, "HSV"): threshold = self._get_threshold(img, mode) if threshold: - hsv_foreground |= img > threshold + hsv_foreground = np.logical_or(hsv_foreground, img > threshold) foregrounds.append(hsv_foreground) mask = np.stack(foregrounds).all(axis=0) From 5391350f66e65f56e376dea731403a246d646f0e Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Tue, 31 May 2022 14:44:08 +0000 Subject: [PATCH 23/23] Add image for the transform Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- docs/source/transforms.rst | 4 ++++ monai/transforms/utils_create_transform_ims.py | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/docs/source/transforms.rst b/docs/source/transforms.rst index 854fd4dbb4..74bd78eef1 100644 --- a/docs/source/transforms.rst +++ b/docs/source/transforms.rst @@ -449,6 +449,8 @@ Intensity `ForegroundMask` """""""""""""""" +.. image:: https://github.com/Project-MONAI/DocImages/raw/main/transforms/ForegroundMask.png + :alt: example of ForegroundMask .. autoclass:: ForegroundMask :members: :special-members: __call__ @@ -1336,6 +1338,8 @@ Intensity (Dict) `ForegroundMaskd` """"""""""""""""" +.. image:: https://github.com/Project-MONAI/DocImages/raw/main/transforms/ForegroundMaskd.png + :alt: example of ForegroundMaskd .. autoclass:: ForegroundMaskd :members: :special-members: __call__ diff --git a/monai/transforms/utils_create_transform_ims.py b/monai/transforms/utils_create_transform_ims.py index b096e1b93d..6165496599 100644 --- a/monai/transforms/utils_create_transform_ims.py +++ b/monai/transforms/utils_create_transform_ims.py @@ -85,6 +85,7 @@ ) from monai.transforms.intensity.array import ( AdjustContrast, + ForegroundMask, GaussianSharpen, GaussianSmooth, GibbsNoise, @@ -115,6 +116,7 @@ ) from monai.transforms.intensity.dictionary import ( AdjustContrastd, + ForegroundMaskd, GaussianSharpend, GaussianSmoothd, GibbsNoised, @@ -584,6 +586,8 @@ def create_transform_im( create_transform_im( MaskIntensityd, dict(keys=CommonKeys.IMAGE, mask_key=CommonKeys.IMAGE, select_fn=lambda x: x > 0.3), data ) + create_transform_im(ForegroundMask, dict(invert=True), data) + create_transform_im(ForegroundMaskd, dict(keys=CommonKeys.IMAGE, invert=True), data) create_transform_im(GaussianSmooth, dict(sigma=2), data) create_transform_im(GaussianSmoothd, dict(keys=CommonKeys.IMAGE, sigma=2), data) create_transform_im(RandGaussianSmooth, dict(prob=1.0, sigma_x=(1, 2)), data)