Files
Dragon-s-Lair-X68k/tools/analysis/08_mode_map.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

108 lines
5.0 KiB
Python

#!/usr/bin/env python3
"""Render what the codec is actually DOING, per block, per frame.
Three panels at 12 fps: the palettised source (the real quality ceiling, not
1080p -- FINDINGS 11), the decoded output, and a block-mode map.
The mode map is not decoration. FINDINGS 24.5 makes the per-frame non-SKIP
fraction the number that selects the decoder's inner loop, and a percentile
cannot show you that the non-SKIP blocks are CLUSTERED (a moving character on a
held background) rather than scattered. Clustering is what a run-length over
the mode headers would exploit.
SKIP left as the previous frame, costs the 68000 nothing
V1 one 4x4 codeword, 1 byte
V4 four 2x2 codewords, 4 bytes
RAW 16 literal palette indices -- the escape that makes lam=0 pixel-exact
Usage: python3 tools/analysis/08_mode_map.py <frames_dir> <out.webm> [--profile p]
[--scale N]
Output format follows the extension. Prefer .webm: GIF re-quantises to 256
colours, which is a poor fit for output whose subject is colour fidelity.
"""
import sys, os
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "encoder"))
import numpy as np
from PIL import Image
import vq as VQ, vq_hybrid as H, ratectl as RC
MODE_RGB = np.array([[ 20, 22, 30], # SKIP - near black, costs nothing
[ 60, 150, 230], # V1 - blue
[ 80, 200, 120], # V4 - green
[235, 90, 70]], # RAW - red, the expensive escape
dtype=np.uint8)
LABEL = ["SKIP", "V1", "V4", "RAW"]
SCALE = 1
LOSSLESS = False
def main():
global SCALE, LOSSLESS
LOSSLESS = "--lossless" in sys.argv
src, out = sys.argv[1], sys.argv[2]
if "--scale" in sys.argv:
SCALE = int(sys.argv[sys.argv.index("--scale")+1])
prof = RC.PROFILES[sys.argv[sys.argv.index("--profile")+1]
if "--profile" in sys.argv else "sasi"]
m = H.build(src, k1=prof["k1"], k4=prof["k4"])
enc = H.encode(m, lam=prof["lam"])
pal, H_, W_ = m["pal"], m["H"], m["W"]
nbx, nby = W_ // 4, H_ // 4
frames, stats = [], []
for f, (rec, mode) in enumerate(zip(enc["recon"], enc["modes"])):
srcp = pal[m["idx"][f]]
decp = pal[rec]
mmap = MODE_RGB[mode.reshape(nby, nbx)].repeat(4, 0).repeat(4, 1)
# tint the mode map with the decoded luma so the action stays legible
luma = decp.mean(2, keepdims=True) / 255.0
mmap = (mmap * (0.45 + 0.55 * luma)).astype(np.uint8)
gap = np.full((H_, 3, 3), 60, np.uint8)
panel = np.hstack([srcp, gap, decp, gap, mmap])
im = Image.fromarray(panel)
if SCALE != 1:
im = im.resize((panel.shape[1]*SCALE, H_*SCALE), Image.NEAREST)
frames.append(im)
stats.append([(mode == i).mean() for i in range(4)])
if out.endswith(".webm") or out.endswith(".mp4"):
# Preferred. GIF would impose its own 256-colour palette on top of
# output whose entire subject is colour fidelity, and costs ~4x the
# bytes doing it. -lossless keeps the panels pixel-exact.
import subprocess
w, h = frames[0].size
cmd = ["ffmpeg", "-v", "error", "-y", "-f", "rawvideo", "-pix_fmt", "rgb24",
"-s", f"{w}x{h}", "-r", "12", "-i", "-"]
# yuv444p, not 420: the mode map is flat saturated colour on a 4-pixel
# grid, and chroma subsampling smears exactly those edges. Lossless is
# available but runs larger than the GIF on this content; crf 18 in 444
# is visually clean at a quarter the size.
if out.endswith(".webm"):
cmd += ["-c:v", "libvpx-vp9", "-pix_fmt", "yuv444p", "-row-mt", "1"]
cmd += ["-lossless", "1"] if LOSSLESS else ["-crf", "18", "-b:v", "0"]
else:
cmd += ["-c:v", "libx264", "-crf", "12", "-pix_fmt", "yuv444p"]
p = subprocess.Popen(cmd + [out], stdin=subprocess.PIPE)
for f in frames:
p.stdin.write(np.asarray(f.convert("RGB")).tobytes())
p.stdin.close(); p.wait()
else:
# One shared adaptive palette: per-frame palettes are what make a naive
# GIF of this enormous, and a stable palette also stops the mode-map
# colours shimmering between frames.
shared = frames[0].quantize(colors=192, method=Image.MEDIANCUT)
q = [f.quantize(palette=shared, dither=Image.NONE) for f in frames]
q[0].save(out, save_all=True, append_images=q[1:],
duration=1000//12, loop=0, optimize=True)
st = np.array(stats)
print(f"{len(frames)} frames -> {out} ({os.path.getsize(out)/1024:.0f} KB)")
print(" panels: palettised source | decoded | block mode map")
for i, n in enumerate(LABEL):
print(f" {n:4s} mean {100*st[:,i].mean():5.1f}% "
f"per-frame range {100*st[:,i].min():5.1f}% .. {100*st[:,i].max():5.1f}%")
ns = 100 * (1 - st[:, 0])
print(f" non-SKIP: median {np.median(ns):.1f}% p90 {np.percentile(ns,90):.1f}%")
if __name__ == "__main__":
main()