Source code for celldetective.utils.masks

from typing import Union, List, Tuple, Optional

import numpy as np
from celldetective.log_manager import get_logger

logger = get_logger(__name__)


[docs] def contour_of_instance_segmentation( label: np.ndarray, distance: Union[int, float, List, Tuple, str], sdf: Optional[np.ndarray] = None, voronoi_map: Optional[np.ndarray] = None, ) -> np.ndarray: """ Generate an instance mask containing the contour of the segmented objects. This updated version uses a Signed Distance Field (SDF) and Voronoi tessellation approach Generic enough to handle Inner contours, Outer contours, and arbitrary "stripes" (annuli). Parameters ---------- label : ndarray The instance segmentation labels. distance : int, float, list, tuple, or str The distance specification. - Scalar > 0: Inner contour (Erosion) from boundary to 'distance' pixels inside. Range [0, distance]. - Scalar < 0: Outer contour (Dilation) from 'distance' pixels outside to boundary. Range [distance, 0]. - Tuple/List (a, b): Explicit range in SDF space. - Positive values are inside the object. - Negative values are outside methods. - Example: (5, 10) -> Inner ring 5 to 10px deep. - Example: (-10, -5) -> Outer ring 5 to 10px away. - String "rad1-rad2": Interpretation for Batch Measurements (Outer Rings). - Interpreted as an annular region OUTSIDE the object. - "5-10" -> Range [-10, -5] in SDF space (5 to 10px away). sdf : ndarray, optional Pre-computed Signed Distance Field (dist_in - dist_out). If provided, avoids recomputing EDT. voronoi_map : ndarray, optional Pre-computed Voronoi map of instance labels. Required if sdf is provided and outer contours are needed. Returns ------- border_label : ndarray An instance mask containing the contour of the segmented objects. Outer contours preserve instance identity via Voronoi propagation. """ from scipy.ndimage import distance_transform_edt # helper to parse string "rad1-rad2" or "-12-13" or "-5--2" if isinstance(distance, str): import re try: # Check for stringified tuple "(a, b)" distance = distance.strip() if distance.startswith("(") and distance.endswith(")"): import ast val_tuple = ast.literal_eval(distance) if isinstance(val_tuple, (list, tuple)) and len(val_tuple) == 2: min_r = val_tuple[0] max_r = val_tuple[1] else: raise ValueError("Tuple string must have 2 elements") else: try: val = float(distance) # It's a scalar string like "5" or "-5" if val >= 0: min_r = 0 max_r = val else: min_r = val max_r = 0 except ValueError: # It's a range string like "5-10", "-12-13", or "-5--2" # Use regex to parse range with potentially negative numbers range_pattern = re.compile(r"^(-?\d+(?:\.\d+)?)-(-?\d+(?:\.\d+)?)$") match = range_pattern.match(distance) if match: r1 = float(match.group(1)) r2 = float(match.group(2)) # Use the values directly as specified (min, max in SDF space) min_r = min(r1, r2) max_r = max(r1, r2) else: raise ValueError(f"Could not parse range string: {distance}") except Exception: logger.warning( f"Could not parse contour string '{distance}'. returning empty." ) return np.zeros_like(label) elif isinstance(distance, (list, tuple)): min_r = distance[0] max_r = distance[1] elif isinstance(distance, (int, float)): if distance >= 0: min_r = 0 max_r = distance else: min_r = distance max_r = 0 else: return np.zeros_like(label) if sdf is None or voronoi_map is None: # Compute SDF maps # We need SDF = dist_in - dist_out # inside > 0, outside < 0 # 1. Dist In (Inside object) dist_in = distance_transform_edt(label > 0) # 2. Dist Out (Outside object) + Voronoi dist_out, indices = distance_transform_edt(label == 0, return_indices=True) # Voronoi Map voronoi_map = label[indices[0], indices[1]] # Composite SDF sdf = dist_in - dist_out # Create Mask mask = (sdf >= min_r) & (sdf <= max_r) # Result border_label = voronoi_map * mask return border_label
[docs] def create_patch_mask( h: int, w: int, center: Optional[Tuple[int, int]] = None, radius: Optional[Union[int, float, List[Union[int, float]]]] = None, ) -> np.ndarray: """ Create a circular patch mask of given dimensions. Adapted from alkasm on https://stackoverflow.com/questions/44865023/how-can-i-create-a-circular-mask-for-a-numpy-array Parameters ---------- h : int Height of the mask. Prefer odd value. w : int Width of the mask. Prefer odd value. center : tuple, optional Coordinates of the center of the patch. If not provided, the middle of the image is used. radius : int or float or list, optional Radius of the circular patch. If not provided, the smallest distance between the center and image walls is used. If a list is provided, it should contain two elements representing the inner and outer radii of a circular annular patch. Returns ------- numpy.ndarray Boolean mask where True values represent pixels within the circular patch or annular patch, and False values represent pixels outside. Notes ----- The function creates a circular patch mask of the given dimensions by determining which pixels fall within the circular patch or annular patch. The circular patch or annular patch is centered at the specified coordinates or at the middle of the image if coordinates are not provided. The radius of the circular patch or annular patch is determined by the provided radius parameter or by the minimum distance between the center and image walls. If an annular patch is desired, the radius parameter should be a list containing the inner and outer radii respectively. Examples -------- >>> mask = create_patch_mask(100, 100, center=(50, 50), radius=30) >>> print(mask) """ if center is None: # use the middle of the image center = (int(w / 2), int(h / 2)) if radius is None: # use the smallest distance between the center and image walls radius = min(center[0], center[1], w - center[0], h - center[1]) Y, X = np.ogrid[:h, :w] dist_from_center = np.sqrt((X - center[0]) ** 2 + (Y - center[1]) ** 2) if isinstance(radius, int) or isinstance(radius, float): mask = dist_from_center <= radius elif isinstance(radius, list): mask = (dist_from_center <= radius[1]) * (dist_from_center >= radius[0]) else: print("Please provide a proper format for the radius") return None return mask