102 lines
4.2 KiB
Python
102 lines
4.2 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"],
|
|
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)
|
|
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-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])
|