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:
@@ -65,6 +65,41 @@ def scene_palette(rgb, colors=256, stride=3, reserve_black=True):
|
||||
return ref, pal
|
||||
|
||||
|
||||
def frame_palette(rgb1, colors=254):
|
||||
"""`scene_palette`'s sibling, for the PACKED layout: ONE FRAME, 254 colours.
|
||||
|
||||
The codec cannot have this. Every codeword it emits is an index INTO
|
||||
`scene_palette`, so its palette is shared scene-wide and 31.33 dB is a
|
||||
ceiling no bitrate crosses (FINDINGS 61.9). A literal frame has no
|
||||
codebooks, so nothing forces a shared palette on it.
|
||||
|
||||
The layout spends TWO entries where `--reserve-black` spends one (47.2):
|
||||
index 0 is the TRANSPARENCY KEY of the top graphics page and must never
|
||||
appear in the picture, and black therefore lives at 255 for the letterbox.
|
||||
So the picture gets 254.
|
||||
|
||||
The +1 shift is the whole mechanism, and it is why this does NOT go through
|
||||
a P-mode reference image the way `scene_palette` does. `palettise` maps
|
||||
against the FINAL 256-entry table, and that table has (0,0,0) at both 0 and
|
||||
255 -- a nearest-colour mapper is free to pick either, and there is no way to
|
||||
forbid the one that must stay unused. Quantising to 254 and shifting keeps
|
||||
index 0 free BY CONSTRUCTION rather than by hoping the mapper agrees, and it
|
||||
is still exact: `pal[idx]` reproduces the quantiser's own rendering.
|
||||
|
||||
Returns (pal (256,3) uint8, idx (H,W) uint8 in 1..254).
|
||||
"""
|
||||
q = Image.fromarray(rgb1).quantize(colors=colors, method=Image.MEDIANCUT,
|
||||
dither=Image.NONE)
|
||||
raw = q.getpalette()
|
||||
if len(raw) < colors * 3:
|
||||
raise ValueError(f"quantiser returned {len(raw)//3} entries, wanted {colors}")
|
||||
pal = np.array(raw[:colors * 3], dtype=np.uint8).reshape(-1, 3)
|
||||
pal = np.vstack([np.zeros((1, 3), np.uint8), pal, np.zeros((1, 3), np.uint8)])
|
||||
idx = np.asarray(q, dtype=np.uint8) + np.uint8(1)
|
||||
if idx.min() < 1 or idx.max() > colors:
|
||||
raise ValueError("index 0 (transparency key) or 255 (black) got used")
|
||||
return pal, idx
|
||||
|
||||
def palettise(rgb, ref):
|
||||
return [np.asarray(Image.fromarray(r).quantize(palette=ref, dither=Image.NONE),
|
||||
dtype=np.uint8) for r in rgb]
|
||||
|
||||
Reference in New Issue
Block a user