#!/usr/bin/env python3 """Read focus_ring_probe.py's accumulators, after ALIGNING them to game space. A grab is the whole root window; game coordinates only address it once the window chrome offset is measured. This script measures that offset by correlating the run's own mean frame against the committed `live-main-menu.png` over a +/-12 px search, and refuses to report anything if the alignment is poor. Then, in game coordinates: ring boxes -- 80x80 around each button's declared rest position; the ring `ptbtneff01` is 42x46 and sits left of the label static boxes -- `ptmsg` (one untimed keyframe) and a background corner: the NEGATIVE controls, which must read sensor noise positive ctrl -- |mean(A) - mean(C)| across the d-pad press must fire at the two rings that changed state, or a null in A is a dead instrument rather than a finding. """ import os, sys import numpy as np from PIL import Image OUT = sys.argv[1] if len(sys.argv) > 1 else "/sylph-home/re/ringcap" REPO = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) REF = os.path.join(REPO, "docs/re/captures/title-builds/live-main-menu.png") BTN_Y = [162, 242, 322, 401, 482] LABEL = ["NEW GAME", "LOAD GAME", "TUTORIAL", "OPTIONS", "EXTRAS"] BOXES = {} for i, y in enumerate(BTN_Y): BOXES[f"ring{i+1} ({LABEL[i]})"] = (480, y - 20, 560, y + 60) BOXES["ptmsg footer [static ctl]"] = (527, 595, 773, 633) BOXES["bg corner [static ctl]"] = (10, 10, 130, 130) BOXES["button1 label [same row]"] = (560, 142, 760, 202) def gray(a): return (0.299 * a[..., 0] + 0.587 * a[..., 1] + 0.114 * a[..., 2]).astype(np.float32) def zncc(x, y): x = x - x.mean(); y = y - y.mean() d = np.sqrt((x * x).sum() * (y * y).sum()) return float((x * y).sum() / d) if d else 0.0 def align(mean_rgb, ref_rgb): """Measure (dy,dx) taking GAME coords -> GRAB coords. Returns (dy,dx,corr).""" g = gray(mean_rgb); r = gray(ref_rgb) rh, rw = r.shape best = (None, None, -1.0) for dy in range(30, 60): # chrome is ~45 rows for dx in range(-12, 13): if dy + rh > g.shape[0] or dx < 0 or dx + rw > g.shape[1]: continue c = zncc(g[dy:dy + rh, dx:dx + rw], r) if c > best[2]: best = (dy, dx, c) return best def main(): mA = np.load(f"{OUT}/A-mean.npy"); sA = np.load(f"{OUT}/A-std.npy") mC = np.load(f"{OUT}/C-mean.npy"); sC = np.load(f"{OUT}/C-std.npy") ref = np.array(Image.open(REF).convert("RGB")).astype(np.float32) dy, dx, corr = align(mA, ref) print(f"alignment: game(0,0) sits at grab({dx},{dy}); ZNCC {corr:+.4f}") if corr < 0.80: print("ALIGNMENT TOO POOR — refusing to report boxes"); return 1 print(f" (independent check: the window chrome measured 45 rows)\n") def box(arr, b): x0, y0, x1, y1 = b return arr[y0 + dy:y1 + dy, x0 + dx:x1 + dx, :] d = np.abs(mA - mC) print(f"{'box':<30} {'A std':>9} {'A p99.9':>9} {'C std':>9} " f"{'|A-C| mean':>11} {'|A-C| max':>10}") print("-" * 84) rows = {} for k, b in BOXES.items(): a_s = box(sA, b); c_s = box(sC, b); dd = box(d, b) rows[k] = (float(a_s.mean()), float(np.percentile(a_s, 99.9)), float(c_s.mean()), float(dd.mean()), float(dd.max())) print(f"{k:<30} {rows[k][0]:9.3f} {rows[k][1]:9.3f} {rows[k][2]:9.3f} " f"{rows[k][3]:11.3f} {rows[k][4]:10.2f}") noise = max(rows["ptmsg footer [static ctl]"][0], rows["bg corner [static ctl]"][0]) print(f"\nnegative-control noise floor (max of the two static boxes): {noise:.3f}") print("A box only counts as MOVING if its phase-A std clears that floor.\n") for k in BOXES: if "ctl" in k: continue v = rows[k][0] print(f" {k:<30} A std {v:7.3f} = {v/noise:6.2f}x the noise floor" f" {'MOVING' if v > 3*noise else 'static'}") # visual artefacts, cropped to the game surface for tag, arr, sc in (("A-std", sA, 8), ("C-std", sC, 8), ("AC-absdiff", d, 4)): g = arr[dy:dy + 675, dx:dx + 1279, :] Image.fromarray(np.clip(g * sc, 0, 255).astype(np.uint8)).save(f"{OUT}/{tag}-game.png") Image.fromarray(mA[dy:dy + 675, dx:dx + 1279, :].astype(np.uint8)).save(f"{OUT}/A-mean-game.png") Image.fromarray(mC[dy:dy + 675, dx:dx + 1279, :].astype(np.uint8)).save(f"{OUT}/C-mean-game.png") print(f"\nwrote game-space artefacts to {OUT}") return 0 if __name__ == "__main__": sys.exit(main())