Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
2b2a31e
Implement foreground mask
bhashemian May 26, 2022
381ac73
Add unittests for foreground mask
bhashemian May 26, 2022
ba45b62
Add several test cases
bhashemian May 26, 2022
582caf7
Merge branch 'dev' of github.com:Project-MONAI/MONAI into tissue-mask
bhashemian May 26, 2022
7294c87
Add ForegroundMaskd
bhashemian May 26, 2022
cb86da5
Add unittests for ForegroundMaskd
bhashemian May 26, 2022
a2feb79
Update init
bhashemian May 26, 2022
d271d98
Update docstring
bhashemian May 26, 2022
aff1005
Merge branch 'dev' of github.com:Project-MONAI/MONAI into tissue-mask
bhashemian May 26, 2022
be4a7d5
Merge branch 'dev' of github.com:Project-MONAI/MONAI into tissue-mask
bhashemian May 28, 2022
32313ec
Update to less or equal for RGB threshold
bhashemian May 28, 2022
4771712
Update RGB and HSV mask combination
bhashemian May 28, 2022
7a72eb9
Add support for torch.Tensor
bhashemian May 28, 2022
d393e35
Update docsting and dict tests
bhashemian May 28, 2022
8902a8a
Add docs
bhashemian May 28, 2022
5209c52
Merge branch 'dev' into tissue-mask
bhashemian May 28, 2022
4a8aa14
Add skipunless for skimage
bhashemian May 29, 2022
33fc743
Exclude form min tests
bhashemian May 29, 2022
aa11706
fix a typo
bhashemian May 29, 2022
558314a
Merge branch 'dev' into tissue-mask
bhashemian May 30, 2022
48aece9
Merge branch 'dev' into tissue-mask
bhashemian May 30, 2022
de94b00
Update no or wrong threshold cases
bhashemian May 30, 2022
2849958
Merge branch 'dev' of github.com:Project-MONAI/MONAI into tissue-mask
bhashemian May 30, 2022
ebde248
Update docstring
bhashemian May 30, 2022
b84b63b
Merge branch 'dev' of github.com:Project-MONAI/MONAI into tissue-mask
bhashemian May 30, 2022
6e860a3
Merge branch 'dev' into tissue-mask
bhashemian May 31, 2022
deaab9e
pMerge branch 'tissue-mask' of github.com:behxyz/MONAI into tissue-mask
bhashemian May 31, 2022
6411373
Update the min verison for skimage to 0.19.0
bhashemian May 31, 2022
44604e6
Merge branch 'dev' into tissue-mask
bhashemian May 31, 2022
1b0b8c4
Added 3D image test case
bhashemian May 31, 2022
2520c60
Add another 3D test case
bhashemian May 31, 2022
2c1b868
Add enums
bhashemian May 31, 2022
7ee299f
Merge branch 'dev' into tissue-mask
bhashemian May 31, 2022
18f6684
Remove a threshold if it explicitly set to None
bhashemian May 31, 2022
5391350
Add image for the transform
bhashemian May 31, 2022
4537a16
Merge dev
bhashemian May 31, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions docs/source/transforms.rst
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,15 @@ Intensity
:members:
:special-members: __call__


`ForegroundMask`
""""""""""""""""
.. image:: https://github.com/Project-MONAI/DocImages/raw/main/transforms/ForegroundMask.png
:alt: example of ForegroundMask
.. autoclass:: ForegroundMask
:members:
:special-members: __call__

IO
^^

Expand Down Expand Up @@ -1339,6 +1348,13 @@ Intensity (Dict)
:members:
:special-members: __call__

`ForegroundMaskd`
"""""""""""""""""
.. image:: https://github.com/Project-MONAI/DocImages/raw/main/transforms/ForegroundMaskd.png
:alt: example of ForegroundMaskd
.. autoclass:: ForegroundMaskd
:members:
:special-members: __call__

