mirror of
https://github.com/KrisKennaway/ii-pix.git
synced 2025-03-11 07:30:56 +00:00
Refactor a bit
This commit is contained in:
parent
0109675bf2
commit
31b565f22a
304
dither.py
304
dither.py
@ -1,294 +1,21 @@
|
||||
import argparse
|
||||
import bz2
|
||||
import functools
|
||||
import os.path
|
||||
import pickle
|
||||
import time
|
||||
from typing import Tuple
|
||||
|
||||
from PIL import Image
|
||||
import numpy as np
|
||||
|
||||
import dither_apply
|
||||
import dither_pattern
|
||||
import image as image_py
|
||||
import palette as palette_py
|
||||
import screen as screen_py
|
||||
|
||||
|
||||
# TODO:
|
||||
# - only lookahead for 560px
|
||||
# - palette class
|
||||
# - compare to bmp2dhr and a2bestpix
|
||||
|
||||
def srgb_to_linear_array(a: np.ndarray, gamma=2.4) -> np.ndarray:
|
||||
return np.where(a <= 0.04045, a / 12.92, ((a + 0.055) / 1.055) ** gamma)
|
||||
|
||||
|
||||
def linear_to_srgb_array(a: np.ndarray, gamma=2.4) -> np.ndarray:
|
||||
return np.where(a <= 0.0031308, a * 12.92, 1.055 * a ** (1.0 / gamma) -
|
||||
0.055)
|
||||
|
||||
|
||||
def srgb_to_linear(im: np.ndarray) -> np.ndarray:
|
||||
rgb_linear = srgb_to_linear_array(im / 255.0, gamma=2.4)
|
||||
return (np.clip(rgb_linear, 0.0, 1.0) * 255).astype(np.float32)
|
||||
|
||||
|
||||
def linear_to_srgb(im: np.ndarray) -> np.ndarray:
|
||||
srgb = linear_to_srgb_array(im / 255.0, gamma=2.4)
|
||||
return (np.clip(srgb, 0.0, 1.0) * 255).astype(np.float32)
|
||||
|
||||
|
||||
# Default bmp2dhr palette
|
||||
RGB = {
|
||||
0: np.array((0, 0, 0)), # Black
|
||||
8: np.array((148, 12, 125)), # Magenta
|
||||
4: np.array((99, 77, 0)), # Brown
|
||||
12: np.array((249, 86, 29)), # Orange
|
||||
2: np.array((51, 111, 0)), # Dark green
|
||||
10: np.array((126, 126, 125)), # Grey2
|
||||
6: np.array((67, 200, 0)), # Green
|
||||
14: np.array((221, 206, 23)), # Yellow
|
||||
1: np.array((32, 54, 212)), # Dark blue
|
||||
9: np.array((188, 55, 255)), # Violet
|
||||
5: np.array((126, 126, 126)), # Grey1
|
||||
13: np.array((255, 129, 236)), # Pink
|
||||
3: np.array((7, 168, 225)), # Med blue
|
||||
11: np.array((158, 172, 255)), # Light blue
|
||||
7: np.array((93, 248, 133)), # Aqua
|
||||
15: np.array((255, 255, 255)), # White
|
||||
}
|
||||
|
||||
# Maps palette values to screen dots. Note that these are the same as
|
||||
# the binary values in reverse order.
|
||||
DOTS = {
|
||||
0: (False, False, False, False),
|
||||
1: (True, False, False, False),
|
||||
2: (False, True, False, False),
|
||||
3: (True, True, False, False),
|
||||
4: (False, False, True, False),
|
||||
5: (True, False, True, False),
|
||||
6: (False, True, True, False),
|
||||
7: (True, True, True, False),
|
||||
8: (False, False, False, True),
|
||||
9: (True, False, False, True),
|
||||
10: (False, True, False, True),
|
||||
11: (True, True, False, True),
|
||||
12: (False, False, True, True),
|
||||
13: (True, False, True, True),
|
||||
14: (False, True, True, True),
|
||||
15: (True, True, True, True)
|
||||
}
|
||||
DOTS_TO_4BIT = {}
|
||||
for k, v in DOTS.items():
|
||||
DOTS_TO_4BIT[v] = k
|
||||
|
||||
# OpenEmulator
|
||||
sRGB = {
|
||||
0: np.array((0, 0, 0)), # Black
|
||||
8: np.array((206, 0, 123)), # Magenta
|
||||
4: np.array((100, 105, 0)), # Brown
|
||||
12: np.array((247, 79, 0)), # Orange
|
||||
2: np.array((0, 153, 0)), # Dark green
|
||||
# XXX RGB values are used as keys in DOTS dict, need to be unique
|
||||
10: np.array((131, 132, 132)), # Grey2
|
||||
6: np.array((0, 242, 0)), # Green
|
||||
14: np.array((216, 220, 0)), # Yellow
|
||||
1: np.array((21, 0, 248)), # Dark blue
|
||||
9: np.array((235, 0, 242)), # Violet
|
||||
5: np.array((140, 140, 140)), # Grey1 # XXX
|
||||
13: np.array((244, 104, 240)), # Pink
|
||||
3: np.array((0, 181, 248)), # Med blue
|
||||
11: np.array((160, 156, 249)), # Light blue
|
||||
7: np.array((21, 241, 132)), # Aqua
|
||||
15: np.array((244, 247, 244)), # White
|
||||
}
|
||||
|
||||
# # Virtual II (sRGB)
|
||||
# sRGB = {
|
||||
# (False, False, False, False): np.array((0, 0, 0)), # Black
|
||||
# (False, False, False, True): np.array((231,36,66)), # Magenta
|
||||
# (False, False, True, False): np.array((154,104,0)), # Brown
|
||||
# (False, False, True, True): np.array((255,124,0)), # Orange
|
||||
# (False, True, False, False): np.array((0,135,45)), # Dark green
|
||||
# (False, True, False, True): np.array((104,104,104)), # Grey2 XXX
|
||||
# (False, True, True, False): np.array((0,222,0)), # Green
|
||||
# (False, True, True, True): np.array((255,252,0)), # Yellow
|
||||
# (True, False, False, False): np.array((1,30,169)), # Dark blue
|
||||
# (True, False, False, True): np.array((230,73,228)), # Violet
|
||||
# (True, False, True, False): np.array((185,185,185)), # Grey1 XXX
|
||||
# (True, False, True, True): np.array((255,171,153)), # Pink
|
||||
# (True, True, False, False): np.array((47,69,255)), # Med blue
|
||||
# (True, True, False, True): np.array((120,187,255)), # Light blue
|
||||
# (True, True, True, False): np.array((83,250,208)), # Aqua
|
||||
# (True, True, True, True): np.array((255, 255, 255)), # White
|
||||
# }
|
||||
RGB = {}
|
||||
for k, v in sRGB.items():
|
||||
RGB[k] = (np.clip(srgb_to_linear_array(v / 255), 0.0, 1.0) * 255).astype(
|
||||
np.uint8)
|
||||
|
||||
|
||||
class ColourDistance:
|
||||
@staticmethod
|
||||
def distance(rgb1: np.ndarray, rgb2: np.ndarray) -> np.ndarray:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class CIE2000Distance(ColourDistance):
|
||||
"""CIE2000 delta-E distance."""
|
||||
|
||||
def __init__(self):
|
||||
self._distances = np.memmap("distances.npy", mode="r+",
|
||||
dtype=np.uint8, shape=(16777216, 16))
|
||||
|
||||
|
||||
class Screen:
|
||||
X_RES = None
|
||||
Y_RES = None
|
||||
X_PIXEL_WIDTH = None
|
||||
|
||||
def __init__(self):
|
||||
self.main = np.zeros(8192, dtype=np.uint8)
|
||||
self.aux = np.zeros(8192, dtype=np.uint8)
|
||||
|
||||
@staticmethod
|
||||
def y_to_base_addr(y: int) -> int:
|
||||
"""Maps y coordinate to screen memory base address."""
|
||||
a = y // 64
|
||||
d = y - 64 * a
|
||||
b = d // 8
|
||||
c = d - 8 * b
|
||||
|
||||
return 1024 * c + 128 * b + 40 * a
|
||||
|
||||
def _image_to_bitmap(self, image: np.ndarray) -> np.ndarray:
|
||||
raise NotImplementedError
|
||||
|
||||
def pack(self, image: np.ndarray):
|
||||
bitmap = self._image_to_bitmap(image)
|
||||
# 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)
|
||||
main_col = np.zeros(
|
||||
(self.Y_RES, self.X_RES * self.X_PIXEL_WIDTH // 14), dtype=np.uint8)
|
||||
aux_col = np.zeros(
|
||||
(self.Y_RES, self.X_RES * self.X_PIXEL_WIDTH // 14), dtype=np.uint8)
|
||||
for byte_offset in range(80):
|
||||
column = np.zeros(self.Y_RES, dtype=np.uint8)
|
||||
for bit in range(7):
|
||||
column |= (bitmap[:, 7 * byte_offset + bit].astype(
|
||||
np.uint8) << bit)
|
||||
if byte_offset % 2 == 0:
|
||||
aux_col[:, byte_offset // 2] = column
|
||||
else:
|
||||
main_col[:, (byte_offset - 1) // 2] = column
|
||||
|
||||
for y in range(self.Y_RES):
|
||||
addr = self.y_to_base_addr(y)
|
||||
self.aux[addr:addr + 40] = aux_col[y, :]
|
||||
self.main[addr:addr + 40] = main_col[y, :]
|
||||
|
||||
@staticmethod
|
||||
def pixel_palette_options(last_pixel, x: int):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class DHGR140Screen(Screen):
|
||||
"""DHGR screen ignoring colour fringing, i.e. treating as 140x192x16."""
|
||||
|
||||
X_RES = 140
|
||||
Y_RES = 192
|
||||
X_PIXEL_WIDTH = 4
|
||||
|
||||
def _image_to_bitmap(self, image_4bit: np.ndarray) -> np.ndarray:
|
||||
bitmap = np.zeros(
|
||||
(self.Y_RES, self.X_RES * self.X_PIXEL_WIDTH), dtype=np.bool)
|
||||
for y in range(self.Y_RES):
|
||||
for x in range(self.X_RES):
|
||||
pixel = image_4bit[y, x].item()
|
||||
dots = DOTS[pixel]
|
||||
bitmap[y, x * self.X_PIXEL_WIDTH:(
|
||||
(x + 1) * self.X_PIXEL_WIDTH)] = dots
|
||||
return bitmap
|
||||
|
||||
@staticmethod
|
||||
def pixel_palette_options(last_pixel_4bit, x: int):
|
||||
return np.array(list(RGB.keys())), np.array(list(RGB.values()))
|
||||
|
||||
|
||||
class DHGR560Screen(Screen):
|
||||
"""DHGR screen including colour fringing."""
|
||||
X_RES = 560
|
||||
Y_RES = 192
|
||||
X_PIXEL_WIDTH = 1
|
||||
|
||||
def _image_to_bitmap(self, image_4bit: np.ndarray) -> np.ndarray:
|
||||
bitmap = np.zeros((self.Y_RES, self.X_RES), dtype=np.bool)
|
||||
for y in range(self.Y_RES):
|
||||
for x in range(self.X_RES):
|
||||
pixel = image_4bit[y, x]
|
||||
dots = DOTS[pixel]
|
||||
phase = x % 4
|
||||
bitmap[y, x] = dots[phase]
|
||||
return bitmap
|
||||
|
||||
@staticmethod
|
||||
def pixel_palette_options(last_pixel_4bit, x: int):
|
||||
last_dots = DOTS[last_pixel_4bit]
|
||||
other_dots = list(last_dots)
|
||||
other_dots[x % 4] = not other_dots[x % 4]
|
||||
other_dots = tuple(other_dots)
|
||||
other_pixel_4bit = DOTS_TO_4BIT[other_dots]
|
||||
return (
|
||||
np.array([last_pixel_4bit, other_pixel_4bit]),
|
||||
np.array([RGB[last_pixel_4bit], RGB[other_pixel_4bit]]))
|
||||
|
||||
|
||||
class Dither:
|
||||
PATTERN = None
|
||||
ORIGIN = None
|
||||
|
||||
|
||||
class FloydSteinbergDither(Dither):
|
||||
# 0 * 7
|
||||
# 3 5 1
|
||||
PATTERN = np.array(((0, 0, 7), (3, 5, 1)),
|
||||
dtype=np.float32).reshape(2, 3, 1) / np.float(16)
|
||||
# XXX X_ORIGIN since ORIGIN[0] == 0
|
||||
ORIGIN = (0, 1)
|
||||
|
||||
|
||||
class BuckelsDither(Dither):
|
||||
# 0 * 2 1
|
||||
# 1 2 1 0
|
||||
# 0 1 0 0
|
||||
PATTERN = np.array(((0, 0, 2, 1), (1, 2, 1, 0), (0, 1, 0, 0)),
|
||||
dtype=np.float32).reshape(3, 4, 1) / np.float32(8)
|
||||
ORIGIN = (0, 1)
|
||||
|
||||
|
||||
class JarvisDither(Dither):
|
||||
# 0 0 X 7 5
|
||||
# 3 5 7 5 3
|
||||
# 1 3 5 3 1
|
||||
PATTERN = np.array(((0, 0, 0, 7, 5), (3, 5, 7, 5, 3), (1, 3, 5, 3, 1)),
|
||||
dtype=np.float32).reshape(3, 5, 1) / np.float32(48)
|
||||
ORIGIN = (0, 2)
|
||||
|
||||
|
||||
def open_image(screen: Screen, filename: str) -> np.ndarray:
|
||||
im = Image.open(filename)
|
||||
# TODO: convert to sRGB colour profile explicitly, in case it has some other
|
||||
# profile already.
|
||||
if im.mode != "RGB":
|
||||
im = im.convert("RGB")
|
||||
|
||||
# Convert to linear RGB before rescaling so that colour interpolation is
|
||||
# in linear space
|
||||
linear = srgb_to_linear(np.asarray(im)).astype(np.uint8)
|
||||
rescaled = Image.fromarray(linear).resize(
|
||||
(screen.X_RES, screen.Y_RES), Image.LANCZOS)
|
||||
# XXX work with malloc'ed array?
|
||||
return np.array(rescaled).astype(np.float32)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
@ -300,26 +27,25 @@ def main():
|
||||
"artifacts."))
|
||||
args = parser.parse_args()
|
||||
|
||||
palette = palette_py.Palette()
|
||||
# screen = DHGR140Screen()
|
||||
screen = DHGR560Screen()
|
||||
screen = screen_py.DHGR560Screen(palette)
|
||||
|
||||
image = open_image(screen, args.input)
|
||||
image = image_py.open(screen.X_RES, screen.Y_RES, args.input)
|
||||
# image_rgb.show()
|
||||
|
||||
# dither = FloydSteinbergDither()
|
||||
# dither = BuckelsDither()
|
||||
dither = JarvisDither()
|
||||
|
||||
differ = CIE2000Distance()
|
||||
# dither = dither_pattern.FloydSteinbergDither()
|
||||
# dither = dither_pattern.BuckelsDither()
|
||||
dither = dither_pattern.JarvisDither()
|
||||
|
||||
start = time.time()
|
||||
output_4bit, output_rgb = dither_apply.dither_image(screen, image, dither,
|
||||
differ,
|
||||
lookahead=args.lookahead)
|
||||
output_4bit, output_rgb = dither_apply.dither_image(
|
||||
screen, image, dither, lookahead=args.lookahead)
|
||||
print(time.time() - start)
|
||||
screen.pack(output_4bit)
|
||||
|
||||
out_image = Image.fromarray(linear_to_srgb(output_rgb).astype(np.uint8))
|
||||
out_image = Image.fromarray(image_py.linear_to_srgb(output_rgb).astype(
|
||||
np.uint8))
|
||||
outfile = os.path.join(os.path.splitext(args.output)[0] + ".png")
|
||||
out_image.save(outfile, "PNG")
|
||||
out_image.show(title=outfile)
|
||||
|
@ -81,7 +81,7 @@ cdef int dither_bounds_yb(float [:, :, ::1] pattern, int y_origin, int y_res, in
|
||||
@cython.boundscheck(False)
|
||||
@cython.wraparound(False)
|
||||
def dither_lookahead(
|
||||
screen, float[:,:,::1] image_rgb, dither, differ, int x, int y, char[:, ::1] options_4bit,
|
||||
screen, float[:,:,::1] image_rgb, dither, int x, int y, char[:, ::1] options_4bit,
|
||||
float[:, :, ::1] options_rgb, int lookahead):
|
||||
cdef float[:, :, ::1] pattern = dither.PATTERN
|
||||
cdef int x_res = screen.X_RES
|
||||
@ -132,7 +132,7 @@ def dither_lookahead(
|
||||
cdef long flat, dist, bit4
|
||||
|
||||
cdef long r, g, b
|
||||
cdef (unsigned char)[:, ::1] distances = differ._distances
|
||||
cdef (unsigned char)[:, ::1] distances = screen.palette.distances
|
||||
for i in range(2**lookahead):
|
||||
total_error = 0
|
||||
for j in range(lookahead):
|
||||
@ -178,7 +178,7 @@ def lookahead_options(screen, lookahead, last_pixel_4bit, x):
|
||||
@cython.boundscheck(False)
|
||||
@cython.wraparound(False)
|
||||
def dither_image(
|
||||
screen, float[:, :, ::1] image_rgb, dither, differ, int lookahead):
|
||||
screen, float[:, :, ::1] image_rgb, dither, int lookahead):
|
||||
cdef (unsigned char)[:, ::1] image_4bit = np.empty(
|
||||
(image_rgb.shape[0], image_rgb.shape[1]), dtype=np.uint8)
|
||||
|
||||
@ -202,7 +202,7 @@ def dither_image(
|
||||
|
||||
output_pixel_4bit, output_pixel_rgb = \
|
||||
dither_lookahead(
|
||||
screen, image_rgb, dither, differ, x, y, options_4bit,
|
||||
screen, image_rgb, dither, x, y, options_4bit,
|
||||
options_rgb, lookahead)
|
||||
for i in range(3):
|
||||
quant_error[i] = input_pixel_rgb[i] - output_pixel_rgb[i]
|
||||
|
34
dither_pattern.py
Normal file
34
dither_pattern.py
Normal file
@ -0,0 +1,34 @@
|
||||
import numpy as np
|
||||
|
||||
|
||||
class DitherPattern:
|
||||
PATTERN = None
|
||||
ORIGIN = None
|
||||
|
||||
|
||||
class FloydSteinbergDither(DitherPattern):
|
||||
# 0 * 7
|
||||
# 3 5 1
|
||||
PATTERN = np.array(((0, 0, 7), (3, 5, 1)),
|
||||
dtype=np.float32).reshape(2, 3, 1) / np.float(16)
|
||||
# XXX X_ORIGIN since ORIGIN[0] == 0
|
||||
ORIGIN = (0, 1)
|
||||
|
||||
|
||||
class BuckelsDither(DitherPattern):
|
||||
# 0 * 2 1
|
||||
# 1 2 1 0
|
||||
# 0 1 0 0
|
||||
PATTERN = np.array(((0, 0, 2, 1), (1, 2, 1, 0), (0, 1, 0, 0)),
|
||||
dtype=np.float32).reshape(3, 4, 1) / np.float32(8)
|
||||
ORIGIN = (0, 1)
|
||||
|
||||
|
||||
class JarvisDither(DitherPattern):
|
||||
# 0 0 X 7 5
|
||||
# 3 5 7 5 3
|
||||
# 1 3 5 3 1
|
||||
PATTERN = np.array(((0, 0, 0, 7, 5), (3, 5, 7, 5, 3), (1, 3, 5, 3, 1)),
|
||||
dtype=np.float32).reshape(3, 5, 1) / np.float32(48)
|
||||
ORIGIN = (0, 2)
|
||||
|
37
image.py
Normal file
37
image.py
Normal file
@ -0,0 +1,37 @@
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
|
||||
def srgb_to_linear_array(a: np.ndarray, gamma=2.4) -> np.ndarray:
|
||||
return np.where(a <= 0.04045, a / 12.92, ((a + 0.055) / 1.055) ** gamma)
|
||||
|
||||
|
||||
def linear_to_srgb_array(a: np.ndarray, gamma=2.4) -> np.ndarray:
|
||||
return np.where(a <= 0.0031308, a * 12.92, 1.055 * a ** (1.0 / gamma) -
|
||||
0.055)
|
||||
|
||||
|
||||
def srgb_to_linear(im: np.ndarray) -> np.ndarray:
|
||||
rgb_linear = srgb_to_linear_array(im / 255.0, gamma=2.4)
|
||||
return (np.clip(rgb_linear, 0.0, 1.0) * 255).astype(np.float32)
|
||||
|
||||
|
||||
def linear_to_srgb(im: np.ndarray) -> np.ndarray:
|
||||
srgb = linear_to_srgb_array(im / 255.0, gamma=2.4)
|
||||
return (np.clip(srgb, 0.0, 1.0) * 255).astype(np.float32)
|
||||
|
||||
|
||||
def open(x_res:int, y_res:int, filename: str) -> np.ndarray:
|
||||
im = Image.open(filename)
|
||||
# TODO: convert to sRGB colour profile explicitly, in case it has some other
|
||||
# profile already.
|
||||
if im.mode != "RGB":
|
||||
im = im.convert("RGB")
|
||||
|
||||
# Convert to linear RGB before rescaling so that colour interpolation is
|
||||
# in linear space
|
||||
linear = srgb_to_linear(np.asarray(im)).astype(np.uint8)
|
||||
rescaled = Image.fromarray(linear).resize((x_res, y_res), Image.LANCZOS)
|
||||
# TODO: better performance with malloc'ed array?
|
||||
return np.array(rescaled).astype(np.float32)
|
||||
|
103
palette.py
Normal file
103
palette.py
Normal file
@ -0,0 +1,103 @@
|
||||
import numpy as np
|
||||
import image
|
||||
|
||||
|
||||
class Palette:
|
||||
RGB = None
|
||||
SRGB = None
|
||||
DOTS = None
|
||||
|
||||
def __init__(self):
|
||||
# CIE2000 colour distance matrix from 24-bit RGB tuple to 4-bit
|
||||
# palette colour.
|
||||
self.distances = np.memmap("distances.npy", mode="r+",
|
||||
dtype=np.uint8, shape=(16777216, 16))
|
||||
|
||||
# Default bmp2dhr palette
|
||||
RGB = {
|
||||
0: np.array((0, 0, 0)), # Black
|
||||
8: np.array((148, 12, 125)), # Magenta
|
||||
4: np.array((99, 77, 0)), # Brown
|
||||
12: np.array((249, 86, 29)), # Orange
|
||||
2: np.array((51, 111, 0)), # Dark green
|
||||
10: np.array((126, 126, 125)), # Grey2
|
||||
6: np.array((67, 200, 0)), # Green
|
||||
14: np.array((221, 206, 23)), # Yellow
|
||||
1: np.array((32, 54, 212)), # Dark blue
|
||||
9: np.array((188, 55, 255)), # Violet
|
||||
5: np.array((126, 126, 126)), # Grey1
|
||||
13: np.array((255, 129, 236)), # Pink
|
||||
3: np.array((7, 168, 225)), # Med blue
|
||||
11: np.array((158, 172, 255)), # Light blue
|
||||
7: np.array((93, 248, 133)), # Aqua
|
||||
15: np.array((255, 255, 255)), # White
|
||||
}
|
||||
|
||||
# Maps palette values to screen dots. Note that these are the same as
|
||||
# the binary values in reverse order.
|
||||
DOTS = {
|
||||
0: (False, False, False, False),
|
||||
1: (True, False, False, False),
|
||||
2: (False, True, False, False),
|
||||
3: (True, True, False, False),
|
||||
4: (False, False, True, False),
|
||||
5: (True, False, True, False),
|
||||
6: (False, True, True, False),
|
||||
7: (True, True, True, False),
|
||||
8: (False, False, False, True),
|
||||
9: (True, False, False, True),
|
||||
10: (False, True, False, True),
|
||||
11: (True, True, False, True),
|
||||
12: (False, False, True, True),
|
||||
13: (True, False, True, True),
|
||||
14: (False, True, True, True),
|
||||
15: (True, True, True, True)
|
||||
}
|
||||
DOTS_TO_4BIT = {}
|
||||
for k, v in DOTS.items():
|
||||
DOTS_TO_4BIT[v] = k
|
||||
|
||||
# OpenEmulator
|
||||
sRGB = {
|
||||
0: np.array((0, 0, 0)), # Black
|
||||
8: np.array((203, 0, 121)), # Magenta
|
||||
4: np.array((99, 103, 0)), # Brown
|
||||
12: np.array((244, 78, 0)), # Orange
|
||||
2: np.array((0, 150, 0)), # Dark green
|
||||
10: np.array((130, 130, 130)), # Grey2
|
||||
6: np.array((0, 235, 0)), # Green
|
||||
14: np.array((214, 218, 0)), # Yellow
|
||||
1: np.array((20, 0, 246)), # Dark blue
|
||||
9: np.array((230, 0, 244)), # Violet
|
||||
5: np.array((130, 130, 130)), # Grey1
|
||||
13: np.array((244, 105, 235)), # Pink
|
||||
3: np.array((0, 174, 243)), # Med blue
|
||||
11: np.array((160, 156, 244)), # Light blue
|
||||
7: np.array((25, 243, 136)), # Aqua
|
||||
15: np.array((244, 247, 244)), # White
|
||||
}
|
||||
|
||||
# # Virtual II (sRGB)
|
||||
# sRGB = {
|
||||
# (False, False, False, False): np.array((0, 0, 0)), # Black
|
||||
# (False, False, False, True): np.array((231,36,66)), # Magenta
|
||||
# (False, False, True, False): np.array((154,104,0)), # Brown
|
||||
# (False, False, True, True): np.array((255,124,0)), # Orange
|
||||
# (False, True, False, False): np.array((0,135,45)), # Dark green
|
||||
# (False, True, False, True): np.array((104,104,104)), # Grey2 XXX
|
||||
# (False, True, True, False): np.array((0,222,0)), # Green
|
||||
# (False, True, True, True): np.array((255,252,0)), # Yellow
|
||||
# (True, False, False, False): np.array((1,30,169)), # Dark blue
|
||||
# (True, False, False, True): np.array((230,73,228)), # Violet
|
||||
# (True, False, True, False): np.array((185,185,185)), # Grey1 XXX
|
||||
# (True, False, True, True): np.array((255,171,153)), # Pink
|
||||
# (True, True, False, False): np.array((47,69,255)), # Med blue
|
||||
# (True, True, False, True): np.array((120,187,255)), # Light blue
|
||||
# (True, True, True, False): np.array((83,250,208)), # Aqua
|
||||
# (True, True, True, True): np.array((255, 255, 255)), # White
|
||||
# }
|
||||
RGB = {}
|
||||
for k, v in sRGB.items():
|
||||
RGB[k] = (np.clip(image.srgb_to_linear_array(v / 255), 0.0,
|
||||
1.0) * 255).astype(
|
||||
np.uint8)
|
106
screen.py
Normal file
106
screen.py
Normal file
@ -0,0 +1,106 @@
|
||||
import numpy as np
|
||||
import palette as palette_py
|
||||
|
||||
|
||||
class Screen:
|
||||
X_RES = None
|
||||
Y_RES = None
|
||||
X_PIXEL_WIDTH = None
|
||||
|
||||
def __init__(self, palette: palette_py.Palette):
|
||||
self.main = np.zeros(8192, dtype=np.uint8)
|
||||
self.aux = np.zeros(8192, dtype=np.uint8)
|
||||
self.palette = palette
|
||||
|
||||
@staticmethod
|
||||
def y_to_base_addr(y: int) -> int:
|
||||
"""Maps y coordinate to screen memory base address."""
|
||||
a = y // 64
|
||||
d = y - 64 * a
|
||||
b = d // 8
|
||||
c = d - 8 * b
|
||||
|
||||
return 1024 * c + 128 * b + 40 * a
|
||||
|
||||
def _image_to_bitmap(self, image: np.ndarray) -> np.ndarray:
|
||||
raise NotImplementedError
|
||||
|
||||
def pack(self, image: np.ndarray):
|
||||
bitmap = self._image_to_bitmap(image)
|
||||
# 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)
|
||||
main_col = np.zeros(
|
||||
(self.Y_RES, self.X_RES * self.X_PIXEL_WIDTH // 14), dtype=np.uint8)
|
||||
aux_col = np.zeros(
|
||||
(self.Y_RES, self.X_RES * self.X_PIXEL_WIDTH // 14), dtype=np.uint8)
|
||||
for byte_offset in range(80):
|
||||
column = np.zeros(self.Y_RES, dtype=np.uint8)
|
||||
for bit in range(7):
|
||||
column |= (bitmap[:, 7 * byte_offset + bit].astype(
|
||||
np.uint8) << bit)
|
||||
if byte_offset % 2 == 0:
|
||||
aux_col[:, byte_offset // 2] = column
|
||||
else:
|
||||
main_col[:, (byte_offset - 1) // 2] = column
|
||||
|
||||
for y in range(self.Y_RES):
|
||||
addr = self.y_to_base_addr(y)
|
||||
self.aux[addr:addr + 40] = aux_col[y, :]
|
||||
self.main[addr:addr + 40] = main_col[y, :]
|
||||
|
||||
def pixel_palette_options(self, last_pixel_4bit, x: int):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class DHGR140Screen(Screen):
|
||||
"""DHGR screen ignoring colour fringing, i.e. treating as 140x192x16."""
|
||||
|
||||
X_RES = 140
|
||||
Y_RES = 192
|
||||
X_PIXEL_WIDTH = 4
|
||||
|
||||
def _image_to_bitmap(self, image_4bit: np.ndarray) -> np.ndarray:
|
||||
bitmap = np.zeros(
|
||||
(self.Y_RES, self.X_RES * self.X_PIXEL_WIDTH), dtype=np.bool)
|
||||
for y in range(self.Y_RES):
|
||||
for x in range(self.X_RES):
|
||||
pixel = image_4bit[y, x].item()
|
||||
dots = self.palette.DOTS[pixel]
|
||||
bitmap[y, x * self.X_PIXEL_WIDTH:(
|
||||
(x + 1) * self.X_PIXEL_WIDTH)] = dots
|
||||
return bitmap
|
||||
|
||||
def pixel_palette_options(self, last_pixel_4bit, x: int):
|
||||
return np.array(list(self.palette.RGB.keys())), np.array(list(
|
||||
self.palette.RGB.values()))
|
||||
|
||||
|
||||
class DHGR560Screen(Screen):
|
||||
"""DHGR screen including colour fringing."""
|
||||
X_RES = 560
|
||||
Y_RES = 192
|
||||
X_PIXEL_WIDTH = 1
|
||||
|
||||
def _image_to_bitmap(self, image_4bit: np.ndarray) -> np.ndarray:
|
||||
bitmap = np.zeros((self.Y_RES, self.X_RES), dtype=np.bool)
|
||||
for y in range(self.Y_RES):
|
||||
for x in range(self.X_RES):
|
||||
pixel = image_4bit[y, x]
|
||||
dots = self.palette.DOTS[pixel]
|
||||
phase = x % 4
|
||||
bitmap[y, x] = dots[phase]
|
||||
return bitmap
|
||||
|
||||
def pixel_palette_options(self, last_pixel_4bit, x: int):
|
||||
last_dots = self.palette.DOTS[last_pixel_4bit]
|
||||
other_dots = list(last_dots)
|
||||
other_dots[x % 4] = not other_dots[x % 4]
|
||||
other_dots = tuple(other_dots)
|
||||
other_pixel_4bit = self.palette.DOTS_TO_4BIT[other_dots]
|
||||
return (
|
||||
np.array([last_pixel_4bit, other_pixel_4bit]),
|
||||
np.array([self.palette.RGB[last_pixel_4bit],
|
||||
self.palette.RGB[other_pixel_4bit]]))
|
||||
|
Loading…
x
Reference in New Issue
Block a user