diff --git a/convert.py b/convert.py index a38331c..00dfe66 100644 --- a/convert.py +++ b/convert.py @@ -13,7 +13,7 @@ import screen as screen_py # TODO: -# - support 140px mode again +# - include fringing in 140px output # - compare to bmp2dhr and a2bestpix # - support LR/DLR # - support HGR @@ -38,11 +38,20 @@ def main(): parser.add_argument( '--show_output', action=argparse.BooleanOptionalAction, default=True, help="Whether to show the output image after conversion.") + parser.add_argument( + '--resolution', type=int, choices=(560, 140), default=560, + help=("Double hi-res resolution to target. 140 treats pixels as " + "groups of 4 and ignores NTSC fringing.") + ) args = parser.parse_args() palette = palette_py.Palette() - # screen = DHGR140Screen() - screen = screen_py.DHGR560Screen(palette) + if args.resolution == 560: + screen = screen_py.DHGR560Screen(palette) + lookahead = args.lookahead + else: + screen = screen_py.DHGR140Screen(palette) + lookahead = 0 image = image_py.open(screen.X_RES, screen.Y_RES, args.input) if args.show_input: @@ -52,7 +61,7 @@ def main(): start = time.time() output_4bit, output_rgb = dither_pyx.dither_image( - screen, image, dither, lookahead=args.lookahead) + screen, image, dither, lookahead) print(time.time() - start) screen.pack(output_4bit) diff --git a/dither.pyx b/dither.pyx index 9cf31b4..36624be 100644 --- a/dither.pyx +++ b/dither.pyx @@ -1,6 +1,7 @@ # cython: infer_types=True cimport cython +import functools import numpy as np # from cython.parallel import prange from cython.view cimport array as cvarray @@ -81,7 +82,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, int x, int y, char[:, ::1] options_4bit, + screen, float[:,:,::1] image_rgb, dither, int x, int y, unsigned char[:, ::1] options_4bit, float[:, :, ::1] options_rgb, int lookahead): cdef float[:, :, ::1] pattern = dither.PATTERN cdef int x_res = screen.X_RES @@ -126,10 +127,11 @@ def dither_lookahead( quant_error[k] = lah_image_rgb[j * lah_shape1 * lah_shape2 + i * lah_shape2 + k] - options_rgb[j, i, k] apply_one_line(pattern, xl, xr, &lah_image_rgb[j * lah_shape1 * lah_shape2], lah_shape2, quant_error) + cdef unsigned char bit4 cdef int best cdef int best_error = 2**31-1 cdef int total_error - cdef long flat, dist, bit4 + cdef long flat, dist cdef long r, g, b cdef (unsigned char)[:, ::1] distances = screen.palette.distances @@ -153,8 +155,6 @@ def dither_lookahead( free(lah_image_rgb) return options_4bit[best, 0], options_rgb[best, 0, :] -import functools - @functools.lru_cache(None) def lookahead_options(screen, lookahead, last_pixel_4bit, x): @@ -175,6 +175,27 @@ def lookahead_options(screen, lookahead, last_pixel_4bit, x): return options_4bit, options_rgb + +@cython.boundscheck(False) +@cython.wraparound(False) +def find_nearest_colour(screen, float[::1] pixel_rgb, unsigned char[::1] options_4bit, unsigned char[:, ::1] options_rgb): + cdef int best, dist + cdef unsigned char bit4 + cdef int best_dist = 2**8 + cdef long flat + + cdef (unsigned char)[:, ::1] distances = screen.palette.distances + for i in range(options_4bit.shape[0]): + flat = (pixel_rgb[0] << 16) + (pixel_rgb[1] << 8) + pixel_rgb[2] + bit4 = options_4bit[i] + dist = distances[flat, bit4] + if dist < best_dist: + best_dist = dist + best = i + + return options_4bit[best], options_rgb[best, :] + + @cython.boundscheck(False) @cython.wraparound(False) def dither_image( @@ -197,13 +218,17 @@ def dither_image( output_pixel_4bit = 0 for x in range(xres): input_pixel_rgb = image_rgb[y, x, :] - options_4bit, options_rgb = lookahead_options( - screen, lookahead, output_pixel_4bit, x % 4) - - output_pixel_4bit, output_pixel_rgb = \ - dither_lookahead( - screen, image_rgb, dither, x, y, options_4bit, - options_rgb, lookahead) + if lookahead: + palette_choices_4bit, palette_choices_rgb = lookahead_options( + screen, lookahead, output_pixel_4bit, x % 4) + output_pixel_4bit, output_pixel_rgb = \ + dither_lookahead( + screen, image_rgb, dither, x, y, palette_choices_4bit, + palette_choices_rgb, lookahead) + else: + palette_choices_4bit, palette_choices_rgb = screen.pixel_palette_options(output_pixel_4bit, x) + output_pixel_4bit, output_pixel_rgb = \ + find_nearest_colour(screen, input_pixel_rgb, palette_choices_4bit, palette_choices_rgb) for i in range(3): quant_error[i] = input_pixel_rgb[i] - output_pixel_rgb[i] image_rgb[y, x, i] = output_pixel_rgb[i] diff --git a/screen.py b/screen.py index 679ff81..0d64bee 100644 --- a/screen.py +++ b/screen.py @@ -66,15 +66,16 @@ class DHGR140Screen(Screen): (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() + pixel = image_4bit[y, x] 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())) + return ( + np.array(list(self.palette.RGB.keys()), dtype=np.uint8), + np.array(list(self.palette.RGB.values()), dtype=np.uint8)) class DHGR560Screen(Screen): @@ -100,7 +101,6 @@ class DHGR560Screen(Screen): 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([last_pixel_4bit, other_pixel_4bit], dtype=np.uint8), np.array([self.palette.RGB[last_pixel_4bit], - self.palette.RGB[other_pixel_4bit]])) - + self.palette.RGB[other_pixel_4bit]], dtype=np.uint8))