diff --git a/dither.py b/dither.py index 98ef136..8666abf 100644 --- a/dither.py +++ b/dither.py @@ -1,294 +1,21 @@ import argparse -import bz2 -import functools import os.path -import pickle import time -from typing import Tuple from PIL import Image import numpy as np + import dither_apply +import dither_pattern +import image as image_py +import palette as palette_py +import screen as screen_py # TODO: # - only lookahead for 560px -# - palette class # - compare to bmp2dhr and a2bestpix -def srgb_to_linear_array(a: np.ndarray, gamma=2.4) -> np.ndarray: - return np.where(a <= 0.04045, a / 12.92, ((a + 0.055) / 1.055) ** gamma) - - -def linear_to_srgb_array(a: np.ndarray, gamma=2.4) -> np.ndarray: - return np.where(a <= 0.0031308, a * 12.92, 1.055 * a ** (1.0 / gamma) - - 0.055) - - -def srgb_to_linear(im: np.ndarray) -> np.ndarray: - rgb_linear = srgb_to_linear_array(im / 255.0, gamma=2.4) - return (np.clip(rgb_linear, 0.0, 1.0) * 255).astype(np.float32) - - -def linear_to_srgb(im: np.ndarray) -> np.ndarray: - srgb = linear_to_srgb_array(im / 255.0, gamma=2.4) - return (np.clip(srgb, 0.0, 1.0) * 255).astype(np.float32) - - -# Default bmp2dhr palette -RGB = { - 0: np.array((0, 0, 0)), # Black - 8: np.array((148, 12, 125)), # Magenta - 4: np.array((99, 77, 0)), # Brown - 12: np.array((249, 86, 29)), # Orange - 2: np.array((51, 111, 0)), # Dark green - 10: np.array((126, 126, 125)), # Grey2 - 6: np.array((67, 200, 0)), # Green - 14: np.array((221, 206, 23)), # Yellow - 1: np.array((32, 54, 212)), # Dark blue - 9: np.array((188, 55, 255)), # Violet - 5: np.array((126, 126, 126)), # Grey1 - 13: np.array((255, 129, 236)), # Pink - 3: np.array((7, 168, 225)), # Med blue - 11: np.array((158, 172, 255)), # Light blue - 7: np.array((93, 248, 133)), # Aqua - 15: np.array((255, 255, 255)), # White -} - -# Maps palette values to screen dots. Note that these are the same as -# the binary values in reverse order. -DOTS = { - 0: (False, False, False, False), - 1: (True, False, False, False), - 2: (False, True, False, False), - 3: (True, True, False, False), - 4: (False, False, True, False), - 5: (True, False, True, False), - 6: (False, True, True, False), - 7: (True, True, True, False), - 8: (False, False, False, True), - 9: (True, False, False, True), - 10: (False, True, False, True), - 11: (True, True, False, True), - 12: (False, False, True, True), - 13: (True, False, True, True), - 14: (False, True, True, True), - 15: (True, True, True, True) -} -DOTS_TO_4BIT = {} -for k, v in DOTS.items(): - DOTS_TO_4BIT[v] = k - -# OpenEmulator -sRGB = { - 0: np.array((0, 0, 0)), # Black - 8: np.array((206, 0, 123)), # Magenta - 4: np.array((100, 105, 0)), # Brown - 12: np.array((247, 79, 0)), # Orange - 2: np.array((0, 153, 0)), # Dark green - # XXX RGB values are used as keys in DOTS dict, need to be unique - 10: np.array((131, 132, 132)), # Grey2 - 6: np.array((0, 242, 0)), # Green - 14: np.array((216, 220, 0)), # Yellow - 1: np.array((21, 0, 248)), # Dark blue - 9: np.array((235, 0, 242)), # Violet - 5: np.array((140, 140, 140)), # Grey1 # XXX - 13: np.array((244, 104, 240)), # Pink - 3: np.array((0, 181, 248)), # Med blue - 11: np.array((160, 156, 249)), # Light blue - 7: np.array((21, 241, 132)), # Aqua - 15: np.array((244, 247, 244)), # White -} - -# # Virtual II (sRGB) -# sRGB = { -# (False, False, False, False): np.array((0, 0, 0)), # Black -# (False, False, False, True): np.array((231,36,66)), # Magenta -# (False, False, True, False): np.array((154,104,0)), # Brown -# (False, False, True, True): np.array((255,124,0)), # Orange -# (False, True, False, False): np.array((0,135,45)), # Dark green -# (False, True, False, True): np.array((104,104,104)), # Grey2 XXX -# (False, True, True, False): np.array((0,222,0)), # Green -# (False, True, True, True): np.array((255,252,0)), # Yellow -# (True, False, False, False): np.array((1,30,169)), # Dark blue -# (True, False, False, True): np.array((230,73,228)), # Violet -# (True, False, True, False): np.array((185,185,185)), # Grey1 XXX -# (True, False, True, True): np.array((255,171,153)), # Pink -# (True, True, False, False): np.array((47,69,255)), # Med blue -# (True, True, False, True): np.array((120,187,255)), # Light blue -# (True, True, True, False): np.array((83,250,208)), # Aqua -# (True, True, True, True): np.array((255, 255, 255)), # White -# } -RGB = {} -for k, v in sRGB.items(): - RGB[k] = (np.clip(srgb_to_linear_array(v / 255), 0.0, 1.0) * 255).astype( - np.uint8) - - -class ColourDistance: - @staticmethod - def distance(rgb1: np.ndarray, rgb2: np.ndarray) -> np.ndarray: - raise NotImplementedError - - -class CIE2000Distance(ColourDistance): - """CIE2000 delta-E distance.""" - - def __init__(self): - self._distances = np.memmap("distances.npy", mode="r+", - dtype=np.uint8, shape=(16777216, 16)) - - -class Screen: - X_RES = None - Y_RES = None - X_PIXEL_WIDTH = None - - def __init__(self): - self.main = np.zeros(8192, dtype=np.uint8) - self.aux = np.zeros(8192, dtype=np.uint8) - - @staticmethod - def y_to_base_addr(y: int) -> int: - """Maps y coordinate to screen memory base address.""" - a = y // 64 - d = y - 64 * a - b = d // 8 - c = d - 8 * b - - return 1024 * c + 128 * b + 40 * a - - def _image_to_bitmap(self, image: np.ndarray) -> np.ndarray: - raise NotImplementedError - - def pack(self, image: np.ndarray): - bitmap = self._image_to_bitmap(image) - # The DHGR display encodes 7 pixels across interleaved 4-byte sequences - # of AUX and MAIN memory, as follows: - # PBBBAAAA PDDCCCCB PFEEEEDD PGGGGFFF - # Aux N Main N Aux N+1 Main N+1 (N even) - main_col = np.zeros( - (self.Y_RES, self.X_RES * self.X_PIXEL_WIDTH // 14), dtype=np.uint8) - aux_col = np.zeros( - (self.Y_RES, self.X_RES * self.X_PIXEL_WIDTH // 14), dtype=np.uint8) - for byte_offset in range(80): - column = np.zeros(self.Y_RES, dtype=np.uint8) - for bit in range(7): - column |= (bitmap[:, 7 * byte_offset + bit].astype( - np.uint8) << bit) - if byte_offset % 2 == 0: - aux_col[:, byte_offset // 2] = column - else: - main_col[:, (byte_offset - 1) // 2] = column - - for y in range(self.Y_RES): - addr = self.y_to_base_addr(y) - self.aux[addr:addr + 40] = aux_col[y, :] - self.main[addr:addr + 40] = main_col[y, :] - - @staticmethod - def pixel_palette_options(last_pixel, x: int): - raise NotImplementedError - - -class DHGR140Screen(Screen): - """DHGR screen ignoring colour fringing, i.e. treating as 140x192x16.""" - - X_RES = 140 - Y_RES = 192 - X_PIXEL_WIDTH = 4 - - def _image_to_bitmap(self, image_4bit: np.ndarray) -> np.ndarray: - bitmap = np.zeros( - (self.Y_RES, self.X_RES * self.X_PIXEL_WIDTH), dtype=np.bool) - for y in range(self.Y_RES): - for x in range(self.X_RES): - pixel = image_4bit[y, x].item() - dots = DOTS[pixel] - bitmap[y, x * self.X_PIXEL_WIDTH:( - (x + 1) * self.X_PIXEL_WIDTH)] = dots - return bitmap - - @staticmethod - def pixel_palette_options(last_pixel_4bit, x: int): - return np.array(list(RGB.keys())), np.array(list(RGB.values())) - - -class DHGR560Screen(Screen): - """DHGR screen including colour fringing.""" - X_RES = 560 - Y_RES = 192 - X_PIXEL_WIDTH = 1 - - def _image_to_bitmap(self, image_4bit: np.ndarray) -> np.ndarray: - bitmap = np.zeros((self.Y_RES, self.X_RES), dtype=np.bool) - for y in range(self.Y_RES): - for x in range(self.X_RES): - pixel = image_4bit[y, x] - dots = DOTS[pixel] - phase = x % 4 - bitmap[y, x] = dots[phase] - return bitmap - - @staticmethod - def pixel_palette_options(last_pixel_4bit, x: int): - last_dots = DOTS[last_pixel_4bit] - other_dots = list(last_dots) - other_dots[x % 4] = not other_dots[x % 4] - other_dots = tuple(other_dots) - other_pixel_4bit = DOTS_TO_4BIT[other_dots] - return ( - np.array([last_pixel_4bit, other_pixel_4bit]), - np.array([RGB[last_pixel_4bit], RGB[other_pixel_4bit]])) - - -class Dither: - PATTERN = None - ORIGIN = None - - -class FloydSteinbergDither(Dither): - # 0 * 7 - # 3 5 1 - PATTERN = np.array(((0, 0, 7), (3, 5, 1)), - dtype=np.float32).reshape(2, 3, 1) / np.float(16) - # XXX X_ORIGIN since ORIGIN[0] == 0 - ORIGIN = (0, 1) - - -class BuckelsDither(Dither): - # 0 * 2 1 - # 1 2 1 0 - # 0 1 0 0 - PATTERN = np.array(((0, 0, 2, 1), (1, 2, 1, 0), (0, 1, 0, 0)), - dtype=np.float32).reshape(3, 4, 1) / np.float32(8) - ORIGIN = (0, 1) - - -class JarvisDither(Dither): - # 0 0 X 7 5 - # 3 5 7 5 3 - # 1 3 5 3 1 - PATTERN = np.array(((0, 0, 0, 7, 5), (3, 5, 7, 5, 3), (1, 3, 5, 3, 1)), - dtype=np.float32).reshape(3, 5, 1) / np.float32(48) - ORIGIN = (0, 2) - - -def open_image(screen: Screen, filename: str) -> np.ndarray: - im = Image.open(filename) - # TODO: convert to sRGB colour profile explicitly, in case it has some other - # profile already. - if im.mode != "RGB": - im = im.convert("RGB") - - # Convert to linear RGB before rescaling so that colour interpolation is - # in linear space - linear = srgb_to_linear(np.asarray(im)).astype(np.uint8) - rescaled = Image.fromarray(linear).resize( - (screen.X_RES, screen.Y_RES), Image.LANCZOS) - # XXX work with malloc'ed array? - return np.array(rescaled).astype(np.float32) - def main(): parser = argparse.ArgumentParser() @@ -300,26 +27,25 @@ def main(): "artifacts.")) args = parser.parse_args() + palette = palette_py.Palette() # screen = DHGR140Screen() - screen = DHGR560Screen() + screen = screen_py.DHGR560Screen(palette) - image = open_image(screen, args.input) + image = image_py.open(screen.X_RES, screen.Y_RES, args.input) # image_rgb.show() - # dither = FloydSteinbergDither() - # dither = BuckelsDither() - dither = JarvisDither() - - differ = CIE2000Distance() + # dither = dither_pattern.FloydSteinbergDither() + # dither = dither_pattern.BuckelsDither() + dither = dither_pattern.JarvisDither() start = time.time() - output_4bit, output_rgb = dither_apply.dither_image(screen, image, dither, - differ, - lookahead=args.lookahead) + output_4bit, output_rgb = dither_apply.dither_image( + screen, image, dither, lookahead=args.lookahead) print(time.time() - start) screen.pack(output_4bit) - out_image = Image.fromarray(linear_to_srgb(output_rgb).astype(np.uint8)) + out_image = Image.fromarray(image_py.linear_to_srgb(output_rgb).astype( + np.uint8)) outfile = os.path.join(os.path.splitext(args.output)[0] + ".png") out_image.save(outfile, "PNG") out_image.show(title=outfile) diff --git a/dither_apply.pyx b/dither_apply.pyx index d90c1b1..9cf31b4 100644 --- a/dither_apply.pyx +++ b/dither_apply.pyx @@ -81,7 +81,7 @@ cdef int dither_bounds_yb(float [:, :, ::1] pattern, int y_origin, int y_res, in @cython.boundscheck(False) @cython.wraparound(False) def dither_lookahead( - screen, float[:,:,::1] image_rgb, dither, differ, int x, int y, char[:, ::1] options_4bit, + screen, float[:,:,::1] image_rgb, dither, int x, int y, char[:, ::1] options_4bit, float[:, :, ::1] options_rgb, int lookahead): cdef float[:, :, ::1] pattern = dither.PATTERN cdef int x_res = screen.X_RES @@ -132,7 +132,7 @@ def dither_lookahead( cdef long flat, dist, bit4 cdef long r, g, b - cdef (unsigned char)[:, ::1] distances = differ._distances + cdef (unsigned char)[:, ::1] distances = screen.palette.distances for i in range(2**lookahead): total_error = 0 for j in range(lookahead): @@ -178,7 +178,7 @@ def lookahead_options(screen, lookahead, last_pixel_4bit, x): @cython.boundscheck(False) @cython.wraparound(False) def dither_image( - screen, float[:, :, ::1] image_rgb, dither, differ, int lookahead): + screen, float[:, :, ::1] image_rgb, dither, int lookahead): cdef (unsigned char)[:, ::1] image_4bit = np.empty( (image_rgb.shape[0], image_rgb.shape[1]), dtype=np.uint8) @@ -202,7 +202,7 @@ def dither_image( output_pixel_4bit, output_pixel_rgb = \ dither_lookahead( - screen, image_rgb, dither, differ, x, y, options_4bit, + screen, image_rgb, dither, x, y, options_4bit, options_rgb, lookahead) for i in range(3): quant_error[i] = input_pixel_rgb[i] - output_pixel_rgb[i] diff --git a/dither_pattern.py b/dither_pattern.py new file mode 100644 index 0000000..ed03ef3 --- /dev/null +++ b/dither_pattern.py @@ -0,0 +1,34 @@ +import numpy as np + + +class DitherPattern: + PATTERN = None + ORIGIN = None + + +class FloydSteinbergDither(DitherPattern): + # 0 * 7 + # 3 5 1 + PATTERN = np.array(((0, 0, 7), (3, 5, 1)), + dtype=np.float32).reshape(2, 3, 1) / np.float(16) + # XXX X_ORIGIN since ORIGIN[0] == 0 + ORIGIN = (0, 1) + + +class BuckelsDither(DitherPattern): + # 0 * 2 1 + # 1 2 1 0 + # 0 1 0 0 + PATTERN = np.array(((0, 0, 2, 1), (1, 2, 1, 0), (0, 1, 0, 0)), + dtype=np.float32).reshape(3, 4, 1) / np.float32(8) + ORIGIN = (0, 1) + + +class JarvisDither(DitherPattern): + # 0 0 X 7 5 + # 3 5 7 5 3 + # 1 3 5 3 1 + PATTERN = np.array(((0, 0, 0, 7, 5), (3, 5, 7, 5, 3), (1, 3, 5, 3, 1)), + dtype=np.float32).reshape(3, 5, 1) / np.float32(48) + ORIGIN = (0, 2) + diff --git a/image.py b/image.py new file mode 100644 index 0000000..3857535 --- /dev/null +++ b/image.py @@ -0,0 +1,37 @@ +import numpy as np +from PIL import Image + + +def srgb_to_linear_array(a: np.ndarray, gamma=2.4) -> np.ndarray: + return np.where(a <= 0.04045, a / 12.92, ((a + 0.055) / 1.055) ** gamma) + + +def linear_to_srgb_array(a: np.ndarray, gamma=2.4) -> np.ndarray: + return np.where(a <= 0.0031308, a * 12.92, 1.055 * a ** (1.0 / gamma) - + 0.055) + + +def srgb_to_linear(im: np.ndarray) -> np.ndarray: + rgb_linear = srgb_to_linear_array(im / 255.0, gamma=2.4) + return (np.clip(rgb_linear, 0.0, 1.0) * 255).astype(np.float32) + + +def linear_to_srgb(im: np.ndarray) -> np.ndarray: + srgb = linear_to_srgb_array(im / 255.0, gamma=2.4) + return (np.clip(srgb, 0.0, 1.0) * 255).astype(np.float32) + + +def open(x_res:int, y_res:int, filename: str) -> np.ndarray: + im = Image.open(filename) + # TODO: convert to sRGB colour profile explicitly, in case it has some other + # profile already. + if im.mode != "RGB": + im = im.convert("RGB") + + # Convert to linear RGB before rescaling so that colour interpolation is + # in linear space + linear = srgb_to_linear(np.asarray(im)).astype(np.uint8) + rescaled = Image.fromarray(linear).resize((x_res, y_res), Image.LANCZOS) + # TODO: better performance with malloc'ed array? + return np.array(rescaled).astype(np.float32) + diff --git a/palette.py b/palette.py new file mode 100644 index 0000000..2c75b2a --- /dev/null +++ b/palette.py @@ -0,0 +1,103 @@ +import numpy as np +import image + + +class Palette: + RGB = None + SRGB = None + DOTS = None + + def __init__(self): + # CIE2000 colour distance matrix from 24-bit RGB tuple to 4-bit + # palette colour. + self.distances = np.memmap("distances.npy", mode="r+", + dtype=np.uint8, shape=(16777216, 16)) + + # Default bmp2dhr palette + RGB = { + 0: np.array((0, 0, 0)), # Black + 8: np.array((148, 12, 125)), # Magenta + 4: np.array((99, 77, 0)), # Brown + 12: np.array((249, 86, 29)), # Orange + 2: np.array((51, 111, 0)), # Dark green + 10: np.array((126, 126, 125)), # Grey2 + 6: np.array((67, 200, 0)), # Green + 14: np.array((221, 206, 23)), # Yellow + 1: np.array((32, 54, 212)), # Dark blue + 9: np.array((188, 55, 255)), # Violet + 5: np.array((126, 126, 126)), # Grey1 + 13: np.array((255, 129, 236)), # Pink + 3: np.array((7, 168, 225)), # Med blue + 11: np.array((158, 172, 255)), # Light blue + 7: np.array((93, 248, 133)), # Aqua + 15: np.array((255, 255, 255)), # White + } + + # Maps palette values to screen dots. Note that these are the same as + # the binary values in reverse order. + DOTS = { + 0: (False, False, False, False), + 1: (True, False, False, False), + 2: (False, True, False, False), + 3: (True, True, False, False), + 4: (False, False, True, False), + 5: (True, False, True, False), + 6: (False, True, True, False), + 7: (True, True, True, False), + 8: (False, False, False, True), + 9: (True, False, False, True), + 10: (False, True, False, True), + 11: (True, True, False, True), + 12: (False, False, True, True), + 13: (True, False, True, True), + 14: (False, True, True, True), + 15: (True, True, True, True) + } + DOTS_TO_4BIT = {} + for k, v in DOTS.items(): + DOTS_TO_4BIT[v] = k + + # OpenEmulator + sRGB = { + 0: np.array((0, 0, 0)), # Black + 8: np.array((203, 0, 121)), # Magenta + 4: np.array((99, 103, 0)), # Brown + 12: np.array((244, 78, 0)), # Orange + 2: np.array((0, 150, 0)), # Dark green + 10: np.array((130, 130, 130)), # Grey2 + 6: np.array((0, 235, 0)), # Green + 14: np.array((214, 218, 0)), # Yellow + 1: np.array((20, 0, 246)), # Dark blue + 9: np.array((230, 0, 244)), # Violet + 5: np.array((130, 130, 130)), # Grey1 + 13: np.array((244, 105, 235)), # Pink + 3: np.array((0, 174, 243)), # Med blue + 11: np.array((160, 156, 244)), # Light blue + 7: np.array((25, 243, 136)), # Aqua + 15: np.array((244, 247, 244)), # White + } + + # # Virtual II (sRGB) + # sRGB = { + # (False, False, False, False): np.array((0, 0, 0)), # Black + # (False, False, False, True): np.array((231,36,66)), # Magenta + # (False, False, True, False): np.array((154,104,0)), # Brown + # (False, False, True, True): np.array((255,124,0)), # Orange + # (False, True, False, False): np.array((0,135,45)), # Dark green + # (False, True, False, True): np.array((104,104,104)), # Grey2 XXX + # (False, True, True, False): np.array((0,222,0)), # Green + # (False, True, True, True): np.array((255,252,0)), # Yellow + # (True, False, False, False): np.array((1,30,169)), # Dark blue + # (True, False, False, True): np.array((230,73,228)), # Violet + # (True, False, True, False): np.array((185,185,185)), # Grey1 XXX + # (True, False, True, True): np.array((255,171,153)), # Pink + # (True, True, False, False): np.array((47,69,255)), # Med blue + # (True, True, False, True): np.array((120,187,255)), # Light blue + # (True, True, True, False): np.array((83,250,208)), # Aqua + # (True, True, True, True): np.array((255, 255, 255)), # White + # } + RGB = {} + for k, v in sRGB.items(): + RGB[k] = (np.clip(image.srgb_to_linear_array(v / 255), 0.0, + 1.0) * 255).astype( + np.uint8) diff --git a/screen.py b/screen.py new file mode 100644 index 0000000..679ff81 --- /dev/null +++ b/screen.py @@ -0,0 +1,106 @@ +import numpy as np +import palette as palette_py + + +class Screen: + X_RES = None + Y_RES = None + X_PIXEL_WIDTH = None + + def __init__(self, palette: palette_py.Palette): + self.main = np.zeros(8192, dtype=np.uint8) + self.aux = np.zeros(8192, dtype=np.uint8) + self.palette = palette + + @staticmethod + def y_to_base_addr(y: int) -> int: + """Maps y coordinate to screen memory base address.""" + a = y // 64 + d = y - 64 * a + b = d // 8 + c = d - 8 * b + + return 1024 * c + 128 * b + 40 * a + + def _image_to_bitmap(self, image: np.ndarray) -> np.ndarray: + raise NotImplementedError + + def pack(self, image: np.ndarray): + bitmap = self._image_to_bitmap(image) + # The DHGR display encodes 7 pixels across interleaved 4-byte sequences + # of AUX and MAIN memory, as follows: + # PBBBAAAA PDDCCCCB PFEEEEDD PGGGGFFF + # Aux N Main N Aux N+1 Main N+1 (N even) + main_col = np.zeros( + (self.Y_RES, self.X_RES * self.X_PIXEL_WIDTH // 14), dtype=np.uint8) + aux_col = np.zeros( + (self.Y_RES, self.X_RES * self.X_PIXEL_WIDTH // 14), dtype=np.uint8) + for byte_offset in range(80): + column = np.zeros(self.Y_RES, dtype=np.uint8) + for bit in range(7): + column |= (bitmap[:, 7 * byte_offset + bit].astype( + np.uint8) << bit) + if byte_offset % 2 == 0: + aux_col[:, byte_offset // 2] = column + else: + main_col[:, (byte_offset - 1) // 2] = column + + for y in range(self.Y_RES): + addr = self.y_to_base_addr(y) + self.aux[addr:addr + 40] = aux_col[y, :] + self.main[addr:addr + 40] = main_col[y, :] + + def pixel_palette_options(self, last_pixel_4bit, x: int): + raise NotImplementedError + + +class DHGR140Screen(Screen): + """DHGR screen ignoring colour fringing, i.e. treating as 140x192x16.""" + + X_RES = 140 + Y_RES = 192 + X_PIXEL_WIDTH = 4 + + def _image_to_bitmap(self, image_4bit: np.ndarray) -> np.ndarray: + bitmap = np.zeros( + (self.Y_RES, self.X_RES * self.X_PIXEL_WIDTH), dtype=np.bool) + for y in range(self.Y_RES): + for x in range(self.X_RES): + pixel = image_4bit[y, x].item() + dots = self.palette.DOTS[pixel] + bitmap[y, x * self.X_PIXEL_WIDTH:( + (x + 1) * self.X_PIXEL_WIDTH)] = dots + return bitmap + + def pixel_palette_options(self, last_pixel_4bit, x: int): + return np.array(list(self.palette.RGB.keys())), np.array(list( + self.palette.RGB.values())) + + +class DHGR560Screen(Screen): + """DHGR screen including colour fringing.""" + X_RES = 560 + Y_RES = 192 + X_PIXEL_WIDTH = 1 + + def _image_to_bitmap(self, image_4bit: np.ndarray) -> np.ndarray: + bitmap = np.zeros((self.Y_RES, self.X_RES), dtype=np.bool) + for y in range(self.Y_RES): + for x in range(self.X_RES): + pixel = image_4bit[y, x] + dots = self.palette.DOTS[pixel] + phase = x % 4 + bitmap[y, x] = dots[phase] + return bitmap + + def pixel_palette_options(self, last_pixel_4bit, x: int): + last_dots = self.palette.DOTS[last_pixel_4bit] + other_dots = list(last_dots) + other_dots[x % 4] = not other_dots[x % 4] + other_dots = tuple(other_dots) + other_pixel_4bit = self.palette.DOTS_TO_4BIT[other_dots] + return ( + np.array([last_pixel_4bit, other_pixel_4bit]), + np.array([self.palette.RGB[last_pixel_4bit], + self.palette.RGB[other_pixel_4bit]])) +