Files
gen1recomp/tools/emerald_ref_compare.py
T
bryanthaboi 690f0cd9a1 TOUCHDOWN
2026-09-27 16:07:31 -04:00

113 lines
4.3 KiB
Python

#!/usr/bin/env python3
import argparse
import json
from pathlib import Path
from PIL import Image, ImageChops
GBA_FPS = 262144 / 4389
LANDMARKS = [
("copyright_white", 14), ("copyright_fade_in", 20), ("copyright_hold_end", 155),
("copyright_fade_out", 162), ("scene1_fade_in", 212), ("gf_logo_blend_in", 345),
("gf_letters", 420), ("gf_logo_blend_out", 480), ("drop_ripple", 500), ("sparkles_pan", 800),
("flygon_silhouette", 1060), ("scene1_white", 1225), ("scene2_fade_in", 1250), ("scene2_mid", 1700),
("scene2_torchic_trip", 2000), ("scene2_white", 2200), ("scene3_pokeball", 2300),
("scene3_groudon_narrow", 2345), ("scene3_groudon", 2440), ("scene3_kyogre", 2560),
("scene3_kyogre_zoom", 2690), ("scene3_clouds", 2800), ("scene3_rayquaza", 3000),
("scene3_orb", 3140), ("title_logo", 3270), ("title_shine", 3300), ("title_banner", 3560),
("title_settle", 3700),
]
def load(path):
return Image.open(path).convert("RGB")
def diff(a, b, bits5, tolerance=0):
if bits5:
a = a.point(lambda v: v >> 3)
b = b.point(lambda v: v >> 3)
d = ImageChops.difference(a, b)
r, g, bl = d.split()
m = ImageChops.lighter(ImageChops.lighter(r, g), bl).point(lambda v: 255 if v > tolerance else 0)
return 100.0 * m.histogram()[255] / (a.width * a.height)
def big(a, b):
return diff(a, b, False, 8)
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--ours", required=True)
ap.add_argument("--ref", required=True)
ap.add_argument("--offset", type=int, default=12)
ap.add_argument("--window", type=int, default=3)
ap.add_argument("--sweep", action="store_true")
ap.add_argument("--json")
a = ap.parse_args()
ours, ref = Path(a.ours), Path(a.ref)
cache = {}
def our(n):
if n not in cache:
p = ours / ("f%05d.png" % n)
cache[n] = load(p) if p.exists() else None
return cache[n]
rows = []
for name, r in LANDMARKS:
rp = ref / ("frame-%06d.png" % r)
if not rp.exists():
continue
rimg = load(rp)
base = r - a.offset
best = None
for d in range(-a.window, a.window + 1):
o = our(base + d)
if o is None:
continue
pct = diff(rimg, o, True)
if best is None or pct < best[1] or (pct == best[1] and abs(d) < abs(best[0])):
best = (d, pct)
at = our(base)
exact = diff(rimg, at, True) if at is not None else None
coarse = big(rimg, at) if at is not None else None
rows.append({
"landmark": name, "ref_frame": r, "ref_s": round(r / GBA_FPS, 4),
"engine_tick": base, "engine_s": round(base / 60, 4),
"diff_at_offset_pct": None if exact is None else round(exact, 3),
"diff_over_one_step_pct": None if coarse is None else round(coarse, 3),
"best_shift": None if best is None else best[0],
"best_diff_pct": None if best is None else round(best[1], 3),
})
print("%-24s ref=%5d engine=%5d diff@0=%7s >1step=%7s best shift=%+d diff=%7s" % (
name, r, base, "-" if exact is None else "%.3f%%" % exact,
"-" if coarse is None else "%.3f%%" % coarse,
0 if best is None else best[0], "-" if best is None else "%.3f%%" % best[1]))
if a.sweep:
worst = []
for p in sorted(ours.glob("f?????.png")):
n = int(p.stem[1:])
rp = ref / ("frame-%06d.png" % (n + a.offset))
if not rp.exists():
continue
ri, oi = load(rp), load(p)
pct = diff(ri, oi, True)
worst.append((big(ri, oi), pct, n))
worst.sort(reverse=True)
exact = sum(1 for w in worst if w[1] == 0)
near = sum(1 for w in worst if w[0] == 0)
print("sweep: %d frames, %d pixel-exact (5-bit), %d within one colour step, mean diff %.3f%%, mean >1step %.3f%%" % (
len(worst), exact, near, sum(w[1] for w in worst) / max(1, len(worst)),
sum(w[0] for w in worst) / max(1, len(worst))))
for coarse, pct, n in worst[:25]:
print(" worst engine=%5d ref=%5d >1step=%.3f%% diff=%.3f%%" % (n, n + a.offset, coarse, pct))
if a.json:
Path(a.json).write_text(json.dumps(rows, indent=1))
if __name__ == "__main__":
main()