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:
kris 2019-06-15 21:02:00 +01:00
parent 696eb61bf4
commit edefe649f4
11 changed files with 236 additions and 156 deletions

Binary file not shown.

View File

@ -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
]) ])

View File

@ -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:

View File

@ -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__":

View File

@ -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)
) )

View File

@ -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

View File

@ -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]]

View File

@ -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

View File

@ -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

View File

@ -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])