formats: the resting pose is the hold, not the longest dwell
Element::rest() picked the keyframe with the largest gap to the next keyframe's
time. That reads a keyframe as a value held until the next one; it is the start
of a ramp toward it. A long gap after keyframe k means the screen spends that
time arriving at k+1, so the settled pose is at the far end of the gap.
The title wordmark zooms in over five frames and holds at (184,193) at 100% from
t=251 to t=264. The old rule picked the frame before the long gap: (179,186) at
101%, still mid-zoom.
Measured against the framebuffer capture of the running title screen, which is a
1:1 crop so frame coordinates map directly (confirmed: the copyright line lands
on row 669 in the capture and in both composites). Edge-correlated over the
wordmark box:
plateau (landed) best 0.4597 at shift (0,0)
longest dwell (old) best 0.1511 at shift (+3,+8), 0.1268 at (0,0)
The old composite scores 3x lower and only peaks after being moved, by about the
(-5,-7) that picking kf4 instead of kf5 predicts.
It also fixes six title elements the old rule rested at alpha 0x00 where the
capture plainly shows them, and pteff00.prm - the full-screen fade quad painted
last - which rested at opaque black. That was the blocker on .prm compositing.
Adds tools/re-capture/align_to_capture.py, which is how this was scored, and
turns the .prm test that deliberately asserted the old defect into a guard on
the fix.
Not settled and now the next item: compose ignores the keyframe fade alpha
entirely (blit modulates by tint only), which is why choosing the wrong keyframe
was invisible until now.
This commit is contained in:
65
tools/re-capture/align_to_capture.py
Executable file
65
tools/re-capture/align_to_capture.py
Executable file
@@ -0,0 +1,65 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user