Use .npy format for RGB to CAM16UCS conversion matrix, and get rid of precomputed CIE2000 distances

This commit is contained in:
kris 2021-07-19 18:35:44 +01:00
parent e979df03bc
commit feefdb5dc6
4 changed files with 118 additions and 200 deletions

View File

@ -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

View File

@ -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
}

31
precompute_conversion.py Normal file
View File

@ -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()

View File

@ -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()