ii-pix/precompute_distance.py
kris e0b732cdaa Reimplement NTSC conversion to closely match openemulator output. I
can't figure out why contrast=2 is needed (openemulator uses a default
value of 1, so there must be a factor of 2x somewhere), or where the
slight hue rotation comes from - perhaps this is somehow introduced by
the more complex band-pass filtering that openemulator does?

I needed to also account for the DHGR timing difference that
introduces a phase offset of 1 pixel between the memory values and
displayed pixel timings.

Fix a last-minute bug with palette precomputation.
2021-02-17 21:29:43 +00:00

88 lines
3.1 KiB
Python

"""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.float)
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', 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("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()