8bitlenser/c64view/convert/mono.py
2026-06-14 17:43:12 -07:00

77 lines
2.9 KiB
Python

"""Monochrome / grayscale mode -- the highest-resolution path.
Renders at hires (320x200) but matches the image by *luminance* to a small ramp
of palette colours, so detail is carried entirely by spatial dithering. With the
grayscale ramp (black -> dark grey -> grey -> light grey -> white) this gives a
proper greyscale photo; pick any base colour and the ramp becomes that hue's
shades (e.g. black -> blue -> light blue -> white) for a tinted monochrome.
Output is ordinary hires-format data, so it reuses the hires viewer.
"""
from __future__ import annotations
import numpy as np
from .. import dither, palette as pal
from . import base, hires
WIDTH, HEIGHT = 320, 200
CELL_W, CELL_H = 8, 8
PIXEL_ASPECT = 1.0
DATA_LOAD = 0x2000
# Luminance-ordered grey ramp: black, dark grey, grey, light grey, white.
GRAY_RAMP = [0, 11, 12, 15, 1]
# A few palette colours have a lighter sibling, giving a richer tinted ramp.
SIBLINGS = {2: 10, 10: 2, 5: 13, 13: 5, 6: 14, 14: 6, 8: 9, 9: 8}
def build_ramp(base_color, plab):
"""Return palette indices (luminance-sorted) used to render the image."""
if base_color is None or base_color in (0, 1, 11, 12, 15):
ramp = list(GRAY_RAMP)
else:
ramp = {0, 1, base_color}
if base_color in SIBLINGS:
ramp.add(SIBLINGS[base_color])
ramp = list(ramp)
ramp.sort(key=lambda i: plab[i, 0]) # by Lab lightness
return ramp
def convert(img_rgb, palette_name="colodore", dither_mode="floyd",
intensive=False, base_color=None):
plab = pal.palette_lab(palette_name)
# Work purely in luminance: collapse image and palette to (L, 0, 0).
L_pix = pal.srgb_to_lab(img_rgb)[..., 0]
img_mono = np.zeros((HEIGHT, WIDTH, 3))
img_mono[..., 0] = L_pix
plab_mono = np.zeros((16, 3))
plab_mono[:, 0] = plab[:, 0]
ramp = build_ramp(base_color, plab)
n_free = min(2, len(ramp))
cells, rows, cols = base.cells_lab(img_mono, CELL_W, CELL_H)
dist = base.cell_distance(cells, plab_mono)
sets, _ = base.select_cell_sets(dist, ramp, n_free=n_free)
if n_free == 1: # pad to 2 colours per cell for hires
sets = np.concatenate([sets, sets], axis=1)
allowed = base.per_pixel_allowed(sets, rows, cols, CELL_W, CELL_H, HEIGHT, WIDTH)
index_image = dither.quantize(img_mono, allowed, plab_mono, dither_mode).astype(np.uint8)
bitmap, screen = hires._encode(index_image, sets, rows, cols)
payload = bytes(bitmap) + bytes(screen)
conv = base.Conversion(
mode="mono", width=WIDTH, height=HEIGHT, pixel_aspect=PIXEL_ASPECT,
index_image=index_image, data=payload, data_addr=DATA_LOAD, viewer="hires",
error=base.mean_error(index_image, img_mono, plab_mono),
meta={"palette": palette_name, "dither": dither_mode,
"base_color": base_color, "ramp": ramp},
)
conv.extra_files = [("picture.art", base.prg(0x2000, payload + b"\x00"))]
return conv