main
Caleb Version 0.3.0: new copper reader, dome via filter, every layer read from the photos, net evidence, measured confidence;… e874d7d
#!/usr/bin/env python3
"""Stills and data for the Driver board video. Inputs: the photos, the merged run, the published run, ledger totals."""
import colorsys, hashlib, json, os, sys
from PIL import Image, ImageDraw
Image.MAX_IMAGE_PIXELS = None
ROOT = os.path.dirname(os.path.abspath(__file__)); A = ROOT + '/a'; os.makedirs(A, exist_ok=True)
NEW = '/home/adom/project/Driver_RE3_test/run/reverse-flow/'
MID = '/home/adom/project/Driver_RE3_test/run-merged1/reverse-flow/'
OLD = '/home/adom/project/Driver_RE2/reverse-flow/'
PX = 40
def jpg(im, n, q=91): im.convert('RGB').save(f'{A}/{n}.jpg', quality=q, subsampling=0); print(n, im.size)
def px(p): return ((p[0] - 100) * PX, (p[1] - 100) * PX)
def runs_of(z):
    if 'rle_px' in z:
        for r in z['rle_px']:
            for i in range(1, len(r) - 1, 2): yield r[0], r[i], r[i + 1]
    else:
        for r in z.get('rle', []):
            for i in range(1, len(r) - 1, 2):
                for k in (0, 1): yield r[0] * 2 + k, r[i] * 2, r[i + 1] * 2
def paint(c, layer, colour, size, bg=(16, 20, 26), base=None):
    im = base.copy() if base is not None else Image.new('RGB', size, bg); d = ImageDraw.Draw(im)
    for z in c['zones']:
        if z['layer'] != layer: continue
        col = colour(z)
        if col is None: continue
        for y, a, b in runs_of(z): d.line((a, y, max(a, b - 1), y), fill=col)
    for t in c['traces']:
        if t['layer'] != layer: continue
        col = colour(t)
        if col is None: continue
        pts = [px(p) for p in t['path_mm']]
        if len(pts) > 1: d.line(pts, fill=col, width=max(1, round(t.get('width_mm', .1) * PX)))
    return im
def net_rgb(n):
    h = int(hashlib.md5(n.encode()).hexdigest()[:6], 16) / 0xffffff
    return tuple(int(255 * v) for v in colorsys.hsv_to_rgb(h, .72, .96))

# photos at full resolution
for n in ['Driver1.JPG', 'Driver2.JPG', 'Driver3.JPG', 'Driver4.jpg']:
    jpg(Image.open(NEW + 'photos/' + n), 'raw_' + n[:7], 93)
for n in ['top', 'bottom']: jpg(Image.open(f'{NEW}board/{n}.jpg'), n)
jpg(Image.open(f'{OLD}board/bottom.jpg'), 'bottom_old')
jpg(Image.open(f'{NEW}audits/vias/sheet-1.jpg'), 'audit_sheet')
for n in ['outline-Driver1', 'outline-Driver3', 'prep-Driver3', 'pin1-U2', 'gaps-top', 'gaps-bottom', 'copper-islands-top', 'copper-islands-bottom', 'overlay-top', 'overlay-bottom', 'materials-top', 'materials-bottom', 'fidelity-F_Cu', 'fidelity-B_Cu', 'fidelity-F_SilkS', 'fidelity-B_SilkS']:
    jpg(Image.open(f'{NEW}evidence/{n}.jpg'), n)
for src, n in [('fidelity-F_Mask opening', 'fidelity-F_Mask'), ('fidelity-B_Mask opening', 'fidelity-B_Mask')]:
    jpg(Image.open(f'{NEW}evidence/{src}.jpg'), n)

cn, co, cm = (json.load(open(p + 'copper.json')) for p in (NEW, OLD, MID))
size = Image.open(f'{NEW}board/top.jpg').size
gold = lambda _: (236, 186, 72)
for nm, c in (('old', co), ('new', cn)):
    for L, s in (('F.Cu', 'top'), ('B.Cu', 'bot')): jpg(paint(c, L, gold, size), f'read_{nm}_{s}')
# dome shadows: first merged run against this one
crop = (1700, 100, 2400, 800)
for nm, c in (('mid', cm), ('new', cn)):
    jpg(paint(c, 'B.Cu', gold, size).crop(crop), f'dome_{nm}')
jpg(Image.open(f'{NEW}board/bottom.jpg').crop(crop), 'dome_photo')

rung = {'copper': (40, 200, 90), 'via': (40, 190, 210), 'inferred': (240, 210, 60), 'group': (240, 140, 40), 'none': (235, 50, 50), 'conflict': (230, 60, 230)}
colr = lambda it: rung.get((it.get('netEvidence') or {}).get('rung'), (120, 120, 120))
named = lambda it: None if str(it.get('net', '')).startswith('UNK') or not it.get('net') else net_rgb(it['net'])
for L, s in (('F.Cu', 'top'), ('B.Cu', 'bot')):
    jpg(paint(cn, L, colr, size), f'rung_{s}'); jpg(paint(cn, L, named, size), f'name_{s}')

