From feefdb5dc610107083be40288bda0e58e51f52d9 Mon Sep 17 00:00:00 2001 From: kris Date: Mon, 19 Jul 2021 18:35:44 +0100 Subject: [PATCH] Use .npy format for RGB to CAM16UCS conversion matrix, and get rid of precomputed CIE2000 distances --- convert.py | 26 ++---- palette.py | 173 ++++++++++++++++++--------------------- precompute_conversion.py | 31 +++++++ precompute_distance.py | 88 -------------------- 4 files changed, 118 insertions(+), 200 deletions(-) create mode 100644 precompute_conversion.py delete mode 100644 precompute_distance.py diff --git a/convert.py b/convert.py index 53940d1..3e3704a 100644 --- a/convert.py +++ b/convert.py @@ -86,32 +86,22 @@ def main(): screen = screen_py.DHGR560Screen(palette) lookahead = args.lookahead + # Conversion matrix from RGB to CAM16UCS colour values. Indexed by + # 24-bit RGB value + rgb_to_cam16 = np.load("data/rgb_to_cam16ucs.npy") + # Open and resize source image image = image_py.open(args.input) if args.show_input: image_py.resize(image, screen.NATIVE_X_RES, screen.NATIVE_Y_RES * 2, srgb_output=True).show() - rgb = np.array(image_py.resize(image, screen.X_RES, - screen.Y_RES, - gamma=args.gamma_correct) / 255).astype( - np.float32) + rgb = np.array( + image_py.resize(image, screen.X_RES, screen.Y_RES, + gamma=args.gamma_correct)).astype(np.float32) / 255 - # bits24 = np.arange(2 ** 24).reshape(-1, 1) - # all_rgb = (np.concatenate( - # [bits24 >> 16 & 0xff, bits24 >> 8 & 0xff, bits24 & 0xff], - # axis=1) / 255).astype(np.float32) - # all_cam16 = colour.convert(all_rgb, "RGB", "CAM16UCS").astype(np.float32) - # f = np.memmap("rgb_to_cam16ucs.data", mode="w+", dtype=np.float32, - # shape=all_cam16.shape) - # f[:] = all_cam16 - # if True: - # return - - all_cam16 = np.memmap("rgb_to_cam16ucs.data", mode="r+", dtype=np.float32, - shape=(2 ** 24, 3)) dither = dither_pattern.PATTERNS[args.dither]() output_nbit, _ = dither_pyx.dither_image( - screen, rgb, dither, lookahead, args.verbose, all_cam16) + screen, rgb, dither, lookahead, args.verbose, rgb_to_cam16) bitmap = screen.pack(output_nbit) # Show output image by rendering in target palette diff --git a/palette.py b/palette.py index be6ec30..155889c 100644 --- a/palette.py +++ b/palette.py @@ -13,30 +13,19 @@ class Palette: XYZ = {} DOTS = {} DOTS_TO_INDEX = {} - DISTANCES_PATH = None # How many successive screen pixels are used to compute output pixel # palette index. PALETTE_DEPTH = None - 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, - # len(self.SRGB))) - + def __init__(self): 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) - self.CAM16UCS[k] = colour.convert( - v / 255, "sRGB", "CAM16UCS").astype(np.float32) - # self.XYZ[k] = colour.convert(v / 255, "sRGB", - # "CIE XYZ").astype(np.float32) - - # print(self.CAM02UCS) + with colour.utilities.suppress_warnings(colour_usage_warnings=True): + self.CAM16UCS[k] = colour.convert( + v / 255, "sRGB", "CAM16UCS").astype(np.float32) # Maps palette values to screen dots. Note that these are the same as # the binary index values in reverse order. @@ -50,86 +39,82 @@ class Palette: self.DOTS_TO_INDEX[v] = k -# class ToHgrPalette(Palette): -# """4-bit palette used as default by other DHGR image converters.""" -# DISTANCES_PATH = "data/distances_tohgr.data" -# PALETTE_DEPTH = 4 -# -# # 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): -# """4-bit palette chosen to approximately match OpenEmulator output.""" -# DISTANCES_PATH = "data/distances_openemulator.data" -# PALETTE_DEPTH = 4 -# -# # 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 -# } -# -# -# class VirtualIIPalette(Palette): -# """4-bit palette exactly matching Virtual II emulator output.""" -# DISTANCES_PATH = "data/distances_virtualii.data" -# PALETTE_DEPTH = 4 -# -# 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 -# } +class ToHgrPalette(Palette): + """4-bit palette used as default by other DHGR image converters.""" + PALETTE_DEPTH = 4 + + # 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): + """4-bit palette chosen to approximately match OpenEmulator output.""" + PALETTE_DEPTH = 4 + + # 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 + } + + +class VirtualIIPalette(Palette): + """4-bit palette exactly matching Virtual II emulator output.""" + PALETTE_DEPTH = 4 + + 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 + } class NTSCPalette(Palette): """8-bit NTSC palette computed by averaging chroma signal over 8 pixels.""" - DISTANCES_PATH = 'data/distances_ntsc.data' PALETTE_DEPTH = 8 # Computed using ntsc_colours.py @@ -137,9 +122,9 @@ class NTSCPalette(Palette): PALETTES = { - # 'openemulator': OpenEmulatorPalette, - # 'virtualii': VirtualIIPalette, - # 'tohgr': ToHgrPalette, + 'openemulator': OpenEmulatorPalette, + 'virtualii': VirtualIIPalette, + 'tohgr': ToHgrPalette, 'ntsc': NTSCPalette } diff --git a/precompute_conversion.py b/precompute_conversion.py new file mode 100644 index 0000000..c0be018 --- /dev/null +++ b/precompute_conversion.py @@ -0,0 +1,31 @@ +"""Precompute CIE2000 perceptual colour distance matrices. + +The matrix of delta-E values is computed for all pairs of 24-bit RGB values, +and Apple II target palette values. This is written out as a file that is +mmapped at runtime for efficient access. For a 16-colour target palette this +file is 256MB; for a 256-colour (NTSC) target palette it is 4GB. +""" + +import colour +import numpy as np + + +def main(): + print("Precomputing conversion matrix from RGB to CAM16UCS colour space") + + # Compute matrix of all 24-bit RGB values, normalized to 0..1 range + bits24 = np.arange(2 ** 24).reshape(-1, 1) + all_rgb = (np.concatenate( + [bits24 >> 16 & 0xff, bits24 >> 8 & 0xff, bits24 & 0xff], + axis=1) / 255).astype(np.float32) + + with colour.utilities.suppress_warnings(colour_usage_warnings=True): + # Compute matrix of corresponding CAM16UCS colour values, indexed + # by 24-bit RGB value + all_cam16 = colour.convert(all_rgb, "RGB", "CAM16UCS").astype( + np.float32) + np.save("data/rgb_to_cam16ucs.npy", all_cam16) + + +if __name__ == "__main__": + main() diff --git a/precompute_distance.py b/precompute_distance.py deleted file mode 100644 index a41951c..0000000 --- a/precompute_distance.py +++ /dev/null @@ -1,88 +0,0 @@ -"""Precompute CIE2000 perceptual colour distance matrices. - -The matrix of delta-E values is computed for all pairs of 24-bit RGB values, -and Apple II target palette values. This is written out as a file that is -mmapped at runtime for efficient access. For a 16-colour target palette this -file is 256MB; for a 256-colour (NTSC) target palette it is 4GB. -""" - -import argparse -import os - -import image -import palette as palette_py -import colour.difference -import numpy as np - -RGB_LEVELS = 256 -# Largest possible value of delta_E_CIE2000 between two RGB values -DELTA_E_MAX = 120 # TODO: fine-tune - - -def rgb_to_lab(rgb: np.ndarray): - srgb = np.clip( - image.linear_to_srgb_array(rgb.astype(np.float32) / 255), 0.0, - 1.0) - xyz = colour.sRGB_to_XYZ(srgb) - return colour.XYZ_to_Lab(xyz) - - -def all_lab_colours(): - all_rgb = np.array(tuple(np.ndindex(RGB_LEVELS, RGB_LEVELS, RGB_LEVELS)), - dtype=np.uint8) - return rgb_to_lab(all_rgb) - - -def nearest_colours(palette, all_lab, diffs): - palette_size = len(palette.RGB) - palette_labs = np.empty((palette_size, 3), dtype=np.float32) - for i, palette_rgb in palette.RGB.items(): - palette_labs[i, :] = rgb_to_lab(palette_rgb) - - print("Computing all 24-bit palette diffs:") - for i in range(palette_size): - print(" %d/%d" % (i, palette_size)) - # Compute all palette diffs for a block of 65536 successive RGB - # source values at once, which bounds the memory use while also writing - # contiguously to the mmapped array. - diffs[i * (1 << 16):(i + 1) * (1 << 16), :] = ( - colour.difference.delta_E_CIE2000( - all_lab[i * (1 << 16):(i + 1) * ( - 1 << 16)].reshape((1 << 16, 1, 3)), - palette_labs.reshape((1, palette_size, 3))) / DELTA_E_MAX * - 255).astype(np.uint8) - - -def main(): - 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', action=argparse.BooleanOptionalAction, - 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("Creating distance file for palette %s" % palette_name) - palette = palette_py.PALETTES[palette_name](load_distances=False) - try: - os.mkdir(os.path.dirname(palette.DISTANCES_PATH)) - except FileExistsError: - pass - out = np.memmap(filename=palette.DISTANCES_PATH, mode="w+", - dtype=np.uint8, shape=(RGB_LEVELS ** 3, - len(palette.RGB))) - nearest_colours(palette, all_lab, out) - - -if __name__ == "__main__": - main()