Put the frame in a container with no decoder, and find the palette is not free
ROADMAP K2. DLXP1: a 49,664 B record that is 97 sectors exactly, no index and no length word, because a packed record's length is geometry rather than content. 582.0 KB/s, which is what FINDINGS 61.9 predicted to the tenth, and it encodes in 3.3 s because there is no k-means in it. px68k's own x68k/gvram.c renders the container's bytes index-exact with the harness computing no interleave -- the only test that can catch an encoder whose byte order is wrong, since a container round-trips against its own inverse either way. Both negative controls fail as they must. The picture is re-derived against this project's builder rather than PIL's (34.05 dB against 61.9's 34.08) and the GGGGGRRRRRBBBBBI word is charged for the first time in this tree: 0.53 dB, on every row, so it moves no comparison. What the control found is the finding. A packed container on a SCENE palette lands exactly on the codec's ceiling, so the whole +2.31 dB is the per-frame palette and nothing else -- and 231 of 256 entries change every frame, which makes a mismatched paint 12.8 dB worse than the correct pairing, on screen for roughly half of every frame slot if buffer mode does not blank. So B2 now decides which packed CONTAINER ships, not only which player. The fallback is already a flag: --scene-palette --no-palette is 30.79 dB, zero churn, 576.0 KB/s and still +2.07 dB on the shipping codec. 62.5 is priced and is a wash: palette first 20.32 dB, palette last 20.33. Claude-Session: https://claude.ai/code/session_01194oWYW8DQXK1SZ2DnChW6
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Encode one scene to the PACKED container -- ROADMAP K2.
|
||||
|
||||
python3 tools/encoder/pack.py <frames_dir> <out.dlxp> [--fps 12]
|
||||
[--nframes N] [--palette-last] [--no-palette]
|
||||
[--scene-palette]
|
||||
|
||||
WHAT IS NOT HERE IS THE POINT. No VQ, no codebooks, no mode map, no rate
|
||||
control, no `lam`, no leaky bucket, no span geometry -- `encode.py` is 452 lines
|
||||
and 95% of its wall clock is k-means. A packed frame is a palette and a
|
||||
picture, and both are geometry. FINDINGS 61: at the 9 clk/B dual-address floor
|
||||
the codec is 110.4% of a 12 fps frame and this is 55.2%, so the thing that
|
||||
replaces the codec is also the thing that is simpler than it.
|
||||
|
||||
THERE IS NO RATE CONTROL BECAUSE THERE IS NO RATE LEVER. A codec's bitrate is
|
||||
adjustable; a literal frame's is geometry. The wire cost of this container is
|
||||
fixed by W, H and fps and nothing an encoder does can move it, which is exactly
|
||||
why FINDINGS 61.6 says the medium question decides which player exists. A
|
||||
`--kbps` argument here would be a lie of the shape FINDINGS 50 removed from the
|
||||
rest of the tree.
|
||||
|
||||
THE QUANTISER IS THIS PROJECT'S, NOT PIL'S DEFAULT PATH. 61.9's +4.89 dB was
|
||||
measured with `18_text_plane_16col.py`'s free 256-colour MEDIANCUT and was filed
|
||||
as "a direction, not the player's number" (risk 2 in the session 30 handoff).
|
||||
`vq.frame_palette` is what actually ships it: 254 colours, index 0 held free for
|
||||
the transparency key, black at 255. `tools/analysis/30_packed_container.py`
|
||||
re-derives the figure against this and charges the GRB555 word on top.
|
||||
"""
|
||||
import argparse, glob, os, sys
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
||||
"..", "bench"))
|
||||
import vq as VQ
|
||||
import dlxp as P
|
||||
from dlxload import pack_palette
|
||||
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("frames_dir")
|
||||
ap.add_argument("out")
|
||||
ap.add_argument("--fps", type=int, default=12)
|
||||
ap.add_argument("--nframes", type=int, default=None,
|
||||
help="encode only the first N frames (the gate window is 120)")
|
||||
ap.add_argument("--palette-last", action="store_true",
|
||||
help="put the palette after the picture in every record. "
|
||||
"FINDINGS 62.5 -- a design choice, not a default to inherit")
|
||||
ap.add_argument("--no-palette", action="store_true",
|
||||
help="picture only, 49,152 B a record. NOT a shipping option: "
|
||||
"it re-imposes the scene palette the codec is capped by")
|
||||
ap.add_argument("--scene-palette", action="store_true",
|
||||
help="one palette for the whole scene, repeated in every record "
|
||||
"-- the CONTROL for 61.9's per-frame claim")
|
||||
a = ap.parse_args()
|
||||
|
||||
files = sorted(glob.glob(f"{a.frames_dir}/f*.png"))
|
||||
if a.nframes:
|
||||
files = files[:a.nframes]
|
||||
if not files:
|
||||
sys.exit(f"no f*.png in {a.frames_dir}")
|
||||
rgb = [np.asarray(Image.open(f).convert("RGB")) for f in files]
|
||||
H, W = rgb[0].shape[:2]
|
||||
if any(r.shape[:2] != (H, W) for r in rgb):
|
||||
sys.exit("frames are not all the same size")
|
||||
|
||||
# The scene-palette control shares ONE palette across every record, which is the
|
||||
# constraint the codec cannot escape (61.9) and this format merely chooses not
|
||||
# to inherit. It is built by the same routine the codec uses, with black at 0
|
||||
# moved to 255 and index 0 vacated, so the only variable between the two runs is
|
||||
# per-frame versus scene-wide.
|
||||
scene = None
|
||||
if a.scene_palette:
|
||||
# 255, not 256: reserve_black spends one entry on black and the packed
|
||||
# layout needs the OTHER end free too, so the picture gets 254 either way.
|
||||
ref, spal = VQ.scene_palette(rgb, colors=255, reserve_black=True)
|
||||
sidx = VQ.palettise(rgb, ref)
|
||||
# 0 -> 255: the packed layout needs index 0 free and black displayed at 255.
|
||||
spal = np.vstack([np.zeros((1, 3), np.uint8), spal[1:],
|
||||
np.zeros((1, 3), np.uint8)])
|
||||
scene = (spal, [np.where(i == 0, np.uint8(255), i) for i in sidx])
|
||||
|
||||
recs, psnr_pal = [], []
|
||||
for n, src in enumerate(rgb):
|
||||
if scene is not None:
|
||||
pal, idx = scene[0], scene[1][n]
|
||||
else:
|
||||
pal, idx = VQ.frame_palette(src)
|
||||
if (idx == 0).any():
|
||||
sys.exit(f"frame {n}: index 0 is the transparency key and got used")
|
||||
palw = None
|
||||
if not a.no_palette:
|
||||
palb, _dark, rendered = pack_palette(pal)
|
||||
palw = palb.tobytes()
|
||||
psnr_pal.append(VQ.psnr(src, pal[idx]))
|
||||
recs.append((palw, P.pack_picture(idx).tobytes()))
|
||||
|
||||
rec_b = P.write(a.out, W, H, a.fps, recs, palette_last=a.palette_last)
|
||||
d = P.DLXP(a.out) # re-read: every invariant is checked
|
||||
if d.nframes != len(recs):
|
||||
sys.exit("writer and reader disagree about the frame count")
|
||||
|
||||
kind = ("scene palette" if a.scene_palette else "per-frame palette")
|
||||
if a.no_palette:
|
||||
kind += ", NONE in the record"
|
||||
print(f"{a.out}: DLXP1 {W}x{H} {a.fps}fps {d.nframes} frames, {kind}"
|
||||
f"{', palette LAST' if a.palette_last else ''}")
|
||||
print(f" record {rec_b:,} B = {rec_b // P.SECTOR} sectors exactly, "
|
||||
f"file {os.path.getsize(a.out):,} B")
|
||||
print(f" wire {d.kbps():.1f} KB/s -- FIXED by geometry, there is no lever")
|
||||
print(f" palette-domain PSNR vs the 24-bit source: "
|
||||
f"{np.mean(psnr_pal):.2f} dB (min {np.min(psnr_pal):.2f})")
|
||||
Reference in New Issue
Block a user