Source code for phenotypic.refine._white_tophat_modifier

from __future__ import annotations

from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from phenotypic import Image

import numpy as np
from skimage.morphology import white_tophat

from phenotypic.abc_ import ObjectRefiner


[docs] class WhiteTophatModifier(ObjectRefiner): """Suppress small bright structures in the mask using white tophat. Intuition: White tophat highlights small, bright features relative to their local background. On agar plates, glare, dust, or bright halos can create thin connections or speckles that pollute colony masks. This modifier detects those bright micro-structures and subtracts them from the binary mask to improve separation and mask quality. Why this is useful for agar plates: Bright artifacts can bridge adjacent colonies or inflate perimeters. Removing those tiny bright elements yields cleaner, more compact masks that better match colony boundaries under uneven illumination. Use cases: - Reducing glare-induced bridges between neighboring colonies. - Removing bright speckles/dust that become embedded in masks after thresholding. Caveats: - Large footprints may remove real bright edges of colonies (e.g., highly reflective rims), slightly eroding edge sharpness. - If the footprint is too small, bright artifacts may remain. Attributes: footprint_shape (str): Shape for the footprint used in the tophat transform. Supported: 'disk', 'square'. Disk tends to preserve round features, while square can be more aggressive along axes. footprint_radius (int | None): Radius of the footprint. Larger values remove broader bright features but risk shrinking thin colony appendages. ``None`` auto-scales with image size. Examples: .. dropdown:: Suppress small bright structures in the mask using white tophat >>> from phenotypic.refine import WhiteTophatModifier >>> op = WhiteTophatModifier(shape='disk', radius=5) >>> image = op.apply(image, inplace=True) # doctest: +SKIP """
[docs] def __init__(self, footprint_shape="disk", footprint_radius: int = None): """Initialize the modifier. Args: footprint_shape (str): Footprint geometry for white tophat. - 'disk': Balanced in all directions; gentle on round colonies. - 'square': Slightly stronger along rows/columns; may remove more rectilinear glare or sensor artifacts. footprint_radius (int | None): Radius in pixels. Increasing removes larger bright structures and can improve background suppression, but may thin colony edges. ``None`` auto-selects ~0.4% of the smaller image dimension. Raises: ValueError: If ``shape`` is not one of the supported values (raised during operation). """ self.footprint_shape = footprint_shape self.footprint_radius = footprint_radius
def _operate(self, image: Image) -> Image: white_tophat_results = white_tophat( image.objmask[:], footprint=self._make_footprint( shape=self.footprint_shape, radius=self._get_footprint_radius(array=image.objmask[:]), ), ) image.objmask[:] = image.objmask[:] & ~white_tophat_results return image def _get_footprint_radius(self, array: np.ndarray) -> int: if self.footprint_radius is None: return int(np.min(array.shape) * 0.004) else: return self.footprint_radius