tools/port/edge-residual-map tiles the frame at 64 px and runs the shift discriminator inside each tile, which is what edge-residual-kind's reach statement said it could not do. Division agreed with the Decoder: the map is mine, the element inventory is theirs. The first control failed and that is the useful part. A known +2 px displacement localises perfectly but reads back +0.839 -- the slope is a linearisation and saturates. So there are two controls now: +1 px asserts localisation AND magnitude (+0.949), +2 px asserts localisation and sign only. A hot tile's slope is a floor on the displacement, never a ceiling. Result: every dx and dy in the top ten tiles is under 0.1 px. Nothing in the hot region has moved, locally or globally, so the misplaced-soft-element candidate now has no support on this screen. Hot tiles cluster at x 384-704, y 64-256 plus an outlier at 640,576; the tool names nothing and the list has gone to the Decoder. And a null: I added a flat-pixel column expecting two families, edge-only against hot-everywhere. The hot tiles run continuously 1.24..3.37 across a median of 1.84. What nearly manufactured the split was carrying over the frame-wide POOLED ratio of 3.16 as the baseline -- pooling is dominated by the tiles with the most edge pixels. Same quantity, wrong population; caught because the baseline was computed before the claim rather than after.
181 lines
7.9 KiB
Python
Executable File
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.")
|