Source code for phenotypic.measure._measure_bounds
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from phenotypic._core._image import Image
import numpy as np
import pandas as pd
import scipy.ndimage as ndi
from skimage.measure import regionprops_table
from phenotypic.abc_ import MeasureFeatures
from ..tools_.constants_ import OBJECT
from ..tools_.measurement_info_ import BBOX
[docs]
class MeasureBounds(MeasureFeatures):
"""Extract bounding box coordinates and centroids of detected colonies.
Compute the axis-aligned bounding box and centroid (geometric and
intensity-weighted) for each detected colony. These spatial
measurements form the foundation for region-of-interest extraction,
grid alignment assessment, and neighbor-distance calculations.
Returns:
pd.DataFrame: Object-level spatial data with columns:
- Label: unique object identifier.
- CenterRR, CenterCC: geometric centroid coordinates.
- IntensityWeightedCenterRR, IntensityWeightedCenterCC:
intensity-weighted centroid coordinates (skimage
``centroid_weighted``).
- DistWeightedCenterRR, DistWeightedCenterCC: row/column of
the per-object distance-transform maximum (deepest interior
point — robust to thin filamentous extensions).
- MinRR, MinCC: top-left corner of bounding box.
- MaxRR, MaxCC: bottom-right corner of bounding box.
Best For:
- Computing centroids for aligning colonies to expected grid
positions in arrayed assays.
- Extracting region-of-interest crops for downstream intensity,
color, or texture analysis.
- Assessing colony positioning relative to plate edges or well
boundaries.
Consider Also:
- :class:`MeasureShape` for full morphological metrics built on
top of bounding box data.
- :class:`MeasureGridLinRegStats` for regression-based grid
alignment quality using centroid positions.
- :class:`MeasureGridSpatial` for neighbor distance calculations
using bounding boxes.
See Also:
:doc:`/tutorials/notebooks/07_measuring_and_exporting` for a
walkthrough of measuring and exporting colony data.
"""
_measurement_info_class = BBOX
def _operate(self, image: Image) -> pd.DataFrame:
objmap = image.objmap[:]
gray = image.gray[:]
results = pd.DataFrame(
data=regionprops_table(
label_image=objmap,
intensity_image=gray,
properties=["label", "centroid", "bbox", "centroid_weighted"]
)
).rename(
columns={
"label" : OBJECT.LABEL,
"centroid-0" : str(BBOX.CENTER_RR),
"centroid-1" : str(BBOX.CENTER_CC),
"centroid_weighted-0": str(BBOX.INTENSITY_WEIGHTED_CENTER_RR),
"centroid_weighted-1": str(BBOX.INTENSITY_WEIGHTED_CENTER_CC),
"bbox-0" : str(BBOX.MIN_RR),
"bbox-1" : str(BBOX.MIN_CC),
"bbox-2" : str(BBOX.MAX_RR),
"bbox-3" : str(BBOX.MAX_CC),
}
)
# Per-object distance-transform maximum (deepest interior point).
# Robust to thin filamentous extensions that pull intensity-weighted
# centroids off-body.
#
# Vectorized via global EDT + ndi.maximum_position. Inter-object
# boundaries are zeroed before the EDT so touching colonies don't
# bleed into each other.
#
# Memory note: the global EDT allocates a full-image float64 array
# (~128 MB at 4000x4000, ~288 MB at 6000x6000). If this becomes a
# bottleneck on large plates, switch to a per-object loop using
# scipy.ndimage.find_objects + per-bbox distance_transform_edt; the
# pattern at _measure_symmetric_zones.py:237-239 is the precedent.
nonzero = objmap > 0
if nonzero.any():
max_label_value = np.iinfo(objmap.dtype).max
masked_for_min = np.where(nonzero, objmap, max_label_value)
nbr_max = ndi.maximum_filter(objmap, size=3)
nbr_min_nz = ndi.minimum_filter(masked_for_min, size=3)
inter_boundary = nonzero & (
(nbr_max > objmap) | (nbr_min_nz < objmap)
)
binary = nonzero & ~inter_boundary
dt = ndi.distance_transform_edt(binary)
labels = np.unique(objmap[nonzero])
positions = ndi.maximum_position(dt, labels=objmap, index=labels)
positions = np.asarray(positions, dtype=float)
dist_df = pd.DataFrame({
OBJECT.LABEL : labels,
str(BBOX.DIST_WEIGHTED_CENTER_RR): positions[:, 0],
str(BBOX.DIST_WEIGHTED_CENTER_CC): positions[:, 1],
})
results = results.merge(dist_df, on=OBJECT.LABEL, how="left")
else:
results[str(BBOX.DIST_WEIGHTED_CENTER_RR)] = np.array([], dtype=float)
results[str(BBOX.DIST_WEIGHTED_CENTER_CC)] = np.array([], dtype=float)
return results
MeasureBounds.__doc__ = BBOX.append_rst_to_doc(MeasureBounds)