Files
Sylpheed/tools/port/edge-residual-map
Sylpheed port agent 50be9578c5 recover: the F5/F6 port work from the deleted auto/port-p6-audio
A snapshot of the non-game files as of 0148cb8 ("port: F5/F6 hand-off --
one-minute human checks, and a refutation attempt that survived",
2026-09-04), the tip of auto/port-p6-audio. The branch was deleted from the
server on 2026-09-17 during the consolidation cleanup; issue #7 asks for
this work as a reviewable PR, so it is recovered here before the commits
are garbage collected.

Contents: the 84 files the branch changed relative to its fork point
b305aa4, which is this commit's parent. The tree is therefore 0148cb8's
tree with the 854 exported game assets left out -- export-probe/,
export-probe2/, three .wav renders of game audio and adv-v2-screenlog.tsv.
Game data stays out of git; the exporter regenerates those from the disc.
docs/port/DECISIONS.md still refers to them by name.

Not recovered: the branch's own 366 commits. Keeping them would make those
assets reachable again, so this is one snapshot instead. The original
commits stay unreferenced in the server's object store, and in this clone
under the local branch archive/port-p6-audio, until either is garbage
collected.

Refs #7. The OPTIONS work that issue #6 asks for is a subset of this
branch, also recovered as recover/options-menu.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 20:48:47 +02:00

181 lines
7.9 KiB
Python
Executable File

