From 154a0b7c2948f699a91cf7dea8b02b03728f5935 Mon Sep 17 00:00:00 2001 From: kris Date: Tue, 31 Jan 2023 21:10:54 +0000 Subject: [PATCH] WIP - HGR support --- convert.py | 29 ++++ convert_hgr.py | 68 ++++++++++ dither_hgr.pyx | 331 ++++++++++++++++++++++++++++++++++++++++++++++ dither_pattern.py | 13 +- screen.py | 129 ++++++++++++++++++ setup.py | 2 +- 6 files changed, 570 insertions(+), 2 deletions(-) create mode 100644 convert_hgr.py create mode 100644 dither_hgr.pyx diff --git a/convert.py b/convert.py index 83d4f2f..fd7bc8a 100644 --- a/convert.py +++ b/convert.py @@ -3,6 +3,7 @@ import argparse import numpy as np +import convert_hgr as convert_hgr_py import convert_dhr as convert_dhr_py import convert_shr as convert_shr_py import dither_pattern @@ -47,6 +48,27 @@ def main(): parser = argparse.ArgumentParser() subparsers = parser.add_subparsers(required=True) + hgr_parser = subparsers.add_parser("hgr") + add_common_args(hgr_parser) + hgr_parser.add_argument( + '--dither', type=str, choices=list(dither_pattern.PATTERNS.keys()), + default=dither_pattern.DEFAULT_PATTERN, + help="Error distribution pattern to apply when dithering (default: " + + dither_pattern.DEFAULT_PATTERN + ")") + hgr_parser.add_argument( + '--palette', type=str, choices=list(set(palette_py.PALETTES.keys())), + default=palette_py.DEFAULT_PALETTE, + help='RGB colour palette to dither to. "ntsc" blends colours over 8 ' + 'pixels and gives better image quality on targets that ' + 'use/emulate NTSC, but can be substantially slower. Other ' + 'palettes determine colours based on 4 pixel sequences ' + '(default: ' + palette_py.DEFAULT_PALETTE + ")") + hgr_parser.add_argument( + '--show-palette', type=str, choices=list(palette_py.PALETTES.keys()), + help="RGB colour palette to use when --show_output (default: " + "value of --palette)") + hgr_parser.set_defaults(func=convert_hgr) + dhr_parser = subparsers.add_parser("dhr") add_common_args(dhr_parser) @@ -126,6 +148,13 @@ def convert_dhr(args): args.gamma_correct) convert_dhr_py.convert(screen, image, args) +def convert_hgr(args): + palette = palette_py.PALETTES[args.palette]() + screen = screen_py.HGRScreen(palette) + image = prepare_image(args.input, args.show_input, screen, + args.gamma_correct) + convert_hgr_py.convert(screen, image, args) + def convert_dhr_mono(args): screen = screen_py.DHGRScreen() diff --git a/convert_hgr.py b/convert_hgr.py new file mode 100644 index 0000000..b0446b5 --- /dev/null +++ b/convert_hgr.py @@ -0,0 +1,68 @@ +import os.path + +from PIL import Image +import numpy as np + +import dither_hgr as dither_hgr_pyx +import dither_pattern +import palette as palette_py +import screen as screen_py +import image as image_py + + +def _output(out_image: Image, args): + if args.show_output: + out_image.show() + + if args.save_preview: + # Save Double hi-res image + outfile = os.path.join( + os.path.splitext(args.output)[0] + "-preview.png") + out_image.save(outfile, "PNG") + + +def _write(screen: screen_py.HGRScreen, linear_bytemap: np.ndarray, args): + screen.pack_bytes(linear_bytemap) + with open(args.output, "wb") as f: + f.write(bytes(screen.main)) + + +def convert(screen: screen_py.HGRScreen, image: Image, args): + rgb = np.array(image).astype(np.float32) / 255 + + # Conversion matrix from RGB to CAM16UCS colour values. Indexed by + # 24-bit RGB value + base_dir = os.path.dirname(__file__) + rgb24_to_cam16ucs = np.load( + os.path.join(base_dir, "data/rgb24_to_cam16ucs.npy")) + + dither = dither_pattern.PATTERNS[args.dither]() + bitmap, linear_bytemap = dither_hgr_pyx.dither_image( + screen, rgb, dither, 8, args.verbose, rgb24_to_cam16ucs) + + # Show output image by rendering in target palette + output_palette_name = args.show_palette or args.palette + output_palette = palette_py.PALETTES[output_palette_name]() + output_screen = screen_py.HGRScreen(output_palette) + if output_palette_name == "ntsc": + output_srgb = output_screen.bitmap_to_image_ntsc(bitmap) + else: + output_srgb = image_py.linear_to_srgb( + 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 * 2, + srgb_output=True) + + _output(out_image, args) + _write(screen, linear_bytemap, args) + + +def convert_mono(screen: screen_py.DHGRScreen, image: Image, args): + image = image.convert("1") + + out_image = Image.fromarray((np.array(image) * 255).astype(np.uint8)) + out_image = image_py.resize( + out_image, screen.X_RES, screen.Y_RES * 2, srgb_output=True) + + _output(out_image, args) + _write(screen, np.array(image).astype(bool), args) diff --git a/dither_hgr.pyx b/dither_hgr.pyx new file mode 100644 index 0000000..407d52e --- /dev/null +++ b/dither_hgr.pyx @@ -0,0 +1,331 @@ +# cython: infer_types=True +# cython: profile=False +# cython: boundscheck=False +# cython: wraparound=False + +cimport cython +import numpy as np +from libc.stdlib cimport malloc, free + +cimport common + + +# TODO: use a cdef class +# C representation of dither_pattern.DitherPattern data, for efficient access. +cdef struct Dither: + + float* pattern # Flattened dither pattern + int x_shape + int y_shape + int x_origin + int y_origin + + +# Compute left-hand bounding box for dithering at horizontal position x. +cdef int dither_bounds_xl(Dither *dither, int x) nogil: + cdef int el = max(dither.x_origin - x, 0) + cdef int xl = x - dither.x_origin + el + return xl + + +#Compute right-hand bounding box for dithering at horizontal position x. +cdef int dither_bounds_xr(Dither *dither, int x_res, int x) nogil: + cdef int er = min(dither.x_shape, x_res - x) + cdef int xr = x - dither.x_origin + er + return xr + + +# Compute upper bounding box for dithering at vertical position y. +cdef int dither_bounds_yt(Dither *dither, int y) nogil: + cdef int et = max(dither.y_origin - y, 0) + cdef int yt = y - dither.y_origin + et + + return yt + + +# Compute lower bounding box for dithering at vertical position y. +cdef int dither_bounds_yb(Dither *dither, int y_res, int y) nogil: + cdef int eb = min(dither.y_shape, y_res - y) + cdef int yb = y - dither.y_origin + eb + return yb + + +cdef inline unsigned char shift_pixel_window( + unsigned char last_pixels, + unsigned int next_pixels, + unsigned char shift_right_by, + unsigned char window_width) nogil: + """Right-shift a sliding window of n pixels to incorporate new pixels. + + Args: + last_pixels: n-bit value representing n pixels from left up to current position (MSB = current pixel). + next_pixels: n-bit value representing n pixels to right of current position (LSB = pixel to right) + shift_right_by: how many pixels of next_pixels to shift into the sliding window + window_width: how many pixels to maintain in the sliding window (must be <= 8) + + Returns: n-bit value representing shifted pixel window + """ + cdef unsigned char window_mask = 0xff >> (8 - window_width) + cdef unsigned int shifted_next_pixels + + if window_width > shift_right_by: + shifted_next_pixels = next_pixels << (window_width - shift_right_by) + else: + shifted_next_pixels = next_pixels >> (shift_right_by - window_width) + return ((last_pixels >> shift_right_by) | shifted_next_pixels) & window_mask + + +cdef unsigned int compute_fat_pixels(unsigned int screen_byte, unsigned char last_pixels) nogil: + cdef int i, bit, fat_bit + cdef unsigned int result = 0 + result = 0 + for i in range(7): + bit = (screen_byte >> i) & 0b1 + fat_bit = bit << 1 | bit + result |= (fat_bit) << (2 * i) + if screen_byte & 0x80: + # Palette bit shifts to the right + result <<= 1 + result |= (last_pixels >> 7) + + return result + + +# Look ahead a number of pixels and compute choice for next pixel with lowest total squared error after dithering. +# +# Args: +# dither: error diffusion pattern to apply +# palette_rgb: matrix of all n-bit colour palette RGB values +# image_rgb: RGB image in the process of dithering +# x: current horizontal screen position +# y: current vertical screen position +# options_nbit: matrix of (2**lookahead, lookahead) possible n-bit colour choices at positions x .. x + lookahead +# lookahead: how many horizontal pixels to look ahead +# distances: matrix of (24-bit RGB, n-bit palette) perceptual colour distances +# x_res: horizontal screen resolution +# +# Returns: index from 0 .. 2**lookahead into options_nbit representing best available choice for position (x,y) +# +cdef int dither_lookahead(Dither* dither, float[:, :, ::1] palette_cam16, float[:, :, ::1] palette_rgb, + float[:, :, ::1] image_rgb, int x, int y, int lookahead, unsigned char last_pixels, + int x_res, float[:,::1] rgb_to_cam16ucs, unsigned char palette_depth) nogil: + cdef int candidate_pixels, i, j, fat_pixels + cdef float[3] quant_error + cdef int best + cdef float best_error = 2**31-1 + cdef float total_error + cdef unsigned char next_pixels + cdef int phase + cdef float[::1] lah_cam16ucs + + # Don't bother dithering past the lookahead horizon or edge of screen. + cdef int xxr = min(x + 14, x_res) # XXX + + cdef int lah_shape1 = xxr - x + cdef int lah_shape2 = 3 + cdef float *lah_image_rgb = malloc(lah_shape1 * lah_shape2 * sizeof(float)) + + # 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. + # i.e. it might be better to make a sub-optimal choice for this pixel if it allows access to much better pixel + # colours at later positions. + for candidate_pixels in range(1 << lookahead): + # Working copy of input pixels + for i in range(xxr - x): + for j in range(3): + lah_image_rgb[i * lah_shape2 + j] = image_rgb[y, x+i, j] + + total_error = 0 + + fat_pixels = compute_fat_pixels(candidate_pixels, last_pixels) + + # Apply dithering to lookahead horizon or edge of screen + for i in range(xxr - x): + xl = dither_bounds_xl(dither, i) + xr = dither_bounds_xr(dither, xxr - x, i) + phase = (x + i + 3) % 4 # XXX + + next_pixels = shift_pixel_window( + last_pixels, next_pixels=fat_pixels, shift_right_by=i+1, window_width=palette_depth) + + # We don't update the input at position x (since we've already chosen fixed outputs), but we do propagate + # quantization errors to positions >x so we can compensate for how good/bad these choices were. i.e. the + # next_pixels choices are fixed, but we can still distribute quantization error from having made these + # choices, in order to compute the total error. + for j in range(3): + quant_error[j] = lah_image_rgb[i * lah_shape2 + j] - palette_rgb[next_pixels, phase, j] + apply_one_line(dither, xl, xr, i, lah_image_rgb, lah_shape2, quant_error) + + lah_cam16ucs = common.convert_rgb_to_cam16ucs( + rgb_to_cam16ucs, lah_image_rgb[i*lah_shape2], lah_image_rgb[i*lah_shape2+1], + lah_image_rgb[i*lah_shape2+2]) + total_error += common.colour_distance_squared(lah_cam16ucs, palette_cam16[next_pixels, phase]) + + if total_error >= best_error: + # No need to continue + break + + if total_error < best_error: + best_error = total_error + best = candidate_pixels + + free(lah_image_rgb) + return best + + +# Perform error diffusion to a single image row. +# +# Args: +# dither: dither pattern to apply +# xl: lower x bounding box +# xr: upper x bounding box +# x: starting horizontal position to apply error diffusion +# image: array of shape (image_shape1, 3) representing RGB pixel data for a single image line, to be mutated. +# image_shape1: horizontal dimension of image +# quant_error: RGB quantization error to be diffused +# +cdef void apply_one_line(Dither* dither, int xl, int xr, int x, float[] image, int image_shape1, + float[] quant_error) nogil: + + cdef int i, j + cdef float error_fraction + + 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] = common.clip(image[i * image_shape1 + j] + error_fraction * quant_error[j], 0, 1) + + +# Perform error diffusion across multiple image rows. +# +# Args: +# dither: dither pattern to apply +# x_res: horizontal image resolution +# y_res: vertical image resolution +# x: starting horizontal position to apply error diffusion +# y: starting vertical position to apply error diffusion +# image: RGB pixel data, to be mutated +# quant_error: RGB quantization error to be diffused +# +cdef void apply(Dither* dither, int x_res, int y_res, int x, int y, float[:,:,::1] image, float[] quant_error) nogil: + + cdef int i, j, k + + cdef int yt = dither_bounds_yt(dither, y) + cdef int yb = dither_bounds_yb(dither, y_res, y) + cdef int xl = dither_bounds_xl(dither, x) + cdef int xr = dither_bounds_xr(dither, x_res, x) + + cdef float error_fraction + for i in range(yt, yb): + 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] = common.clip(image[i,j,k] + error_fraction * quant_error[k], 0, 1) + + +cdef image_nbit_to_bitmap( + (unsigned char)[:, ::1] image_nbit, unsigned int x_res, unsigned int y_res, unsigned char palette_depth): + cdef unsigned int x, y + bitmap = np.zeros((y_res, x_res), dtype=bool) + for y in range(y_res): + for x in range(x_res): + # MSB of each array element is the pixel state at (x, y) + bitmap[y, x] = image_nbit[y, x] >> (palette_depth - 1) + return bitmap + + +# Dither a source image +# +# Args: +# screen: screen.Screen object +# image_rgb: input RGB image +# dither: dither_pattern.DitherPattern to apply during dithering +# lookahead: how many x positions to look ahead to optimize colour choices +# verbose: whether to output progress during image conversion +# +# Returns: tuple of n-bit output image array and RGB output image array +# +def dither_image( + screen, float[:, :, ::1] image_rgb, dither, int lookahead, unsigned char verbose, float[:,::1] rgb_to_cam16ucs): + cdef int y, x + cdef unsigned char i, j, pixels_nbit, phase + # cdef float[3] input_pixel_rgb + cdef float[3] quant_error + cdef unsigned char output_pixel_nbit + cdef unsigned int best_next_pixels + cdef float[3] output_pixel_rgb + + # Hoist some python attribute accesses into C variables for efficient access during the main loop + + cdef int yres = screen.Y_RES + cdef int xres = screen.X_RES + + # TODO: convert this instead of storing on palette? + cdef float[:, :, ::1] palette_cam16 = np.zeros((len(screen.palette.CAM16UCS), 4, 3), dtype=np.float32) + for pixels_nbit, phase in screen.palette.CAM16UCS.keys(): + for i in range(3): + palette_cam16[pixels_nbit, phase, i] = screen.palette.CAM16UCS[pixels_nbit, phase][i] + + cdef float[:, :, ::1] palette_rgb = np.zeros((len(screen.palette.RGB), 4, 3), dtype=np.float32) + for pixels_nbit, phase in screen.palette.RGB.keys(): + for i in range(3): + palette_rgb[pixels_nbit, phase, i] = screen.palette.RGB[pixels_nbit, phase][i] / 255 + + cdef Dither cdither + cdither.y_shape = dither.PATTERN.shape[0] + cdither.x_shape = dither.PATTERN.shape[1] + cdither.y_origin = dither.ORIGIN[0] + cdither.x_origin = dither.ORIGIN[1] + # TODO: should be just as efficient to use a memoryview? + cdither.pattern = malloc(cdither.x_shape * cdither.y_shape * sizeof(float)) + for i in range(cdither.y_shape): + for j in range(cdither.x_shape): + cdither.pattern[i * cdither.x_shape + j] = dither.PATTERN[i, j] + + cdef unsigned char palette_depth = screen.palette.PALETTE_DEPTH + + # The nbit image representation contains the trailing n dot values as an n-bit value with MSB representing the + # current pixel. This choice (cf LSB) is slightly awkward but matches the DHGR behaviour that bit positions in + # screen memory map LSB to MSB from L to R. The value of n is chosen by the palette depth, i.e. how many trailing + # dot positions are used to determine the colour of a given pixel. + cdef (unsigned char)[:, ::1] image_nbit = np.empty((image_rgb.shape[0], image_rgb.shape[1]), dtype=np.uint8) + + cdef (unsigned char)[:, ::1] linear_bytemap = np.zeros((192, 40), dtype=np.uint8) + cdef unsigned int fat_pixels + + for y in range(yres): + if verbose: + print("%d/%d" % (y, yres)) + output_pixel_nbit = 0 + for x in range(xres): + if x % 14 == 0: + # Compute all possible 2**N choices of n-bit pixel colours for positions x .. x + lookahead + # lookahead_palette_choices_nbit = lookahead_options(lookahead, output_pixel_nbit) + # Apply error diffusion for each of these 2**N choices, and compute which produces the closest match + # to the source image over the succeeding N pixels + best_next_pixels = dither_lookahead( + &cdither, palette_cam16, palette_rgb, image_rgb, x, y, lookahead, output_pixel_nbit, xres, + rgb_to_cam16ucs, palette_depth) + linear_bytemap[y, x // 14] = best_next_pixels + fat_pixels = compute_fat_pixels(best_next_pixels, output_pixel_nbit) + # print(y, x, best_next_pixels,bin(fat_pixels)) + + # Apply best choice for next 1 pixel + output_pixel_nbit = shift_pixel_window( + output_pixel_nbit, fat_pixels, shift_right_by=x%14 + 1, window_width=palette_depth) + # print(x, bin(output_pixel_nbit)) + # Apply error diffusion from chosen output pixel value + for i in range(3): + output_pixel_rgb[i] = palette_rgb[output_pixel_nbit, x % 4, i] + quant_error[i] = image_rgb[y,x,i] - output_pixel_rgb[i] + apply(&cdither, xres, yres, x, y, image_rgb, quant_error) + + # Update image with our chosen image pixel + image_nbit[y, x] = output_pixel_nbit + for i in range(3): + image_rgb[y, x, i] = output_pixel_rgb[i] + + free(cdither.pattern) + return image_nbit_to_bitmap(image_nbit, xres, yres, palette_depth), linear_bytemap diff --git a/dither_pattern.py b/dither_pattern.py index 8cb8e84..2ae73cb 100644 --- a/dither_pattern.py +++ b/dither_pattern.py @@ -76,6 +76,15 @@ class JarvisModifiedDither(DitherPattern): PATTERN /= np.sum(PATTERN) ORIGIN = (0, 2) +class KrisDither(DitherPattern): + """Default dither from bmp2dhr.""" + + # 0 * 7 + # 3 5 1 + PATTERN = np.array(((0, 0, 7), (3, 5, 1)), + dtype=np.float32).reshape(2, 3) / np.float32(24) + ORIGIN = (0, 1) + PATTERNS = { 'floyd': FloydSteinbergDither, @@ -84,7 +93,9 @@ PATTERNS = { 'buckels': BuckelsDither, 'jarvis': JarvisDither, 'jarvis-mod': JarvisModifiedDither, - 'none': NoDither + 'none': NoDither, + 'kris': KrisDither, + } DEFAULT_PATTERN = 'floyd' diff --git a/screen.py b/screen.py index 32108ca..dce4200 100644 --- a/screen.py +++ b/screen.py @@ -110,6 +110,135 @@ class DHGRScreen: self.main[addr:addr + 40] = main_col[y, :] return +class HGRScreen: + X_RES = 560 + Y_RES = 192 + + def __init__(self, palette: palette_py.Palette): + self.main = np.zeros(8192, dtype=np.uint8) + self.palette = palette + super(HGRScreen, self).__init__() + + @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 + + @staticmethod + def compute_fat_pixels(screen_byte, last_pixels): + result = 0 + for i in range(7): + bit = (screen_byte >> i) & 0b1 + fat_bit = bit << 1 | bit + result |= fat_bit << (2 * i) + if screen_byte & 0x80: + # Palette bit shifts to the right + result <<= 1 + result |= (last_pixels >> 7) + + return result + + def pack_bytes(self, linear_bytemap: np.ndarray): + """Packs an image into memory format (8K main).""" + + for y in range(self.Y_RES): + addr = self.y_to_base_addr(y) + self.main[addr:addr + 40] = linear_bytemap[y, :] + return + + def bitmap_to_image_rgb(self, bitmap: np.ndarray) -> np.ndarray: + """Convert our 2-bit bitmap image into a RGB image. + + Colour at every pixel is determined by the value of an n-bit sliding + window and x % 4, which give the index into our RGB palette. + """ + image_rgb = np.empty((self.Y_RES, self.X_RES, 3), dtype=np.uint8) + for y in range(self.Y_RES): + bitmap_window = [False] * self.palette.PALETTE_DEPTH + for x in range(self.X_RES): + # Maintain a sliding window of pixels of width PALETTE_DEPTH + bitmap_window = bitmap_window[1:] + [bitmap[y, x]] + image_rgb[y, x, :] = self.palette.RGB[ + self.palette.bitmap_to_idx( + np.array(bitmap_window, dtype=bool)), x % 4] + return image_rgb + + @staticmethod + def _sin(pos, phase0=9): + x = pos % 12 + phase0 + return np.sin(x * 2 * np.pi / 12) + + @staticmethod + def _cos(pos, phase0=9): + x = pos % 12 + phase0 + return np.cos(x * 2 * np.pi / 12) + + def _read(self, line, pos): + if pos < 0: + return 0 + return 1 if line[pos] else 0 + + def bitmap_to_image_ntsc(self, bitmap: np.ndarray) -> np.ndarray: + y_width = 12 + u_width = 24 + v_width = 24 + + contrast = 1 + # TODO: This is necessary to match OpenEmulator. I think it is because + # they introduce an extra (unexplained) factor of 2 when applying the + # Chebyshev/Lanczos filtering to the u and v components. + saturation = 2 + # TODO: this phase shift is necessary to match OpenEmulator. I'm not + # sure where it comes from - e.g. it doesn't match the phaseInfo + # calculation for the signal phase at the start of the visible region. + hue = 0.2 * (2 * np.pi) + + # Apply effect of saturation + yuv_to_rgb = np.array( + ((1, 0, 0), (0, saturation, 0), (0, 0, saturation)), + dtype=np.float32) + # Apply hue phase rotation + yuv_to_rgb = np.matmul(np.array( + ((1, 0, 0), (0, np.cos(hue), np.sin(hue)), (0, -np.sin(hue), + np.cos(hue)))), + yuv_to_rgb) + # Y'UV to R'G'B' conversion + yuv_to_rgb = np.matmul(np.array( + ((1, 0, 1.139883), (1, -0.394642, -.5806227), (1, 2.032062, 0))), + yuv_to_rgb) + # Apply effect of contrast + yuv_to_rgb *= contrast + + out_rgb = np.empty((bitmap.shape[0], bitmap.shape[1] * 3, 3), + dtype=np.uint8) + for y in range(bitmap.shape[0]): + ysum = 0 + usum = 0 + vsum = 0 + line = np.repeat(bitmap[y], 3) + + for x in range(bitmap.shape[1] * 3): + ysum += self._read(line, x) - self._read(line, x - y_width) + usum += self._read(line, x) * self._sin(x) - self._read( + line, x - u_width) * self._sin((x - u_width)) + vsum += self._read(line, x) * self._cos(x) - self._read( + line, x - v_width) * self._cos((x - v_width)) + rgb = np.matmul( + yuv_to_rgb, np.array( + (ysum / y_width, usum / u_width, + vsum / v_width)).reshape((3, 1))).reshape(3) + r = min(255, max(0, rgb[0] * 255)) + g = min(255, max(0, rgb[1] * 255)) + b = min(255, max(0, rgb[2] * 255)) + out_rgb[y, x, :] = (r, g, b) + + return out_rgb + class DHGRNTSCScreen(DHGRScreen): def __init__(self, palette: palette_py.Palette): diff --git a/setup.py b/setup.py index c2271bc..5c273c6 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ Cython.Compiler.Options.annotate = True setup( ext_modules=cythonize( - ["common.pyx", "dither_dhr.pyx", "dither_shr.pyx"], + ["common.pyx", "dither_dhr.pyx", "dither_hgr.pyx", "dither_shr.pyx"], annotate=True, compiler_directives={'language_level': "3"} )