# Fit the user's side-view screenshot (Blender Right Orthographic, nose to the left) to the model: # u = a * y + bu, v = -a * z + bv (pixels; model metres) # from the hull's top and bottom silhouette, then convert the red strokes to model (y, z). import json, sys import numpy as np from PIL import Image S = r"C:\Users\PC\AppData\Local\Temp\claude\D--HADAL---The-Descent\1fbd6834-d7f7-4606-97c8-f13f3d7e4dae" T = json.load(open(S + r"\scratchpad\shape_tables.json")) tab = np.array(T["tab"]) Y, TOP, BOT = tab[:, 0], tab[:, 1], tab[:, 2] im = np.asarray(Image.open(S + r"\images\5.webp").convert("RGB")).astype(int) H, W, _ = im.shape R, G, Bc = im[..., 0], im[..., 1], im[..., 2] red = (R > 170) & (G < 90) & (Bc < 90) lum = (R + G + Bc) / 3 hull = (lum > 92) & ~red # silhouette per column: topmost and bottommost hull pixel (ignore the small header text, rows < 120 in the left part) top_v, bot_v = {}, {} for u in range(40, 1450, 4): col = hull[:, u].copy() col[:115] = False if u < 260 else col[:115] idx = np.nonzero(col)[0] if len(idx) < 20: continue top_v[u] = idx[0] bot_v[u] = idx[-1] def resid(a, bu, bv, use_top=(40, 1150), use_bot=(300, 1450)): r = [] for u, v in top_v.items(): if use_top[0] <= u <= use_top[1]: y = (u - bu) / a if Y[0] <= y <= Y[-1]: r.append(v - (-a * np.interp(y, Y, TOP) + bv)) for u, v in bot_v.items(): if use_bot[0] <= u <= use_bot[1]: y = (u - bu) / a if Y[0] <= y <= Y[-1]: r.append(v - (-a * np.interp(y, Y, BOT) + bv)) r = np.array(r) return r def cost(p): r = resid(*p) return float(np.mean(np.minimum(r * r, 400.0))) # robust: cap outliers (fittings, text) p = np.array([417.0, 2060.0, 390.0]) best = (cost(p), p.copy()) step = np.array([20.0, 60.0, 30.0]) for it in range(200): improved = False for k in range(3): for s in (1, -1): q = best[1].copy(); q[k] += s * step[k] c = cost(q) if c < best[0]: best = (c, q); improved = True if not improved: step *= 0.5 if step.max() < 0.01: break a, bu, bv = best[1] r = resid(a, bu, bv) print("fit a=%.2f px/m bu=%.1f bv=%.1f rms=%.2f px n=%d median|r|=%.2f" % (a, bu, bv, np.sqrt(best[0]), len(r), np.median(np.abs(r)))) # red strokes -> model coordinates vs, us = np.nonzero(red) pts = [((u - bu) / a, (bv - v) / a) for u, v in zip(us, vs)] json.dump({"a": a, "bu": bu, "bv": bv, "red_yz": pts}, open(S + r"\scratchpad\side_fit.json", "w")) # a check picture: the model's silhouette (blue) drawn from the fit over the screenshot, red kept out = Image.open(S + r"\images\5.webp").convert("RGB") px = out.load() for y, t, b in zip(Y, TOP, BOT): u = int(round(a * y + bu)) for z in (t, b): v = int(round(-a * z + bv)) if 0 <= u < W and 0 <= v < H: for dv in (-1, 0, 1): if 0 <= v + dv < H: px[u, v + dv] = (40, 160, 255) out.save(S + r"\scratchpad\side_fit_check.png") print("ok")