#!/usr/bin/env python3
"""WHERE does the edge residual sit, and is that region locally shifted?
RENDER=<png> tools/port/edge-residual-map [screen]
`edge-residual-kind` fits the whole frame and excludes a GLOBAL translation. Its
own reach statement says the thing it cannot do: one misplaced element is a small
share of 38 752 edge pixels and would not move a whole-frame number. This tiles
the frame and runs the same discriminator INSIDE each tile, so a single displaced
element shows up as one hot tile with a local slope -- which is invisible to the
global fit by construction, not by accident.
Division of labour, agreed with the Decoder 2026-08-31: the residual map is the
port's (it needs the render beside the capture), the element inventory is theirs
(it needs the disc). This tool produces the map and NAMES NOTHING.
THE CONTROL IS A KNOWN LOCAL SHIFT. A map that cannot localise a displacement it
was told about cannot be trusted to have found one it was not. Exit 0 the report
is trustworthy, 2 the control failed and the report is suppressed. No 1.
"""
import math, os, subprocess, sys, tempfile
CAPS = "docs/re/captures/title-builds"
SCREEN = sys.argv[1] if len(sys.argv) > 1 else "main_menu"
W, H = 1279, 675 # top-left crop of the guest surface; never scaled
TILE = 64
EDGE = 12
MIN_EDGE_PX = 150 # below this a tile's slope is noise
# The controls displace this region and the map must find it there.
CTRL_BOX = (448, 320, 640, 448) # x0, y0, x1, y1
# TWO controls, because ONE OF THEM FAILED AND TAUGHT ME THE LIMIT. The slope is
# a linearisation, residual ~ dx * gradient, which holds only while dx is small
# against the width of an edge. A +2 px displacement localises perfectly but reads
# back +0.8..+1.25, so the estimator SATURATES. Control A checks magnitude in the
# regime where magnitude means something; control B checks that a displacement too
# large to measure is still FOUND. Reporting a saturating slope as a distance
# would understate a real displacement by more than half.
CTRL_A_DX = 1 # linear regime: localisation AND magnitude
CTRL_B_DX = 2 # saturating: localisation and SIGN only
def gray(png, out):
subprocess.run(["convert", png, "-colorspace", "Gray", "-depth", "8",
"gray:" + out], check=True)
return open(out, "rb").read()
def lutfit(a, b):
tot = [0] * 256; cnt = [0] * 256
for i in range(len(a)):
tot[a[i]] += b[i]; cnt[a[i]] += 1
return [(tot[v] // cnt[v]) if cnt[v] else v for v in range(256)]
def tiles(a, b):
"""Per-tile mean |residual| on edge pixels, and the local shift slope."""
lut = lutfit(a, b) # ONE global LUT: tone is global, displacement is not
out = {}
for ty in range(0, H - 1, TILE):
for tx in range(0, W - 1, TILE):
gx = []; gy = []; rs = []; flat = []
for y in range(max(1, ty), min(H - 1, ty + TILE)):
o = y * W
for x in range(max(1, tx), min(W - 1, tx + TILE)):
i = o + x
ax = (a[i + 1] - a[i - 1]) * 0.5
ay = (a[i + W] - a[i - W]) * 0.5
d = float(lut[a[i]] - b[i])
if abs(ax) + abs(ay) < EDGE:
flat.append(abs(d)); continue
gx.append(ax); gy.append(ay); rs.append(d)
n = len(rs)
if n < MIN_EDGE_PX:
continue
mabs = sum(abs(v) for v in rs) / n
mflat = (sum(flat) / len(flat)) if flat else 0.0
mr = sum(rs) / n
def slope(u):
mu = sum(u) / n
suu = sum((v - mu) ** 2 for v in u)
if suu <= 0:
return 0.0
return sum((u[k] - mu) * (rs[k] - mr) for k in range(n)) / suu
out[(tx, ty)] = (mabs, slope(gx), slope(gy), n, mflat)
return out
def top(t, k=8):
return sorted(t.items(), key=lambda kv: -kv[1][0])[:k]
def show(t, label, k=8):
print(f" {label}")
print(f" {'tile':>12} {'edge':>7} {'flat':>7} {'e/f':>6} "
f"{'dx':>7} {'dy':>7} {'edge px':>8}")
for (tx, ty), (m, sx, sy, n, mf) in top(t, k):
ef = (m / mf) if mf > 0.01 else float('inf')
print(f" {tx:4d},{ty:4d} {m:7.2f} {mf:7.2f} {ef:6.2f} "
f"{sx:+7.3f} {sy:+7.3f} {n:8d}")
def shift_box(a, box, dx):
x0, y0, x1, y1 = box
out = bytearray(a)
for y in range(y0, y1):
for x in range(x0, x1):
out[y * W + x] = a[y * W + min(W - 1, max(0, x - dx))]
return bytes(out)
tmp = tempfile.mkdtemp()
cap = f"{CAPS}/live-{SCREEN.replace('_', '-')}.png"
render = os.environ.get("RENDER", "")
for p in (cap, render):
if not p or not os.path.exists(p):
print(f" 🔴 missing: {p or 'RENDER=<png>'}"); sys.exit(2)
subprocess.run(["convert", render, "-crop", f"{W}x{H}+0+0", "+repage",
f"{tmp}/crop.png"], check=True)
r = gray(f"{tmp}/crop.png", f"{tmp}/r.gray")
c = gray(cap, f"{tmp}/c.gray")
print("CONTROLS -- the render against itself with ONE REGION displaced.\n"
"The map must put that region on top; magnitude only in the linear regime.\n")
x0, y0, x1, y1 = CTRL_BOX
bad = []
def control(dx, check_magnitude):
t = tiles(r, shift_box(r, CTRL_BOX, dx))
show(t, f"known +{dx} px shift inside x {x0}-{x1}, y {y0}-{y1}", 4)
hits = [(k, v) for k, v in top(t, 4)
if x0 - TILE < k[0] < x1 and y0 - TILE < k[1] < y1]
if not hits:
bad.append(f"+{dx} px: displaced region not in the top 4 tiles")
return
best = max(hits, key=lambda kv: kv[1][0])[1][1]
if best <= 0.3:
bad.append(f"+{dx} px: local slope {best:+.3f} has the wrong sign or is flat")
elif check_magnitude and abs(best - dx) > 0.4:
bad.append(f"+{dx} px: local slope {best:+.3f} does not recover it")
print(f" -> localised, local slope {best:+.3f} px"
f"{'' if check_magnitude else ' (saturating -- a LOWER BOUND)'}\n")
control(CTRL_A_DX, True)
control(CTRL_B_DX, False)
if bad:
print(" 🔴 CONTROL FAILED: " + "; ".join(bad))
print(" A map that cannot find a displacement it was told about cannot be")
print(" trusted to have found one it was not. Report suppressed.")
sys.exit(2)
print(" ✅ controls pass: a 1 px displacement is localised and measured, a 2 px\n"
" one is localised with its magnitude understated. So a hot tile with a\n"
" real slope is a floor on the displacement, never a ceiling.\n")
print(f"THE REAL PAIR -- {SCREEN}\n")
rt = tiles(r, c)
show(rt, f"{SCREEN}: hottest tiles, whole-frame LUT applied", 10)
ms = sorted(v[0] for v in rt.values())
med = ms[len(ms) // 2]
efs = sorted(v[0] / v[4] for v in rt.values() if v[4] > 0.01)
med_ef = efs[len(efs) // 2]
hot = max(rt.items(), key=lambda kv: kv[1][0])
print(f"\n median tile |resid| {med:.2f} hottest {hot[1][0]:.2f} "
f"at {hot[0][0]},{hot[0][1]} ({hot[1][0]/med:.2f}x median)")
print(f" median tile edge/flat {med_ef:.2f}")
efs_hot = [v[0] / v[4] for _, v in top(rt, 10) if v[4] > 0.01]
print(f" hot tiles span edge/flat {min(efs_hot):.2f}..{max(efs_hot):.2f}, "
f"straddling that median")
print(" 📌 SO THE COLUMN DOES NOT SPLIT THEM. I added it expecting two families --")
print(" tiles hot only at edges (an edge-rendering difference) against tiles")
print(" hot everywhere (a local tone the global LUT mis-serves). The hot tiles")
print(" run continuously across the median instead, so the hot region is NOT")
print(" one anomalous element with a character of its own. Note the frame-wide")
print(" pooled edge/flat is 3.16 while the per-tile median is 1.84: pooling is")
print(" dominated by the tiles carrying the most edge pixels, and reading a")
print(" per-tile threshold off it would have manufactured the split.")
print("\n ⚠️ THIS TOOL NAMES NOTHING. A hot tile is a coordinate, not an element.")
print(" What sits under it is the Decoder's to say -- they hold the disc.")