# Fit the user's front screenshot (Blender Front Orthographic: screen right = model +X) to the model: # u = a * x + bu, v = -a * z + bv # from the orange selection outline's lower edge (the hull's bottom silhouette), then convert the red strokes. import json import numpy as np from PIL import Image from collections import deque S = r"C:\Users\PC\AppData\Local\Temp\claude\D--HADAL---The-Descent\1fbd6834-d7f7-4606-97c8-f13f3d7e4dae" F = json.load(open(S + r"\scratchpad\front_tables.json")) low = np.array(F["low"]) im = np.asarray(Image.open(S + r"\images\4.png").convert("RGB")).astype(int) H, W, _ = im.shape R, G, B = im[..., 0], im[..., 1], im[..., 2] org = (R > 200) & (G > 110) & (G < 190) & (B < 80) red = (R > 170) & (G < 90) & (B < 90) # lower outline: for each column, the lowest orange pixel (the outline runs 2-3 px outside the silhouette) bot = {} for u in range(380, 1320, 3): idx = np.nonzero(org[:, u])[0] if len(idx): bot[u] = idx[-1] - 2.0 def cost(p): a, bu, bv = p r = [] for u, v in bot.items(): x = (u - bu) / a if -1.05 < x < 1.05: r.append(v - (-a * np.interp(x, low[:, 0], low[:, 1]) + bv)) r = np.array(r) return float(np.mean(np.minimum(r * r, 225.0))), r p = np.array([550.0, 850.0, 435.0]) best = (cost(p)[0], p.copy()); step = np.array([20.0, 20.0, 20.0]) for it in range(300): imp = False for k in range(3): for s in (1, -1): q = best[1].copy(); q[k] += s * step[k] c = cost(q)[0] if c < best[0]: best = (c, q); imp = True if not imp: step *= 0.5 if step.max() < 0.01: break a, bu, bv = best[1] c, r = cost(best[1]) print("front fit a=%.2f px/m bu=%.1f bv=%.1f rms=%.2f px median=%.2f n=%d" % (a, bu, bv, np.sqrt(c), np.median(np.abs(r)), len(r))) # red strokes, components (drop the thin X-axis line) lab = np.zeros((H, W), int); comps = [] n = 0 for v0, u0 in zip(*np.nonzero(red)): if lab[v0, u0]: continue n += 1; q = deque([(v0, u0)]); lab[v0, u0] = n; pts = [] while q: v, u = q.popleft(); pts.append((u, v)) for dv in (-2, -1, 0, 1, 2): for du in (-2, -1, 0, 1, 2): vv, uu = v + dv, u + du if 0 <= vv < H and 0 <= uu < W and red[vv, uu] and not lab[vv, uu]: lab[vv, uu] = n; q.append((vv, uu)) comps.append(pts) comps = [c for c in comps if len(c) > 1000] comps.sort(key=len, reverse=True) out = {"a": a, "bu": bu, "bv": bv, "strokes": []} for c in comps: out["strokes"].append([((u - bu) / a, (bv - v) / a) for u, v in c]) json.dump(out, open(S + r"\scratchpad\front_fit.json", "w")) # check picture: the model's bottom silhouette in blue img = Image.open(S + r"\images\4.png").convert("RGB"); px = img.load() for x, z in low: if z > 5: continue uu, vv = int(round(a * x + bu)), int(round(-a * z + bv)) if 0 <= uu < W and 0 <= vv < H: for d in (-1, 0, 1): px[uu, min(H - 1, max(0, vv + d))] = (40, 160, 255) img.save(S + r"\scratchpad\front_fit_check.png") for i, c in enumerate(out["strokes"]): a_ = np.array(c) print("stroke", i, len(c), "x %.3f..%.3f z %.3f..%.3f" % (a_[:, 0].min(), a_[:, 0].max(), a_[:, 1].min(), a_[:, 1].max()))