IO (Dict)
^^^^^^^^^
Expand Down
4 changes: 4 additions & 0 deletions monai/transforms/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@
from .intensity.array import (
AdjustContrast,
DetectEnvelope,
ForegroundMask,
GaussianSharpen,
GaussianSmooth,
GibbsNoise,
Expand Down Expand Up @@ -117,6 +118,9 @@
AdjustContrastd,
AdjustContrastD,
AdjustContrastDict,
ForegroundMaskd,
ForegroundMaskD,
ForegroundMaskDict,
GaussianSharpend,
GaussianSharpenD,
GaussianSharpenDict,
Expand Down
113 changes: 103 additions & 10 deletions monai/transforms/intensity/array.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
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
Expand All @@ -29,17 +29,13 @@
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.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", "0.19.0", min_version)

__all__ = [
"RandGaussianNoise",
Expand Down Expand Up @@ -2161,3 +2157,100 @@ 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):
Comment thread
bhashemian marked this conversation as resolved.
"""
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 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 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}
Comment thread
bhashemian marked this conversation as resolved.
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.

"""

backend = [TransformBackends.TORCH, TransformBackends.NUMPY]

def __init__(
self,
threshold: Union[Dict, Callable, str, float, int] = "otsu",
hsv_threshold: Optional[Union[Dict, Callable, str, float, int]] = None,
invert: bool = False,
) -> None:
self.thresholds: Dict[str, Union[Callable, float]] = {}
if threshold is not None:
if isinstance(threshold, dict):
for mode, th in threshold.items():
self._set_threshold(th, mode.upper())
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 hsv_threshold.items():
self._set_threshold(th, mode.upper())
else:
self._set_threshold(hsv_threshold, "H")
self._set_threshold(hsv_threshold, "S")
self._set_threshold(hsv_threshold, "V")

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
elif isinstance(threshold, str):
self.thresholds[mode] = getattr(skimage.filters, "threshold_" + threshold.lower())
elif isinstance(threshold, (float, int)):
self.thresholds[mode] = float(threshold)
else:
raise ValueError(
f"`threshold` should be either a callable, string, or float number, {type(threshold)} was given."
)

def _get_threshold(self, image, mode):
threshold = self.thresholds.get(mode)
if callable(threshold):
return threshold(image)
return threshold

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:
Comment thread
Nic-Ma marked this conversation as resolved.
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)
hsv_foreground = np.zeros_like(img_rgb[:1])
for img, mode in zip(img_hsv, "HSV"):
threshold = self._get_threshold(img, mode)
if threshold:
hsv_foreground = np.logical_or(hsv_foreground, img > threshold)
foregrounds.append(hsv_foreground)

mask = np.stack(foregrounds).all(axis=0)
return convert_to_dst_type(src=mask, dst=image)[0]
51 changes: 51 additions & 0 deletions monai/transforms/intensity/dictionary.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from monai.config.type_definitions import NdarrayOrTensor
from monai.transforms.intensity.array import (
AdjustContrast,
ForegroundMask,
GaussianSharpen,
GaussianSmooth,
GibbsNoise,
Expand Down Expand Up @@ -88,6 +89,7 @@
"RandCoarseDropoutd",
"RandCoarseShuffled",
"HistogramNormalized",
"ForegroundMaskd",
"RandGaussianNoiseD",
"RandGaussianNoiseDict",
"ShiftIntensityD",
Expand Down Expand Up @@ -146,6 +148,8 @@
"HistogramNormalizeDict",
"RandKSpaceSpikeNoiseD",
"RandKSpaceSpikeNoiseDict",
"ForegroundMaskD",
"ForegroundMaskDict",
]

DEFAULT_POST_FIX = PostFix.meta()
Expand Down Expand Up @@ -1654,6 +1658,52 @@ def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> Dict[Hashable, N
return d


class ForegroundMaskd(MapTransform):
"""
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 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 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").
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.
Comment thread
bhashemian marked this conversation as resolved.
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.

"""

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
Expand Down Expand Up @@ -1683,3 +1733,4 @@ def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> Dict[Hashable, N
RandCoarseDropoutD = RandCoarseDropoutDict = RandCoarseDropoutd
HistogramNormalizeD = HistogramNormalizeDict = HistogramNormalized
RandCoarseShuffleD = RandCoarseShuffleDict = RandCoarseShuffled
ForegroundMaskD = ForegroundMaskDict = ForegroundMaskd
4 changes: 4 additions & 0 deletions monai/transforms/utils_create_transform_ims.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@
)
from monai.transforms.intensity.array import (
AdjustContrast,
ForegroundMask,
GaussianSharpen,
GaussianSmooth,
GibbsNoise,
Expand Down Expand Up @@ -115,6 +116,7 @@
)
from monai.transforms.intensity.dictionary import (
AdjustContrastd,
ForegroundMaskd,
GaussianSharpend,
GaussianSmoothd,
GibbsNoised,
Expand Down Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions monai/utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
MetricReduction,
NumpyPadMode,
PostFix,
ProbMapKeys,
PytorchPadMode,
SkipMode,
TraceKeys,
Expand Down
12 changes: 12 additions & 0 deletions monai/utils/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,18 @@ class BoxModeName(Enum):
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"


class GridPatchSort(Enum):
"""
The sorting method for the generated patches in `GridPatch`
Expand Down
2 changes: 1 addition & 1 deletion requirements-dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions tests/min_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading