"""Atari GR.15 (ANTIC mode E): 160x192, 4 colours chosen globally from 256. No per-cell colour limit, so this is a clean 4-colour dithered image. """ from __future__ import annotations import numpy as np from ... import palette as c64pal # for srgb_to_lab (shared) from ...convert.base import (Conversion, mean_error, perceptual_error, DIFFUSION_DITHERS) from .. import palette as apal from . import _common WIDTH, HEIGHT = 160, 192 PIXEL_ASPECT = 2.0 def convert(img_rgb, palette_name="ntsc", dither_mode="floyd", intensive=False, base_color=None): plab = apal.palette_lab(palette_name) prgb = apal.get_palette(palette_name).astype(np.uint8) img_lab = c64pal.srgb_to_lab(img_rgb) # Dither-aware palette for error-diffusion modes: pick 4 colours whose blends # span the image gamut (so dithering reproduces saturated/intermediate shades) # instead of k-means centroids the dither can't reach. if dither_mode in DIFFUSION_DITHERS: colors = _common.choose_palette_dither(img_lab, plab, k=4) else: colors = _common.choose_palette(img_lab, plab, k=4) colors.sort(key=lambda c: plab[c, 0]) # value 0 = darkest (background) idx = _common.quantize_global(img_lab, plab, colors, dither_mode) value_of = {c: v for v, c in enumerate(colors)} val_image = np.vectorize(value_of.get)(idx).astype(np.uint8) lines = _common.pack_2bpp(val_image) data = _common.split_screen(lines) + bytes(colors) # 4 colour regs at $6000 preview = np.repeat(prgb[idx], int(PIXEL_ASPECT), axis=1) err = (perceptual_error if dither_mode in DIFFUSION_DITHERS else mean_error) return Conversion( mode="gr15", width=WIDTH, height=HEIGHT, pixel_aspect=PIXEL_ASPECT, index_image=idx.astype(np.uint16), data=data, data_addr=_common.DATA_ADDR, viewer="gr15", preview_rgb=preview, error=err(idx, img_lab, plab), meta={"palette": palette_name, "dither": dither_mode, "colors": colors}, )