Source code for phenotypic.refine._mask_closing

from __future__ import annotations

from typing import Annotated, Literal, TYPE_CHECKING

if TYPE_CHECKING:
    from phenotypic._core._image import Image

from phenotypic.abc_ import ObjectRefiner
from phenotypic.sdk_.mixin import FootprintMixin
from phenotypic.sdk_.typing_ import NdArrayField, TuneSpec

import numpy as np
from skimage.morphology import closing


[docs] class MaskClosing(ObjectRefiner, FootprintMixin): """Bridge narrow background gaps in colony masks using morphological closing. Applies binary closing (dilation followed by erosion) to the object mask, reconnecting nearby fragments of the same colony that are separated by thin background channels and filling small internal holes. Colony size is largely preserved because the dilation and erosion nearly cancel outside solid regions. For an overview of morphological refinement methods, see :doc:`/explanation/refinement_strategies`. Best For: - Colonies fragmented by uneven pigmentation or shadow-induced background channels across the colony body. - Masks with small internal holes caused by condensation droplets or specular glare on the agar surface. - Pre-measurement cleanup to reconnect fragments before area or shape features are computed. Consider Also: - :class:`MaskFill` for filling enclosed holes within objects without bridging separately labeled colonies. - :class:`MaskOpening` for the opposite effect — removing thin connections between objects that should remain distinct. - :class:`NearestNeighborMerger` for merging distant fragments that are too far apart for morphological closing to bridge. Args: shape: Structuring element shape for the closing footprint. ``"auto"`` scales a disk to the image size; ``"disk"``, ``"square"``, and ``"diamond"`` use named shapes at the given ``width``; a NumPy array provides a custom element; ``None`` uses the skimage library default. Default: ``None``. width: Footprint width in pixels when using a named shape. Larger values bridge wider gaps but risk merging distinct adjacent colonies. Typical range: 3--9. Default: 5. n_iter: Number of closing iterations applied sequentially. Each additional iteration extends the effective reach by one footprint radius. Default: 1. Returns: Image: Input image with ``objmask`` and ``objmap`` morphologically closed. See Also: :doc:`/how_to/notebooks/merge_fragmented_detections` for fragment merging strategies including morphological closing. """ shape: Literal["auto", "square", "diamond", "disk"] | NdArrayField | None = None width: Annotated[int, TuneSpec(3, 9)] = 5 n_iter: Annotated[int, TuneSpec(1, 3)] = 1 def _operate(self, image: Image) -> Image: if self.shape == "auto": footprint = FootprintMixin._make_footprint( "disk", 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=self.width) elif not self.shape: footprint = None else: raise AttributeError("Invalid shape type") for _ in range(self.n_iter): image.objmask[:] = closing(image.objmask[:], footprint=footprint) return image