diff --git a/dither.pyx b/dither.pyx index 5f91f0d..bdf3d38 100644 --- a/dither.pyx +++ b/dither.pyx @@ -1,6 +1,7 @@ # cython: infer_types=True # cython: profile=True +import math cimport cython import functools import numpy as np @@ -82,8 +83,12 @@ def lookahead_options(object screen, int lookahead, unsigned char last_pixel_nbi # Two possible n-bit palette choices at position xx, given state of n pixels to left. # TODO: port screen.py to pyx palette_choices_nbit = screen.pixel_palette_options(output_pixel_nbit, xx) - output_pixel_nbit = palette_choices_nbit[(i & (1 << j)) >> j] - options_nbit[i, j] = output_pixel_nbit + next_bit_choice = (i & (1 << j)) >> j + options_nbit[i, j] = palette_choices_nbit[next_bit_choice] + output_pixel_nbit >>= 1 + output_pixel_nbit |= (next_bit_choice << 7) + # print(bin(i),j,bin(last_pixel_nbit), bin(output_pixel_nbit), bin(options_nbit[i, j])) + #print("Picking %s" % ((i & (1 << j)) >> j)) return options_nbit @@ -123,6 +128,8 @@ cdef int dither_lookahead(Dither* dither, float[:, ::1] palette_rgb, cdef int lah_shape2 = 3 cdef float *lah_image_rgb = malloc(lah_shape1 * lah_shape2 * sizeof(float)) + cdef unsigned char lookahead_bits + # For each 2**lookahead possibilities for the on/off state of the next lookahead pixels, apply error diffusion # and compute the total squared error to the source image. Since we only have two possible colours for each # given pixel (dependent on the state already chosen for pixels to the left), we need to look beyond local minima. @@ -153,13 +160,27 @@ cdef int dither_lookahead(Dither* dither, float[:, ::1] palette_rgb, #flat = (r << 16) + (g << 8) + b # bit4 = options_nbit[i, j] + ## XXX parametrize number of palette bits + #if j < 8: + # lookahead_bits = (last_bits >> (j+1)) + #else: + # lookahead_bits = 0 + #lookahead_bits |= (options_nbit[i, j]) << (7-j) + #lookahead_bits &= (1<<8)-1 + total_error += colour_distance_squared(lah_image_rgb[j*lah_shape2], lah_image_rgb[j*lah_shape2+1], lah_image_rgb[j*lah_shape2+2], palette_rgb[options_nbit[i,j]]) + + #if x > (560*3/4) and y == 180: + # print(x, bin(i), j, bin(options_nbit[i, j]), bin(best), best_error, total_error) + if total_error >= best_error: break + if total_error < best_error: best_error = total_error best = i + free(lah_image_rgb) return best @@ -193,7 +214,7 @@ cdef void apply_one_line(Dither* dither, int xl, int xr, int x, float[] image, i for i in range(xl, xr): error_fraction = dither.pattern[i - x + dither.x_origin] for j in range(3): - image[i * image_shape1 + j] = clip(image[i * image_shape1 + j] + error_fraction * quant_error[j], -100, 100) + image[i * image_shape1 + j] = clip(image[i * image_shape1 + j] + error_fraction * quant_error[j], 0 if j == 0 else -1, 1) # Perform error diffusion across multiple image rows. @@ -227,7 +248,7 @@ cdef void apply(Dither* dither, int x_res, int y_res, int x, int y, float[:,:,:: for j in range(xl, xr): error_fraction = dither.pattern[(i - y) * dither.x_shape + j - x + dither.x_origin] for k in range(3): - image[i,j,k] = clip(image[i,j,k] + error_fraction * quant_error[k], -100, 100) + image[i,j,k] = clip(image[i,j,k] + error_fraction * quant_error[k], 0 if k == 0 else -1, 1) # Compute closest colour from array of candidate n-bit colour palette values. # @@ -311,6 +332,8 @@ def dither_image(screen, float[:, :, ::1] image_rgb, dither, int lookahead, unsi cdef (unsigned char)[:, ::1] image_nbit = np.empty( (image_rgb.shape[0], image_rgb.shape[1]), dtype=np.uint8) + # print(lookahead_options(screen, lookahead, 0b11111111, 0)) + for y in range(yres): if verbose: print("%d/%d" % (y, yres)) @@ -327,17 +350,21 @@ def dither_image(screen, float[:, :, ::1] image_rgb, dither, int lookahead, unsi best_idx = dither_lookahead( &cdither, palette_rgb, image_rgb, x, y, lookahead_palette_choices_nbit, lookahead, xres) - output_pixel_nbit = lookahead_palette_choices_nbit[best_idx, 0] + output_pixel_nbit >>= 1 + output_pixel_nbit |= (best_idx & 0b1) << 7 # XXX n bit shift + # print("Picked %d" % (best_idx & 0b1)) + # lookahead_palette_choices_nbit[best_idx, 0] #else: # # Choose the closest colour among the available n-bit palette options # palette_choices_nbit = screen.pixel_palette_options(output_pixel_nbit, x) # output_pixel_nbit = find_nearest_colour(input_pixel_rgb, palette_choices_nbit, distances) # Apply error diffusion from chosen output pixel value - output_pixel_rgb = palette_rgb[output_pixel_nbit] + # print("picked %d, %s" % (lookahead_palette_choices_nbit[best_idx, 0], bin(lookahead_palette_choices_nbit[best_idx, 0]))) + output_pixel_rgb = palette_rgb[lookahead_palette_choices_nbit[best_idx, 0]] for i in range(3): quant_error[i] = input_pixel_rgb[i] - output_pixel_rgb[i] - image_nbit[y, x] = output_pixel_nbit + image_nbit[y, x] = lookahead_palette_choices_nbit[best_idx, 0] # output_pixel_nbit apply(&cdither, xres, yres, x, y, image_rgb, quant_error) for i in range(3): diff --git a/screen.py b/screen.py index f6e4054..157288f 100644 --- a/screen.py +++ b/screen.py @@ -140,15 +140,25 @@ class DHGR560Screen(Screen): return bitmap def pixel_palette_options(self, last_pixel_nbit, x: int): - # The two available colours for position x are given by the 4-bit - # value of position x-1, and the 4-bit value produced by toggling the - # value of the x % 4 bit (the current value of NTSC phase) - last_dots = self.palette.DOTS[last_pixel_nbit] - other_dots = list(last_dots) - other_dots[x % 4] = not other_dots[x % 4] - other_dots = tuple(other_dots) - other_pixel_nbit = self.palette.DOTS_TO_INDEX[other_dots] - return np.array([last_pixel_nbit, other_pixel_nbit], dtype=np.uint8) + last_dots = self.palette.DOTS[last_pixel_nbit][1:] + [None] + + # rearrange into palette order + next_dots = [None] * 8 + for i in range(4): + next_dots[(i - x) % 4] = last_dots[i] + next_dots[(i - x) % 4 + 4] = last_dots[i + 4] + + # XXX wrong + + assert next_dots[(3 - x) % 4 + 4] is None + #print(x, last_dots, next_dots) + + next_dots[(3 - x) % 4 + 4] = False + next_pixel_nbit_0 = self.palette.DOTS_TO_INDEX[next_dots] + + next_dots[(3 - x) % 4 + 4] = True + next_pixel_nbit_1 = self.palette.DOTS_TO_INDEX[next_dots] + return np.array([next_pixel_nbit_0, next_pixel_nbit_1], dtype=np.uint8) # TODO: refactor to share implementation with DHGR560Screen @@ -175,7 +185,8 @@ class DHGR560NTSCScreen(Screen): window indexed by x % 4, which gives the index into our 256-colour RGB palette. """ - image_rgb = np.empty((self.NATIVE_Y_RES, self.NATIVE_X_RES, 3), dtype=np.uint8) + image_rgb = np.empty((self.NATIVE_Y_RES, self.NATIVE_X_RES, 3), + dtype=np.uint8) for y in range(self.Y_RES): pixel = [False, False, False, False, False, False, False, False] for x in range(self.NATIVE_X_RES): @@ -186,19 +197,37 @@ class DHGR560NTSCScreen(Screen): return image_rgb def pixel_palette_options(self, last_pixel_nbit, x: int): - # The two available 8-bit pixel colour choices are given by: - # - Rotating the pixel value from the current x % 4 + 4 position to - # x % 4 - # - choosing 0 and 1 for the new values of x % 4 + 4 - next_dots0 = list(self.palette.DOTS[last_pixel_nbit]) - next_dots1 = list(next_dots0) - next_dots0[x % 4] = next_dots0[x % 4 + 4] - next_dots0[x % 4 + 4] = False - next_dots1[x % 4] = next_dots1[x % 4 + 4] - next_dots1[x % 4 + 4] = True - pixel_nbit_0 = self.palette.DOTS_TO_INDEX[tuple(next_dots0)] - pixel_nbit_1 = self.palette.DOTS_TO_INDEX[tuple(next_dots1)] - return np.array([pixel_nbit_0, pixel_nbit_1], dtype=np.uint8) + # # The two available 8-bit pixel colour choices are given by: + # # - Rotating the pixel value from the current x % 4 + 4 position to + # # x % 4 + # # - choosing 0 and 1 for the new values of x % 4 + 4 + # next_dots0 = list(self.palette.DOTS[last_pixel_nbit]) + # next_dots1 = list(next_dots0) + # next_dots0[x % 4] = next_dots0[x % 4 + 4] + # next_dots0[x % 4 + 4] = False + # next_dots1[x % 4] = next_dots1[x % 4 + 4] + # next_dots1[x % 4 + 4] = True + # pixel_nbit_0 = self.palette.DOTS_TO_INDEX[tuple(next_dots0)] + # pixel_nbit_1 = self.palette.DOTS_TO_INDEX[tuple(next_dots1)] + # return np.array([pixel_nbit_0, pixel_nbit_1], dtype=np.uint8) + last_dots = list(self.palette.DOTS[last_pixel_nbit][1:]) + [None] + + # rearrange into palette order + next_dots = [None] * 8 + for i in range(4): + next_dots[i] = last_dots[(i - x) % 4] + next_dots[i + 4] = last_dots[(i - x) % 4 + 4] + + assert next_dots[(3 + x) % 4 + 4] is None + # print(x, last_dots, next_dots) + + next_dots[(3 + x) % 4 + 4] = False + next_pixel_nbit_0 = self.palette.DOTS_TO_INDEX[tuple(next_dots)] + + next_dots[(3 + x) % 4 + 4] = True + next_pixel_nbit_1 = self.palette.DOTS_TO_INDEX[tuple(next_dots)] + return np.array([next_pixel_nbit_0, next_pixel_nbit_1], + dtype=np.uint8) def bitmap_to_ntsc(self, bitmap: np.ndarray) -> np.ndarray: y_width = 12