"""studio.rigging.overlay — numpy + PIL debug overlay renderer. Composites: source image → mask-edge ring → bones (cyan) → joints (red dots). Output is a PIL.Image in RGBA, ready to save as PNG. """ from __future__ import annotations import numpy as np from PIL import Image, ImageDraw from scipy.ndimage import binary_erosion from pixel_cursor.rigging import BONES, JOINT_NAMES, Skeleton JOINT_COLOR = (255, 80, 80, 255) JOINT_OUTLINE = (0, 0, 0, 255) BONE_COLOR = (60, 200, 255, 255) MASK_EDGE_COLOR = (255, 255, 0, 200) JOINT_RADIUS = 4 BONE_WIDTH = 2 def render_overlay( image: np.ndarray, mask: np.ndarray, skeleton: Skeleton, *, show_labels: bool = False, ) -> Image.Image: if image.ndim == 2: rgba = np.stack( [image, image, image, np.full_like(image, 255)], axis=-1, ) elif image.shape[-1] == 3: rgba = np.concatenate( [image, np.full(image.shape[:2] + (1,), 255, dtype=np.uint8)], axis=-1, ) elif image.shape[-1] == 4: rgba = image.copy() else: raise ValueError(f"unsupported image shape {image.shape}") H, W = rgba.shape[:2] mask_bool = mask.astype(bool) edge = mask_bool & ~binary_erosion(mask_bool, iterations=1, border_value=0) edge_layer_arr = np.zeros((H, W, 4), dtype=np.uint8) edge_layer_arr[edge] = MASK_EDGE_COLOR canvas = Image.alpha_composite( Image.fromarray(rgba, mode="RGBA"), Image.fromarray(edge_layer_arr, mode="RGBA"), ) draw = ImageDraw.Draw(canvas) for parent_idx, child_idx in BONES: py, px = skeleton.positions[parent_idx] cy, cx = skeleton.positions[child_idx] draw.line( [(float(px), float(py)), (float(cx), float(cy))], fill=BONE_COLOR, width=BONE_WIDTH, ) for i in range(skeleton.positions.shape[0]): y, x = skeleton.positions[i] r = JOINT_RADIUS draw.ellipse( [(float(x) - r, float(y) - r), (float(x) + r, float(y) + r)], fill=JOINT_COLOR, outline=JOINT_OUTLINE, width=1, ) if show_labels: draw.text( (float(x) + r + 2, float(y) - r), JOINT_NAMES[i], fill=(255, 255, 255, 255), ) return canvas __all__ = ["render_overlay"]