Source code for phenotypic.refine._mask_opener
from __future__ import annotations
from typing import Literal, TYPE_CHECKING
if TYPE_CHECKING:
from phenotypic import Image
from phenotypic.abc_ import ObjectRefiner
import numpy as np
from skimage.morphology import binary_opening
[docs]
class MaskOpener(ObjectRefiner):
"""Morphologically open binary masks to remove thin connections and specks.
Intuition:
Binary opening (erosion followed by dilation) removes small isolated
pixels and breaks narrow bridges between objects. On agar plates, this
helps separate touching colonies and suppresses tiny artifacts from
dust or condensation without overly shrinking well-formed colonies.
Why this is useful for agar plates:
Colonies may develop halos or be linked by faint film on the agar. A
gentle opening step can restore separated masks, improving count and
phenotype accuracy.
Use cases:
- After thresholding, to split colonies connected by 1–2-pixel bridges.
- To remove tiny noise specks before measuring morphology.
Caveats:
- Too large a footprint erodes small colonies or weakly-stained edges,
lowering recall and edge sharpness.
- Opening can remove thin filaments that are biologically meaningful in
spreading/filamentous phenotypes.
Attributes:
footprint (Literal["auto"] | np.ndarray | int | None): Structuring
element used for opening. A larger or denser footprint removes more
thin connections and specks but risks eroding colony boundaries.
Examples:
.. dropdown:: Morphologically open masks to separate touching colonies
>>> from phenotypic.refine import MaskOpener
>>> op = MaskOpener(footprint='auto')
>>> image = op.apply(image, inplace=True) # doctest: +SKIP
Raises:
AttributeError: If an invalid ``footprint`` type is provided (checked
during operation).
"""
[docs]
def __init__(self, footprint: Literal["auto"] | np.ndarray | int | None = None):
"""Initialize the opener.
Args:
footprint (Literal["auto"] | np.ndarray | int | None): Structuring
element for opening. Use:
- "auto" to select a diamond footprint scaled to image size
(larger plates → slightly larger radius),
- a NumPy array to pass a custom footprint,
- an ``int`` radius to build a diamond footprint of that size,
- or ``None`` to use the library default.
Larger radii disconnect wider bridges and suppress more
speckles, but erode edges and can remove small colonies.
"""
super().__init__()
self.footprint: Literal["auto"] | np.ndarray | int | None = footprint
def _operate(self, image: Image) -> Image:
if self.footprint == "auto":
footprint = self._make_footprint(
"diamond", radius=max(3, round(np.min(image.shape) * 0.005))
)
elif isinstance(self.footprint, np.ndarray):
footprint = self.footprint
elif isinstance(self.footprint, (int, float)):
footprint = self._make_footprint("diamond", radius=int(self.footprint))
elif not self.footprint:
footprint = self.footprint
else:
raise AttributeError("Invalid footprint type")
image.objmask[:] = binary_opening(image.objmask[:], footprint=footprint)
return image