Source code for phenotypic.refine._mask_opener
from __future__ import annotations
from typing import Literal, TYPE_CHECKING
if TYPE_CHECKING:
from phenotypic._core._image import Image
from phenotypic.abc_ import ObjectRefiner
from phenotypic.tools_.mixin import FootprintMixin
import numpy as np
from skimage.morphology import opening
from phenotypic.tools_.typing_ import FootprintShape
[docs]
class MaskOpener(ObjectRefiner, FootprintMixin):
"""Smooth mask boundaries and break thin bridges between touching colonies.
Applies binary opening (erosion then dilation) to remove small isolated
pixels and narrow connections. Colonies linked by faint film or agar
artifacts become separated without significantly shrinking well-formed
colony masks.
Args:
shape: Structuring element. ``'auto'`` selects based on detected
objects. ``'diamond'``, ``'square'``, ``'disk'``, or a custom
ndarray. Default: ``None``.
width: Size of the structuring element in pixels. Larger values
smooth more aggressively. Typical range: 3--9. Default: 5.
n_iter: Number of opening iterations. Default: 1.
Returns:
Image: Input image with ``objmask`` and ``objmap`` morphologically
opened.
Best For:
- Splitting colonies connected by 1--2 pixel bridges after thresholding.
- Removing tiny noise specks from the detection mask.
- Smoothing jagged mask edges before measurement.
Consider Also:
- :class:`MaskCloser` for the opposite effect — filling small gaps
and bridging fragments.
- :class:`SmallObjectRemover` for removing small objects by area
rather than morphology.
- :class:`SeparateObjects` for splitting merged colonies using
watershed-based separation.
See Also:
:doc:`/how_to/notebooks/refine_noisy_boundaries` for a walkthrough
of refinement operations.
:doc:`/explanation/refinement_strategies` for the recommended
refinement sequence.
"""
[docs]
def __init__(
self,
shape: Literal["auto"] | FootprintShape | np.ndarray | None = None,
width: int = 5,
n_iter: int = 1,
):
"""Initialize the opener.
Args:
shape (Literal["auto"] | np.ndarray | int | None): Structuring
element for opening. Use:
- "auto" to select a diamond shape scaled to image size
(larger plates → slightly larger width),
- a NumPy array to pass a custom shape,
- an ``int`` width to build a diamond shape of that size,
- or ``None`` to use the library default.
Larger widths disconnect wider bridges and suppress more
speckles, but erode edges and can remove small colonies.
n_iter (int): Number of times to apply opening. Repeated opening
with a small element produces smoother results than a single
pass with a larger element. Default: 1.
"""
super().__init__()
self.shape: Literal["auto"] | FootprintShape | np.ndarray | None = shape
self.width = width
self.n_iter = n_iter
def _operate(self, image: Image) -> Image:
if self.shape == "auto":
footprint = FootprintMixin._make_footprint(
"diamond", width=max(3, round(np.min(image.shape) * 0.005))
)
elif isinstance(self.shape, np.ndarray):
footprint = self.shape
elif self.shape in self._footprint_shapes:
footprint = FootprintMixin._make_footprint(self.shape,
width=int(self.width))
elif not self.shape:
footprint = self.shape
else:
raise AttributeError("Invalid shape type")
for _ in range(self.n_iter):
image.objmask[:] = opening(image.objmask[:], footprint=footprint)
return image