mirror of
https://github.com/KrisKennaway/ii-vision.git
synced 2024-12-26 21:31:34 +00:00
Parametrize the RGB palette to encode with, and support both NTSC and
IIGS palettes. Move the palette diff_matrix generation into make_data_tables.py since that is the only place it is used. Demand-load the edit distance matrices when transcoding.
This commit is contained in:
parent
696eb61bf4
commit
edefe649f4
BIN
transcoder/data/palette_0_edit_distance.pickle.bz2
Normal file
BIN
transcoder/data/palette_0_edit_distance.pickle.bz2
Normal file
Binary file not shown.
@ -11,6 +11,7 @@ import skvideo.io
|
|||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
import screen
|
import screen
|
||||||
|
from palette import Palette
|
||||||
from video_mode import VideoMode
|
from video_mode import VideoMode
|
||||||
|
|
||||||
|
|
||||||
@ -24,10 +25,11 @@ class FrameGrabber:
|
|||||||
|
|
||||||
|
|
||||||
class FileFrameGrabber(FrameGrabber):
|
class FileFrameGrabber(FrameGrabber):
|
||||||
def __init__(self, filename, mode: VideoMode):
|
def __init__(self, filename, mode: VideoMode, palette: Palette):
|
||||||
super(FileFrameGrabber, self).__init__(mode)
|
super(FileFrameGrabber, self).__init__(mode)
|
||||||
|
|
||||||
self.filename = filename # type: str
|
self.filename = filename # type: str
|
||||||
|
self.palette = palette # type: Palette
|
||||||
self._reader = skvideo.io.FFmpegReader(filename)
|
self._reader = skvideo.io.FFmpegReader(filename)
|
||||||
|
|
||||||
# Compute frame rate from input video
|
# Compute frame rate from input video
|
||||||
@ -43,8 +45,12 @@ class FileFrameGrabber(FrameGrabber):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _output_dir(filename) -> str:
|
def _output_dir(filename) -> str:
|
||||||
|
# TODO: should include palette
|
||||||
return ".".join(filename.split(".")[:-1])
|
return ".".join(filename.split(".")[:-1])
|
||||||
|
|
||||||
|
def _palette_arg(self) -> str:
|
||||||
|
return "P%d" % self.palette.value
|
||||||
|
|
||||||
def frames(self) -> Iterator[screen.MemoryMap]:
|
def frames(self) -> Iterator[screen.MemoryMap]:
|
||||||
"""Encode frame to HGR using bmp2dhr.
|
"""Encode frame to HGR using bmp2dhr.
|
||||||
|
|
||||||
@ -69,11 +75,9 @@ class FileFrameGrabber(FrameGrabber):
|
|||||||
_frame = _frame.resize((280, 192), resample=Image.LANCZOS)
|
_frame = _frame.resize((280, 192), resample=Image.LANCZOS)
|
||||||
_frame.save(bmpfile)
|
_frame.save(bmpfile)
|
||||||
|
|
||||||
# TODO: parametrize palette
|
|
||||||
subprocess.call([
|
subprocess.call([
|
||||||
"/usr/local/bin/bmp2dhr", bmpfile, "hgr",
|
"/usr/local/bin/bmp2dhr", bmpfile, "hgr",
|
||||||
"P5",
|
self._palette_arg(),
|
||||||
# "P0", # Kegs32 RGB Color palette(for //gs playback)
|
|
||||||
"D9" # Buckels dither
|
"D9" # Buckels dither
|
||||||
])
|
])
|
||||||
|
|
||||||
@ -96,11 +100,9 @@ class FileFrameGrabber(FrameGrabber):
|
|||||||
_frame = _frame.resize((280, 192), resample=Image.LANCZOS)
|
_frame = _frame.resize((280, 192), resample=Image.LANCZOS)
|
||||||
_frame.save(bmpfile)
|
_frame.save(bmpfile)
|
||||||
|
|
||||||
# TODO: parametrize palette
|
|
||||||
subprocess.call([
|
subprocess.call([
|
||||||
"/usr/local/bin/bmp2dhr", bmpfile, "dhgr", # "v",
|
"/usr/local/bin/bmp2dhr", bmpfile, "dhgr", # "v",
|
||||||
"P5", # "P0", # Kegs32 RGB Color palette (for //gs
|
self._palette_arg(),
|
||||||
# playback)
|
|
||||||
"A", # Output separate .BIN and .AUX files
|
"A", # Output separate .BIN and .AUX files
|
||||||
"D9" # Buckels dither
|
"D9" # Buckels dither
|
||||||
])
|
])
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
import argparse
|
import argparse
|
||||||
|
|
||||||
import movie
|
import movie
|
||||||
|
import palette
|
||||||
import video_mode
|
import video_mode
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
@ -28,6 +29,10 @@ parser.add_argument(
|
|||||||
'--video_mode', type=str, choices=video_mode.VideoMode.__members__.keys(),
|
'--video_mode', type=str, choices=video_mode.VideoMode.__members__.keys(),
|
||||||
help='Video display mode to encode for (HGR/DHGR)'
|
help='Video display mode to encode for (HGR/DHGR)'
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--palette', type=str, choices=palette.Palette.__members__.keys(),
|
||||||
|
help='Video palette to encode for (default=NTSC)'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def main(args):
|
def main(args):
|
||||||
@ -37,9 +42,12 @@ def main(args):
|
|||||||
every_n_video_frames=args.every_n_video_frames,
|
every_n_video_frames=args.every_n_video_frames,
|
||||||
audio_normalization=args.audio_normalization,
|
audio_normalization=args.audio_normalization,
|
||||||
max_bytes_out=1024. * 1024 * args.max_output_mb,
|
max_bytes_out=1024. * 1024 * args.max_output_mb,
|
||||||
video_mode=video_mode.VideoMode[args.video_mode]
|
video_mode=video_mode.VideoMode[args.video_mode],
|
||||||
|
palette=palette.Palette[args.palette],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
print("Palette %s" % args.palette)
|
||||||
|
|
||||||
print("Input frame rate = %f" % m.frame_grabber.input_frame_rate)
|
print("Input frame rate = %f" % m.frame_grabber.input_frame_rate)
|
||||||
|
|
||||||
if args.output:
|
if args.output:
|
||||||
|
@ -1,11 +1,16 @@
|
|||||||
import bz2
|
import bz2
|
||||||
import functools
|
import functools
|
||||||
import pickle
|
import pickle
|
||||||
from typing import Iterable
|
from typing import Dict, List, Iterable, Type
|
||||||
|
|
||||||
|
import colormath.color_conversions
|
||||||
|
import colormath.color_diff
|
||||||
|
import colormath.color_objects
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import weighted_levenshtein
|
import weighted_levenshtein
|
||||||
|
|
||||||
|
import colours
|
||||||
import palette
|
import palette
|
||||||
|
|
||||||
# The DHGR display encodes 7 pixels across interleaved 4-byte sequences
|
# The DHGR display encodes 7 pixels across interleaved 4-byte sequences
|
||||||
@ -65,7 +70,6 @@ import palette
|
|||||||
# contiguously into an array whose index is the (source, target) pair and
|
# contiguously into an array whose index is the (source, target) pair and
|
||||||
# the value is the edit distance.
|
# the value is the edit distance.
|
||||||
|
|
||||||
|
|
||||||
PIXEL_CHARS = "0123456789ABCDEF"
|
PIXEL_CHARS = "0123456789ABCDEF"
|
||||||
|
|
||||||
|
|
||||||
@ -74,14 +78,14 @@ def pixel_char(i: int) -> str:
|
|||||||
|
|
||||||
|
|
||||||
@functools.lru_cache(None)
|
@functools.lru_cache(None)
|
||||||
def pixel_string(pixels: Iterable[palette.DHGRColours]) -> str:
|
def pixel_string(pixels: Iterable[colours.DHGRColours]) -> str:
|
||||||
return "".join(pixel_char(p.value) for p in pixels)
|
return "".join(pixel_char(p.value) for p in pixels)
|
||||||
|
|
||||||
|
|
||||||
@functools.lru_cache(None)
|
@functools.lru_cache(None)
|
||||||
def pixels_influenced_by_byte_index(
|
def pixels_influenced_by_byte_index(
|
||||||
pixels: Iterable[palette.DHGRColours],
|
pixels: str,
|
||||||
idx: int) -> Iterable[palette.DHGRColours]:
|
idx: int) -> str:
|
||||||
"""Return subset of pixels that are influenced by given byte index (0..4)"""
|
"""Return subset of pixels that are influenced by given byte index (0..4)"""
|
||||||
start, end = {
|
start, end = {
|
||||||
0: (0, 1),
|
0: (0, 1),
|
||||||
@ -93,52 +97,6 @@ def pixels_influenced_by_byte_index(
|
|||||||
return pixels[start:end + 1]
|
return pixels[start:end + 1]
|
||||||
|
|
||||||
|
|
||||||
# Don't even consider insertions and deletions into the string, they don't
|
|
||||||
# make sense for comparing pixel strings
|
|
||||||
insert_costs = np.ones(128, dtype=np.float64) * 100000
|
|
||||||
delete_costs = np.ones(128, dtype=np.float64) * 100000
|
|
||||||
|
|
||||||
# Smallest substitution value is ~20 from palette.diff_matrix, i.e.
|
|
||||||
# we always prefer to transpose 2 pixels rather than substituting colours.
|
|
||||||
transpose_costs = np.ones((128, 128), dtype=np.float64) * 10
|
|
||||||
|
|
||||||
substitute_costs = np.zeros((128, 128), dtype=np.float64)
|
|
||||||
|
|
||||||
# Substitution costs to use when evaluating other potential offsets at which
|
|
||||||
# to store a content byte. We penalize more harshly for introducing
|
|
||||||
# errors that alter pixel colours, since these tend to be very
|
|
||||||
# noticeable as visual noise.
|
|
||||||
error_substitute_costs = np.zeros((128, 128), dtype=np.float64)
|
|
||||||
|
|
||||||
|
|
||||||
def make_substitute_costs():
|
|
||||||
# Penalty for changing colour
|
|
||||||
for i, c in enumerate(PIXEL_CHARS):
|
|
||||||
for j, d in enumerate(PIXEL_CHARS):
|
|
||||||
cost = palette.diff_matrix[i, j]
|
|
||||||
substitute_costs[(ord(c), ord(d))] = cost # / 20
|
|
||||||
substitute_costs[(ord(d), ord(c))] = cost # / 20
|
|
||||||
error_substitute_costs[(ord(c), ord(d))] = 5 * cost # / 4
|
|
||||||
error_substitute_costs[(ord(d), ord(c))] = 5 * cost # / 4
|
|
||||||
|
|
||||||
|
|
||||||
make_substitute_costs()
|
|
||||||
|
|
||||||
|
|
||||||
@functools.lru_cache(None)
|
|
||||||
def edit_distance(a, b, error: bool):
|
|
||||||
res = weighted_levenshtein.dam_lev(
|
|
||||||
a, b,
|
|
||||||
|
|
||||||
insert_costs=insert_costs,
|
|
||||||
delete_costs=delete_costs,
|
|
||||||
substitute_costs=error_substitute_costs if error else substitute_costs,
|
|
||||||
)
|
|
||||||
|
|
||||||
assert res == 0 or (1 <= res < 2 ** 16), res
|
|
||||||
return res
|
|
||||||
|
|
||||||
|
|
||||||
@functools.lru_cache(None)
|
@functools.lru_cache(None)
|
||||||
def int28_to_pixels(int28):
|
def int28_to_pixels(int28):
|
||||||
return tuple(
|
return tuple(
|
||||||
@ -170,7 +128,77 @@ def map_int8_to_mask32_3(int8):
|
|||||||
return int8 << 20
|
return int8 << 20
|
||||||
|
|
||||||
|
|
||||||
def make_edit_distance():
|
class EditDistanceParams:
|
||||||
|
# Don't even consider insertions and deletions into the string, they don't
|
||||||
|
# make sense for comparing pixel strings
|
||||||
|
insert_costs = np.ones(128, dtype=np.float64) * 100000
|
||||||
|
delete_costs = np.ones(128, dtype=np.float64) * 100000
|
||||||
|
|
||||||
|
# Smallest substitution value is ~20 from palette.diff_matrices, i.e.
|
||||||
|
# we always prefer to transpose 2 pixels rather than substituting colours.
|
||||||
|
transpose_costs = np.ones((128, 128), dtype=np.float64) * 10
|
||||||
|
|
||||||
|
substitute_costs = np.zeros((128, 128), dtype=np.float64)
|
||||||
|
|
||||||
|
# Substitution costs to use when evaluating other potential offsets at which
|
||||||
|
# to store a content byte. We penalize more harshly for introducing
|
||||||
|
# errors that alter pixel colours, since these tend to be very
|
||||||
|
# noticeable as visual noise.
|
||||||
|
error_substitute_costs = np.zeros((128, 128), dtype=np.float64)
|
||||||
|
|
||||||
|
|
||||||
|
def compute_diff_matrix(pal: Type[palette.BasePalette]):
|
||||||
|
# Compute matrix of CIE2000 delta values for this pal, representing
|
||||||
|
# perceptual distance between colours.
|
||||||
|
dm = np.ndarray(shape=(16, 16), dtype=np.int)
|
||||||
|
|
||||||
|
for colour1, a in pal.RGB.items():
|
||||||
|
alab = colormath.color_conversions.convert_color(
|
||||||
|
a, colormath.color_objects.LabColor)
|
||||||
|
for colour2, b in pal.RGB.items():
|
||||||
|
blab = colormath.color_conversions.convert_color(
|
||||||
|
b, colormath.color_objects.LabColor)
|
||||||
|
dm[colour1.value, colour2.value] = int(
|
||||||
|
colormath.color_diff.delta_e_cie2000(alab, blab))
|
||||||
|
return dm
|
||||||
|
|
||||||
|
|
||||||
|
def make_substitute_costs(pal: Type[palette.BasePalette]):
|
||||||
|
edp = EditDistanceParams()
|
||||||
|
|
||||||
|
diff_matrix = compute_diff_matrix(pal)
|
||||||
|
|
||||||
|
# Penalty for changing colour
|
||||||
|
for i, c in enumerate(PIXEL_CHARS):
|
||||||
|
for j, d in enumerate(PIXEL_CHARS):
|
||||||
|
cost = diff_matrix[i, j]
|
||||||
|
edp.substitute_costs[(ord(c), ord(d))] = cost # / 20
|
||||||
|
edp.substitute_costs[(ord(d), ord(c))] = cost # / 20
|
||||||
|
edp.error_substitute_costs[(ord(c), ord(d))] = 5 * cost # / 4
|
||||||
|
edp.error_substitute_costs[(ord(d), ord(c))] = 5 * cost # / 4
|
||||||
|
|
||||||
|
return edp
|
||||||
|
|
||||||
|
|
||||||
|
@functools.lru_cache(None)
|
||||||
|
def edit_distance(
|
||||||
|
edp: EditDistanceParams,
|
||||||
|
a: str,
|
||||||
|
b: str,
|
||||||
|
error: bool) -> np.float64:
|
||||||
|
res = weighted_levenshtein.dam_lev(
|
||||||
|
a, b,
|
||||||
|
insert_costs=edp.insert_costs,
|
||||||
|
delete_costs=edp.delete_costs,
|
||||||
|
substitute_costs=(
|
||||||
|
edp.error_substitute_costs if error else edp.substitute_costs),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert res == 0 or (1 <= res < 2 ** 16), res
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
def make_edit_distance(edp: EditDistanceParams):
|
||||||
edit = [
|
edit = [
|
||||||
np.zeros(shape=(2 ** 16), dtype=np.int16),
|
np.zeros(shape=(2 ** 16), dtype=np.int16),
|
||||||
np.zeros(shape=(2 ** 24), dtype=np.int16),
|
np.zeros(shape=(2 ** 24), dtype=np.int16),
|
||||||
@ -191,8 +219,8 @@ def make_edit_distance():
|
|||||||
second_pixels = pixels_influenced_by_byte_index(
|
second_pixels = pixels_influenced_by_byte_index(
|
||||||
pixel_string(int28_to_pixels(second)), 0)
|
pixel_string(int28_to_pixels(second)), 0)
|
||||||
|
|
||||||
edit[0][pair] = edit_distance(first_pixels, second_pixels,
|
edit[0][pair] = edit_distance(
|
||||||
error=False)
|
edp, first_pixels, second_pixels, error=False)
|
||||||
|
|
||||||
first = map_int8_to_mask32_3(i)
|
first = map_int8_to_mask32_3(i)
|
||||||
second = map_int8_to_mask32_3(j)
|
second = map_int8_to_mask32_3(j)
|
||||||
@ -202,8 +230,8 @@ def make_edit_distance():
|
|||||||
second_pixels = pixels_influenced_by_byte_index(
|
second_pixels = pixels_influenced_by_byte_index(
|
||||||
pixel_string(int28_to_pixels(second)), 3)
|
pixel_string(int28_to_pixels(second)), 3)
|
||||||
|
|
||||||
edit[3][pair] = edit_distance(first_pixels, second_pixels,
|
edit[3][pair] = edit_distance(
|
||||||
error=False)
|
edp, first_pixels, second_pixels, error=False)
|
||||||
|
|
||||||
for i in range(2 ** 12):
|
for i in range(2 ** 12):
|
||||||
print(i)
|
print(i)
|
||||||
@ -218,8 +246,8 @@ def make_edit_distance():
|
|||||||
second_pixels = pixels_influenced_by_byte_index(
|
second_pixels = pixels_influenced_by_byte_index(
|
||||||
pixel_string(int28_to_pixels(second)), 1)
|
pixel_string(int28_to_pixels(second)), 1)
|
||||||
|
|
||||||
edit[1][pair] = edit_distance(first_pixels, second_pixels,
|
edit[1][pair] = edit_distance(
|
||||||
error=False)
|
edp, first_pixels, second_pixels, error=False)
|
||||||
|
|
||||||
first = map_int12_to_mask32_2(i)
|
first = map_int12_to_mask32_2(i)
|
||||||
second = map_int12_to_mask32_2(j)
|
second = map_int12_to_mask32_2(j)
|
||||||
@ -229,22 +257,23 @@ def make_edit_distance():
|
|||||||
second_pixels = pixels_influenced_by_byte_index(
|
second_pixels = pixels_influenced_by_byte_index(
|
||||||
pixel_string(int28_to_pixels(second)), 2)
|
pixel_string(int28_to_pixels(second)), 2)
|
||||||
|
|
||||||
edit[2][pair] = edit_distance(first_pixels, second_pixels,
|
edit[2][pair] = edit_distance(
|
||||||
error=False)
|
edp, first_pixels, second_pixels, error=False)
|
||||||
|
|
||||||
return edit
|
return edit
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
edit = make_edit_distance()
|
for p in palette.PALETTES.values():
|
||||||
|
print("Processing palette %s" % p)
|
||||||
|
edp = make_substitute_costs(p)
|
||||||
|
edit = make_edit_distance(edp)
|
||||||
|
|
||||||
# TODO: error distance matrices
|
# TODO: error distance matrices
|
||||||
|
data = "transcoder/data/palette_%d_edit_distance.pickle" \
|
||||||
with bz2.open(
|
".bz2" % p.ID.value
|
||||||
"transcoder/edit_distance.pickle.bz2", "wb",
|
with bz2.open(data, "wb", compresslevel=9) as out:
|
||||||
compresslevel=9) as out:
|
pickle.dump(edit, out, protocol=pickle.HIGHEST_PROTOCOL)
|
||||||
pickle.dump(
|
|
||||||
edit, out, protocol=pickle.HIGHEST_PROTOCOL)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
@ -10,31 +10,15 @@ class TestMakeDataTables(unittest.TestCase):
|
|||||||
self.assertEqual("0FC", make_data_tables.pixel_string(pixels))
|
self.assertEqual("0FC", make_data_tables.pixel_string(pixels))
|
||||||
|
|
||||||
def test_pixels_influenced_by_byte_index(self):
|
def test_pixels_influenced_by_byte_index(self):
|
||||||
pixels = (
|
pixels = "CB00000"
|
||||||
DHGRColours.ORANGE,
|
|
||||||
DHGRColours.GREEN,
|
|
||||||
DHGRColours.BLACK,
|
|
||||||
DHGRColours.BLACK,
|
|
||||||
DHGRColours.BLACK,
|
|
||||||
DHGRColours.BLACK,
|
|
||||||
DHGRColours.BLACK,
|
|
||||||
)
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
(DHGRColours.ORANGE, DHGRColours.GREEN),
|
"CB",
|
||||||
make_data_tables.pixels_influenced_by_byte_index(pixels, 0)
|
make_data_tables.pixels_influenced_by_byte_index(pixels, 0)
|
||||||
)
|
)
|
||||||
|
|
||||||
pixels = (
|
pixels = "CBA9000"
|
||||||
DHGRColours.BLACK,
|
|
||||||
DHGRColours.BROWN,
|
|
||||||
DHGRColours.YELLOW,
|
|
||||||
DHGRColours.GREY1,
|
|
||||||
DHGRColours.BLACK,
|
|
||||||
DHGRColours.BLACK,
|
|
||||||
DHGRColours.BLACK,
|
|
||||||
)
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
(DHGRColours.BROWN, DHGRColours.YELLOW, DHGRColours.GREY1),
|
"BA9",
|
||||||
make_data_tables.pixels_influenced_by_byte_index(pixels, 1)
|
make_data_tables.pixels_influenced_by_byte_index(pixels, 1)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@ import frame_grabber
|
|||||||
import machine
|
import machine
|
||||||
import opcodes
|
import opcodes
|
||||||
import video
|
import video
|
||||||
|
from palette import Palette
|
||||||
from video_mode import VideoMode
|
from video_mode import VideoMode
|
||||||
|
|
||||||
|
|
||||||
@ -17,19 +18,22 @@ class Movie:
|
|||||||
audio_normalization: float = None,
|
audio_normalization: float = None,
|
||||||
max_bytes_out: int = None,
|
max_bytes_out: int = None,
|
||||||
video_mode: VideoMode = VideoMode.HGR,
|
video_mode: VideoMode = VideoMode.HGR,
|
||||||
|
palette: Palette = Palette.NTSC,
|
||||||
):
|
):
|
||||||
self.filename = filename # type: str
|
self.filename = filename # type: str
|
||||||
self.every_n_video_frames = every_n_video_frames # type: int
|
self.every_n_video_frames = every_n_video_frames # type: int
|
||||||
self.max_bytes_out = max_bytes_out # type: int
|
self.max_bytes_out = max_bytes_out # type: int
|
||||||
self.video_mode = video_mode # type: VideoMode
|
self.video_mode = video_mode # type: VideoMode
|
||||||
|
self.palette = palette # type: Palette
|
||||||
|
|
||||||
self.audio = audio.Audio(
|
self.audio = audio.Audio(
|
||||||
filename, normalization=audio_normalization) # type: audio.Audio
|
filename, normalization=audio_normalization) # type: audio.Audio
|
||||||
|
|
||||||
self.frame_grabber = frame_grabber.FileFrameGrabber(
|
self.frame_grabber = frame_grabber.FileFrameGrabber(
|
||||||
filename, mode=video_mode)
|
filename, mode=video_mode, palette=self.palette)
|
||||||
self.video = video.Video(
|
self.video = video.Video(
|
||||||
self.frame_sequencer, mode=video_mode,
|
self.frame_sequencer, mode=video_mode,
|
||||||
|
palette=self.palette,
|
||||||
ticks_per_second=self.audio.sample_rate
|
ticks_per_second=self.audio.sample_rate
|
||||||
) # type: video.Video
|
) # type: video.Video
|
||||||
|
|
||||||
|
@ -1,18 +1,37 @@
|
|||||||
|
import enum
|
||||||
|
from typing import Dict, Type
|
||||||
|
|
||||||
import colormath.color_objects
|
import colormath.color_objects
|
||||||
import colormath.color_diff
|
|
||||||
import colormath.color_conversions
|
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
from colours import DHGRColours
|
from colours import DHGRColours
|
||||||
|
|
||||||
|
# Type annotation
|
||||||
|
RGB = colormath.color_objects.sRGBColor
|
||||||
|
|
||||||
|
|
||||||
def rgb(r, g, b):
|
def rgb(r, g, b):
|
||||||
return colormath.color_objects.sRGBColor(r, g, b, is_upscaled=True)
|
return RGB(r, g, b, is_upscaled=True)
|
||||||
|
|
||||||
|
|
||||||
|
class Palette(enum.Enum):
|
||||||
|
"""BMP2DHR palette numbers"""
|
||||||
|
UNKNOWN = -1
|
||||||
|
IIGS = 0
|
||||||
|
NTSC = 5
|
||||||
|
|
||||||
|
|
||||||
|
class BasePalette:
|
||||||
|
ID = Palette.UNKNOWN # type: Palette
|
||||||
|
|
||||||
|
# Palette RGB map
|
||||||
|
RGB = {} # type: Dict[DHGRColours: RGB]
|
||||||
|
|
||||||
|
|
||||||
|
class NTSCPalette(BasePalette):
|
||||||
|
ID = Palette.NTSC
|
||||||
|
|
||||||
# Palette RGB values taken from BMP2DHGR's default NTSC palette
|
# Palette RGB values taken from BMP2DHGR's default NTSC palette
|
||||||
# TODO: support other palettes as well, e.g. //gs RGB
|
RGB = {
|
||||||
palette = {
|
|
||||||
DHGRColours.BLACK: rgb(0, 0, 0),
|
DHGRColours.BLACK: rgb(0, 0, 0),
|
||||||
DHGRColours.MAGENTA: rgb(148, 12, 125),
|
DHGRColours.MAGENTA: rgb(148, 12, 125),
|
||||||
DHGRColours.BROWN: rgb(99, 77, 0),
|
DHGRColours.BROWN: rgb(99, 77, 0),
|
||||||
@ -32,20 +51,31 @@ palette = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def compute_diff_matrix():
|
class IIGSPalette(BasePalette):
|
||||||
# Compute matrix of CIE2000 delta values for this palette, representing
|
ID = Palette.IIGS
|
||||||
# perceptual distance between colours.
|
|
||||||
dm = np.ndarray(shape=(16, 16), dtype=np.int)
|
|
||||||
|
|
||||||
for colour1, a in palette.items():
|
# Palette RGB values taken from BMP2DHGR's KEGS32 palette
|
||||||
alab = colormath.color_conversions.convert_color(
|
RGB = {
|
||||||
a, colormath.color_objects.LabColor)
|
DHGRColours.BLACK: rgb(0, 0, 0),
|
||||||
for colour2, b in palette.items():
|
DHGRColours.MAGENTA: rgb(221, 0, 51),
|
||||||
blab = colormath.color_conversions.convert_color(
|
DHGRColours.BROWN: rgb(136, 85, 34),
|
||||||
b, colormath.color_objects.LabColor)
|
DHGRColours.ORANGE: rgb(255, 102, 0),
|
||||||
dm[colour1.value, colour2.value] = int(
|
DHGRColours.DARK_GREEN: rgb(0, 119, 0),
|
||||||
colormath.color_diff.delta_e_cie2000(alab, blab))
|
DHGRColours.GREY1: rgb(85, 85, 85),
|
||||||
return dm
|
DHGRColours.GREEN: rgb(0, 221, 0),
|
||||||
|
DHGRColours.YELLOW: rgb(255, 255, 0),
|
||||||
|
DHGRColours.DARK_BLUE: rgb(0, 0, 153),
|
||||||
|
DHGRColours.VIOLET: rgb(221, 0, 221),
|
||||||
|
DHGRColours.GREY2: rgb(170, 170, 170),
|
||||||
|
DHGRColours.PINK: rgb(255, 153, 136),
|
||||||
|
DHGRColours.MED_BLUE: rgb(34, 34, 255),
|
||||||
|
DHGRColours.LIGHT_BLUE: rgb(102, 170, 255),
|
||||||
|
DHGRColours.AQUA: rgb(0, 255, 153),
|
||||||
|
DHGRColours.WHITE: rgb(255, 255, 255)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
diff_matrix = compute_diff_matrix()
|
PALETTES = {
|
||||||
|
Palette.IIGS: IIGSPalette,
|
||||||
|
Palette.NTSC: NTSCPalette
|
||||||
|
} # type: Dict[Palette, Type[BasePalette]]
|
||||||
|
@ -3,9 +3,10 @@
|
|||||||
import bz2
|
import bz2
|
||||||
import functools
|
import functools
|
||||||
import pickle
|
import pickle
|
||||||
from typing import Union
|
from typing import Union, List
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
import palette
|
||||||
|
|
||||||
# Type annotation for cases where we may process either an int or a numpy array.
|
# Type annotation for cases where we may process either an int or a numpy array.
|
||||||
IntOrArray = Union[int, np.ndarray]
|
IntOrArray = Union[int, np.ndarray]
|
||||||
@ -140,10 +141,15 @@ class DHGRBitmap:
|
|||||||
# How much to right-shift bits after masking to bring into int8/int12 range
|
# How much to right-shift bits after masking to bring into int8/int12 range
|
||||||
BYTE_SHIFTS = [0, 4, 12, 20]
|
BYTE_SHIFTS = [0, 4, 12, 20]
|
||||||
|
|
||||||
# Load edit distance matrices for masked, shifted byte 0..3 values
|
@staticmethod
|
||||||
# TODO: should go somewhere else since we don't use it here at all
|
@functools.lru_cache(None)
|
||||||
with bz2.open("transcoder/edit_distance.pickle.bz2", "rb") as ed:
|
def edit_distances(palette_id: palette.Palette) -> List[np.ndarray]:
|
||||||
edit_distances = pickle.load(ed)
|
"""Load edit distance matrices for masked, shifted byte 0..3 values."""
|
||||||
|
data = "transcoder/data/palette_%d_edit_distance.pickle.bz2" % (
|
||||||
|
palette_id.value
|
||||||
|
)
|
||||||
|
with bz2.open(data, "rb") as ed:
|
||||||
|
return pickle.load(ed) # type: List[np.ndarray]
|
||||||
|
|
||||||
def __init__(self, main_memory: MemoryMap, aux_memory: MemoryMap):
|
def __init__(self, main_memory: MemoryMap, aux_memory: MemoryMap):
|
||||||
self.main_memory = main_memory
|
self.main_memory = main_memory
|
||||||
|
@ -10,6 +10,7 @@ import numpy as np
|
|||||||
import opcodes
|
import opcodes
|
||||||
import screen
|
import screen
|
||||||
from frame_grabber import FrameGrabber
|
from frame_grabber import FrameGrabber
|
||||||
|
from palette import Palette
|
||||||
from video_mode import VideoMode
|
from video_mode import VideoMode
|
||||||
|
|
||||||
|
|
||||||
@ -21,13 +22,17 @@ class Video:
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
frame_grabber: FrameGrabber,
|
frame_grabber: FrameGrabber,
|
||||||
mode: VideoMode = VideoMode.HGR
|
mode: VideoMode = VideoMode.HGR,
|
||||||
|
palette: Palette = Palette.NTSC,
|
||||||
|
ticks_per_second: int,
|
||||||
):
|
):
|
||||||
self.mode = mode # type: VideoMode
|
self.mode = mode # type: VideoMode
|
||||||
self.frame_grabber = frame_grabber # type: FrameGrabber
|
self.frame_grabber = frame_grabber # type: FrameGrabber
|
||||||
|
self.ticks_per_second = float(ticks_per_second) # type: float
|
||||||
self.ticks_per_frame = (
|
self.ticks_per_frame = (
|
||||||
self.ticks_per_second / self.input_frame_rate) # type: float
|
self.ticks_per_second / self.input_frame_rate) # type: float
|
||||||
self.frame_number = 0 # type: int
|
self.frame_number = 0 # type: int
|
||||||
|
self.palette = palette # type: Palette
|
||||||
|
|
||||||
# Initialize empty screen
|
# Initialize empty screen
|
||||||
self.memory_map = screen.MemoryMap(
|
self.memory_map = screen.MemoryMap(
|
||||||
@ -213,8 +218,8 @@ class Video:
|
|||||||
heapq.heapify(priorities)
|
heapq.heapify(priorities)
|
||||||
return priorities
|
return priorities
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _diff_weights(
|
def _diff_weights(
|
||||||
|
self,
|
||||||
source: screen.DHGRBitmap,
|
source: screen.DHGRBitmap,
|
||||||
target: screen.DHGRBitmap,
|
target: screen.DHGRBitmap,
|
||||||
is_aux: bool
|
is_aux: bool
|
||||||
@ -228,14 +233,16 @@ class Video:
|
|||||||
|
|
||||||
# Concatenate 8-bit source and target into 16-bit values
|
# Concatenate 8-bit source and target into 16-bit values
|
||||||
pair0 = (source_pixels0 << 8) + target_pixels0
|
pair0 = (source_pixels0 << 8) + target_pixels0
|
||||||
dist0 = source.edit_distances[0][pair0].reshape(pair0.shape)
|
dist0 = source.edit_distances(self.palette)[0][pair0].reshape(
|
||||||
|
pair0.shape)
|
||||||
|
|
||||||
# Pixels influenced by byte offset 2
|
# Pixels influenced by byte offset 2
|
||||||
source_pixels2 = source.mask_and_shift_data(source.packed, 2)
|
source_pixels2 = source.mask_and_shift_data(source.packed, 2)
|
||||||
target_pixels2 = target.mask_and_shift_data(target.packed, 2)
|
target_pixels2 = target.mask_and_shift_data(target.packed, 2)
|
||||||
# Concatenate 12-bit source and target into 24-bit values
|
# Concatenate 12-bit source and target into 24-bit values
|
||||||
pair2 = (source_pixels2 << 12) + target_pixels2
|
pair2 = (source_pixels2 << 12) + target_pixels2
|
||||||
dist2 = source.edit_distances[2][pair2].reshape(pair2.shape)
|
dist2 = source.edit_distances(self.palette)[2][pair2].reshape(
|
||||||
|
pair2.shape)
|
||||||
|
|
||||||
diff[:, 0::2] = dist0
|
diff[:, 0::2] = dist0
|
||||||
diff[:, 1::2] = dist2
|
diff[:, 1::2] = dist2
|
||||||
@ -245,13 +252,15 @@ class Video:
|
|||||||
source_pixels1 = source.mask_and_shift_data(source.packed, 1)
|
source_pixels1 = source.mask_and_shift_data(source.packed, 1)
|
||||||
target_pixels1 = target.mask_and_shift_data(target.packed, 1)
|
target_pixels1 = target.mask_and_shift_data(target.packed, 1)
|
||||||
pair1 = (source_pixels1 << 12) + target_pixels1
|
pair1 = (source_pixels1 << 12) + target_pixels1
|
||||||
dist1 = source.edit_distances[1][pair1].reshape(pair1.shape)
|
dist1 = source.edit_distances(self.palette)[1][pair1].reshape(
|
||||||
|
pair1.shape)
|
||||||
|
|
||||||
# Pixels influenced by byte offset 3
|
# Pixels influenced by byte offset 3
|
||||||
source_pixels3 = source.mask_and_shift_data(source.packed, 3)
|
source_pixels3 = source.mask_and_shift_data(source.packed, 3)
|
||||||
target_pixels3 = target.mask_and_shift_data(target.packed, 3)
|
target_pixels3 = target.mask_and_shift_data(target.packed, 3)
|
||||||
pair3 = (source_pixels3 << 8) + target_pixels3
|
pair3 = (source_pixels3 << 8) + target_pixels3
|
||||||
dist3 = source.edit_distances[3][pair3].reshape(pair3.shape)
|
dist3 = source.edit_distances(self.palette)[3][pair3].reshape(
|
||||||
|
pair3.shape)
|
||||||
|
|
||||||
diff[:, 0::2] = dist1
|
diff[:, 0::2] = dist1
|
||||||
diff[:, 1::2] = dist3
|
diff[:, 1::2] = dist3
|
||||||
@ -278,12 +287,12 @@ class Video:
|
|||||||
else:
|
else:
|
||||||
pair = (old_pixels << 12) + new_pixels
|
pair = (old_pixels << 12) + new_pixels
|
||||||
|
|
||||||
p = target_pixelmap.edit_distances[byte_offset][pair]
|
p = target_pixelmap.edit_distances(self.palette)[byte_offset][pair]
|
||||||
|
|
||||||
return p
|
return p
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _compute_delta(
|
def _compute_delta(
|
||||||
|
self,
|
||||||
content: int,
|
content: int,
|
||||||
target: screen.DHGRBitmap,
|
target: screen.DHGRBitmap,
|
||||||
old,
|
old,
|
||||||
@ -301,7 +310,8 @@ class Video:
|
|||||||
|
|
||||||
# Concatenate 8-bit source and target into 16-bit values
|
# Concatenate 8-bit source and target into 16-bit values
|
||||||
pair0 = (source_pixels0 << 8) + target_pixels0
|
pair0 = (source_pixels0 << 8) + target_pixels0
|
||||||
dist0 = target.edit_distances[0][pair0].reshape(pair0.shape)
|
dist0 = target.edit_distances(self.palette)[0][pair0].reshape(
|
||||||
|
pair0.shape)
|
||||||
|
|
||||||
# Pixels influenced by byte offset 2
|
# Pixels influenced by byte offset 2
|
||||||
source_pixels2 = target.mask_and_shift_data(
|
source_pixels2 = target.mask_and_shift_data(
|
||||||
@ -309,7 +319,8 @@ class Video:
|
|||||||
target_pixels2 = target.mask_and_shift_data(target.packed, 2)
|
target_pixels2 = target.mask_and_shift_data(target.packed, 2)
|
||||||
# Concatenate 12-bit source and target into 24-bit values
|
# Concatenate 12-bit source and target into 24-bit values
|
||||||
pair2 = (source_pixels2 << 12) + target_pixels2
|
pair2 = (source_pixels2 << 12) + target_pixels2
|
||||||
dist2 = target.edit_distances[2][pair2].reshape(pair2.shape)
|
dist2 = target.edit_distances(self.palette)[2][pair2].reshape(
|
||||||
|
pair2.shape)
|
||||||
|
|
||||||
diff[:, 0::2] = dist0
|
diff[:, 0::2] = dist0
|
||||||
diff[:, 1::2] = dist2
|
diff[:, 1::2] = dist2
|
||||||
@ -320,14 +331,16 @@ class Video:
|
|||||||
target.masked_update(1, target.packed, content), 1)
|
target.masked_update(1, target.packed, content), 1)
|
||||||
target_pixels1 = target.mask_and_shift_data(target.packed, 1)
|
target_pixels1 = target.mask_and_shift_data(target.packed, 1)
|
||||||
pair1 = (source_pixels1 << 12) + target_pixels1
|
pair1 = (source_pixels1 << 12) + target_pixels1
|
||||||
dist1 = target.edit_distances[1][pair1].reshape(pair1.shape)
|
dist1 = target.edit_distances(self.palette)[1][pair1].reshape(
|
||||||
|
pair1.shape)
|
||||||
|
|
||||||
# Pixels influenced by byte offset 3
|
# Pixels influenced by byte offset 3
|
||||||
source_pixels3 = target.mask_and_shift_data(
|
source_pixels3 = target.mask_and_shift_data(
|
||||||
target.masked_update(3, target.packed, content), 3)
|
target.masked_update(3, target.packed, content), 3)
|
||||||
target_pixels3 = target.mask_and_shift_data(target.packed, 3)
|
target_pixels3 = target.mask_and_shift_data(target.packed, 3)
|
||||||
pair3 = (source_pixels3 << 8) + target_pixels3
|
pair3 = (source_pixels3 << 8) + target_pixels3
|
||||||
dist3 = target.edit_distances[3][pair3].reshape(pair3.shape)
|
dist3 = target.edit_distances(self.palette)[3][pair3].reshape(
|
||||||
|
pair3.shape)
|
||||||
|
|
||||||
diff[:, 0::2] = dist1
|
diff[:, 0::2] = dist1
|
||||||
diff[:, 1::2] = dist3
|
diff[:, 1::2] = dist3
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
import frame_grabber
|
import frame_grabber
|
||||||
|
import palette
|
||||||
import screen
|
import screen
|
||||||
import video
|
import video
|
||||||
import video_mode
|
import video_mode
|
||||||
@ -27,11 +28,13 @@ class TestVideo(unittest.TestCase):
|
|||||||
|
|
||||||
diff = v._diff_weights(v.pixelmap, target_pixelmap, is_aux=True)
|
diff = v._diff_weights(v.pixelmap, target_pixelmap, is_aux=True)
|
||||||
|
|
||||||
|
pal = palette.NTSCPalette
|
||||||
|
|
||||||
# Expect byte 0 to map to 0b00000000 01111111
|
# Expect byte 0 to map to 0b00000000 01111111
|
||||||
expect0 = target_pixelmap.edit_distances[0][0b0000000001111111]
|
expect0 = target_pixelmap.edit_distances(pal.ID)[0][0b0000000001111111]
|
||||||
|
|
||||||
# Expect byte 2 to map to 0b000000000000 000101010100
|
# Expect byte 2 to map to 0b000000000000 000101010100
|
||||||
expect2 = target_pixelmap.edit_distances[2][0b000101010100]
|
expect2 = target_pixelmap.edit_distances(pal.ID)[2][0b000101010100]
|
||||||
|
|
||||||
self.assertEqual(expect0, diff[0, 0])
|
self.assertEqual(expect0, diff[0, 0])
|
||||||
self.assertEqual(expect2, diff[0, 1])
|
self.assertEqual(expect2, diff[0, 1])
|
||||||
@ -61,10 +64,11 @@ class TestVideo(unittest.TestCase):
|
|||||||
diff = v._diff_weights(v.pixelmap, target_pixelmap, is_aux=True)
|
diff = v._diff_weights(v.pixelmap, target_pixelmap, is_aux=True)
|
||||||
|
|
||||||
# Expect byte 0 to map to 0b01111111 01101101
|
# Expect byte 0 to map to 0b01111111 01101101
|
||||||
expect0 = target_pixelmap.edit_distances[0][0b0111111101101101]
|
expect0 = target_pixelmap.edit_distances(pal.ID)[0][0b0111111101101101]
|
||||||
|
|
||||||
# Expect byte 2 to map to 0b000101010100 000011011000
|
# Expect byte 2 to map to 0b000101010100 000011011000
|
||||||
expect2 = target_pixelmap.edit_distances[2][0b0000101010100000011011000]
|
expect2 = target_pixelmap.edit_distances(pal.ID)[2][
|
||||||
|
0b0000101010100000011011000]
|
||||||
|
|
||||||
self.assertEqual(expect0, diff[0, 0])
|
self.assertEqual(expect0, diff[0, 0])
|
||||||
self.assertEqual(expect2, diff[0, 1])
|
self.assertEqual(expect2, diff[0, 1])
|
||||||
|
Loading…
Reference in New Issue
Block a user