142 lines
5.3 KiB
Python
142 lines
5.3 KiB
Python
"""FLI (Flexible Line Interpretation) multicolor mode.
|
|
|
|
A stable raster routine re-points the VIC video matrix every scanline, so the two
|
|
screen-RAM-derived colours gain per-line (4x1) resolution while the colour-RAM
|
|
colour stays per-cell (4x8) and the background is global. Per 4x1 strip the
|
|
displayable colours are therefore {background, colourRAM(cell), screen01(line),
|
|
screen10(line)} -- four colours that refresh every line, far more than plain
|
|
multicolor.
|
|
|
|
Memory layout of the appended data block (loads from $4000), matched to
|
|
viewer/fli.s:
|
|
$4000+L*$400 screen RAM for line L of each char row (L=0..7), 1000 bytes each
|
|
$6000 bitmap 8000 (VIC reads here, offset $2000 in bank 1)
|
|
$8000 colour RAM 1000 (viewer copies to $D800)
|
|
$83E8 background 1
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from itertools import combinations
|
|
|
|
import numpy as np
|
|
|
|
from .. import dither, palette as pal
|
|
from . import base
|
|
|
|
WIDTH, HEIGHT = 160, 200
|
|
CELL_W, CELL_H = 4, 8
|
|
PIXEL_ASPECT = 2.0
|
|
DATA_ADDR = 0x4000
|
|
N_COLS, N_ROWS = 40, 25
|
|
N_CELLS = N_COLS * N_ROWS
|
|
|
|
|
|
def convert(img_rgb, palette_name="colodore", dither_mode="bayer", intensive=False):
|
|
plab = pal.palette_lab(palette_name)
|
|
img_lab = pal.srgb_to_lab(img_rgb)
|
|
|
|
# (n_cells, 8 rows, 4 px, 3): one 4x1 strip per (cell, line).
|
|
a = img_lab.reshape(N_ROWS, CELL_H, N_COLS, CELL_W, 3)
|
|
a = a.transpose(0, 2, 1, 3, 4).reshape(N_CELLS, CELL_H, CELL_W, 3)
|
|
dist = np.sum((a[:, :, :, None, :] - plab[None, None, None, :, :]) ** 2, axis=-1)
|
|
# dist: (n_cells, 8, 4, 16)
|
|
|
|
bg_candidates = range(16) if intensive else [base.best_global_color(img_lab, plab)]
|
|
best = None
|
|
for bg in bg_candidates:
|
|
c11, c01, c10, err = _solve(dist, bg)
|
|
if best is None or err < best[-1]:
|
|
best = (bg, c11, c01, c10, err)
|
|
bg, c11, c01, c10, _ = best
|
|
|
|
index_image = _quantize(img_lab, plab, bg, c11, c01, c10, dither_mode)
|
|
data = _encode(index_image, bg, c11, c01, c10)
|
|
|
|
conv = base.Conversion(
|
|
mode="fli", width=WIDTH, height=HEIGHT, pixel_aspect=PIXEL_ASPECT,
|
|
index_image=index_image, data=data, data_addr=DATA_ADDR, viewer="fli",
|
|
error=base.mean_error(index_image, img_lab, plab),
|
|
meta={"palette": palette_name, "dither": dither_mode, "background": int(bg)},
|
|
)
|
|
return conv
|
|
|
|
|
|
def _solve(dist, bg):
|
|
"""Pick per-cell colour-RAM colour c11 and per-line free colours c01,c10."""
|
|
n = dist.shape[0]
|
|
dbg = dist[:, :, :, bg] # (n,8,4)
|
|
|
|
# c11: the single shared colour that best complements bg across the whole cell.
|
|
cell_err = np.empty((16, n))
|
|
for c in range(16):
|
|
m = np.minimum(dbg, dist[:, :, :, c])
|
|
cell_err[c] = m.sum(axis=(1, 2))
|
|
cell_err[bg] = np.inf
|
|
c11 = np.argmin(cell_err, axis=0) # (n,)
|
|
|
|
# base error per strip using {bg, c11}.
|
|
dc11 = np.take_along_axis(dist, c11[:, None, None, None], axis=3)[..., 0] # (n,8,4)
|
|
sbase = np.minimum(dbg, dc11) # (n,8,4)
|
|
|
|
# per strip (cell,line) choose the best 2 free colours.
|
|
best_err = np.full((n, 8), np.inf)
|
|
c01 = np.zeros((n, 8), dtype=np.int64)
|
|
c10 = np.zeros((n, 8), dtype=np.int64)
|
|
for x, y in combinations(range(16), 2):
|
|
e = np.minimum(np.minimum(sbase, dist[:, :, :, x]), dist[:, :, :, y]).sum(axis=2)
|
|
better = e < best_err
|
|
best_err = np.where(better, e, best_err)
|
|
c01 = np.where(better, x, c01)
|
|
c10 = np.where(better, y, c10)
|
|
|
|
total = best_err.sum()
|
|
return c11, c01, c10, float(total)
|
|
|
|
|
|
def _allowed_map(bg, c11, c01, c10):
|
|
"""(H, W, 4) per-pixel allowed palette indices."""
|
|
yy, xx = np.indices((HEIGHT, WIDTH))
|
|
ci = (yy // CELL_H) * N_COLS + (xx // CELL_W)
|
|
r = yy % CELL_H
|
|
allowed = np.empty((HEIGHT, WIDTH, 4), dtype=np.int64)
|
|
allowed[:, :, 0] = bg
|
|
allowed[:, :, 1] = c11[ci]
|
|
allowed[:, :, 2] = c01[ci, r]
|
|
allowed[:, :, 3] = c10[ci, r]
|
|
return allowed
|
|
|
|
|
|
def _quantize(img_lab, plab, bg, c11, c01, c10, dither_mode):
|
|
allowed = _allowed_map(bg, c11, c01, c10)
|
|
return dither.quantize(img_lab, allowed, plab, dither_mode).astype(np.uint8)
|
|
|
|
|
|
def _encode(index_image, bg, c11, c01, c10):
|
|
bitmap = np.zeros(8000, dtype=np.uint8)
|
|
screens = [np.zeros(1000, dtype=np.uint8) for _ in range(8)]
|
|
colram = np.zeros(1000, dtype=np.uint8)
|
|
|
|
for cr in range(N_ROWS):
|
|
for cc in range(N_COLS):
|
|
ci = cr * N_COLS + cc
|
|
colram[ci] = c11[ci] & 0x0F
|
|
base_addr = cr * 320 + cc * 8
|
|
for r in range(8):
|
|
a01, a10 = int(c01[ci, r]), int(c10[ci, r])
|
|
screens[r][ci] = ((a01 & 0x0F) << 4) | (a10 & 0x0F)
|
|
lut = {int(bg): 0b00, int(c11[ci]): 0b11, a01: 0b01, a10: 0b10}
|
|
row = index_image[cr * 8 + r, cc * 4:cc * 4 + 4]
|
|
byte = 0
|
|
for x in range(4):
|
|
byte = (byte << 2) | lut.get(int(row[x]), 0b00)
|
|
bitmap[base_addr + r] = byte
|
|
|
|
block = bytearray()
|
|
for r in range(8):
|
|
block += bytes(screens[r]) + bytes(24) # pad each screen to 1K
|
|
block += bytes(bitmap) # $6000
|
|
block += bytes(0x8000 - (0x6000 + 8000)) # pad to $8000
|
|
block += bytes(colram) # $8000
|
|
block += bytes([int(bg) & 0x0F]) # $83E8
|
|
return bytes(block)
|