"""Mattel Intellivision monochrome / tinted-mono mode. 160x96 matched by luminance to two greys (black + white, or black + a tinted base colour). With the colours fixed, the whole image budget goes into the 64-tile GRAM shapes, which are optimised by the same vector-quantisation reshape/reassign used by the colour encoder -- a clean two-tone Intellivision picture. """ from __future__ import annotations import numpy as np from ... import dither, palette as c64pal from ...convert import base from .. import palette as ipal from . import stic BLACK = 0 WHITE = 7 # white is a foreground-legal colour (0-7) def convert(img_rgb, palette_name="stic", dither_mode="none", intensive=False, base_color=None): # mono is carried entirely by dithering, so it needs error diffusion -- the # colour mode's "none" default would give a stark, high-contrast result. if dither_mode not in base.DIFFUSION_DITHERS: dither_mode = "floyd" plab = ipal.palette_lab() prgb = ipal.get_palette().astype(np.uint8) fg = base_color if base_color in range(1, 8) else WHITE bg = BLACK # luminance image + luminance-only palette L = c64pal.srgb_to_lab(img_rgb)[..., 0] img_mono = np.zeros((stic.HEIGHT, stic.WIDTH, 3)) img_mono[..., 0] = L plab_mono = np.zeros_like(plab) plab_mono[:, 0] = plab[:, 0] cells, rows, cols = base.cells_lab(img_mono, stic.CELL_W, stic.CELL_H) sets = np.tile(np.array([bg, fg], np.int64), (stic.N_ROWS * stic.N_COLS, 1)) allowed = base.per_pixel_allowed(sets, rows, cols, stic.CELL_W, stic.CELL_H, stic.HEIGHT, stic.WIDTH) idx = dither.quantize(img_mono, allowed, plab_mono, dither_mode).astype(np.uint8) bitmaps = np.zeros((stic.N_ROWS * stic.N_COLS, 64), np.uint8) for cr in range(rows): for cc in range(cols): ci = cr * cols + cc block = idx[cr * 8:cr * 8 + 8, cc * 8:cc * 8 + 8] bitmaps[ci] = (block == fg).astype(np.uint8).reshape(-1) tiles, labels = base.mono_codebook(bitmaps, stic.N_TILES) prev_idx = np.empty((stic.HEIGHT, stic.WIDTH), np.uint8) cards = np.zeros(stic.N_ROWS * stic.N_COLS, np.uint16) for ci in range(stic.N_ROWS * stic.N_COLS): cr, cc = divmod(ci, cols) tile = tiles[labels[ci]].reshape(8, 8) prev_idx[cr * 8:cr * 8 + 8, cc * 8:cc * 8 + 8] = np.where(tile == 1, fg, bg) cards[ci] = (fg & 0x07) | stic.GRAM_BIT | ((labels[ci] & 0x3F) << 3) \ | stic._bg_bits(bg) gram = np.zeros(stic.N_TILES * 8, np.uint16) for t in range(stic.N_TILES): rb = tiles[t].reshape(8, 8) for r in range(8): byte = 0 for x in range(8): byte = (byte << 1) | int(rb[r, x]) gram[t * 8 + r] = byte data = {"gram": gram, "cards": cards} preview = prgb[prev_idx] disp_w = int(round(stic.WIDTH * stic.PIXEL_ASPECT)) xs = (np.arange(disp_w) * stic.WIDTH) // disp_w preview = preview[:, xs] return base.Conversion( mode="mono", width=stic.WIDTH, height=stic.HEIGHT, pixel_aspect=stic.PIXEL_ASPECT, index_image=prev_idx.astype(np.uint16), data=data, data_addr=0, viewer="stic", preview_rgb=preview, error=base.perceptual_error(prev_idx, img_mono, plab_mono), meta={"palette": "stic", "dither": dither_mode, "base_color": base_color, "mode": "fgbg"}, )