diff --git a/convert.py b/convert.py index 074551a..369603f 100644 --- a/convert.py +++ b/convert.py @@ -34,11 +34,9 @@ def cluster_palette(image: Image): with colour.utilities.suppress_warnings(colour_usage_warnings=True): palette_rgb = colour.convert(palette_cam, "CAM16UCS", "RGB") # SHR colour palette only uses 4-bit values - # TODO: do this more carefully - palette_rgb = np.clip(np.round(palette_rgb * 16).astype(np.uint32) * - 16, 0, 255) - palette_rgb = palette_rgb.astype(np.float32) / 255 - return palette_rgb + palette_rgb = np.round(palette_rgb * 15) / 15 + # palette_rgb = palette_rgb.astype(np.float32) / 255 + return palette_rgb.astype(np.float32) @@ -96,13 +94,21 @@ def main(): image = image_py.open(args.input) if args.show_input: image_py.resize(image, screen.X_RES, screen.Y_RES, - srgb_output=True).show() + srgb_output=False).show() rgb = np.array( image_py.resize(image, screen.X_RES, screen.Y_RES, - gamma=args.gamma_correct)).astype(np.float32) / 255 + gamma=args.gamma_correct, srgb_output=True)).astype( + np.float32) / 255 palette_rgb = cluster_palette(rgb) - output_rgb = dither_pyx.dither_shr(rgb, palette_rgb, rgb_to_cam16) + # print(palette_rgb) + # screen.set_palette(0, (image_py.linear_to_srgb_array(palette_rgb) * + # 15).astype(np.uint8)) + screen.set_palette(0, (np.round(palette_rgb * 15)).astype(np.uint8)) + + output_4bit = dither_pyx.dither_shr(rgb, palette_rgb, rgb_to_cam16) + screen.set_pixels(output_4bit) + output_rgb = (palette_rgb[output_4bit] * 255).astype(np.uint8) output_srgb = image_py.linear_to_srgb(output_rgb).astype(np.uint8) # dither = dither_pattern.PATTERNS[args.dither]() @@ -120,18 +126,20 @@ def main(): # output_screen.bitmap_to_image_rgb(bitmap)).astype(np.uint8) out_image = image_py.resize( Image.fromarray(output_srgb), screen.X_RES, screen.Y_RES, - srgb_output=True) + srgb_output=False) # XXX true if args.show_output: out_image.show() # Save Double hi-res image - # outfile = os.path.join(os.path.splitext(args.output)[0] + "-preview.png") - # out_image.save(outfile, "PNG") - # screen.pack(bitmap) + outfile = os.path.join(os.path.splitext(args.output)[0] + "-preview.png") + out_image.save(outfile, "PNG") + screen.pack() # with open(args.output, "wb") as f: # f.write(bytes(screen.aux)) # f.write(bytes(screen.main)) + with open(args.output, "wb") as f: + f.write(bytes(screen.memory)) if __name__ == "__main__": diff --git a/dither.pyx b/dither.pyx index 95822da..fc23520 100644 --- a/dither.pyx +++ b/dither.pyx @@ -329,11 +329,13 @@ import colour @cython.boundscheck(False) @cython.wraparound(False) def dither_shr(float[:, :, ::1] working_image, float[:, ::1] palette_rgb, float[:,::1] rgb_to_cam16ucs): - cdef int y, x, idx + cdef int y, x, idx, best_colour_idx cdef float best_distance, distance cdef float[::1] best_colour_rgb, pixel_cam, colour_rgb, colour_cam cdef float[3] quant_error + cdef (unsigned char)[:, ::1] output_4bit = np.zeros((200, 320), dtype=np.uint8) + for y in range(200): print(y) for x in range(320): @@ -341,12 +343,15 @@ def dither_shr(float[:, :, ::1] working_image, float[:, ::1] palette_rgb, float[ rgb_to_cam16ucs, working_image[y, x, 0], working_image[y, x, 1], working_image[y, x, 2]) best_distance = 1e9 + best_colour_idx = 0 for idx, colour_rgb in enumerate(palette_rgb): colour_cam = convert_rgb_to_cam16ucs(rgb_to_cam16ucs, colour_rgb[0], colour_rgb[1], colour_rgb[2]) distance = colour_distance_squared(pixel_cam, colour_cam) if distance < best_distance: best_distance = distance best_colour_rgb = colour_rgb + best_colour_idx = idx + output_4bit[y, x] = best_colour_idx for i in range(3): quant_error[i] = working_image[y, x, i] - best_colour_rgb[i] @@ -359,14 +364,63 @@ def dither_shr(float[:, :, ::1] working_image, float[:, ::1] palette_rgb, float[ working_image[y, x + 1, i] = clip( working_image[y, x + 1, i] + quant_error[i] * (7 / 16), 0, 1) if y < 199: + if x > 0: + working_image[y + 1, x - 1, i] = clip( + working_image[y + 1, x - 1, i] + quant_error[i] * (3 / 16), 0, + 1) working_image[y + 1, x, i] = clip( working_image[y + 1, x, i] + quant_error[i] * (5 / 16), 0, 1) if x < 319: working_image[y + 1, x + 1, i] = clip( working_image[y + 1, x + 1, i] + quant_error[i] * (1 / 16), 0, 1) - if x > 0: - working_image[y + 1, x - 1, i] = clip( - working_image[y + 1, x - 1, i] + quant_error[i] * (3 / 16), 0, - 1) - return np.array(working_image).astype(np.float32) * 255 + +# # 0 0 X 7 5 +# # 3 5 7 5 3 +# # 1 3 5 3 1 +# if x < 319: +# working_image[y, x + 1, i] = clip( +# working_image[y, x + 1, i] + quant_error[i] * (7 / 48), 0, 1) +# if x < 318: +# working_image[y, x + 2, i] = clip( +# working_image[y, x + 2, i] + quant_error[i] * (5 / 48), 0, 1) +# if y < 199: +# if x > 1: +# working_image[y + 1, x - 2, i] = clip( +# working_image[y + 1, x - 2, i] + quant_error[i] * (3 / 48), 0, +# 1) +# if x > 0: +# working_image[y + 1, x - 1, i] = clip( +# working_image[y + 1, x - 1, i] + quant_error[i] * (5 / 48), 0, +# 1) +# working_image[y + 1, x, i] = clip( +# working_image[y + 1, x, i] + quant_error[i] * (7 / 48), 0, 1) +# if x < 319: +# working_image[y + 1, x + 1, i] = clip( +# working_image[y + 1, x + 1, i] + quant_error[i] * (5 / 48), +# 0, 1) +# if x < 318: +# working_image[y + 1, x + 2, i] = clip( +# working_image[y + 1, x + 2, i] + quant_error[i] * (3 / 48), +# 0, 1) +# if y < 198: +# if x > 1: +# working_image[y + 2, x - 2, i] = clip( +# working_image[y + 2, x - 2, i] + quant_error[i] * (1 / 48), 0, +# 1) +# if x > 0: +# working_image[y + 2, x - 1, i] = clip( +# working_image[y + 2, x - 1, i] + quant_error[i] * (3 / 48), 0, +# 1) +# working_image[y + 2, x, i] = clip( +# working_image[y + 2, x, i] + quant_error[i] * (5 / 48), 0, 1) +# if x < 319: +# working_image[y + 2, x + 1, i] = clip( +# working_image[y + 2, x + 1, i] + quant_error[i] * (3 / 48), +# 0, 1) +# if x < 318: +# working_image[y + 2, x + 2, i] = clip( +# working_image[y + 2, x + 2, i] + quant_error[i] * (1 / 48), +# 0, 1) + + return np.array(output_4bit, dtype=np.uint8) \ No newline at end of file diff --git a/screen.py b/screen.py index d164be7..3c526dd 100644 --- a/screen.py +++ b/screen.py @@ -9,6 +9,62 @@ class SHR320Screen: X_RES = 320 Y_RES = 200 + def __init__(self): + self.palettes = {k: np.zeros((16, 3), dtype=np.uint8) for k in + range(16)} + # Really 4-bit values, indexing into palette + self.pixels = np.array((self.Y_RES, self.X_RES), dtype=np.uint8) + + # Choice of palette per scan-line + self.line_palette = np.zeros(self.Y_RES, dtype=np.uint8) + + self.memory = None + + def set_palette(self, idx: int, palette: np.array): + if idx < 0 or idx > 15: + raise ValueError("Palette index %s must be in range 0 .. 15" % idx) + if palette.shape != (16, 3): + raise ValueError("Palette size %s != (16, 3)" % palette.shape) + # XXX check element range + if palette.dtype != np.uint8: + raise ValueError("Palette must be of type np.uint8") + print(palette) + self.palettes[idx] = np.array(palette) + + def set_pixels(self, pixels): + self.pixels = np.array(pixels) + + def pack(self): + dump = np.zeros(32768, dtype=np.uint8) + for y in range(self.Y_RES): + pixel_pair = 0 + for x in range(self.X_RES): + if x % 2 == 0: + pixel_pair |= (self.pixels[y, x] << 4) + else: + pixel_pair |= self.pixels[y, x] + # print(pixel_pair) + dump[y * 160 + (x - 1) // 2] = pixel_pair + pixel_pair = 0 + + scan_control_offset = 320 * 200 // 2 + for y in range(self.Y_RES): + dump[scan_control_offset + y] = self.line_palette[y] + + palette_offset = scan_control_offset + 256 + for palette_idx, palette in self.palettes.items(): + for rgb_idx, rgb in enumerate(palette): + r, g, b = rgb + # print(r, g, b) + rgb_low = (g << 4) | b + rgb_hi = r + print(hex(rgb_hi), hex(rgb_low)) + palette_idx_offset = palette_offset + (32 * palette_idx) + dump[palette_idx_offset + (2 * rgb_idx)] = rgb_low + dump[palette_idx_offset + (2 * rgb_idx + 1)] = rgb_hi + + self.memory = dump + class DHGRScreen: X_RES = 560