#!/usr/bin/env python3 """Score a composite against a framebuffer capture of the running game. Edge-correlates the two over a region and reports the best shift. A composite that is *right* peaks at (0,0); one that is displaced peaks somewhere else and scores lower, which is how the resting-pose rule was settled (docs/re/structures/ui-resting-pose.md). Colour is deliberately discarded — the capture and the composite differ in palette (undecoded materials, a different animation moment for the background), so a raw pixel diff is dominated by things the geometry question does not care about. Gradient magnitude keeps the edges, which is where placement lives. Both images must share an origin. A capture that is a 1:1 *crop* of the frame is fine; a scaled one is not, and must be resampled first. align_to_capture.py CAPTURE COMPOSITE [COMPOSITE...] [--region X0 Y0 X1 Y1] """ import argparse import numpy as np from PIL import Image def edges(path): a = np.asarray(Image.open(path).convert("L"), dtype=float) gy, gx = np.gradient(a) return np.hypot(gx, gy) def norm(a): return (a - a.mean()) / (a.std() + 1e-9) def main(): ap = argparse.ArgumentParser() ap.add_argument("capture") ap.add_argument("composite", nargs="+") ap.add_argument("--region", nargs=4, type=int, metavar=("X0", "Y0", "X1", "Y1"), default=[150, 200, 1150, 400], help="frame-space box to score over (default: the title wordmark)") ap.add_argument("--radius", type=int, default=14, help="max shift searched, px") args = ap.parse_args() x0, y0, x1, y1 = args.region ref = norm(edges(args.capture)[y0:y1, x0:x1]) r = args.radius for path in args.composite: img = edges(path) best = (-2.0, 0, 0) for dy in range(-r, r + 1): for dx in range(-r, r + 1): pat = img[y0 + dy:y1 + dy, x0 + dx:x1 + dx] if pat.shape != ref.shape: continue s = float((ref * norm(pat)).mean()) if s > best[0]: best = (s, dx, dy) zero = float((ref * norm(img[y0:y1, x0:x1])).mean()) print(f"{path}: best {best[0]:.4f} at ({best[1]:+d},{best[2]:+d}) " f"at (0,0): {zero:.4f}") if __name__ == "__main__": main()