mirror of
https://github.com/KrisKennaway/ii-vision.git
synced 2024-12-26 06:30:29 +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
|
||||
|
||||
import screen
|
||||
from palette import Palette
|
||||
from video_mode import VideoMode
|
||||
|
||||
|
||||
@ -24,10 +25,11 @@ class FrameGrabber:
|
||||
|
||||
|
||||
class FileFrameGrabber(FrameGrabber):
|
||||
def __init__(self, filename, mode: VideoMode):
|
||||
def __init__(self, filename, mode: VideoMode, palette: Palette):
|
||||
super(FileFrameGrabber, self).__init__(mode)
|
||||
|
||||
self.filename = filename # type: str
|
||||
self.palette = palette # type: Palette
|
||||
self._reader = skvideo.io.FFmpegReader(filename)
|
||||
|
||||
# Compute frame rate from input video
|
||||
@ -43,8 +45,12 @@ class FileFrameGrabber(FrameGrabber):
|
||||
|
||||
@staticmethod
|
||||
def _output_dir(filename) -> str:
|
||||
# TODO: should include palette
|
||||
return ".".join(filename.split(".")[:-1])
|
||||
|
||||
def _palette_arg(self) -> str:
|
||||
return "P%d" % self.palette.value
|
||||
|
||||
def frames(self) -> Iterator[screen.MemoryMap]:
|
||||
"""Encode frame to HGR using bmp2dhr.
|
||||
|
||||
@ -69,11 +75,9 @@ class FileFrameGrabber(FrameGrabber):
|
||||
_frame = _frame.resize((280, 192), resample=Image.LANCZOS)
|
||||
_frame.save(bmpfile)
|
||||
|
||||
# TODO: parametrize palette
|
||||
subprocess.call([
|
||||
"/usr/local/bin/bmp2dhr", bmpfile, "hgr",
|
||||
"P5",
|
||||
# "P0", # Kegs32 RGB Color palette(for //gs playback)
|
||||
self._palette_arg(),
|
||||
"D9" # Buckels dither
|
||||
])
|
||||
|
||||
@ -96,11 +100,9 @@ class FileFrameGrabber(FrameGrabber):
|
||||
_frame = _frame.resize((280, 192), resample=Image.LANCZOS)
|
||||
_frame.save(bmpfile)
|
||||
|
||||
# TODO: parametrize palette
|
||||
subprocess.call([
|
||||
"/usr/local/bin/bmp2dhr", bmpfile, "dhgr", # "v",
|
||||
"P5", # "P0", # Kegs32 RGB Color palette (for //gs
|
||||
# playback)
|
||||
self._palette_arg(),
|
||||
"A", # Output separate .BIN and .AUX files
|
||||
"D9" # Buckels dither
|
||||
])
|
||||
|
@ -3,6 +3,7 @@
|
||||
import argparse
|
||||
|
||||
import movie
|
||||
import palette
|
||||
import video_mode
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
@ -28,6 +29,10 @@ parser.add_argument(
|
||||
'--video_mode', type=str, choices=video_mode.VideoMode.__members__.keys(),
|
||||
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):
|
||||
@ -37,9 +42,12 @@ def main(args):
|
||||
every_n_video_frames=args.every_n_video_frames,
|
||||
audio_normalization=args.audio_normalization,
|
||||
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)
|
||||
|
||||
if args.output:
|
||||
|
@ -1,11 +1,16 @@
|
||||
import bz2
|
||||
import functools
|
||||
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 weighted_levenshtein
|
||||
|
||||
import colours
|
||||
import palette
|
||||
|
||||
# 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
|
||||
# the value is the edit distance.
|
||||
|
||||
|
||||
PIXEL_CHARS = "0123456789ABCDEF"
|
||||
|
||||
|
||||
@ -74,14 +78,14 @@ def pixel_char(i: int) -> str:
|
||||
|
||||
|
||||
@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)
|
||||
|
||||
|
||||
@functools.lru_cache(None)
|
||||
def pixels_influenced_by_byte_index(
|
||||
pixels: Iterable[palette.DHGRColours],
|
||||
idx: int) -> Iterable[palette.DHGRColours]:
|
||||
pixels: str,
|
||||
idx: int) -> str:
|
||||
"""Return subset of pixels that are influenced by given byte index (0..4)"""
|
||||
start, end = {
|
||||
0: (0, 1),
|
||||
@ -93,52 +97,6 @@ def pixels_influenced_by_byte_index(
|
||||
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)
|
||||
def int28_to_pixels(int28):
|
||||
return tuple(
|
||||
@ -170,7 +128,77 @@ def map_int8_to_mask32_3(int8):
|
||||
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 = [
|
||||
np.zeros(shape=(2 ** 16), 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(
|
||||
pixel_string(int28_to_pixels(second)), 0)
|
||||
|
||||
edit[0][pair] = edit_distance(first_pixels, second_pixels,
|
||||
error=False)
|
||||
edit[0][pair] = edit_distance(
|
||||
edp, first_pixels, second_pixels, error=False)
|
||||
|
||||
first = map_int8_to_mask32_3(i)
|
||||
second = map_int8_to_mask32_3(j)
|
||||
@ -202,8 +230,8 @@ def make_edit_distance():
|
||||
second_pixels = pixels_influenced_by_byte_index(
|
||||
pixel_string(int28_to_pixels(second)), 3)
|
||||
|
||||
edit[3][pair] = edit_distance(first_pixels, second_pixels,
|
||||
error=False)
|
||||
edit[3][pair] = edit_distance(
|
||||
edp, first_pixels, second_pixels, error=False)
|
||||
|
||||
for i in range(2 ** 12):
|
||||
print(i)
|
||||
@ -218,8 +246,8 @@ def make_edit_distance():
|
||||
second_pixels = pixels_influenced_by_byte_index(
|
||||
pixel_string(int28_to_pixels(second)), 1)
|
||||
|
||||
edit[1][pair] = edit_distance(first_pixels, second_pixels,
|
||||
error=False)
|
||||
edit[1][pair] = edit_distance(
|
||||
edp, first_pixels, second_pixels, error=False)
|
||||
|
||||
first = map_int12_to_mask32_2(i)
|
||||
second = map_int12_to_mask32_2(j)
|
||||
@ -229,22 +257,23 @@ def make_edit_distance():
|
||||
second_pixels = pixels_influenced_by_byte_index(
|
||||
pixel_string(int28_to_pixels(second)), 2)
|
||||
|
||||
edit[2][pair] = edit_distance(first_pixels, second_pixels,
|
||||
error=False)
|
||||
edit[2][pair] = edit_distance(
|
||||
edp, first_pixels, second_pixels, error=False)
|
||||
|
||||
return edit
|
||||
|
||||
|
||||
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
|
||||
|
||||
with bz2.open(
|
||||
"transcoder/edit_distance.pickle.bz2", "wb",
|
||||
compresslevel=9) as out:
|
||||
pickle.dump(
|
||||
edit, out, protocol=pickle.HIGHEST_PROTOCOL)
|
||||
# TODO: error distance matrices
|
||||
data = "transcoder/data/palette_%d_edit_distance.pickle" \
|
||||
".bz2" % p.ID.value
|
||||
with bz2.open(data, "wb", compresslevel=9) as out:
|
||||
pickle.dump(edit, out, protocol=pickle.HIGHEST_PROTOCOL)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
@ -10,31 +10,15 @@ class TestMakeDataTables(unittest.TestCase):
|
||||
self.assertEqual("0FC", make_data_tables.pixel_string(pixels))
|
||||
|
||||
def test_pixels_influenced_by_byte_index(self):
|
||||
pixels = (
|
||||
DHGRColours.ORANGE,
|
||||
DHGRColours.GREEN,
|
||||
DHGRColours.BLACK,
|
||||
DHGRColours.BLACK,
|
||||
DHGRColours.BLACK,
|
||||
DHGRColours.BLACK,
|
||||
DHGRColours.BLACK,
|
||||
)
|
||||
pixels = "CB00000"
|
||||
self.assertEqual(
|
||||
(DHGRColours.ORANGE, DHGRColours.GREEN),
|
||||
"CB",
|
||||
make_data_tables.pixels_influenced_by_byte_index(pixels, 0)
|
||||
)
|
||||
|
||||
pixels = (
|
||||
DHGRColours.BLACK,
|
||||
DHGRColours.BROWN,
|
||||
DHGRColours.YELLOW,
|
||||
DHGRColours.GREY1,
|
||||
DHGRColours.BLACK,
|
||||
DHGRColours.BLACK,
|
||||
DHGRColours.BLACK,
|
||||
)
|
||||
pixels = "CBA9000"
|
||||
self.assertEqual(
|
||||
(DHGRColours.BROWN, DHGRColours.YELLOW, DHGRColours.GREY1),
|
||||
"BA9",
|
||||
make_data_tables.pixels_influenced_by_byte_index(pixels, 1)
|
||||
)
|
||||
|
||||
|
@ -7,6 +7,7 @@ import frame_grabber
|
||||
import machine
|
||||
import opcodes
|
||||
import video
|
||||
from palette import Palette
|
||||
from video_mode import VideoMode
|
||||
|
||||
|
||||
@ -17,19 +18,22 @@ class Movie:
|
||||
audio_normalization: float = None,
|
||||
max_bytes_out: int = None,
|
||||
video_mode: VideoMode = VideoMode.HGR,
|
||||
palette: Palette = Palette.NTSC,
|
||||
):
|
||||
self.filename = filename # type: str
|
||||
self.every_n_video_frames = every_n_video_frames # type: int
|
||||
self.max_bytes_out = max_bytes_out # type: int
|
||||
self.video_mode = video_mode # type: VideoMode
|
||||
self.palette = palette # type: Palette
|
||||
|
||||
self.audio = audio.Audio(
|
||||
filename, normalization=audio_normalization) # type: audio.Audio
|
||||
|
||||
self.frame_grabber = frame_grabber.FileFrameGrabber(
|
||||
filename, mode=video_mode)
|
||||
filename, mode=video_mode, palette=self.palette)
|
||||
self.video = video.Video(
|
||||
self.frame_sequencer, mode=video_mode,
|
||||
palette=self.palette,
|
||||
ticks_per_second=self.audio.sample_rate
|
||||
) # type: video.Video
|
||||
|
||||
|
@ -1,51 +1,81 @@
|
||||
import enum
|
||||
from typing import Dict, Type
|
||||
|
||||
import colormath.color_objects
|
||||
import colormath.color_diff
|
||||
import colormath.color_conversions
|
||||
import numpy as np
|
||||
|
||||
from colours import DHGRColours
|
||||
|
||||
# Type annotation
|
||||
RGB = colormath.color_objects.sRGBColor
|
||||
|
||||
|
||||
def rgb(r, g, b):
|
||||
return colormath.color_objects.sRGBColor(r, g, b, is_upscaled=True)
|
||||
return RGB(r, g, b, is_upscaled=True)
|
||||
|
||||
|
||||
# Palette RGB values taken from BMP2DHGR's default NTSC palette
|
||||
# TODO: support other palettes as well, e.g. //gs RGB
|
||||
palette = {
|
||||
DHGRColours.BLACK: rgb(0, 0, 0),
|
||||
DHGRColours.MAGENTA: rgb(148, 12, 125),
|
||||
DHGRColours.BROWN: rgb(99, 77, 0),
|
||||
DHGRColours.ORANGE: rgb(249, 86, 29),
|
||||
DHGRColours.DARK_GREEN: rgb(51, 111, 0),
|
||||
DHGRColours.GREY1: rgb(126, 126, 126),
|
||||
DHGRColours.GREEN: rgb(67, 200, 0),
|
||||
DHGRColours.YELLOW: rgb(221, 206, 23),
|
||||
DHGRColours.DARK_BLUE: rgb(32, 54, 212),
|
||||
DHGRColours.VIOLET: rgb(188, 55, 255),
|
||||
DHGRColours.GREY2: rgb(126, 126, 126),
|
||||
DHGRColours.PINK: rgb(255, 129, 236),
|
||||
DHGRColours.MED_BLUE: rgb(7, 168, 225),
|
||||
DHGRColours.LIGHT_BLUE: rgb(158, 172, 255),
|
||||
DHGRColours.AQUA: rgb(93, 248, 133),
|
||||
DHGRColours.WHITE: rgb(255, 255, 255)
|
||||
}
|
||||
class Palette(enum.Enum):
|
||||
"""BMP2DHR palette numbers"""
|
||||
UNKNOWN = -1
|
||||
IIGS = 0
|
||||
NTSC = 5
|
||||
|
||||
|
||||
def compute_diff_matrix():
|
||||
# Compute matrix of CIE2000 delta values for this palette, representing
|
||||
# perceptual distance between colours.
|
||||
dm = np.ndarray(shape=(16, 16), dtype=np.int)
|
||||
class BasePalette:
|
||||
ID = Palette.UNKNOWN # type: Palette
|
||||
|
||||
for colour1, a in palette.items():
|
||||
alab = colormath.color_conversions.convert_color(
|
||||
a, colormath.color_objects.LabColor)
|
||||
for colour2, b in palette.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
|
||||
# Palette RGB map
|
||||
RGB = {} # type: Dict[DHGRColours: RGB]
|
||||
|
||||
|
||||
diff_matrix = compute_diff_matrix()
|
||||
class NTSCPalette(BasePalette):
|
||||
ID = Palette.NTSC
|
||||
|
||||
# Palette RGB values taken from BMP2DHGR's default NTSC palette
|
||||
RGB = {
|
||||
DHGRColours.BLACK: rgb(0, 0, 0),
|
||||
DHGRColours.MAGENTA: rgb(148, 12, 125),
|
||||
DHGRColours.BROWN: rgb(99, 77, 0),
|
||||
DHGRColours.ORANGE: rgb(249, 86, 29),
|
||||
DHGRColours.DARK_GREEN: rgb(51, 111, 0),
|
||||
DHGRColours.GREY1: rgb(126, 126, 126),
|
||||
DHGRColours.GREEN: rgb(67, 200, 0),
|
||||
DHGRColours.YELLOW: rgb(221, 206, 23),
|
||||
DHGRColours.DARK_BLUE: rgb(32, 54, 212),
|
||||
DHGRColours.VIOLET: rgb(188, 55, 255),
|
||||
DHGRColours.GREY2: rgb(126, 126, 126),
|
||||
DHGRColours.PINK: rgb(255, 129, 236),
|
||||
DHGRColours.MED_BLUE: rgb(7, 168, 225),
|
||||
DHGRColours.LIGHT_BLUE: rgb(158, 172, 255),
|
||||
DHGRColours.AQUA: rgb(93, 248, 133),
|
||||
DHGRColours.WHITE: rgb(255, 255, 255)
|
||||
}
|
||||
|
||||
|
||||
class IIGSPalette(BasePalette):
|
||||
ID = Palette.IIGS
|
||||
|
||||
# Palette RGB values taken from BMP2DHGR's KEGS32 palette
|
||||
RGB = {
|
||||
DHGRColours.BLACK: rgb(0, 0, 0),
|
||||
DHGRColours.MAGENTA: rgb(221, 0, 51),
|
||||
DHGRColours.BROWN: rgb(136, 85, 34),
|
||||
DHGRColours.ORANGE: rgb(255, 102, 0),
|
||||
DHGRColours.DARK_GREEN: rgb(0, 119, 0),
|
||||
DHGRColours.GREY1: rgb(85, 85, 85),
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
PALETTES = {
|
||||
Palette.IIGS: IIGSPalette,
|
||||
Palette.NTSC: NTSCPalette
|
||||
} # type: Dict[Palette, Type[BasePalette]]
|
||||
|
@ -3,9 +3,10 @@
|
||||
import bz2
|
||||
import functools
|
||||
import pickle
|
||||
from typing import Union
|
||||
from typing import Union, List
|
||||
|
||||
import numpy as np
|
||||
import palette
|
||||
|
||||
# Type annotation for cases where we may process either an int or a numpy array.
|
||||
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
|
||||
BYTE_SHIFTS = [0, 4, 12, 20]
|
||||
|
||||
# Load edit distance matrices for masked, shifted byte 0..3 values
|
||||
# TODO: should go somewhere else since we don't use it here at all
|
||||
with bz2.open("transcoder/edit_distance.pickle.bz2", "rb") as ed:
|
||||
edit_distances = pickle.load(ed)
|
||||
@staticmethod
|
||||
@functools.lru_cache(None)
|
||||
def edit_distances(palette_id: palette.Palette) -> List[np.ndarray]:
|
||||
"""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):
|
||||
self.main_memory = main_memory
|
||||
|
@ -10,6 +10,7 @@ import numpy as np
|
||||
import opcodes
|
||||
import screen
|
||||
from frame_grabber import FrameGrabber
|
||||
from palette import Palette
|
||||
from video_mode import VideoMode
|
||||
|
||||
|
||||
@ -21,13 +22,17 @@ class Video:
|
||||
def __init__(
|
||||
self,
|
||||
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.frame_grabber = frame_grabber # type: FrameGrabber
|
||||
self.ticks_per_second = float(ticks_per_second) # type: float
|
||||
self.ticks_per_frame = (
|
||||
self.ticks_per_second / self.input_frame_rate) # type: float
|
||||
self.frame_number = 0 # type: int
|
||||
self.palette = palette # type: Palette
|
||||
|
||||
# Initialize empty screen
|
||||
self.memory_map = screen.MemoryMap(
|
||||
@ -213,8 +218,8 @@ class Video:
|
||||
heapq.heapify(priorities)
|
||||
return priorities
|
||||
|
||||
@staticmethod
|
||||
def _diff_weights(
|
||||
self,
|
||||
source: screen.DHGRBitmap,
|
||||
target: screen.DHGRBitmap,
|
||||
is_aux: bool
|
||||
@ -228,14 +233,16 @@ class Video:
|
||||
|
||||
# Concatenate 8-bit source and target into 16-bit values
|
||||
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
|
||||
source_pixels2 = source.mask_and_shift_data(source.packed, 2)
|
||||
target_pixels2 = target.mask_and_shift_data(target.packed, 2)
|
||||
# Concatenate 12-bit source and target into 24-bit values
|
||||
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[:, 1::2] = dist2
|
||||
@ -245,13 +252,15 @@ class Video:
|
||||
source_pixels1 = source.mask_and_shift_data(source.packed, 1)
|
||||
target_pixels1 = target.mask_and_shift_data(target.packed, 1)
|
||||
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
|
||||
source_pixels3 = source.mask_and_shift_data(source.packed, 3)
|
||||
target_pixels3 = target.mask_and_shift_data(target.packed, 3)
|
||||
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[:, 1::2] = dist3
|
||||
@ -278,12 +287,12 @@ class Video:
|
||||
else:
|
||||
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
|
||||
|
||||
@staticmethod
|
||||
def _compute_delta(
|
||||
self,
|
||||
content: int,
|
||||
target: screen.DHGRBitmap,
|
||||
old,
|
||||
@ -301,7 +310,8 @@ class Video:
|
||||
|
||||
# Concatenate 8-bit source and target into 16-bit values
|
||||
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
|
||||
source_pixels2 = target.mask_and_shift_data(
|
||||
@ -309,7 +319,8 @@ class Video:
|
||||
target_pixels2 = target.mask_and_shift_data(target.packed, 2)
|
||||
# Concatenate 12-bit source and target into 24-bit values
|
||||
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[:, 1::2] = dist2
|
||||
@ -320,14 +331,16 @@ class Video:
|
||||
target.masked_update(1, target.packed, content), 1)
|
||||
target_pixels1 = target.mask_and_shift_data(target.packed, 1)
|
||||
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
|
||||
source_pixels3 = target.mask_and_shift_data(
|
||||
target.masked_update(3, target.packed, content), 3)
|
||||
target_pixels3 = target.mask_and_shift_data(target.packed, 3)
|
||||
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[:, 1::2] = dist3
|
||||
|
@ -3,6 +3,7 @@
|
||||
import unittest
|
||||
|
||||
import frame_grabber
|
||||
import palette
|
||||
import screen
|
||||
import video
|
||||
import video_mode
|
||||
@ -27,11 +28,13 @@ class TestVideo(unittest.TestCase):
|
||||
|
||||
diff = v._diff_weights(v.pixelmap, target_pixelmap, is_aux=True)
|
||||
|
||||
pal = palette.NTSCPalette
|
||||
|
||||
# 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
|
||||
expect2 = target_pixelmap.edit_distances[2][0b000101010100]
|
||||
expect2 = target_pixelmap.edit_distances(pal.ID)[2][0b000101010100]
|
||||
|
||||
self.assertEqual(expect0, diff[0, 0])
|
||||
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)
|
||||
|
||||
# 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
|
||||
expect2 = target_pixelmap.edit_distances[2][0b0000101010100000011011000]
|
||||
expect2 = target_pixelmap.edit_distances(pal.ID)[2][
|
||||
0b0000101010100000011011000]
|
||||
|
||||
self.assertEqual(expect0, diff[0, 0])
|
||||
self.assertEqual(expect2, diff[0, 1])
|
||||
|
Loading…
Reference in New Issue
Block a user