Source code for phenotypic.tools_.branch_pathfinding._voronoi_partition
"""Euclidean Voronoi partition with connectivity-based fragment correction.
Replaces watershed-based territory assignment with a two-phase approach:
1. Pure Euclidean Voronoi partition (each pixel to nearest seed)
2. Connected-component-based fragment reassignment
"""
from __future__ import annotations
import numpy as np
from scipy.ndimage import distance_transform_edt, label as ndi_label
from scipy.sparse import coo_matrix
from skimage.measure import label as skimage_label
from skimage.segmentation import expand_labels
[docs]
def euclidean_voronoi_assign(
markers: np.ndarray,
mask: np.ndarray,
restrict_to_seeded_cc: bool = True,
) -> np.ndarray:
"""Assign masked pixels to nearest marker via Euclidean Voronoi partition.
Args:
markers: 2D int32 array with seed labels at marker positions.
mask: Binary mask restricting the assignment region.
restrict_to_seeded_cc: When True (default), only mask connected
components that contain at least one seed are labeled;
disconnected mask regions with no seed remain 0. When False,
seeds are used globally — every mask pixel gets the label of
its nearest seed regardless of connectivity.
Returns:
2D int32 labeled array. Each masked pixel has the label of its
nearest marker by Euclidean distance. Pixels outside mask are 0.
When *restrict_to_seeded_cc* is True, pixels in seedless
connected components are also 0.
"""
if mask.dtype != np.bool_:
mask = mask > 0
seed_mask = markers > 0
if restrict_to_seeded_cc:
effective_markers = markers.copy()
effective_markers[~mask] = 0
seed_mask = effective_markers > 0
else:
effective_markers = markers
if not seed_mask.any():
return np.zeros(mask.shape, dtype=np.int32)
_, nearest_idx = distance_transform_edt(~seed_mask, return_indices=True)
voronoi_labels = effective_markers[
nearest_idx[0], nearest_idx[1]
].astype(np.int32)
voronoi_labels[~mask] = 0
if restrict_to_seeded_cc:
cc_map, n_cc = ndi_label(mask) # type: ignore[misc]
seeded_ccs = np.unique(cc_map[seed_mask])
has_seed = np.zeros(n_cc + 1, dtype=bool)
has_seed[seeded_ccs] = True
voronoi_labels[~has_seed[cc_map]] = 0
return voronoi_labels
def _mode(arr: np.ndarray) -> int:
"""Return the most frequent value in a 1-D integer array (ignoring 0)."""
counts = np.bincount(arr)
counts[0] = 0
return int(np.argmax(counts))
[docs]
def connectivity_correct_labels(
voronoi_labels: np.ndarray,
mask: np.ndarray,
markers: np.ndarray,
) -> np.ndarray:
"""Correct Voronoi misassignments using fragment-based iterative fill.
Separates seed pixels from unseeded fragments, then iteratively
assigns each fragment the mode label of its neighborhood until
convergence.
Args:
voronoi_labels: Label map from ``euclidean_voronoi_assign``.
mask: Binary foreground mask (same as used for Voronoi).
markers: Seed marker array (same as used for Voronoi).
Returns:
Corrected label map with fragment reassignments applied.
"""
if mask.dtype != np.bool_:
mask = mask > 0
seed_mask = markers > 0
# Fragment map — per-voronoi-value CC labeling.
# skimage.measure.label connects only same-value neighbors; 0 is bg.
fragment_voronoi = np.where(seed_mask, 0, voronoi_labels)
fragment_map = skimage_label(fragment_voronoi, connectivity=1)
n_frags = int(fragment_map.max())
# Seeded map — labels only at seed pixels; fragments zeroed.
seeded_map = voronoi_labels.copy()
seeded_map[fragment_map > 0] = 0
# Anchor seeds even outside mask (voronoi_labels is 0 there).
seeded_map[seed_mask] = markers[seed_mask]
if n_frags == 0:
seeded_map[~mask] = 0
return seeded_map
# COO index of all fragment pixels (built once).
frag_coo = coo_matrix(fragment_map)
frag_label = np.zeros(n_frags + 1, dtype=np.int32) # LUT: frag_id → label
unfilled = set(range(1, n_frags + 1))
while unfilled:
# Expand filled regions by 1 pixel.
expanded = expand_labels(seeded_map, distance=1)
# Look up expanded values at all fragment pixel positions.
expanded_at_frags = expanded[frag_coo.row, frag_coo.col]
filled_this_pass = []
for frag_id in sorted(unfilled):
frag_px = frag_coo.data == frag_id
vals = expanded_at_frags[frag_px]
nonzero = vals[vals > 0]
if nonzero.size == 0:
continue
frag_label[frag_id] = _mode(nonzero)
filled_this_pass.append(frag_id)
if not filled_this_pass:
break
# LUT paint all newly-filled fragments in one vectorized step.
newly_filled_px = np.isin(frag_coo.data, filled_this_pass)
assigned = frag_label[frag_coo.data[newly_filled_px]]
seeded_map[
frag_coo.row[newly_filled_px],
frag_coo.col[newly_filled_px],
] = assigned
unfilled -= set(filled_this_pass)
seeded_map[~mask] = 0
return seeded_map