#!/usr/bin/env python3 """Segment the delivery-arc heroes into per-part polygons. Phase 'debug': label bright components on the void, save numbered overlays for eyeball assignment. Phase 'emit': given a component->part mapping, write the scene manifest (percent-coordinate hulls + collapse offsets toward the scene anchor). """ import json, sys, pathlib import numpy as np from PIL import Image, ImageDraw, ImageFont from scipy import ndimage from scipy.spatial import ConvexHull ROOT = pathlib.Path(__file__).resolve().parent.parent W = 1376 # working width def load(name): im = Image.open(ROOT / "assets/raw" / name).convert("RGB") r = W / im.width im2 = im.resize((W, int(im.height * r))) a = np.asarray(im2).astype(np.float32) lum = a @ np.array([0.299, 0.587, 0.114], dtype=np.float32) return im2, lum def components(lum, thresh=26, dilate=6, min_area=350): mask = lum > thresh mask = ndimage.binary_dilation(mask, iterations=dilate) lab, n = ndimage.label(mask) out = [] for i in range(1, n + 1): ys, xs = np.nonzero(lab == i) if len(xs) < min_area: continue out.append(dict(id=len(out) + 1, xs=xs, ys=ys, cx=float(xs.mean()), cy=float(ys.mean()), bbox=(int(xs.min()), int(ys.min()), int(xs.max()), int(ys.max())), area=int(len(xs)))) return out def debug(name, tag): im, lum = load(name) comps = components(lum) d = ImageDraw.Draw(im) for c in comps: x0, y0, x1, y1 = c["bbox"] d.rectangle([x0, y0, x1, y1], outline=(63, 186, 245), width=2) d.text((c["cx"] - 8, c["cy"] - 10), str(c["id"]), fill=(255, 80, 80)) out = pathlib.Path(sys.argv[3]) / f"debug-{tag}.png" im.save(out) h = im.height print(f"== {tag} ({len(comps)} components, {W}x{h})") for c in comps: print(f" #{c['id']:2d} centroid=({c['cx']/W*100:5.1f}%,{c['cy']/h*100:5.1f}%) " f"bbox%=({c['bbox'][0]/W*100:.0f},{c['bbox'][1]/h*100:.0f}," f"{c['bbox'][2]/W*100:.0f},{c['bbox'][3]/h*100:.0f}) area={c['area']}") def emit(name, tag, spec): im, lum = load(name) comps = {c["id"]: c for c in components(lum)} h = im.height parts = [] for part in spec["parts"]: xs = np.concatenate([comps[i]["xs"] for i in part["comps"]]) ys = np.concatenate([comps[i]["ys"] for i in part["comps"]]) if "clip" in part: x0, y0, x1, y1 = part["clip"] W2, H2 = W, lum.shape[0] keep = (xs >= x0*W2) & (xs < x1*W2) & (ys >= y0*H2) & (ys < y1*H2) xs, ys = xs[keep], ys[keep] pts = np.stack([xs, ys], 1).astype(np.float64) hull = ConvexHull(pts) poly = pts[hull.vertices] # pad the hull outward from its centroid by ~1.2% of width (glow safety) c = poly.mean(0) v = poly - c poly = c + v * (1 + (W * 0.012) / (np.abs(v).max(1, keepdims=True) + 1e-6)) # simplify: keep every k-th vertex to <= 18 points k = max(1, len(poly) // 18) poly = poly[::k] parts.append(dict( name=part["name"], title=part["title"], blurb=part["blurb"], z=part.get("z", 0), points=[[round(float(x) / W * 100, 2), round(float(y) / h * 100, 2)] for x, y in poly], cx=round(float(np.mean(xs)) / W * 100, 2), cy=round(float(np.mean(ys)) / h * 100, 2))) if "anchor_xy" in spec: ax, ay = spec["anchor_xy"] else: ax, ay = next((p["cx"], p["cy"]) for p in parts if p["name"] == spec["anchor"]) for p in parts: nest = spec.get("nest_overrides", {}).get(p["name"], spec.get("nest", 0.72)) p["dx"] = round((ax - p["cx"]) * nest, 2) p["dy"] = round((ay - p["cy"]) * nest, 2) del p["cx"]; del p["cy"] man = dict(scene=tag, source=f"assets/raw/{name}", parts=parts) if "stagger" in spec: man["stagger"] = spec["stagger"] if spec.get("reverse"): man["reverse"] = True for kk in ("motion","bow","orbit"): if kk in spec: man[kk]=spec[kk] out = ROOT / "pilot" / f"manifest-{tag}.json" out.write_text(json.dumps(man, indent=1)) print(f"manifest-{tag}.json: {len(parts)} parts ->", ", ".join(f"{p['name']}({p['dx']},{p['dy']})" for p in parts)) if __name__ == "__main__": mode, scenes_json, outdir = sys.argv[1], sys.argv[2], sys.argv[3] scenes = json.loads(pathlib.Path(scenes_json).read_text()) if mode == "emit" else None heroes = [("course-00-surround.jpeg", "surround"), ("course-III-node.jpeg", "node"), ("course-III-pod.jpeg", "pod"), ("course-IV-traffic.jpeg", "traffic"), ("course-V-orbit.jpeg", "orbit"), ("course-II-cluster.jpeg", "cluster"), ("course-VI-gitops.jpeg", "gitops"), ("course-VII-supply-chain.jpeg", "supply"), ("course-VIII-helm-press.jpeg", "helm")] for name, tag in heroes: if mode == "debug": debug(name, tag) else: emit(name, tag, scenes[tag])