diff --git a/convert.py b/convert.py index dc0ec16..377c299 100644 --- a/convert.py +++ b/convert.py @@ -48,9 +48,13 @@ def main(): "and looking ahead over next --lookahead pixels to optimize the " "colour sequence.") ) + parser.add_argument( + '--palette', type=str, choices=list(palette_py.PALETTES.keys()), + default=palette_py.DEFAULT_PALETTE, + help="RGB colour palette to dither to.") args = parser.parse_args() - palette = palette_py.Palette() + palette = palette_py.PALETTES[args.palette]() if args.resolution == 560: screen = screen_py.DHGR560Screen(palette) lookahead = args.lookahead @@ -59,7 +63,6 @@ def main(): lookahead = 0 image = image_py.open(args.input) - # TODO: better performance with malloc'ed array? resized = np.array( image_py.resize(image, screen.X_RES, screen.Y_RES)).astype(np.float32) if args.show_input: @@ -85,11 +88,11 @@ def main(): # Show output image out_image = Image.fromarray(image_py.linear_to_srgb(output_rgb).astype( np.uint8)) + out_image = image_py.resize(out_image, 560, 384, srgb_output=True) if args.show_output: - # out_image.show() - image_py.resize(out_image, 560, 384, srgb_output=True).show() + out_image.show() - outfile = os.path.join(os.path.splitext(args.output)[0] + "_preview.png") + outfile = os.path.join(os.path.splitext(args.output)[0] + "-preview.png") out_image.save(outfile, "PNG") with open(args.output, "wb") as f: diff --git a/palette.py b/palette.py index 691f0e6..2d53988 100644 --- a/palette.py +++ b/palette.py @@ -5,33 +5,7 @@ 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 - sRGB = { - 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, 126)), # 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 - } + DISTANCES_PATH = None # Maps palette values to screen dots. Note that these are the same as # the binary values in reverse order. @@ -57,8 +31,48 @@ class Palette: for k, v in DOTS.items(): DOTS_TO_4BIT[v] = k + def __init__(self, load_distances=True): + if load_distances: + # CIE2000 colour distance matrix from 24-bit RGB tuple to 4-bit + # palette colour. + self.distances = np.memmap(self.DISTANCES_PATH, mode="r+", + dtype=np.uint8, shape=(16777216, 16)) + + self.RGB = {} + for k, v in self.SRGB.items(): + self.RGB[k] = (np.clip(image.srgb_to_linear_array(v / 255), 0.0, + 1.0) * 255).astype(np.uint8) + + +class ToHgrPalette(Palette): + DISTANCES_PATH = "data/distances_tohgr.data" + + # Default tohgr/bmp2dhr palette + SRGB = { + 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, 126)), # 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 + } + + +class OpenEmulatorPalette(Palette): + DISTANCES_PATH = "data/distances_openemulator.data" + # OpenEmulator - sRGB = { + SRGB = { 0: np.array((0, 0, 0)), # Black 8: np.array((203, 0, 121)), # Magenta 4: np.array((99, 103, 0)), # Brown @@ -77,27 +91,34 @@ class Palette: 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) + +class VirtualIIPalette(Palette): + DISTANCES_PATH = "data/distances_virtualii.data" + + SRGB = { + 0: np.array((0, 0, 0)), # Black + 8: np.array((231, 36, 66)), # Magenta + 4: np.array((154, 104, 0)), # Brown + 12: np.array((255, 124, 0)), # Orange + 2: np.array((0, 135, 45)), # Dark green + 10: np.array((104, 104, 104)), # Grey2 + 6: np.array((0, 222, 0)), # Green + 14: np.array((255, 252, 0)), # Yellow + 1: np.array((1, 30, 169)), # Dark blue + 9: np.array((230, 73, 228)), # Violet + 5: np.array((185, 185, 185)), # Grey1 + 13: np.array((255, 171, 153)), # Pink + 3: np.array((47, 69, 255)), # Med blue + 11: np.array((120, 187, 255)), # Light blue + 7: np.array((83, 250, 208)), # Aqua + 15: np.array((255, 255, 255)), # White + } + + +PALETTES = { + 'openemulator': OpenEmulatorPalette, + 'virtualii': VirtualIIPalette, + 'tohgr': ToHgrPalette, +} + +DEFAULT_PALETTE = 'openemulator' diff --git a/precompute_distance.py b/precompute_distance.py index 4be2ef7..f59a906 100644 --- a/precompute_distance.py +++ b/precompute_distance.py @@ -1,3 +1,4 @@ +import argparse import image import palette as palette_py import colour.difference @@ -14,14 +15,17 @@ def rgb_to_lab(rgb: np.ndarray): return colour.XYZ_to_Lab(xyz) -def nearest_colours(palette): - diffs = np.empty((COLOURS ** 3, 16), dtype=np.float32) +def all_lab_colours(): all_rgb = np.array(tuple(np.ndindex(COLOURS, COLOURS, COLOURS)), dtype=np.uint8) - all_lab = rgb_to_lab(all_rgb) + return rgb_to_lab(all_rgb) - for i, palette_rgb in palette.RGB.items(): - print(i) + +def nearest_colours(palette, all_lab): + diffs = np.empty((COLOURS ** 3, 16), dtype=np.float32) + + for i, palette_rgb in sorted(palette.RGB.items()): + print("...palette colour %d" % i) palette_lab = rgb_to_lab(palette_rgb) diffs[:, i] = colour.difference.delta_E_CIE2000(all_lab, palette_lab) @@ -30,12 +34,30 @@ def nearest_colours(palette): def main(): - palette = palette_py.Palette() - n = nearest_colours(palette) - out = np.memmap(filename="distances_default.npy", mode="w+", dtype=np.uint8, - shape=n.shape) - out[:] = n[:] + parser = argparse.ArgumentParser() + parser.add_argument('--palette', type=str, choices=list( + palette_py.PALETTES.keys()), + default=palette_py.DEFAULT_PALETTE, + help="Palette for which to compute distance matrix.") + parser.add_argument('--all', type=bool, default=False, + help="Whether to compute distances for all palettes") + args = parser.parse_args() + + if args.all: + palette_names = list(palette_py.PALETTES.keys()) + else: + palette_names = [args.palette] + + print("Precomputing matrix of all 24-bit LAB colours") + all_lab = all_lab_colours() + for palette_name in palette_names: + print("Processing palette %s" % palette_name) + palette = palette_py.PALETTES[palette_name](load_distances=False) + n = nearest_colours(palette, all_lab) + out = np.memmap(filename=palette.DISTANCES_PATH, mode="w+", + dtype=np.uint8, shape=n.shape) + out[:] = n[:] if __name__ == "__main__": - main() \ No newline at end of file + main()