Source code for phenotypic.refine._mask_closer

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 closing


[docs] class MaskCloser(ObjectRefiner, FootprintMixin): """Bridge small gaps and fill holes in colony masks using morphological closing. Applies binary closing (dilation then erosion) to reconnect nearby fragments of the same colony separated by thin background channels. Preserves overall colony shape and size while reducing fragmentation. Args: shape: Structuring element. ``'auto'``, ``'disk'``, ``'square'``, ``'diamond'``, or custom ndarray. Default: ``None``. width: Footprint width in pixels. Larger values bridge wider gaps but risk merging distinct colonies. Typical range: 3--9. Default: 5. n_iter: Number of closing iterations. Default: 1. Returns: Image: Input image with ``objmask`` and ``objmap`` morphologically closed. Best For: - Colonies fragmented by uneven pigmentation or shadow effects. - Small internal holes from condensation or glare. - Reconnecting nearby fragments before measurement. Consider Also: - :class:`MaskFill` for filling enclosed holes without bridging separate objects. - :class:`MaskOpener` for the opposite effect — breaking thin connections between distinct colonies. - :class:`NearestNeighborMerger` for merging distant fragments based on proximity. See Also: :doc:`/how_to/notebooks/merge_fragmented_detections` for fragment merging strategies. """
[docs] def __init__( self, shape: Literal[ "auto", "square", "diamond", "disk"] | np.ndarray | None = None, width: int = 5, n_iter: int = 1, ): """Initialize the closer. Args: shape (Literal["auto", "square", "diamond", "disk"] | np.ndarray | None): Structuring element for closing. Use: - "auto" to select a disk shape scaled to image size (larger plates → slightly larger width), - a NumPy array to pass a custom shape, - one of the named shapes ("disk", "square", "diamond") with a specified width, - or ``None`` to use the library default. Larger widths fill wider gaps and smoother colony boundaries, but risk merging adjacent colonies and losing edge sharpness. width (int): Footprint width in pixels when using named shapes or auto-scaling. Default: 5 pixels (moderate gap-filling). n_iter (int): Number of times to apply closing. Repeated closing with a small element produces smoother results than a single pass with a larger element. Default: 1. """ super().__init__() self.shape = shape self.width = width self.n_iter = n_iter
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