Update comments and fix some bugs

make_edit_distance
- use MASKED_DOTS since it does not have a simple relationship to the
  HEADER_BITS/BODY_BITS for HGR
- try disabling transposition distances for Damerau-Levenshtein, this
  may give better quality

screen
- introduce separate notion of MASKED_DOTS which is the number of
  (coloured) pixels we can extract from MASKED_BITS.  For HGR this is
  not the same.
- fix bug in _fix_array_neighbours that was not fixing headers for HGR
- don't cache everything in byte_pair_differences, it's effectively
  unbounded.  Using 1M for LRU size seems to work just as well in
  practise, without leaking memory.
- fix bug in _diff_weights when comparing content, we want to evaluate
  the effect of storing content byte in each offset separately, not
  cumulatively.
- add a consistency check function (not currently wired up) to assert
  that headers/footers are in sync across columns
- HGR should have 16 body bits, this was causing headers not to
  propagate correctly to/from neighbouring column
- add test case for this bug

video
- Use 8 random bits consistently, using 16 in some places may have
  introduced bias
- ignore palette bit when comparing 0x00 and 0x7f in sanity check
This commit is contained in:
kris
2019-07-11 23:40:00 +01:00
parent 722e9c5d70
commit f3d03a1b87
13 changed files with 623 additions and 403 deletions

View File

@ -15,62 +15,6 @@ import colours
import palette
import screen
# The DHGR display encodes 7 pixels across interleaved 4-byte sequences
# of AUX and MAIN memory, as follows:
#
# PBBBAAAA PDDCCCCB PFEEEEDD PGGGGFFF
# Aux N Main N Aux N+1 Main N+1 (N even)
#
# Where A..G are the pixels, and P represents the (unused) palette bit.
#
# This layout makes more sense when written as a (little-endian) 32-bit integer:
#
# 33222222222211111111110000000000 <- bit pos in uint32
# 10987654321098765432109876543210
# PGGGGFFFPFEEEEDDPDDCCCCBPBBBAAAA
#
# i.e. apart from the palette bits this is a linear ordering of pixels,
# when read from LSB to MSB (i.e. right-to-left). i.e. the screen layout order
# of bits is opposite to the usual binary representation ordering.
#
# If we now look at the effect of storing a byte in each of the 4
# byte-offset positions within this uint32,
#
# PGGGGFFFPFEEEEDDPDDCCCCBPBBBAAAA
# 33333333222222221111111100000000
#
# We see that these byte offsets cause changes to the following pixels:
#
# 0: A B
# 1: B C D
# 2: D E F
# 3: F G
#
# i.e. DHGR byte stores to offsets 0 and 3 result in changing one 8-bit value
# (2 DHGR pixels) into another; offsets 1 and 3 result in changing one 12-bit
# value (3 DHGR pixels).
#
# We can simplify things by stripping out the palette bit and packing
# down to a 28-bit integer representation:
#
# 33222222222211111111110000000000 <- bit pos in uint32
# 10987654321098765432109876543210
#
# 0000GGGGFFFFEEEEDDDDCCCCBBBBAAAA <- pixel A..G
# 3210321032103210321032103210 <- bit pos in A..G pixel
#
# 3333333222222211111110000000 <- byte offset 0.3
#
# With this representation, we can precompute an edit distance for the
# pixel changes resulting from all possible DHGR byte stores.
#
# We further encode these (source, target) -> distance mappings by
# concatenating source and target into 16- or 24-bit values. This is
# efficient to work with in the video transcoder.
#
# Since we are enumerating all such 16- or 24-bit values, these can be packed
# contiguously into an array whose index is the (source, target) pair and
# the value is the edit distance.
PIXEL_CHARS = "0123456789ABCDEF"
@ -85,6 +29,8 @@ def pixel_string(pixels: Iterable[int]) -> str:
class EditDistanceParams:
"""Data class for parameters to Damerau-Levenshtein edit distance."""
# 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
@ -92,20 +38,26 @@ class EditDistanceParams:
# 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
# TODO: is quality really better allowing transposes?
transpose_costs = np.ones((128, 128), dtype=np.float64) * 100000 # 10
# These will be filled in later
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.
#
# TODO: currently unused
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.
"""Compute matrix of perceptual distance between colour pairs.
Specifically CIE2000 delta values for this palette.
"""
dm = np.ndarray(shape=(16, 16), dtype=np.int)
for colour1, a in pal.RGB.items():
@ -120,6 +72,8 @@ def compute_diff_matrix(pal: Type[palette.BasePalette]):
def compute_substitute_costs(pal: Type[palette.BasePalette]):
"""Compute costs for substituting one colour pixel for another."""
edp = EditDistanceParams()
diff_matrix = compute_diff_matrix(pal)
@ -128,10 +82,10 @@ def compute_substitute_costs(pal: Type[palette.BasePalette]):
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
edp.substitute_costs[(ord(c), ord(d))] = cost
edp.substitute_costs[(ord(d), ord(c))] = cost
edp.error_substitute_costs[(ord(c), ord(d))] = 5 * cost
edp.error_substitute_costs[(ord(d), ord(c))] = 5 * cost
return edp
@ -141,6 +95,7 @@ def edit_distance(
a: str,
b: str,
error: bool) -> np.float64:
"""Damerau-Levenshtein edit distance between two pixel strings."""
res = weighted_levenshtein.dam_lev(
a, b,
insert_costs=edp.insert_costs,
@ -149,7 +104,8 @@ def edit_distance(
edp.error_substitute_costs if error else edp.substitute_costs),
)
assert res == 0 or (1 <= res < 2 ** 16), res
# Make sure result can fit in a uint16
assert (0 <= res < 2 ** 16), res
return res
@ -158,6 +114,19 @@ def compute_edit_distance(
bitmap_cls: Type[screen.Bitmap],
nominal_colours: Type[colours.NominalColours]
):
"""Computes edit distance matrix between all pairs of pixel strings.
Enumerates all possible values of the masked bit representation from
bitmap_cls (assuming it is contiguous, i.e. we enumerate all
2**bitmap_cls.MASKED_BITS values). These are mapped to the dot
representation, turned into coloured pixel strings, and we compute the
edit distance.
The effect of this is that we precompute the effect of storing all possible
byte values against all possible screen backgrounds (e.g. as
influencing/influenced by neighbouring bytes).
"""
bits = bitmap_cls.MASKED_BITS
bitrange = np.uint64(2 ** bits)
@ -171,7 +140,7 @@ def compute_edit_distance(
# triangle
bar = ProgressBar((bitrange * (bitrange - 1)) / 2, max_width=80)
num_dots = bitmap_cls.HEADER_BITS + bitmap_cls.BODY_BITS
num_dots = bitmap_cls.MASKED_DOTS
cnt = 0
for i in range(np.uint64(bitrange)):
@ -211,6 +180,8 @@ def make_edit_distance(
bitmap_cls: Type[screen.Bitmap],
nominal_colours: Type[colours.NominalColours]
):
"""Write file containing (D)HGR edit distance matrix for a palette."""
dist = compute_edit_distance(edp, bitmap_cls, nominal_colours)
data = "transcoder/data/%s_palette_%d_edit_distance.pickle.bz2" % (
bitmap_cls.NAME, pal.ID.value)
@ -223,7 +194,7 @@ def main():
print("Processing palette %s" % p)
edp = compute_substitute_costs(p)
# TODO: error distance matrices
# TODO: still worth using error distance matrices?
make_edit_distance(p, edp, screen.HGRBitmap, colours.HGRColours)
make_edit_distance(p, edp, screen.DHGRBitmap, colours.DHGRColours)