# routed copper for 4, 6 and 8 layers on the dimmed top photo
pal = {'F.Cu': (255, 80, 80), 'B.Cu': (80, 160, 255)}; inner = [(250, 210, 60), (70, 220, 120), (200, 120, 255), (255, 150, 60), (120, 230, 230), (240, 120, 200)]
def route(L):
    r = json.load(open(f'{NEW}routes/L{L}/routed.json'))
    base = Image.eval(Image.open(f'{NEW}board/top.jpg').convert('RGB'), lambda v: v // 4); d = ImageDraw.Draw(base)
    names = [u['layer'] for u in r['layerUse'] if u['layer'] not in pal]
    col = {**pal, **{n: inner[i % len(inner)] for i, n in enumerate(names)}}
    for t in r['traces']:
        pts = [px(p) for p in t['path_mm']]
        if len(pts) > 1: d.line(pts, fill=col[t['layer']], width=max(3, round(t['width_mm'] * PX)))
    for v in r['routedVias']:
        x, y = px(v['pos_mm']); d.ellipse((x - 11, y - 11, x + 11, y + 11), outline=(255, 255, 255), width=3)
    fails = [f for f in r['failed'] if f['at_mm'] != [0.0, 0.0]]
    for f in fails:
        x, y = px(f['at_mm']); d.line((x - 15, y - 15, x + 15, y + 15), fill=(255, 255, 255), width=6); d.line((x - 15, y + 15, x + 15, y - 15), fill=(255, 255, 255), width=6)
    return base, r, fails
for L in (2, 4, 6, 8):
    im, r, f = route(L); jpg(im, f'route_{L}')

# pages and board views captured by cap.js
crop_box = None
for n in sorted(os.listdir(ROOT + '/cap')):
    if not n.endswith('.png') or n in ('root.png', 'v126.png'): continue
    jpg(Image.open(f'{ROOT}/cap/{n}'), n[:-4])

# data
P = json.load(open(NEW + 'parts.json'))['parts']
parts = [dict(r=p['ref'], x=p['pos_mm'][0], y=p['pos_mm'][1], pads=(p.get('snap') or {}).get('method') == 'pads', unk=any(v in ('UNKNOWN', 'INFERRED') for v in (p.get('fieldTags') or {}).values())) for p in P]
pos = {p['r']: (p['x'], p['y']) for p in parts}
def hull(pts):
    pts = sorted(set(pts))
    if len(pts) < 3: return pts
    cr = lambda o, a, b: (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0])
    lo, up = [], []
    for q in pts:
        while len(lo) > 1 and cr(lo[-2], lo[-1], q) <= 0: lo.pop()
        lo.append(q)
    for q in pts[::-1]:
        while len(up) > 1 and cr(up[-2], up[-1], q) <= 0: up.pop()
        up.append(q)
    return lo[:-1] + up[:-1]
blocks = [dict(id=b['id'], name=b['name'], n=len(b['parts']), pts=[pos[x] for x in b['parts'] if x in pos], hull=hull([pos[x] for x in b['parts'] if x in pos])) for b in json.load(open(NEW + 'blocks.json'))['blocks']]
vn = [[v['pos_mm'][0], v['pos_mm'][1]] for v in cn['vias']]; vo = [[v['pos_mm'][0], v['pos_mm'][1]] for v in co['vias']]
h = json.load(open(NEW + 'detect/holes-top.json'))
T = json.load(open(NEW + 'tests.json'))['tests']
tests = sorted([dict(id=t['id'], m=t['method'][:64]) for t in T], key=lambda t: (not t['id'].startswith(('T-layers', 'T-via', 'T-net', 'T-plane', 'T-pour')), t['id']))
L = json.load(open(NEW + 'layers.json'))
cand = [dict(l=c['layers'], open=c['open'], conn=c['connections'], drc=c['drcErrors'], kopen=c['kicadOpen'], cross=c['seenCrossingMm'], vias=c['vias'], track=c['trackMm']) for c in L['candidates']]
led = [json.loads(l) for l in open(ROOT + '/../sealed-key/ledger.jsonl')]
ev = json.load(open(NEW + 'net-evidence.json'))['summary']
F = json.load(open(NEW + 'fidelity.json'))
json.dump(dict(parts=parts, blocks=blocks, vn=vn, vo=vo, holes=h['holes'], rows=[r for r in h['rows'] if r['pitch_mm'] < 3], tests=tests, cand=cand, led0=led[0]['result'], led1=led[-1]['result'], ladder=ev['ladder'], ev={k: ev[k] for k in ('pieces', 'conductors', 'stitches', 'pinsSeen', 'pinsTotal', 'pinsMoved', 'pinClashes')}, fid=F['categories']), open(A + '/data.json', 'w'))
print('tests', len(tests), 'cand', cand)