Source code for phenotypic.refine._border_object_modifier

from __future__ import annotations

from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from phenotypic import Image

import numpy as np
from typing import Optional, Union

from phenotypic.abc_ import ObjectRefiner


[docs] class BorderObjectRemover(ObjectRefiner): """Remove objects that touch the image border within a configurable margin. Intuition: Colonies that intersect the plate boundary or image crop edge are often partial, poorly segmented, and bias size/shape measurements. This operation zeroes any labeled objects in ``image.objmap`` whose pixels fall within a user-defined border band, ensuring only fully contained colonies are analyzed. Why this is useful for agar-plate imaging: Plate crops or grid layouts frequently clip edge colonies. Removing border-touching objects stabilizes downstream phenotyping (area, circularity, intensity) and prevents partial colonies from contaminating statistics or training data. Use cases: - Single-plate captures where the plate rim truncates colonies. - Grid assays where wells or positions near the frame boundary are partially visible. - Automated crops that shift slightly between frames, cutting off colonies at the margins. Caveats: - A large border may remove valid edge colonies (lower recall). Too small a border may retain partial objects. - With very tight crops (little background), even modest margins can eliminate many colonies. Attributes: border_size (int): Width of the exclusion border around the image. Examples: .. dropdown:: Remove objects that touch the image border within a margin >>> from phenotypic.refine import BorderObjectRemover >>> op = BorderObjectRemover(border_size=15) >>> image = op.apply(image, inplace=True) # doctest: +SKIP >>> # All colonies intersecting a 15-pixel frame margin are removed Raises: TypeError: If an invalid ``border_size`` type is provided (raised during operation when parameters are validated). """
[docs] def __init__(self, border_size: Optional[Union[int, float]] = 1): """Initialize the remover. Args: border_size: Width of the exclusion border around the image. - ``None``: Use a default margin equal to 1% of the smaller image dimension. - ``float`` in (0, 1): Interpret as a fraction of the minimum image dimension, producing a resolution-adaptive margin. - ``int`` or ``float`` ≥ 1: Interpret as an absolute number of pixels. Notes: Larger margins remove more edge-touching colonies and are useful when crops are loose or the plate rim intrudes. Smaller margins preserve edge colonies but risk including partial objects. """ self.border_size = border_size
def _operate(self, image: Image) -> Image: if self.border_size is None: edge_size = int(np.min(image.shape[[1, 2]]) * 0.01) elif type(self.border_size) == float and 0.0 < self.border_size < 1.0: edge_size = int(np.min(image.shape) * self.border_size) elif isinstance(self.border_size, (int, float)): edge_size = self.border_size else: raise TypeError( "Invalid edge size. Should be int, float, or None to use default edge size." ) obj_map = image.objmap[:] edges = [ obj_map[: edge_size - 1, :].ravel(), obj_map[-edge_size:, :].ravel(), obj_map[:, : edge_size - 1].ravel(), obj_map[:, -edge_size:].ravel(), ] edge_labels = np.unique(np.concatenate(edges)) for label in edge_labels: obj_map[obj_map == label] = 0 image.objmap = obj_map return image