main
John Lauer Component record TPS16850VMAR: files batch 1 6db00ed 20h ago
#!/usr/bin/env python3
"""Prove the datasheet-built VMA0023A model against TI's PACKAGE OUTLINE drawing (SLVSHA1D p.54, 300 dpi render).

The model STEP is rendered orthographically (ortho.py, face colours kept) in the drawing's projections:
  bottom view: TI draws it turned over about the X axis (ortho "bottom": +X right, +Y down);
  side view:   the drawing under the top view (ortho "front" or "back", whichever fits).
Scale and offset of the bottom view are fitted by maximising the share of the model's terminal and body edge
pixels that land on the drawing's blue strokes (scale searched 100..140 px/mm; it lands on TI's 10X =
118.1 px/mm); the side view uses the same scale with only its offset searched.
Output: <prefix>-bottom.png, <prefix>-side.png (drawing, model at 45 % opacity, model edges in red), <prefix>.json.
usage: overlay_vma.py model.step p54-300dpi.png out_prefix
"""
import sys, json, os
import numpy as np
from PIL import Image, ImageDraw, ImageFont, ImageFilter
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import ortho

step, p54, pre = sys.argv[1:4]
tris = ortho.load_tris(step)
page = Image.open(p54).convert("RGB")
arr = np.asarray(page).astype(int)
blue = (arr[:, :, 2] > 170) & (arr[:, :, 0] < 140) & (arr[:, :, 1] > 60) & (arr[:, :, 1] < 210)
dil = np.array(Image.fromarray(blue).filter(ImageFilter.MaxFilter(3)))
REF = 60.0

def edges(img):
    a = np.asarray(img)
    alpha = a[:, :, 3] > 128
    dark = (a[:, :, :3].sum(2) < 200) & alpha
    metal = alpha & ~dark
    e = np.zeros_like(alpha)
    for m in (alpha, metal):
        e |= m & ~np.array(Image.fromarray(m).filter(ImageFilter.MinFilter(3)))
    ys, xs = np.nonzero(e)
    return np.stack([xs - a.shape[1] / 2, ys - a.shape[0] / 2], 1)

def fit(view, region, scales=None):
    P = edges(ortho.render(tris, view, REF, shade=False))
    P = P[np.linspace(0, len(P) - 1, min(len(P), 2500)).astype(int)]
    x0, y0, x1, y1 = region
    best = (-1,)
    for s in (scales if scales is not None else np.arange(100, 141, 2.0)):
        q0 = P * s / REF
        for ox in range(x0, x1, 5):
            for oy in range(y0, y1, 5):
                q = (q0 + (ox, oy)).astype(int)
                ok = (q[:, 0] >= 0) & (q[:, 0] < blue.shape[1]) & (q[:, 1] >= 0) & (q[:, 1] < blue.shape[0])
                sc = dil[q[ok, 1], q[ok, 0]].mean() * ok.mean()
                if sc > best[0]: best = (sc, s, ox, oy)
    sc, s, ox, oy = best
    for s2 in ([s] if scales is not None else np.arange(s - 2, s + 2.01, 0.25)):
        for ox2 in range(ox - 5, ox + 6):
            for oy2 in range(oy - 5, oy + 6):
                q = (P * s2 / REF + (ox2, oy2)).astype(int)
                v = dil[q[:, 1], q[:, 0]].mean()
                if v > sc: sc, s, ox, oy = v, s2, ox2, oy2
    return sc, s, ox, oy

def draw(view, sc, s, ox, oy, crop, title, out):
    img = ortho.render(tris, view, s)
    lay = Image.new("RGBA", page.size, (0, 0, 0, 0))
    a = np.asarray(img).copy(); a[:, :, 3] = (a[:, :, 3] * 0.45).astype(np.uint8)
    lay.alpha_composite(Image.fromarray(a), (int(ox - img.width / 2), int(oy - img.height / 2)))
    P = edges(ortho.render(tris, view, s, shade=False)) + (ox, oy)
    d = ImageDraw.Draw(lay)
    for x, y in P.astype(int): d.point((x, y), fill=(230, 20, 20, 255))
    ov = Image.alpha_composite(page.convert("RGBA"), lay).convert("RGB").crop(crop)
    c = Image.new("RGB", (ov.width, ov.height + 80), "white"); c.paste(ov, (0, 0))
    font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 22)
    dd = ImageDraw.Draw(c)
    dd.text((16, ov.height + 8), title, fill="black", font=font)
    dd.text((16, ov.height + 42), f"fitted scale {s:.1f} px/mm at 300 dpi (TI 10X = 118.1); {sc*100:.1f} % of the model's edge pixels lie on TI's blue strokes", fill="black", font=font)
    c.save(out)

res = {}
sc, s, ox, oy = fit("bottom", (1110, 2090, 1280, 2270))
res["bottom"] = dict(score=round(float(sc), 4), scale=round(float(s), 2), origin=[ox, oy], image=os.path.basename(pre) + "-bottom.png")
draw("bottom", sc, s, ox, oy, (int(ox - 3.9 * s), int(oy - 3.2 * s), int(ox + 3.9 * s), int(oy + 3.2 * s)),
     "TPS16850VMAR model (bottom, red edges) over TI SLVSHA1D p.54 VMA0023A bottom view", pre + "-bottom.png")
best = None
for view in ("front", "back"):
    r = fit(view, (1110, 1380, 1280, 1490), scales=[s])
    if best is None or r[0] > best[1][0]: best = (view, r)
view, (sc2, s2, ox2, oy2) = best
res["side"] = dict(view=view, score=round(float(sc2), 4), scale=round(float(s2), 2), origin=[ox2, oy2], image=os.path.basename(pre) + "-side.png",
                   note="TI draws the side view at A max (1.5 mm); the model is at the nominal 1.4 mm, so its top edge sits 0.1 mm below TI's")
draw(view, sc2, s2, ox2, oy2, (int(ox2 - 4.2 * s2), int(oy2 - 1.6 * s2), int(ox2 + 4.6 * s2), int(oy2 + 1.6 * s2)),
     f"TPS16850VMAR model ({view} side) over TI SLVSHA1D p.54 VMA0023A side view", pre + "-side.png")
json.dump(res, open(pre + ".json", "w"), indent=1)
print(json.dumps(res))