"""Grouper: the comparison sheets (plain Python 3.14 + Pillow + numpy, after gr_render.py's lookalike and concept renders): previews/compare/lookalike.png each sheet view's panel (left, 2x) beside the model rendered orthographically in the same view and framing in the studio light (right; FRONT and BACK stretched by 1/KF, 1/KB as the sheet draws them narrower), and SECTION A-A beside the cut through the cabin at the A plane previews/compare/cmp_.png the model's outline (red) and the Designer's silhouette (reference/mask_.png, yellow) over the sheet's view, registered in sheet pixels (drawn at 2x); the overlap (IoU) in the title previews/compare/side_inkover.png the sheet's own drawn lines (reference/lines_side.png, magenta) over the model's SIDE render previews/compare/concept_pair.png the concept's crop (left) beside the model rendered from the concept's fitted camera in dark water light (right), the same crop; the landmarks and their fit residuals listed build/checks/compare.json the IoU per view python build/gr_pairs.py""" import json import os import numpy as np from PIL import Image, ImageDraw, ImageFilter, ImageFont HERE = os.path.dirname(os.path.abspath(__file__)) ROOT = os.path.dirname(HERE) CMP = os.path.join(ROOT, "previews", "compare") REF = os.path.join(ROOT, "reference") G = {} exec(open(os.path.join(HERE, "gr_measure.py"), encoding="utf-8").read(), G) KF, KB = G["KF"], G["KB"] sheet = Image.open(os.path.join(REF, "user_blueprint.jpg")).convert("RGB") K = 2 BOX = {"side": (430, 255, 1010, 520), "top": (440, 20, 1000, 250), "bottom": (440, 520, 1000, 745), "front": (95, 215, 385, 550), "back": (1075, 235, 1345, 550)} try: FONT = ImageFont.truetype(os.path.join(os.environ.get("WINDIR", r"C:\Windows"), "Fonts", "segoeuib.ttf"), 20) FONT_S = ImageFont.truetype(os.path.join(os.environ.get("WINDIR", r"C:\Windows"), "Fonts", "segoeui.ttf"), 15) except Exception: FONT = FONT_S = ImageFont.load_default() STUDIO_BG = (205, 208, 212) def model_img(view, box): """the lookalike render at the sheet's framing (K px per sheet px), RGBA""" im = Image.open(os.path.join(CMP, "lk_%s.png" % view)).convert("RGBA") w, h = (box[2] - box[0]) * K, (box[3] - box[1]) * K if im.size != (w, h): im = im.resize((w, h), Image.LANCZOS) return im def label(img, text, xy=(10, 6), fill=(255, 220, 90)): d = ImageDraw.Draw(img) x, y = xy tb = d.textbbox((x, y), text, font=FONT) d.rectangle([tb[0] - 4, tb[1] - 3, tb[2] + 4, tb[3] + 3], fill=(10, 12, 16)) d.text((x, y), text, fill=fill, font=FONT) # ------------------------------------------------------------------ lookalike.png rows = [] for view, box in BOX.items(): ref = sheet.crop(box).resize(((box[2] - box[0]) * K, (box[3] - box[1]) * K), Image.LANCZOS) mod = model_img(view, box) bg = Image.new("RGBA", mod.size, STUDIO_BG + (255,)) bg.alpha_composite(mod) row = Image.new("RGB", (ref.width * 2 + 16, ref.height), (20, 22, 26)) row.paste(ref, (0, 0)) row.paste(bg.convert("RGB"), (ref.width + 16, 0)) label(row, "SHEET " + view.upper()) label(row, "MODEL " + view.upper() + (" (stretched 1/%.3f as the sheet draws it)" % (KF if view == "front" else KB) if view in ("front", "back") else ""), (ref.width + 26, 6)) rows.append(row) sb = (1040, 20, 1360, 240) ref = sheet.crop(sb).resize(((sb[2] - sb[0]) * K, (sb[3] - sb[1]) * K), Image.LANCZOS) sec = Image.open(os.path.join(CMP, "lk_section.png")).convert("RGBA").resize(ref.size, Image.LANCZOS) bg = Image.new("RGBA", sec.size, STUDIO_BG + (255,)) bg.alpha_composite(sec) row = Image.new("RGB", (ref.width * 2 + 16, ref.height), (20, 22, 26)) row.paste(ref, (0, 0)) row.paste(bg.convert("RGB"), (ref.width + 16, 0)) label(row, "SHEET SECTION A-A") label(row, "MODEL cut at the A plane (Y -0.09, looking forward)", (ref.width + 26, 6)) rows.append(row) for p in ("lk_section_close.png",): pp = os.path.join(CMP, p) if os.path.exists(pp): cl = Image.open(pp).convert("RGB") cl = cl.resize((int(cl.width * ref.height / cl.height), ref.height), Image.LANCZOS) row2 = Image.new("RGB", (ref.width + 16 + cl.width, ref.height), (20, 22, 26)) row2.paste(ref, (0, 0)) row2.paste(cl, (ref.width + 16, 0)) label(row2, "SHEET SECTION A-A") label(row2, "MODEL cut at the A plane, closer, the cabin lit", (ref.width + 26, 6)) rows.append(row2) Wd = max(r.width for r in rows) Ht = sum(r.height for r in rows) + 12 * (len(rows) - 1) out = Image.new("RGB", (Wd, Ht), (20, 22, 26)) y = 0 for r in rows: out.paste(r, (0, y)) y += r.height + 12 out.save(os.path.join(CMP, "lookalike.png")) # ------------------------------------------------------------------ cmp_.png (outlines over the sheet, IoU) ious = {} for view, box in BOX.items(): x0, y0, x1, y1 = box mod = model_img(view, box) a = np.asarray(mod)[..., 3] > 127 # the model's silhouette at 2x mk = Image.open(os.path.join(REF, "mask_%s.png" % view)).convert("L").crop(box) m1 = np.asarray(mk) > 127 a1 = np.asarray(Image.fromarray((a * 255).astype(np.uint8)).resize(mk.size, Image.BILINEAR)) > 127 inter, union = np.logical_and(a1, m1).sum(), np.logical_or(a1, m1).sum() ious[view] = {"iou": round(float(inter / max(1, union)), 4), "model_px": int(a1.sum()), "sheet_px": int(m1.sum()), "model_only_px": int(np.logical_and(a1, ~m1).sum()), "sheet_only_px": int(np.logical_and(m1, ~a1).sum())} big = sheet.crop(box).resize(((x1 - x0) * K, (y1 - y0) * K), Image.LANCZOS) ov = np.asarray(big).copy() m2 = np.asarray(mk.resize(big.size, Image.NEAREST)) > 127 e_sheet = np.asarray(Image.fromarray((m2 * 255).astype(np.uint8)).filter(ImageFilter.FIND_EDGES)) > 0 e_model = np.asarray(Image.fromarray((a * 255).astype(np.uint8)).filter(ImageFilter.FIND_EDGES).filter(ImageFilter.MaxFilter(3))) > 0 ov[e_sheet] = (255, 230, 40) ov[e_model] = (255, 40, 40) img = Image.fromarray(ov) label(img, "%s: model outline (red) and the Designer's silhouette (yellow) over the sheet; IoU %.3f" % (view.upper(), ious[view]["iou"])) img.save(os.path.join(CMP, "cmp_%s.png" % view)) # ------------------------------------------------------------------ side_inkover.png (the sheet's lines over the model) box = BOX["side"] mod = model_img("side", box) bg = Image.new("RGBA", mod.size, STUDIO_BG + (255,)) bg.alpha_composite(mod) ln = Image.open(os.path.join(REF, "lines_side.png")).convert("L").crop(box).resize(mod.size, Image.BILINEAR) L = np.asarray(ln) > 110 im = np.asarray(bg.convert("RGB")).copy() im[L] = (255, 0, 200) ink = Image.fromarray(im) label(ink, "SIDE: the sheet's drawn lines (magenta, reference/lines_side.png) over the model's orthographic render") ink.save(os.path.join(CMP, "side_inkover.png")) # ------------------------------------------------------------------ concept_pair.png cb = (170, 85, 720, 420) cam = json.load(open(os.path.join(HERE, "checks", "concept_cam.json"))) con = Image.open(os.path.join(REF, "user_concept.jpg")).convert("RGB").crop(cb) ren = Image.open(os.path.join(CMP, "concept_render.png")).convert("RGB").crop(cb) Kc = 2 con = con.resize((con.width * Kc, con.height * Kc), Image.LANCZOS) ren = ren.resize(con.size, Image.LANCZOS) pair = Image.new("RGB", (con.width * 2 + 16, con.height + 70), (12, 14, 18)) pair.paste(con, (0, 0)) pair.paste(ren, (con.width + 16, 0)) label(pair, "THE USER'S CONCEPT RENDER (crop)") label(pair, "MODEL from the concept's fitted camera (%.1f mm lens), dark water, lights on" % cam["lens_mm_36mm_sensor_horizontal"], (con.width + 26, 6)) d = ImageDraw.Draw(pair) worst = sorted(cam["err_px"].items(), key=lambda kv: -kv[1])[:2] d.text((10, con.height + 8), "camera fitted to %d landmarks (lamps, the lamp beside the drum, drum face, porthole, windows, markings, " "duct, collar): rms %.1f px over the main ones, %.1f px over all (largest: %s, the AI picture's own proportions)" % (len(cam["err_px"]), cam.get("rms_px_weight_0.7_up", cam["rms_px"]), cam["rms_px"], ", ".join("%s %.0f px" % (k, v) for k, v in worst)), fill=(200, 205, 210), font=FONT_S) d.text((10, con.height + 32), "blueprint rules the shape, the concept the look; the blue jet and the scanner's rings are effects, not geometry", fill=(200, 205, 210), font=FONT_S) pair.save(os.path.join(CMP, "concept_pair.png")) json.dump({"iou_vs_designer_masks": ious}, open(os.path.join(HERE, "checks", "compare.json"), "w"), indent=1) print("PAIRS OK", json.dumps({k: v["iou"] for k, v in ious.items()}))