ii-pix/precompute_distance.py
kris ad9515dcf2 Implement NTSC emulation, using an 8 pixel window for chroma signal.
Use this to precompute a new ntsc palette with 256 entries (though
only 84 unique colours) that are available by appropriate pixel
sequences.  Unfortunately the precomputed distance matrix for this
palette is 4GB!

Optimize the precomputation to be less memory hungry, while also
making efficient use of the mmapped output file.

Add support for dithering images using this 8-bit palette depth,
i.e. to optimize for NTSC rendering.  This often gives better image
quality since more colours are available, especially when modulating
areas of similar colour.

Fix 140 pixel dithering and render the output including NTSC fringing
instead of the unrealistic 140px output that doesn't include it.

Add support for rendering output image using any target palette, which
is useful e.g. for comparing how an 8-pixel NTSC rendered image will
be displayed on an emulator using 4-pixel ntsc emulation (there is
usually some colour bias, because the 8 pixel chroma blending tends to
average away colours).

Switch the output binary format to write AUX memory first, which
matches the image format of other utilities.
2021-02-14 23:34:25 +00:00

87 lines
3.0 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)
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)))
nearest_colours(palette, all_lab, out)
if __name__ == "__main__":
main()