The open risk since session 2 was "a sustained action sequence could still break the bitrate", with every clip measured so far being 1.2-1.7 s. Closed by measurement rather than by sampling clips by hand. 07_motion_survey.py scans a whole stream at 96x72 for the hottest sliding window of inter-frame difference. On 00223 the spread between the quietest and hottest sustained 10 s windows is 10.6x, which is the argument for not eyeballing it. Hottest is t=539.4s, the Singe endgame. There, with the fixed lam the CLI uses, sasi overshoots 110 -> 129.6 KB/s (+18%) and scsi 280 -> 373.8 KB/s (+34%). Rate control moves from "insurance, not a fix" to required, and is promoted above the full-disc survey. The bus is not broken -- 381.6 KB/s still fits the 488 KB/s figure -- so FINDINGS 21 survives, at 78% of the pipe instead of a comfortable margin. Three further corrections fall out: - The two largest streams on the disc are bonus material. 00216 is the feature with a burned-in commentary PiP; 00215 is the commentary. 00223 is the clean 9.4 min. A size-ranked survey would have encoded live action. - On hard content the 256-colour scene palette (31.33 dB) binds well before the X68000 display (40.81 dB); scsi is already within 0.51 dB of it. - FINDINGS 24.5's architecture question resolves to "both paths, chosen per frame": 30-53% of frames sit above the 70% crossover. Picking per frame costs a median 37.0% of the frame budget and caps at 53.6%. Reporting for this is wired into encode.py, which previously only printed a mean over all frames -- the one statistic that cannot answer a per-frame question. extract.py takes optional start/dur; 08_mode_map.py renders source | decoded | block-mode map to .webm. Claude-Session: https://claude.ai/code/session_01194oWYW8DQXK1SZ2DnChW6
58 lines
2.4 KiB
Python
58 lines
2.4 KiB
Python
#!/usr/bin/env python3
|
|
"""Find the worst sustained-motion window in a stream, cheaply.
|
|
|
|
STATUS lists "a sustained action sequence is the one thing that could still
|
|
break the bitrate" as the open risk, and every clip measured so far has been
|
|
1.2-1.7 s. Picking a hot clip by eye is how you get a comfortable answer, so
|
|
this scans the whole stream instead.
|
|
|
|
Proxy: mean absolute inter-frame difference at 96x72, decimated to the target
|
|
12 fps. It is a proxy, not a bitrate -- but the codec's cost is dominated by
|
|
how many blocks fail SKIP, and that is what frame difference measures. The
|
|
window it picks then gets encoded for real.
|
|
|
|
Usage: python3 tools/analysis/07_motion_survey.py 00223 [window_seconds]
|
|
"""
|
|
import subprocess, sys
|
|
import numpy as np
|
|
|
|
STREAM_DIR = "/media/reala-misaki/BDROM/BDMV/STREAM"
|
|
W, H, FPS = 96, 72, 12
|
|
|
|
def frames(stream):
|
|
src = f"{STREAM_DIR}/{stream}.m2ts"
|
|
vf = f"fps={FPS},crop=1440:1080:240:0,scale={W}:{H}:flags=bilinear"
|
|
p = subprocess.Popen(["ffmpeg", "-v", "error", "-i", src, "-vf", vf,
|
|
"-f", "rawvideo", "-pix_fmt", "gray", "-"],
|
|
stdout=subprocess.PIPE)
|
|
buf = p.stdout.read()
|
|
p.wait()
|
|
n = len(buf) // (W * H)
|
|
return np.frombuffer(buf[:n*W*H], np.uint8).reshape(n, H, W).astype(np.int16)
|
|
|
|
def main():
|
|
stream = sys.argv[1]
|
|
win_s = float(sys.argv[2]) if len(sys.argv) > 2 else 10.0
|
|
f = frames(stream)
|
|
d = np.abs(np.diff(f, axis=0)).mean(axis=(1, 2)) # per-frame motion energy
|
|
print(f"{stream}: {len(f)} frames @ {FPS}fps = {len(f)/FPS:.1f}s")
|
|
print(f" motion energy mean {d.mean():.2f} median {np.median(d):.2f} "
|
|
f"p90 {np.percentile(d,90):.2f} max {d.max():.2f}")
|
|
|
|
w = int(win_s * FPS)
|
|
if len(d) < w:
|
|
print("stream shorter than the window"); return
|
|
# sustained = highest mean over a sliding window, not the single hottest frame
|
|
k = np.convolve(d, np.ones(w) / w, mode="valid")
|
|
best = int(np.argmax(k))
|
|
print(f" hottest sustained {win_s:.0f}s window: t = {best/FPS:.1f}s "
|
|
f"(mean {k[best]:.2f}, {k[best]/d.mean():.2f}x stream mean)")
|
|
quiet = int(np.argmin(k))
|
|
print(f" quietest {win_s:.0f}s window: t = {quiet/FPS:.1f}s "
|
|
f"(mean {k[quiet]:.2f}, {k[quiet]/d.mean():.2f}x stream mean)")
|
|
np.save(f"tmp/motion_{stream}.npy", d)
|
|
print(f" per-frame energy -> tmp/motion_{stream}.npy")
|
|
|
|
if __name__ == "__main__":
|
|
main()
|