2020-12-29 18:24:29 +00:00
|
|
|
import argparse
|
|
|
|
from PIL import Image
|
|
|
|
|
|
|
|
import numpy as np
|
|
|
|
|
2020-12-29 21:03:17 +00:00
|
|
|
# TODO:
|
|
|
|
# - use perceptual colour difference model
|
2020-12-30 10:27:33 +00:00
|
|
|
# - compare to bmp2dhr and a2bestpix
|
2020-12-29 21:03:17 +00:00
|
|
|
# - look ahead N pixels and compute all 2^N bit patterns, then minimize
|
|
|
|
# average error
|
|
|
|
# - optimize Dither.apply() critical path
|
|
|
|
|
2020-12-29 18:24:29 +00:00
|
|
|
X_RES = 560
|
|
|
|
Y_RES = 192
|
|
|
|
|
|
|
|
RGB = {
|
|
|
|
(False, False, False, False): np.array((0, 0, 0)), # Black
|
|
|
|
(False, False, False, True): np.array((148, 12, 125)), # Magenta
|
|
|
|
(False, False, True, False): np.array((99, 77, 0)), # Brown
|
|
|
|
(False, False, True, True): np.array((249, 86, 29)), # Orange
|
|
|
|
(False, True, False, False): np.array((51, 111, 0)), # Dark green
|
2020-12-29 20:47:33 +00:00
|
|
|
# XXX RGB values are used as keys in DOTS dict, need to be unique
|
|
|
|
(False, True, False, True): np.array((126, 126, 125)), # Grey1
|
2020-12-29 18:24:29 +00:00
|
|
|
(False, True, True, False): np.array((67, 200, 0)), # Green
|
|
|
|
(False, True, True, True): np.array((221, 206, 23)), # Yellow
|
|
|
|
(True, False, False, False): np.array((32, 54, 212)), # Dark blue
|
|
|
|
(True, False, False, True): np.array((188, 55, 255)), # Violet
|
|
|
|
(True, False, True, False): np.array((126, 126, 126)), # Grey2
|
|
|
|
(True, False, True, True): np.array((255, 129, 236)), # Pink
|
|
|
|
(True, True, False, False): np.array((7, 168, 225)), # Med blue
|
|
|
|
(True, True, False, True): np.array((158, 172, 255)), # Light blue
|
|
|
|
(True, True, True, False): np.array((93, 248, 133)), # Aqua
|
|
|
|
(True, True, True, True): np.array((255, 255, 255)), # White
|
|
|
|
}
|
|
|
|
|
2020-12-29 20:47:33 +00:00
|
|
|
NAMES = {
|
|
|
|
(0, 0, 0): "Black",
|
|
|
|
(148, 12, 125): "Magenta",
|
|
|
|
(99, 77, 0): "Brown",
|
|
|
|
(249, 86, 29): "Orange",
|
|
|
|
(51, 111, 0): "Dark green",
|
|
|
|
(126, 126, 125): "Grey1", # XXX
|
|
|
|
(67, 200, 0): "Green",
|
|
|
|
(221, 206, 23): "Yellow",
|
|
|
|
(32, 54, 212): "Dark blue",
|
|
|
|
(188, 55, 255): "Violet",
|
|
|
|
(126, 126, 126): "Grey2",
|
|
|
|
(255, 129, 236): "Pink",
|
|
|
|
(7, 168, 225): "Med blue",
|
|
|
|
(158, 172, 255): "Light blue",
|
|
|
|
(93, 248, 133): "Aqua",
|
|
|
|
(255, 255, 255): "White"
|
|
|
|
}
|
|
|
|
|
|
|
|
DOTS = {}
|
|
|
|
for k, v in RGB.items():
|
|
|
|
DOTS[tuple(v)] = k
|
|
|
|
|
2020-12-29 18:24:29 +00:00
|
|
|
|
2020-12-29 20:47:33 +00:00
|
|
|
def find_closest_color(pixel, last_pixel, x: int):
|
2020-12-29 18:24:29 +00:00
|
|
|
least_diff = 1e9
|
|
|
|
best_colour = None
|
2020-12-29 20:47:33 +00:00
|
|
|
|
|
|
|
last_dots = DOTS[tuple(last_pixel)]
|
|
|
|
other_dots = list(last_dots)
|
|
|
|
other_dots[x % 4] = not other_dots[x % 4]
|
|
|
|
other_dots = tuple(other_dots)
|
|
|
|
for v in (RGB[last_dots], RGB[other_dots]):
|
2020-12-29 18:24:29 +00:00
|
|
|
diff = np.sum(np.power(v - np.array(pixel), 2))
|
|
|
|
if diff < least_diff:
|
|
|
|
least_diff = diff
|
|
|
|
best_colour = v
|
|
|
|
return best_colour
|
|
|
|
|
|
|
|
|
2020-12-29 20:47:33 +00:00
|
|
|
class Dither:
|
|
|
|
PATTERN = None
|
|
|
|
ORIGIN = None
|
|
|
|
|
|
|
|
def apply(self, image, x, y, quant_error):
|
2020-12-29 21:03:17 +00:00
|
|
|
for offset, error_fraction in np.ndenumerate(self.PATTERN / np.sum(
|
|
|
|
self.PATTERN)):
|
|
|
|
xx = x + offset[1] - self.ORIGIN[1]
|
|
|
|
yy = y + offset[0] - self.ORIGIN[0]
|
|
|
|
if xx < 0 or yy < 0 or xx > (X_RES - 1) or yy > (Y_RES - 1):
|
|
|
|
continue
|
|
|
|
new_pixel = image.getpixel((xx, yy)) + error_fraction * quant_error
|
|
|
|
image.putpixel((xx, yy), tuple(new_pixel.astype(int)))
|
2020-12-29 20:47:33 +00:00
|
|
|
|
|
|
|
|
|
|
|
class FloydSteinbergDither(Dither):
|
|
|
|
# 0 * 7
|
|
|
|
# 3 5 1
|
|
|
|
PATTERN = np.array(((0, 0, 7), (3, 5, 1)))
|
|
|
|
ORIGIN = (0, 1)
|
|
|
|
|
|
|
|
|
2020-12-30 10:27:33 +00:00
|
|
|
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)))
|
2020-12-29 20:47:33 +00:00
|
|
|
ORIGIN = (0, 1)
|
|
|
|
|
|
|
|
|
2020-12-30 10:27:33 +00:00
|
|
|
def open_image(filename: str) -> Image:
|
2020-12-29 18:24:29 +00:00
|
|
|
im = Image.open(filename)
|
|
|
|
if im.mode != "RGB":
|
|
|
|
im = im.convert("RGB")
|
|
|
|
im.resize((X_RES, Y_RES), resample=Image.LANCZOS)
|
2020-12-30 10:27:33 +00:00
|
|
|
return im
|
2020-12-29 20:47:33 +00:00
|
|
|
|
2020-12-30 10:27:33 +00:00
|
|
|
|
|
|
|
def dither_image(image: Image, dither: Dither) -> Image:
|
2020-12-29 18:24:29 +00:00
|
|
|
for y in range(Y_RES):
|
|
|
|
print(y)
|
2020-12-30 10:27:33 +00:00
|
|
|
new_pixel = (0, 0, 0)
|
2020-12-29 18:24:29 +00:00
|
|
|
for x in range(X_RES):
|
2020-12-30 10:27:33 +00:00
|
|
|
old_pixel = image.getpixel((x, y))
|
|
|
|
new_pixel = find_closest_color(old_pixel, new_pixel, x)
|
|
|
|
image.putpixel((x, y), tuple(new_pixel))
|
|
|
|
quant_error = old_pixel - new_pixel
|
|
|
|
dither.apply(image, x, y, quant_error)
|
|
|
|
return image
|
|
|
|
|
|
|
|
|
|
|
|
class Screen:
|
|
|
|
def __init__(self, image: Image):
|
|
|
|
self.bitmap = np.zeros((Y_RES, X_RES), dtype=np.bool)
|
|
|
|
|
|
|
|
self.main = np.zeros(8192, dtype=np.uint8)
|
|
|
|
self.aux = np.zeros(8192, dtype=np.uint8)
|
|
|
|
|
|
|
|
for y in range(Y_RES):
|
|
|
|
for x in range(X_RES):
|
|
|
|
pixel = image.getpixel((x, y))
|
|
|
|
dots = DOTS[pixel]
|
|
|
|
phase = x % 4
|
|
|
|
self.bitmap[y, x] = dots[phase]
|
|
|
|
|
|
|
|
@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 pack(self):
|
|
|
|
# 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((Y_RES, X_RES // 14), dtype=np.uint8)
|
|
|
|
aux_col = np.zeros((Y_RES, X_RES // 14), dtype=np.uint8)
|
|
|
|
for byte_offset in range(80):
|
|
|
|
column = np.zeros(Y_RES, dtype=np.uint8)
|
|
|
|
for bit in range(7):
|
|
|
|
column |= (self.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(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, :]
|
2020-12-29 18:24:29 +00:00
|
|
|
|
|
|
|
|
|
|
|
def main():
|
|
|
|
parser = argparse.ArgumentParser()
|
|
|
|
parser.add_argument("input", type=str, help="Input file to process")
|
2020-12-30 10:27:33 +00:00
|
|
|
parser.add_argument("output", type=str, help="Output file for ")
|
2020-12-29 18:24:29 +00:00
|
|
|
|
|
|
|
args = parser.parse_args()
|
2020-12-30 10:27:33 +00:00
|
|
|
image = open_image(args.input)
|
|
|
|
# image.show()
|
|
|
|
|
|
|
|
dither = FloydSteinbergDither()
|
|
|
|
# dither = BuckelsDither()
|
|
|
|
|
|
|
|
output = dither_image(image, dither)
|
|
|
|
output.show()
|
|
|
|
screen = Screen(output)
|
|
|
|
bitmap = Image.fromarray(screen.bitmap.astype('uint8') * 255)
|
|
|
|
# bitmap.show()
|
|
|
|
screen.pack()
|
|
|
|
|
|
|
|
with open("output.bin", "wb") as f:
|
|
|
|
f.write(screen.main)
|
|
|
|
f.write(screen.aux)
|
2020-12-29 18:24:29 +00:00
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
main()
|