"""studio.rigging.segment — flood-fill-from-corners cartoon segmentation. Real alpha channel short-circuits the heuristic: `alpha > 127` is the mask. Otherwise: connected components of the "background-similar" mask, take the union of components that touch any of the 4 image corners, invert. Deterministic, numpy + scipy.ndimage.label, no model imports. """ from __future__ import annotations import numpy as np from scipy.ndimage import label DEFAULT_TOLERANCE: int = 15 def flood_fill_segment( image: np.ndarray, *, tolerance: int = DEFAULT_TOLERANCE, alpha_threshold: int = 127, ) -> np.ndarray: """Return a boolean (H, W) foreground mask. image: (H, W) uint8 grayscale, (H, W, 3) RGB uint8, or (H, W, 4) RGBA uint8. tolerance: per-channel Chebyshev tolerance for "background-similar" pixels. alpha_threshold: cutoff for the RGBA fast path (only used if the alpha channel actually varies across the image). """ if image.ndim == 3 and image.shape[-1] == 4: alpha = image[..., 3] if alpha.min() < 255 and alpha.max() > 0 and alpha.max() != alpha.min(): return alpha > alpha_threshold image = image[..., :3] if image.ndim == 2: rgb = np.stack([image, image, image], axis=-1) elif image.ndim == 3 and image.shape[-1] == 3: rgb = image else: raise ValueError(f"unsupported image shape {image.shape}") H, W, _ = rgb.shape rgb_i16 = rgb.astype(np.int16) corners = np.stack( [rgb_i16[0, 0], rgb_i16[0, -1], rgb_i16[-1, 0], rgb_i16[-1, -1]], axis=0, ) bg_color = corners.mean(axis=0) diff = np.abs(rgb_i16 - bg_color).max(axis=-1) similar = diff <= tolerance labels, _ = label(similar.astype(np.uint8), structure=np.ones((3, 3), dtype=np.int8)) bg_label_ids = set() for y, x in ((0, 0), (0, W - 1), (H - 1, 0), (H - 1, W - 1)): L = int(labels[y, x]) if L > 0: bg_label_ids.add(L) if not bg_label_ids: return np.ones((H, W), dtype=bool) bg_mask = np.isin(labels, np.asarray(sorted(bg_label_ids), dtype=labels.dtype)) return ~bg_mask __all__ = ["flood_fill_segment", "DEFAULT_TOLERANCE"]