97 lines
3.6 KiB
Python
97 lines
3.6 KiB
Python
"""Shared CPC encoder helpers: global palette selection + per-mode bit packing.
|
|
|
|
The CPC has no per-cell colour limit -- a mode draws a flat palette of pens
|
|
(16 in mode 0, 4 in mode 1, 2 in mode 2), each pen any of the 27 colours. So the
|
|
job is: choose the best N-colour sub-palette for the whole image, dither to it,
|
|
then pack pens into the CPC's (scrambled) screen bytes.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import numpy as np
|
|
|
|
from ... import dither, palette as c64pal
|
|
from .. import palette as cpcpal
|
|
from ..snapshot import screen_offset
|
|
|
|
|
|
def choose_inks(img_lab, plab, n):
|
|
"""Greedy forward selection of the ``n`` palette colours (indices into the
|
|
27-colour PALETTE) that minimise nearest-colour error over the image."""
|
|
flat = img_lab.reshape(-1, 3)
|
|
d = np.sum((flat[:, None, :] - plab[None, :, :]) ** 2, axis=-1) # (px, 27)
|
|
chosen = []
|
|
best = np.full(flat.shape[0], np.inf)
|
|
for _ in range(n):
|
|
# pick the colour that most reduces the running per-pixel min distance
|
|
cand_err = np.minimum(best[:, None], d).sum(0) # (27,)
|
|
for c in chosen:
|
|
cand_err[c] = np.inf
|
|
c = int(cand_err.argmin())
|
|
chosen.append(c)
|
|
best = np.minimum(best, d[:, c])
|
|
return sorted(chosen)
|
|
|
|
|
|
def render(img_rgb, n, dither_mode, ramp=None):
|
|
"""Pick (or use ``ramp``) an n-colour palette, dither the image to it, and
|
|
return (pen_image (H,W) of 0..n-1, inks (n hardware-ink numbers), idx_image
|
|
(H,W) palette indices, plab, prgb)."""
|
|
plab = cpcpal.palette_lab()
|
|
prgb = cpcpal.get_palette().astype(np.uint8)
|
|
img_lab = c64pal.srgb_to_lab(img_rgb)
|
|
|
|
pal_idx = ramp if ramp is not None else choose_inks(img_lab, plab, n)
|
|
pal_idx = list(pal_idx)
|
|
allowed = np.tile(np.array(pal_idx), (*img_lab.shape[:2], 1))
|
|
idx = dither.quantize(img_lab, allowed, plab, dither_mode).astype(np.int64)
|
|
|
|
# map palette index -> pen (0..n-1)
|
|
lut = {p: k for k, p in enumerate(pal_idx)}
|
|
pen = np.vectorize(lut.get)(idx).astype(np.uint8)
|
|
inks = [cpcpal.ink_byte(p) for p in pal_idx]
|
|
return pen, inks, idx.astype(np.uint16), img_lab, plab, prgb
|
|
|
|
|
|
def _pack_mode0(pen):
|
|
"""160x200 pens (0-15) -> 16K screen. 2 pixels/byte, scrambled bits."""
|
|
scr = bytearray(0x4000)
|
|
for y in range(200):
|
|
row = pen[y]
|
|
for bx in range(80):
|
|
L = int(row[bx * 2]); R = int(row[bx * 2 + 1])
|
|
byte = (((L & 1) << 7) | ((R & 1) << 6) | (((L >> 2) & 1) << 5) |
|
|
(((R >> 2) & 1) << 4) | (((L >> 1) & 1) << 3) |
|
|
(((R >> 1) & 1) << 2) | (((L >> 3) & 1) << 1) | ((R >> 3) & 1))
|
|
scr[screen_offset(y, bx)] = byte
|
|
return bytes(scr)
|
|
|
|
|
|
def _pack_mode1(pen):
|
|
"""320x200 pens (0-3) -> 16K screen. 4 pixels/byte."""
|
|
scr = bytearray(0x4000)
|
|
for y in range(200):
|
|
row = pen[y]
|
|
for bx in range(80):
|
|
p = [int(row[bx * 4 + k]) for k in range(4)]
|
|
byte = 0
|
|
for k in range(4):
|
|
byte |= (p[k] & 1) << (7 - k) # bit0 of each pen
|
|
byte |= ((p[k] >> 1) & 1) << (3 - k) # bit1 of each pen
|
|
scr[screen_offset(y, bx)] = byte
|
|
return bytes(scr)
|
|
|
|
|
|
def _pack_mode2(pen):
|
|
"""640x200 pens (0-1) -> 16K screen. 8 pixels/byte, MSB leftmost."""
|
|
scr = bytearray(0x4000)
|
|
for y in range(200):
|
|
row = pen[y]
|
|
for bx in range(80):
|
|
byte = 0
|
|
for k in range(8):
|
|
byte |= (int(row[bx * 8 + k]) & 1) << (7 - k)
|
|
scr[screen_offset(y, bx)] = byte
|
|
return bytes(scr)
|
|
|
|
|
|
PACK = {0: _pack_mode0, 1: _pack_mode1, 2: _pack_mode2}
|