diff --git a/palette.py b/palette.py index 0f32de3..64cdd1a 100644 --- a/palette.py +++ b/palette.py @@ -7,17 +7,40 @@ import palette_ntsc class Palette: - SRGB = None - RGB = {} - CAM16UCS = {} - - # How many successive screen pixels are used to compute output pixel + # How many successive screen pixels are used to compute output pixel # palette index. PALETTE_DEPTH = None + # These next three dictionaries are all indexed by a tuple of (n-bit pixel + # value, NTSC phase), where: + # n == PALETTE_DEPTH + # MSB of the pixel value represents the current pixel on/off state + # LSB of the pixel value is the on/off state of the pixel n-1 positions + # to the left of current + # NTSC phase = 0 .. 3 (= x position % 4) + # + # The choice of LSB --> MSB increasing from left to right across the + # screen matches the ordering used by the mapping of double hi-res memory + # to screen pixels. + # + # Dictionary values are the colour of the corresponding pixel in various + # colour spaces. + + # Values are pixel colour in sRGB colour space. Palettes are defined in + # this colour space. + SRGB = None + + # Values are pixel colour in (linear) RGB colour space. Dithering is + # performed in this colour space. + RGB = {} + + # Values are pixel colour in CAM16-UCS colour space. This is used for + # computing perceptual differences between colour values when optimizing + # the image dithering. + CAM16UCS = {} + def __init__(self): self.RGB = {} - # XXX RGB and CAM16UCS should be indexed by (pixels_nbit, phase) for k, v in self.SRGB.items(): self.RGB[k] = (np.clip(image.srgb_to_linear_array(v / 255), 0.0, 1.0) * 255).astype(np.uint8) @@ -25,15 +48,30 @@ class Palette: self.CAM16UCS[k] = colour.convert( v / 255, "sRGB", "CAM16UCS").astype(np.float32) - def _pixel_phase_shifts(self, phase_0_rgb): - rgb_phases = {} - for pixels, rgb in phase_0_rgb.items(): - rgb_phases[pixels, 0] = rgb - for phase in range(1, 4): - msb = pixels & (1 << (self.PALETTE_DEPTH - 1)) - pixels <<= 1 | (msb >> (self.PALETTE_DEPTH - 1)) - rgb_phases[pixels, phase] = rgb - return rgb_phases + @staticmethod + def _pixel_phase_shifts(phase_3_srgb): + """Constructs dictionary of 4-bit pixel sequences for each NTSC phase. + Assumes PALETTE_DEPTH == 3 + + Args: + phase_3_rgb: dict mapping 4-bit pixel sequence to sRGB values, + for NTSC phase 3. + + Returns: + dict mapping (shifted 4-bit pixel sequence, phase 0..3) to sRGB + values + """ + srgb_phases = {} + for pixels, srgb in phase_3_srgb.items(): + srgb_phases[pixels, 3] = srgb + # Rotate to compute 4-bit pixel sequences that produce the same + # colour for NTSC phases 0..2 + for phase in range(0, 3): + lsb = pixels & 1 + pixels >>= 1 + pixels |= lsb << 3 + srgb_phases[pixels, phase] = srgb + return srgb_phases def bitmap_to_idx(self, pixels: np.array) -> int: """Converts a bitmap of pixels into integer representation. @@ -51,7 +89,7 @@ class Palette: # order to screen representation (i.e. LSB is the left-most # screen pixel), so we need to flip the order np.flip(pixels, axis=0) - )[0] + )[0] >> (8 - pixels.shape[0]) class ToHgrPalette(Palette): @@ -59,7 +97,7 @@ class ToHgrPalette(Palette): PALETTE_DEPTH = 4 # Default tohgr/bmp2dhr palette - SRGB = { + SRGB = Palette._pixel_phase_shifts({ 0: np.array((0, 0, 0)), # Black 8: np.array((148, 12, 125)), # Magenta 4: np.array((99, 77, 0)), # Brown @@ -76,7 +114,7 @@ class ToHgrPalette(Palette): 11: np.array((158, 172, 255)), # Light blue 7: np.array((93, 248, 133)), # Aqua 15: np.array((255, 255, 255)), # White - } + }) class OpenEmulatorPalette(Palette): @@ -84,7 +122,7 @@ class OpenEmulatorPalette(Palette): PALETTE_DEPTH = 4 # OpenEmulator - SRGB = { + SRGB = Palette._pixel_phase_shifts({ 0: np.array((0, 0, 0)), # Black 8: np.array((203, 0, 121)), # Magenta 4: np.array((99, 103, 0)), # Brown @@ -101,14 +139,14 @@ class OpenEmulatorPalette(Palette): 11: np.array((160, 156, 244)), # Light blue 7: np.array((25, 243, 136)), # Aqua 15: np.array((244, 247, 244)), # White - } + }) class VirtualIIPalette(Palette): """4-bit palette exactly matching Virtual II emulator output.""" PALETTE_DEPTH = 4 - SRGB = { + SRGB = Palette._pixel_phase_shifts({ 0: np.array((0, 0, 0)), # Black 8: np.array((231, 36, 66)), # Magenta 4: np.array((154, 104, 0)), # Brown @@ -125,7 +163,7 @@ class VirtualIIPalette(Palette): 11: np.array((120, 187, 255)), # Light blue 7: np.array((83, 250, 208)), # Aqua 15: np.array((255, 255, 255)), # White - } + }) class NTSCPalette(Palette):