From 2458bf98f7681b3e19f7e5496e53b12453c47737 Mon Sep 17 00:00:00 2001 From: kris Date: Wed, 30 Dec 2020 10:27:33 +0000 Subject: [PATCH] Pack output in Apple II screen format and save as binary file. --- dither.py | 98 +++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 81 insertions(+), 17 deletions(-) diff --git a/dither.py b/dither.py index e12846a..f75dfa7 100644 --- a/dither.py +++ b/dither.py @@ -4,8 +4,8 @@ from PIL import Image import numpy as np # TODO: -# - output binary files that can be viewed on Apple II # - use perceptual colour difference model +# - compare to bmp2dhr and a2bestpix # - look ahead N pixels and compute all 2^N bit patterns, then minimize # average error # - optimize Dither.apply() critical path @@ -95,40 +95,104 @@ class FloydSteinbergDither(Dither): ORIGIN = (0, 1) -class KennawayDither(Dither): - # 0 * 7 5 3 1 - # 3 5 3 1 1 0 - PATTERN = np.array(((0, 0, 7, 5, 3, 1), (3, 5, 3, 1, 1, 0))) +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))) ORIGIN = (0, 1) -def dither(filename): +def open_image(filename: str) -> Image: im = Image.open(filename) if im.mode != "RGB": im = im.convert("RGB") im.resize((X_RES, Y_RES), resample=Image.LANCZOS) - im.show() + return im - # ditherer = FloydSteinbergDither() - ditherer = KennawayDither() + +def dither_image(image: Image, dither: Dither) -> Image: for y in range(Y_RES): print(y) - newpixel = (0, 0, 0) + new_pixel = (0, 0, 0) for x in range(X_RES): - oldpixel = im.getpixel((x, y)) - newpixel = find_closest_color(oldpixel, newpixel, x) - im.putpixel((x, y), tuple(newpixel)) - quant_error = oldpixel - newpixel - ditherer.apply(im, x, y, quant_error) - im.show() + 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, :] def main(): parser = argparse.ArgumentParser() parser.add_argument("input", type=str, help="Input file to process") + parser.add_argument("output", type=str, help="Output file for ") args = parser.parse_args() - dither(args.input) + 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) if __name__ == "__main__":