mirror of
https://github.com/KrisKennaway/ii-pix.git
synced 2025-02-20 17:29:03 +00:00
Use .npy format for RGB to CAM16UCS conversion matrix, and get rid of precomputed CIE2000 distances
This commit is contained in:
parent
e979df03bc
commit
feefdb5dc6
26
convert.py
26
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
|
||||
|
173
palette.py
173
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
|
||||
}
|
||||
|
||||
|
31
precompute_conversion.py
Normal file
31
precompute_conversion.py
Normal 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()
|
@ -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()
|
Loading…
x
Reference in New Issue
Block a user