Source code for phenotypic.refine._circularity_modifier

from __future__ import annotations

from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from phenotypic._core._image import Image

import pandas as pd
import numpy as np
from skimage.measure import regionprops_table
import math

from ..abc_ import ObjectRefiner
from ..tools_.constants_ import OBJECT


[docs] class LowCircularityRemover(ObjectRefiner): """Remove objects whose Polsby-Popper circularity falls below a cutoff. Computes circularity as ``4 * pi * area / perimeter^2`` for each labeled object and discards those below the threshold. Keeps well-formed, roughly circular colonies while filtering out elongated artifacts, merged blobs, and segmentation debris. Args: cutoff: Minimum Polsby-Popper circularity in ``[0, 1]`` required to retain an object. Typical range: 0.5--0.9. Higher values keep only near-circular shapes; lower values tolerate irregular morphologies. Default: 0.785. Returns: Image: Input image with ``objmap`` and ``objmask`` updated to exclude objects below the circularity cutoff. Raises: ValueError: If ``cutoff`` is outside ``[0, 1]``. Best For: - Post-threshold cleanup to exclude elongated scratches or merged colonies before phenotyping. - Enforcing morphology consistency in high-throughput grid assays. - Plates with round yeast or bacterial colonies where irregular detections indicate artifacts. Consider Also: - :class:`SmallObjectRemover` when artifacts are distinguished by size rather than shape. - :class:`BorderObjectRemover` when irregular detections cluster near plate edges. - :class:`MaskOpener` for smoothing jagged boundaries before circularity filtering. See Also: :doc:`/how_to/notebooks/refine_noisy_boundaries` for shape-based cleanup workflows. :doc:`/explanation/refinement_strategies` for a comparison of morphological refinement methods. """
[docs] def __init__(self, cutoff: float = 0.785): """Initialize the remover. Args: cutoff (float): Minimum allowed circularity in [0, 1]. Increasing the cutoff favors compact, round objects (often cleaner masks), whereas lowering it retains irregular colonies but may keep more debris or merged objects. Raises: ValueError: If ``cutoff`` is outside [0, 1]. """ if cutoff < 0 or cutoff > 1: raise ValueError("threshold should be a number between 0 and 1.") self.cutoff = cutoff
def _operate(self, image: Image) -> Image: # Create intial measurement table table = ( pd.DataFrame( regionprops_table( label_image=image.objmap[:], intensity_image=image.gray[:], properties=["label", "area", "perimeter"], ) ) .rename(columns={"label": OBJECT.LABEL}) .set_index(OBJECT.LABEL) ) # Calculate circularity based on Polsby-Popper Score table["circularity"] = (4 * math.pi * table["area"]) / (table["perimeter"] ** 2) passing_objects = table[table["circularity"] > self.cutoff] failed_object_boolean_indices = ~( np.isin( element=image.objmap[:], test_elements=passing_objects.index.to_numpy() ) ) image.objmap[failed_object_boolean_indices] = 0 return image