Source code for phenotypic.tools_.branch_pathfinding._fragment_prescreening

"""Fragment pre-screening against the composite cost surface.

Rapidly identifies fragments surrounded by high-cost regions that have
no viable path corridor to any colony.  These fragments are rejected
before Dijkstra propagation, saving significant computation.

The approach uses ``scipy.ndimage.minimum_filter`` to precompute the
local minimum cost within *r_screen* pixels of every pixel in a single
O(M) pass.  Each fragment's boundary pixels are then checked against
this pre-filtered map.  If no boundary pixel sees a low-cost region
within *r_screen*, no Dijkstra path through that fragment can succeed.
"""

from __future__ import annotations

import numpy as np
from scipy.ndimage import minimum_filter
from skimage.measure import regionprops
from skimage.segmentation import find_boundaries

from ._dataclasses import PrescreenResult


[docs] def compute_min_cost_envelope( cost_surface: np.ndarray, r_screen: int, ) -> np.ndarray: """Precompute local minimum cost within *r_screen* of every pixel. Applies a minimum filter with kernel size ``2 * r_screen + 1`` to the cost surface. The output value at each pixel is the minimum cost found within an *r_screen*-pixel square neighbourhood. Args: cost_surface: Composite cost array, shape ``(H, W)``, values > 0. r_screen: Screening radius in pixels. Should match or slightly exceed the maximum biologically plausible gap distance. Typical range: 25--40 px. Returns: Minimum-filtered cost array, same shape as input. Value at ``(y, x)`` is the minimum cost within *r_screen* pixels. """ kernel_size = 2 * r_screen + 1 return minimum_filter(cost_surface, size=kernel_size)
def _compute_screening_envelope( cost_surface: np.ndarray, colony_branch_mask: np.ndarray, r_screen: int, ) -> tuple[np.ndarray, np.ndarray]: """Compute the screening envelope and filtered cost surface. Shared helper for :func:`calibrate_screening_threshold` and :func:`prescreen_fragments` to avoid duplicate ``minimum_filter`` passes. Args: cost_surface: Composite cost array, shape ``(H, W)``. colony_branch_mask: Inoculum+branch mask, shape ``(H, W)``. Nonzero values indicate colony pixels. r_screen: Screening radius in pixels. Returns: Tuple of ``(min_cost_envelope, cost_for_filter)`` where *min_cost_envelope* is the minimum-filtered cost and *cost_for_filter* is the modified cost surface with colony pixels replaced by background median. """ cost_for_filter = cost_surface.copy() colony_pixels = colony_branch_mask > 0 non_colony_mask = ~colony_pixels if np.any(non_colony_mask): fill_value = float(np.percentile(cost_surface[non_colony_mask], 50)) else: fill_value = 1.0 cost_for_filter[colony_pixels] = fill_value min_cost_envelope = compute_min_cost_envelope(cost_for_filter, r_screen) return min_cost_envelope, cost_for_filter
[docs] def calibrate_screening_threshold( cost_surface: np.ndarray, colony_branch_mask: np.ndarray, r_screen: int = 30, percentile: float = 99.0, min_cost_envelope: np.ndarray | None = None, ) -> tuple[float, np.ndarray]: """Derive the screening threshold from known-good branch endpoints. Identifies pixels at the outer boundary of connected colonies in the inoculum+branch mask and computes their local minimum cost environment. The threshold is set at the specified percentile of this distribution, ensuring that fragments in environments comparable to known-good branches are retained. This should be run once per image (or per imaging session if conditions are stable) before calling :func:`prescreen_fragments`. Args: cost_surface: Composite cost array, shape ``(H, W)``. colony_branch_mask: Inoculum+branch mask, shape ``(H, W)``. Nonzero values indicate colony pixels. r_screen: Screening radius, same value as will be used in :func:`prescreen_fragments`. percentile: Threshold percentile. Default 99.0 (very permissive: only the worst 1% of known-good environments would be rejected). min_cost_envelope: Pre-computed minimum cost envelope from :func:`_compute_screening_envelope`. If ``None``, computed internally. Returns: Tuple of ``(tau_screen, calibration_values)`` where *tau_screen* is the derived threshold and *calibration_values* is the array of per-boundary-pixel minimum costs used to compute it. The *calibration_values* can be passed to :func:`prescreen_fragments` or used for diagnostic plotting. Raises: ValueError: If no boundary pixels are found in *colony_branch_mask*. """ if min_cost_envelope is None: min_cost_envelope, _ = _compute_screening_envelope( cost_surface, colony_branch_mask, r_screen ) colony_pixels = colony_branch_mask > 0 # Outer boundary of the colony mask: pixels at tips/edges of # known-good branches representing worst-case connected environment. outer_boundary = find_boundaries(colony_pixels, mode="outer") boundary_coords = np.argwhere(outer_boundary) if boundary_coords.size == 0: raise ValueError( "No boundary pixels found in colony_branch_mask. " "Is the mask empty?" ) rows, cols = boundary_coords[:, 0], boundary_coords[:, 1] calibration_values = min_cost_envelope[rows, cols].astype(np.float64) tau_screen = float(np.percentile(calibration_values, percentile)) return tau_screen, calibration_values
def _extract_fragment_boundaries( fragment_labels: np.ndarray, ) -> dict[int, np.ndarray]: """Extract boundary pixel coordinates for each labelled fragment. Boundary pixels are those within the fragment mask that have at least one 8-connected neighbour outside the fragment (including neighbours belonging to other fragments). For very small fragments (< 4 pixels), all pixels are treated as boundary pixels since they are entirely "surface." Args: fragment_labels: Labelled fragment array, shape ``(H, W)``. 0 = background, positive integers = fragment IDs. Returns: Dict mapping ``fragment_id`` to an ``(N, 2)`` array of ``(row, col)`` boundary pixel coordinates. """ # Single-pass boundary detection across all fragments all_boundaries = find_boundaries(fragment_labels, mode="inner") boundaries: dict[int, np.ndarray] = {} for prop in regionprops(fragment_labels): fid = prop.label pixel_count = prop.area if pixel_count < 4: # Very small fragments: all pixels are boundary. boundaries[int(fid)] = prop.coords else: r0, c0, r1, c1 = prop.bbox local_labels = fragment_labels[r0:r1, c0:c1] local_bounds = all_boundaries[r0:r1, c0:c1] local_mask = (local_labels == fid) & local_bounds local_coords = np.argwhere(local_mask) if local_coords.size == 0: # Fallback: if find_boundaries returns nothing boundaries[int(fid)] = prop.coords else: # Offset local coords to global coords = local_coords + np.array([r0, c0]) boundaries[int(fid)] = coords return boundaries
[docs] def prescreen_fragments( cost_surface: np.ndarray, fragment_labels: np.ndarray, r_screen: int = 30, tau_screen: float | None = None, calibration_cost_values: np.ndarray | None = None, calibration_percentile: float = 99.0, colony_branch_mask: np.ndarray | None = None, min_cost_envelope: np.ndarray | None = None, ) -> PrescreenResult: """Pre-screen fragments against the local cost environment. Rejects fragments surrounded entirely by high-cost regions before Dijkstra propagation. Uses a minimum filter over the cost surface to efficiently check whether any low-cost corridor exists within *r_screen* pixels of each fragment boundary. The threshold *tau_screen* can be set explicitly or derived from calibration data (known-good fragment boundary cost values). If calibration data is provided, the threshold is set at the specified percentile, making screening very permissive (only reject clearly hopeless cases). Args: cost_surface: Composite cost array, shape ``(H, W)``, values > 0. Output of cost surface construction, with known objects already masked to epsilon_free. fragment_labels: Labelled fragment array, shape ``(H, W)``. 0 = background, positive integers = fragment IDs. Fragments already connected to colonies (in the inoculum+branch mask) should *not* be included here. r_screen: Screening radius in pixels. Maximum gap distance considered biologically plausible. Typical: 25--40 px. tau_screen: Cost threshold for rejection. Fragments whose minimum environmental cost exceeds this are rejected. If ``None``, *calibration_cost_values* must be provided. calibration_cost_values: Array of minimum environmental cost values from known-good fragments (those already in the inoculum+branch mask). Used to derive *tau_screen* at the specified percentile. Ignored if *tau_screen* is set. calibration_percentile: Percentile of calibration distribution to use as threshold. Default 99.0 (very permissive). colony_branch_mask: If provided, the inoculum+branch mask (any nonzero value = colony). Used to exclude colony pixels from the minimum-filtered cost map so that fragment screening reflects the gap environment, not the free-traversal zone inside colonies. Shape ``(H, W)``. min_cost_envelope: Pre-computed minimum cost envelope from :func:`_compute_screening_envelope`. If ``None``, computed internally. Passing a pre-computed envelope avoids a duplicate ``minimum_filter`` pass when both :func:`calibrate_screening_threshold` and this function are called on the same cost surface. Returns: :class:`PrescreenResult` containing the filtered fragment labels, sets of passed/rejected IDs, and the threshold used. Raises: ValueError: If neither *tau_screen* nor *calibration_cost_values* is provided, if *fragment_labels* contains no fragments, or if the shapes of *cost_surface* and *fragment_labels* do not match. """ # --- Input validation --- if cost_surface.shape != fragment_labels.shape: raise ValueError( f"Shape mismatch: cost_surface {cost_surface.shape} vs " f"fragment_labels {fragment_labels.shape}" ) fragment_ids = np.unique(fragment_labels) fragment_ids = fragment_ids[fragment_ids > 0] if fragment_ids.size == 0: raise ValueError("fragment_labels contains no labeled fragments") if tau_screen is None and calibration_cost_values is None: raise ValueError( "Must provide either tau_screen or calibration_cost_values" ) # --- Threshold determination --- if tau_screen is None: assert calibration_cost_values is not None # guarded above tau_screen = float( np.percentile(calibration_cost_values, calibration_percentile) ) # --- Precompute minimum cost envelope --- if min_cost_envelope is None: if colony_branch_mask is not None: min_cost_envelope, _ = _compute_screening_envelope( cost_surface, colony_branch_mask, r_screen ) else: min_cost_envelope = compute_min_cost_envelope(cost_surface, r_screen) # --- Extract fragment boundaries --- boundaries = _extract_fragment_boundaries(fragment_labels) # --- Screen each fragment --- passed: list[int] = [] rejected: list[int] = [] for fid in fragment_ids: fid_int = int(fid) if fid_int not in boundaries: # Fragment had no extractable boundary (should not happen). rejected.append(fid_int) continue coords = boundaries[fid_int] rows, cols = coords[:, 0], coords[:, 1] # Sample the minimum-filtered cost at boundary pixels. # The value at each boundary pixel already represents the # minimum cost within r_screen pixels of that point. boundary_min_costs = min_cost_envelope[rows, cols] # The fragment's environmental cost is the best (lowest) # value across all its boundary pixels. This asks: "is there # ANY low-cost region within r_screen of ANY part of this # fragment's boundary?" frag_min_env = float(np.min(boundary_min_costs)) if frag_min_env > tau_screen: rejected.append(fid_int) else: passed.append(fid_int) # --- Build output --- screened_labels = fragment_labels.copy() if rejected: rejection_mask = np.isin(fragment_labels, rejected) screened_labels[rejection_mask] = 0 return PrescreenResult( screened_fragment_labels=screened_labels, passed_ids=set(passed), rejected_ids=set(rejected), threshold_used=tau_screen, )