# encoding: utf-8
# ------------------------------------------------------------------------
# Copyright 2022 All Histolab Contributors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ------------------------------------------------------------------------
from abc import abstractmethod
import numpy as np
import scipy.ndimage.morphology
import skimage.morphology
from . import morphological_filters_functional as F
from .image_filters import Filter
try:
from typing import Protocol, runtime_checkable
except ImportError:
from typing_extensions import Protocol, runtime_checkable
[docs]@runtime_checkable
class MorphologicalFilter(Filter, Protocol):
"""Morphological filter base class"""
@abstractmethod
def __call__(self, np_mask: np.ndarray) -> np.ndarray:
pass # pragma: no cover
[docs]class RemoveSmallObjects(MorphologicalFilter):
"""Remove objects smaller than the specified size.
If avoid_overmask is True, this function can recursively call itself with
progressively halved minimum size objects to avoid removing too many
objects in the mask.
Parameters
----------
np_img : np.ndarray (arbitrary shape, int or bool type)
Input mask
min_size : int, optional
Minimum size of small object to remove. Default is 3000
avoid_overmask : bool, optional (default is True)
If True, avoid masking above the overmask_thresh percentage.
overmask_thresh : int, optional (default is 95)
If avoid_overmask is True, avoid masking above this threshold percentage value.
Returns
-------
np.ndarray
Mask with small objects filtered out
Example:
>>> from PIL import Image
>>> from histolab.filters.image_filters import RgbToGrayscale, OtsuThreshold
>>> from histolab.filters.morphological_filters import RemoveSmallObjects
>>> image_rgb = Image.open("tests/fixtures/pil-images-rgb/tcga-lung-rgb.png")
>>> rgb_to_grayscale = RgbToGrayscale()
>>> otsu_threshold = OtsuThreshold()
>>> remove_small_objects = RemoveSmallObjects()
>>> image_gray = rgb_to_grayscale(image_rgb)
>>> binary_image = otsu_threshold(image_gray)
>>> image_no_small_objects = remove_small_objects(binary_image)
""" # noqa
def __init__(
self,
min_size: int = 3000,
avoid_overmask: bool = True,
overmask_thresh: int = 95,
):
self.min_size = min_size
self.avoid_overmask = avoid_overmask
self.overmask_thresh = overmask_thresh
def __call__(self, np_mask: np.ndarray) -> np.ndarray:
return F.remove_small_objects(
np_mask, self.min_size, self.avoid_overmask, self.overmask_thresh
)
[docs]class RemoveSmallHoles(MorphologicalFilter):
"""Remove holes smaller than a specified size.
Parameters
----------
np_img : np.ndarray (arbitrary shape, int or bool type)
Input mask
area_threshold: int, optional (default is 3000)
Remove small holes below this size.
Returns
-------
np.ndarray
Mask with small holes filtered out
Example:
>>> from PIL import Image
>>> from histolab.filters.image_filters import RgbToGrayscale, OtsuThreshold
>>> from histolab.filters.morphological_filters import RemoveSmallHoles
>>> image_rgb = Image.open("tests/fixtures/pil-images-rgb/tcga-lung-rgb.png")
>>> rgb_to_grayscale = RgbToGrayscale()
>>> otsu_threshold = OtsuThreshold()
>>> remove_small_holes = RemoveSmallHoles()
>>> image_gray = rgb_to_grayscale(image_rgb)
>>> binary_image = otsu_threshold(image_gray)
>>> image_no_small_holes = remove_small_holes(binary_image)
""" # noqa
def __init__(self, area_threshold: int = 3000):
self.area_threshold = area_threshold
def __call__(self, np_mask: np.ndarray) -> np.ndarray:
return skimage.morphology.remove_small_holes(np_mask, self.area_threshold)
[docs]class BinaryErosion(MorphologicalFilter):
"""Erode a binary mask.
Parameters
----------
np_mask : np.ndarray (arbitrary shape, int or bool type)
Numpy array of the binary mask
disk_size : int, optional (default is 5)
Radius of the disk structuring element used for erosion.
iterations : int, optional (default is 1)
How many times to repeat the erosion.
Returns
-------
np.ndarray
Mask after the erosion
Example:
>>> from PIL import Image
>>> from histolab.filters.image_filters import RgbToGrayscale, OtsuThreshold
>>> from histolab.filters.morphological_filters import BinaryErosion
>>> image_rgb = Image.open("tests/fixtures/pil-images-rgb/tcga-lung-rgb.png")
>>> rgb_to_grayscale = RgbToGrayscale()
>>> otsu_threshold = OtsuThreshold()
>>> binary_erosion = BinaryErosion(disk_size=6)
>>> image_gray = rgb_to_grayscale(image_rgb)
>>> binary_image = otsu_threshold(image_gray)
>>> image_eroded = binary_erosion(binary_image)
""" # noqa
def __init__(self, disk_size: int = 5, iterations: int = 1):
self.disk_size = disk_size
self.iterations = iterations
def __call__(self, np_mask: np.ndarray) -> np.ndarray:
if not np.array_equal(np_mask, np_mask.astype(bool)):
raise ValueError("Mask must be binary")
return scipy.ndimage.morphology.binary_erosion(
np_mask, skimage.morphology.disk(self.disk_size), self.iterations
)
[docs]class BinaryDilation(MorphologicalFilter):
"""Dilate a binary mask.
Parameters
----------
np_mask : np.ndarray (arbitrary shape, int or bool type)
Numpy array of the binary mask
disk_size : int, optional (default is 5)
Radius of the disk structuring element used for dilation.
iterations : int, optional (default is 1)
How many times to repeat the dilation.
Returns
-------
np.ndarray
Mask after the dilation
Example:
>>> from PIL import Image
>>> from histolab.filters.image_filters import RgbToGrayscale, OtsuThreshold
>>> from histolab.filters.morphological_filters import BinaryDilation
>>> image_rgb = Image.open("tests/fixtures/pil-images-rgb/tcga-lung-rgb.png")
>>> rgb_to_grayscale = RgbToGrayscale()
>>> otsu_threshold = OtsuThreshold()
>>> binary_dilation = BinaryDilation()
>>> image_gray = rgb_to_grayscale(image_rgb)
>>> binary_image = otsu_threshold(image_gray)
>>> image_dilated = binary_dilation(binary_image)
""" # noqa
def __init__(self, disk_size: int = 5, iterations: int = 1):
self.disk_size = disk_size
self.iterations = iterations
def __call__(self, np_mask: np.ndarray) -> np.ndarray:
if not np.array_equal(np_mask, np_mask.astype(bool)):
raise ValueError("Mask must be binary")
return scipy.ndimage.morphology.binary_dilation(
np_mask, skimage.morphology.disk(self.disk_size), self.iterations
)
[docs]class BinaryFillHoles(MorphologicalFilter):
"""Fill the holes in binary objects.
Parameters
----------
np_img : np.ndarray (arbitrary shape, int or bool type)
Numpy array of the binary mask
structure: np.ndarray, optional
Structuring element used in the computation; The default element yields the
intuitive result where all holes in the input have been filled.
Returns
-------
np.ndarray
Transformation of the initial image input where holes have been filled.
Example:
>>> from PIL import Image
>>> from histolab.filters.image_filters import RgbToGrayscale, OtsuThreshold
>>> from histolab.filters.morphological_filters import BinaryFillHoles
>>> image_rgb = Image.open("tests/fixtures/pil-images-rgb/tcga-lung-rgb.png")
>>> rgb_to_grayscale = RgbToGrayscale()
>>> otsu_threshold = OtsuThreshold()
>>> binary_fill_holes = BinaryFillHoles()
>>> image_gray = rgb_to_grayscale(image_rgb)
>>> binary_image = otsu_threshold(image_gray)
>>> image_filled_holes = binary_fill_holes(binary_image)
"""
def __init__(self, structure: np.ndarray = None):
self.structure = structure
def __call__(self, np_img: np.ndarray) -> np.ndarray:
return scipy.ndimage.morphology.binary_fill_holes(np_img, self.structure)
[docs]class BinaryOpening(MorphologicalFilter):
"""Open a binary mask.
Opening is an erosion followed by a dilation. Opening can be used to remove
small objects.
Parameters
----------
np_mask : np.ndarray (arbitrary shape, int or bool type)
Numpy array of the binary mask
disk_size : int, optional (default is 3)
Radius of the disk structuring element used for opening.
iterations : int, optional (default is 1)
How many times to repeat the opening.
Returns
-------
np.ndarray
Mask after the opening
Example:
>>> from PIL import Image
>>> from histolab.filters.image_filters import RgbToGrayscale, OtsuThreshold
>>> from histolab.filters.morphological_filters import BinaryOpening
>>> image_rgb = Image.open("tests/fixtures/pil-images-rgb/tcga-lung-rgb.png")
>>> rgb_to_grayscale = RgbToGrayscale()
>>> otsu_threshold = OtsuThreshold()
>>> binary_opening = BinaryOpening()
>>> image_gray = rgb_to_grayscale(image_rgb)
>>> binary_image = otsu_threshold(image_gray)
>>> image_opened = binary_opening(binary_image)
""" # noqa
def __init__(self, disk_size: int = 3, iterations: int = 1):
self.disk_size = disk_size
self.iterations = iterations
def __call__(self, np_mask: np.ndarray) -> np.ndarray:
if not np.array_equal(np_mask, np_mask.astype(bool)):
raise ValueError("Mask must be binary")
return scipy.ndimage.morphology.binary_opening(
np_mask, skimage.morphology.disk(self.disk_size), self.iterations
)
[docs]class BinaryClosing(MorphologicalFilter):
"""Close a binary mask.
Closing is a dilation followed by an erosion. Closing can be used to remove
small holes.
Parameters
----------
np_mask : np.ndarray (arbitrary shape, int or bool type)
Numpy array of the binary mask
disk_size : int, optional (default is 3)
Radius of the disk structuring element used for closing.
iterations : int, optional (default is 1)
How many times to repeat the closing.
Returns
-------
np.ndarray
Mask after the closing
Example:
>>> from PIL import Image
>>> from histolab.filters.image_filters import RgbToGrayscale, OtsuThreshold
>>> from histolab.filters.morphological_filters import BinaryClosing
>>> image_rgb = Image.open("tests/fixtures/pil-images-rgb/tcga-lung-rgb.png")
>>> rgb_to_grayscale = RgbToGrayscale()
>>> otsu_threshold = OtsuThreshold()
>>> binary_closing = BinaryClosing()
>>> image_gray = rgb_to_grayscale(image_rgb)
>>> binary_image = otsu_threshold(image_gray)
>>> image_closed = binary_closing(binary_image)
""" # noqa
def __init__(self, disk_size: int = 3, iterations: int = 1):
self.disk_size = disk_size
self.iterations = iterations
def __call__(self, np_mask: np.ndarray) -> np.ndarray:
if not np.array_equal(np_mask, np_mask.astype(bool)):
raise ValueError("Mask must be binary")
return scipy.ndimage.morphology.binary_closing(
np_mask, skimage.morphology.disk(self.disk_size), self.iterations
)
[docs]class WatershedSegmentation(MorphologicalFilter):
"""Segment and label an binary mask with Watershed segmentation [1]_
The watershed algorithm treats pixels values as a local topography (elevation).
Parameters
----------
np_mask : np.ndarray
Input mask
region_shape : int, optional
The local region within which to search for image peaks is defined as a squared
area region_shape x region_shape. Default is 6.
Returns
-------
np.ndarray
Labelled segmentation mask
References
--------
.. [1] Watershed segmentation.
https://scikit-image.org/docs/dev/auto_examples/segmentation/plot_watershed.html
Example:
>>> import numpy as np
>>> from histolab.filters.morphological_filters import WatershedSegmentation
>>> mask = np.array([[0,1],[1,0]]) # or np.load("/path/my_array_mask.npy")
>>> watershed_segmentation = WatershedSegmentation()
>>> mask_segmented = watershed_segmentation(mask)
""" # noqa
def __init__(self, region_shape: int = 6) -> None:
self.region_shape = region_shape
def __call__(self, np_mask: np.ndarray) -> np.ndarray:
return F.watershed_segmentation(np_mask, self.region_shape)
[docs]class WhiteTopHat(MorphologicalFilter):
"""Return white top hat of an image.
The white top hat of an image is defined as the image minus its morphological
opening with respect to a structuring element. This operation returns the bright
spots of the image that are smaller than the structuring element.
Parameters
----------
np_mask : np.ndarray (arbitrary shape, int or bool type)
Numpy array of the binary mask
structure : np.ndarray, optional
The neighborhood expressed as an array of 1 and 0. If None, use cross-shaped
structuring element (connectivity=1).
Example:
>>> from PIL import Image
>>> import numpy as np
>>> from histolab.filters.image_filters import RgbToGrayscale, OtsuThreshold
>>> from histolab.filters.morphological_filters import WhiteTopHat
>>> image_rgb = Image.open("tests/fixtures/pil-images-rgb/tcga-lung-rgb.png")
>>> rgb_to_grayscale = RgbToGrayscale()
>>> otsu_threshold = OtsuThreshold()
>>> white_that = WhiteTopHat(np.ones((5,5)))
>>> image_gray = rgb_to_grayscale(image_rgb)
>>> binary_image = otsu_threshold(image_gray)
>>> image_out = white_that(binary_image)
"""
def __init__(self, structure: np.ndarray = None):
self.structure = structure
def __call__(self, np_mask: np.ndarray) -> np.ndarray:
return skimage.morphology.white_tophat(np_mask, self.structure)