Files
exploded-cluster/tools/slice-arc.py
T

106 lines
4.4 KiB
Python

#!/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"]])
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)))
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"]
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-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])