Files
Dragon-s-Lair-X68k/tools/encoder/extract.py
T
prosolis e00264a058 Find the sustained action sequence: it breaks both profiles
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
2026-08-23 14:00:12 -07:00

48 lines
2.1 KiB
Python

#!/usr/bin/env python3
"""Extract decimated frames from a Blu-ray .m2ts into 256x192 PNGs.
Source is 1920x1080 (16:9). The arcade original is 4:3, so we CENTER-CROP to
1440x1080 by default -- see docs/STATUS.md open question on framing.
"""
import subprocess, sys, os, shutil
STREAM_DIR = "/media/reala-misaki/BDROM/BDMV/STREAM"
W, H = 256, 192
def duration(path):
out = subprocess.check_output(["ffprobe","-v","error","-show_entries",
"format=duration","-of","csv=p=0",path], text=True)
return float(out.strip())
def extract(stream, outdir, fps=12, mode="crop", start=None, dur=None):
src = f"{STREAM_DIR}/{stream}.m2ts"
total = duration(src)
if start is None: start = 0.0
if dur is None: dur = total - start
shutil.rmtree(outdir, ignore_errors=True); os.makedirs(outdir)
if mode == "crop": # 4:3 centre crop, arcade framing
vf = f"crop=1440:1080:240:0,hqdn3d=4:3:0:0,scale={W}:{H}:flags=lanczos"
elif mode == "squash": # full 16:9 squeezed into 4:3
vf = f"hqdn3d=4:3:0:0,scale={W}:{H}:flags=lanczos"
elif mode == "wide": # 16:9 preserved, letterboxed later
vf = f"hqdn3d=4:3:0:0,scale={W}:144:flags=lanczos"
else: raise ValueError(mode)
vf = f"fps={fps}," + vf
subprocess.check_call(["ffmpeg","-v","error","-ss",str(start),"-t",str(dur),
"-i",src,"-vf",vf,"-vsync","0",f"{outdir}/f%04d.png","-y"])
n = len(os.listdir(outdir))
print(f"{stream}: dur={total:.2f}s -> {n} frames @{fps}fps ({mode})")
return n
if __name__ == "__main__":
# extract.py <stream> <outdir> [fps] [mode] [start_s] [dur_s]
# start/dur matter for the long streams: 00223 is 9.4 min and the parts
# that stress the codec are a few seconds each (tools/analysis/07 finds
# them). Without them a survey silently averages action into idle scenes.
stream, outdir = sys.argv[1], sys.argv[2]
fps = int(sys.argv[3]) if len(sys.argv) > 3 else 12
mode = sys.argv[4] if len(sys.argv) > 4 else "crop"
start = float(sys.argv[5]) if len(sys.argv) > 5 else None
dur = float(sys.argv[6]) if len(sys.argv) > 6 else None
extract(stream, outdir, fps, mode, start, dur)