From eaf8bc0abe323c7ee2c0ee855eb191718bf14f6d Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Fri, 29 Oct 2021 14:01:47 -0500 Subject: [PATCH] bitmaptools: add dither This can convert a BGR565_SWAPPED bitmap to B&W in about 82ms on esp32-s2. --- locale/circuitpython.pot | 20 +++ shared-bindings/bitmaptools/__init__.c | 90 ++++++++++- shared-bindings/bitmaptools/__init__.h | 9 ++ shared-module/bitmaptools/__init__.c | 201 ++++++++++++++++++++++++- 4 files changed, 317 insertions(+), 3 deletions(-) diff --git a/locale/circuitpython.pot b/locale/circuitpython.pot index e39fef97e7..79b0464e3d 100644 --- a/locale/circuitpython.pot +++ b/locale/circuitpython.pot @@ -2611,6 +2611,10 @@ msgstr "" msgid "binary op %q not implemented" msgstr "" +#: shared-bindings/bitmaptools/__init__.c +msgid "bitmap sizes must match" +msgstr "" + #: extmod/modurandom.c msgid "bits must be 32 or less" msgstr "" @@ -4102,6 +4106,18 @@ msgstr "" msgid "source palette too large" msgstr "" +#: shared-bindings/bitmaptools/__init__.c +msgid "source_bitmap must have value_count of 2 or 65536" +msgstr "" + +#: shared-bindings/bitmaptools/__init__.c +msgid "source_bitmap must have value_count of 65536" +msgstr "" + +#: shared-bindings/bitmaptools/__init__.c +msgid "source_bitmap must have value_count of 8" +msgstr "" + #: py/objstr.c msgid "start/end indices" msgstr "" @@ -4350,6 +4366,10 @@ msgstr "" msgid "unsupported colorspace for GifWriter" msgstr "" +#: shared-bindings/bitmaptools/__init__.c +msgid "unsupported colorspace for dither" +msgstr "" + #: py/objstr.c #, c-format msgid "unsupported format character '%c' (0x%x) at index %d" diff --git a/shared-bindings/bitmaptools/__init__.c b/shared-bindings/bitmaptools/__init__.c index ad814e8881..69e2a35b01 100644 --- a/shared-bindings/bitmaptools/__init__.c +++ b/shared-bindings/bitmaptools/__init__.c @@ -25,11 +25,14 @@ */ #include "shared-bindings/displayio/Bitmap.h" +#include "shared-bindings/displayio/Palette.h" +#include "shared-bindings/displayio/ColorConverter.h" #include "shared-bindings/bitmaptools/__init__.h" #include #include "py/binary.h" +#include "py/enum.h" #include "py/obj.h" #include "py/runtime.h" @@ -565,9 +568,92 @@ STATIC mp_obj_t bitmaptools_readinto(size_t n_args, const mp_obj_t *pos_args, mp return mp_const_none; } - MP_DEFINE_CONST_FUN_OBJ_KW(bitmaptools_readinto_obj, 0, bitmaptools_readinto); +//| class DitherAlgorithm: +//| """Identifies the algorith for dither to use""" +//| +//| Atkinson: object +//| """The classic Atkinson dither, often associated with the Hypercard esthetic""" +//| +//| FloydStenberg: object +//| """The Floyd-Stenberg dither""" +//| +MAKE_ENUM_VALUE(bitmaptools_dither_algorithm_type, dither_algorithm, Atkinson, DITHER_ALGORITHM_ATKINSON); +MAKE_ENUM_VALUE(bitmaptools_dither_algorithm_type, dither_algorithm, FloydStenberg, DITHER_ALGORITHM_ATKINSON); + +MAKE_ENUM_MAP(bitmaptools_dither_algorithm) { + MAKE_ENUM_MAP_ENTRY(dither_algorithm, Atkinson), + MAKE_ENUM_MAP_ENTRY(dither_algorithm, FloydStenberg), +}; +STATIC MP_DEFINE_CONST_DICT(bitmaptools_dither_algorithm_locals_dict, bitmaptools_dither_algorithm_locals_table); + +MAKE_PRINTER(bitmaptools, bitmaptools_dither_algorithm); + +MAKE_ENUM_TYPE(bitmaptools, DitherAlgorithm, bitmaptools_dither_algorithm); + +//| def dither(dest_bitmap: displayio.Bitmap, source_bitmapp: displayio.Bitmap, source_colorspace: displayio.Colorspace, algorithm: DitherAlgorithm=DitherAlgorithm.Atkinson) -> None: +//| """Convert the input image into a 2-level output image using the given dither algorithm. +//| +//| :param bitmap dest_bitmap: Destination bitmap. It must have a value_count of 2 or 65536. The stored values are 0 and the maximum pixel value. +//| :param bitmap source_bitmap: Source bitmap that contains the graphical region to be dithered. It must have a value_count of 65536. +//| :param colorspace: The colorspace of the image. The supported colorspaces are ``RGB565``, ``BGR565``, ``RGB565_SWAPPED``, and ``BGR565_SWAPPED`` +//| :param algorithm: The dither algorithm to use, one of the `DitherAlgorithm `values. +//| """ +//| ... +//| +STATIC mp_obj_t bitmaptools_dither(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { + enum { ARG_dest_bitmap, ARG_source_bitmap, ARG_source_colorspace, ARG_algorithm }; + static const mp_arg_t allowed_args[] = { + { MP_QSTR_dest_bitmap, MP_ARG_REQUIRED | MP_ARG_OBJ }, + { MP_QSTR_source_bitmap, MP_ARG_REQUIRED | MP_ARG_OBJ }, + { MP_QSTR_source_colorspace, MP_ARG_REQUIRED | MP_ARG_OBJ }, + { MP_QSTR_algorithm, MP_ARG_OBJ, { .u_obj = MP_ROM_PTR((void *)&dither_algorithm_Atkinson_obj) } }, + }; + mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)]; + mp_arg_parse_all(n_args, pos_args, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args); + displayio_bitmap_t *source_bitmap = mp_arg_validate_type(args[ARG_source_bitmap].u_obj, &displayio_bitmap_type, MP_QSTR_source_bitmap); + displayio_bitmap_t *dest_bitmap = mp_arg_validate_type(args[ARG_dest_bitmap].u_obj, &displayio_bitmap_type, MP_QSTR_dest_bitmap); + bitmaptools_dither_algorithm_t algorithm = cp_enum_value(&bitmaptools_dither_algorithm_type, args[ARG_algorithm].u_obj); + displayio_colorspace_t colorspace = cp_enum_value(&displayio_colorspace_type, args[ARG_source_colorspace].u_obj); + + if (source_bitmap->width != dest_bitmap->width || source_bitmap->height != dest_bitmap->height) { + mp_raise_TypeError(translate("bitmap sizes must match")); + } + + if (dest_bitmap->bits_per_value != 16 && dest_bitmap->bits_per_value != 1) { + mp_raise_TypeError(translate("source_bitmap must have value_count of 2 or 65536")); + } + + + switch (colorspace) { + case DISPLAYIO_COLORSPACE_RGB565: + case DISPLAYIO_COLORSPACE_RGB565_SWAPPED: + case DISPLAYIO_COLORSPACE_BGR565: + case DISPLAYIO_COLORSPACE_BGR565_SWAPPED: + if (source_bitmap->bits_per_value != 16) { + mp_raise_TypeError(translate("source_bitmap must have value_count of 65536")); + } + break; + + case DISPLAYIO_COLORSPACE_L8: + if (source_bitmap->bits_per_value != 8) { + mp_raise_TypeError(translate("source_bitmap must have value_count of 8")); + } + break; + + default: + mp_raise_TypeError(translate("unsupported colorspace for dither")); + } + + + common_hal_bitmaptools_dither(dest_bitmap, source_bitmap, colorspace, algorithm); + + return mp_const_none; +} +MP_DEFINE_CONST_FUN_OBJ_KW(bitmaptools_dither_obj, 0, bitmaptools_dither); + + STATIC const mp_rom_map_elem_t bitmaptools_module_globals_table[] = { { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_bitmaptools) }, { MP_ROM_QSTR(MP_QSTR_readinto), MP_ROM_PTR(&bitmaptools_readinto_obj) }, @@ -576,6 +662,8 @@ STATIC const mp_rom_map_elem_t bitmaptools_module_globals_table[] = { { MP_ROM_QSTR(MP_QSTR_fill_region), MP_ROM_PTR(&bitmaptools_fill_region_obj) }, { MP_ROM_QSTR(MP_QSTR_boundary_fill), MP_ROM_PTR(&bitmaptools_boundary_fill_obj) }, { MP_ROM_QSTR(MP_QSTR_draw_line), MP_ROM_PTR(&bitmaptools_draw_line_obj) }, + { MP_ROM_QSTR(MP_QSTR_dither), MP_ROM_PTR(&bitmaptools_dither_obj) }, + { MP_ROM_QSTR(MP_QSTR_DitherAlgorithm), MP_ROM_PTR(&bitmaptools_dither_algorithm_type) }, }; STATIC MP_DEFINE_CONST_DICT(bitmaptools_module_globals, bitmaptools_module_globals_table); diff --git a/shared-bindings/bitmaptools/__init__.h b/shared-bindings/bitmaptools/__init__.h index 6415b20258..fb9d783e55 100644 --- a/shared-bindings/bitmaptools/__init__.h +++ b/shared-bindings/bitmaptools/__init__.h @@ -28,9 +28,17 @@ #define MICROPY_INCLUDED_SHARED_BINDINGS_BITMAPTOOLS__INIT__H #include "shared-module/displayio/Bitmap.h" +#include "shared-module/displayio/Palette.h" +#include "shared-bindings/displayio/ColorConverter.h" #include "py/obj.h" #include "extmod/vfs_fat.h" +typedef enum { + DITHER_ALGORITHM_ATKINSON, DITHER_ALGORITHM_FLOYD_STENBERG, +} bitmaptools_dither_algorithm_t; + +extern const mp_obj_type_t bitmaptools_dither_algorithm_type; + void common_hal_bitmaptools_rotozoom(displayio_bitmap_t *self, int16_t ox, int16_t oy, int16_t dest_clip0_x, int16_t dest_clip0_y, int16_t dest_clip1_x, int16_t dest_clip1_y, @@ -57,5 +65,6 @@ void common_hal_bitmaptools_draw_line(displayio_bitmap_t *destination, void common_hal_bitmaptools_readinto(displayio_bitmap_t *self, pyb_file_obj_t *file, int element_size, int bits_per_pixel, bool reverse_pixels_in_word, bool swap_bytes, bool reverse_rows); void common_hal_bitmaptools_arrayblit(displayio_bitmap_t *self, void *data, int element_size, int x1, int y1, int x2, int y2, bool skip_specified, uint32_t skip_index); +void common_hal_bitmaptools_dither(displayio_bitmap_t *dest_bitmap, displayio_bitmap_t *source_bitmap, displayio_colorspace_t colorspace, bitmaptools_dither_algorithm_t algorithm); #endif // MICROPY_INCLUDED_SHARED_BINDINGS_BITMAPTOOLS__INIT__H diff --git a/shared-module/bitmaptools/__init__.c b/shared-module/bitmaptools/__init__.c index b6e0764fdb..c75bc599cb 100644 --- a/shared-module/bitmaptools/__init__.c +++ b/shared-module/bitmaptools/__init__.c @@ -26,13 +26,17 @@ #include "shared-bindings/bitmaptools/__init__.h" #include "shared-bindings/displayio/Bitmap.h" +#include "shared-bindings/displayio/Palette.h" +#include "shared-bindings/displayio/ColorConverter.h" #include "shared-module/displayio/Bitmap.h" #include "py/runtime.h" #include "py/mperrno.h" -#include "math.h" -#include "stdlib.h" +#include +#include +#include +#include void common_hal_bitmaptools_rotozoom(displayio_bitmap_t *self, int16_t ox, int16_t oy, int16_t dest_clip0_x, int16_t dest_clip0_y, @@ -602,3 +606,196 @@ void common_hal_bitmaptools_readinto(displayio_bitmap_t *self, pyb_file_obj_t *f } } } + +typedef struct { + uint8_t count; // The number of items in terms[] + uint8_t mx; // the maximum of the absolute value of the dx values + uint8_t dl; // the scaled dither value applied to the pixel at distance [1,0] + struct { // dl is the scaled dither values applied to the pixel at [dx,dy] + int8_t dx, dy, dl; + } terms[]; +} bitmaptools_dither_algorithm_info_t; + +static bitmaptools_dither_algorithm_info_t atkinson = { + 4, 2, 256 / 8, { + {2, 0, 256 / 8}, + {-1, 1, 256 / 8}, + {0, 1, 256 / 8}, + {0, 2, 256 / 8}, + } +}; + +static bitmaptools_dither_algorithm_info_t floyd_stenberg = { + 3, 1, 7 * 256 / 16, + { + {-1, 1, 3 * 256 / 16}, + {0, 1, 5 * 256 / 16}, + {1, 1, 1 * 256 / 16}, + } +}; + +bitmaptools_dither_algorithm_info_t *algorithms[] = { + [DITHER_ALGORITHM_ATKINSON] = &atkinson, + [DITHER_ALGORITHM_FLOYD_STENBERG] = &floyd_stenberg, +}; + +enum { + SWAP_BYTES = 1 << 0, + SWAP_RB = 1 << 1, +}; + +STATIC void fill_row(displayio_bitmap_t *bitmap, int swap, int16_t *luminance_data, int y, int mx) { + if (y >= bitmap->height) { + return; + } + + // zero out padding area + for (int i = 0; i < mx; i++) { + luminance_data[-mx + i] = 0; + luminance_data[bitmap->width + i] = 0; + } + + if (bitmap->bits_per_value == 8) { + uint8_t *pixel_data = (uint8_t *)(bitmap->data + bitmap->stride * y); + for (int x = 0; x < bitmap->width; x++) { + *luminance_data++ = *pixel_data++; + } + } else { + uint16_t *pixel_data = (uint16_t *)(bitmap->data + bitmap->stride * y); + for (int x = 0; x < bitmap->width; x++) { + uint16_t pixel = *pixel_data++; + if (swap & SWAP_BYTES) { + pixel = __builtin_bswap16(pixel); + } + int r = (pixel >> 8) & 0xf8; + int g = (pixel >> 3) & 0xfc; + int b = (pixel << 3) & 0xf8; + + if (swap & SWAP_RB) { + uint8_t tmp = r; + r = b; + b = tmp; + } + + // ideal coefficients are around .299, .587, .114 (according to + // ppmtopnm), this differs from the 'other' luma-converting + // function in circuitpython (why?) + + // we correct for the fact that the input ranges are 0..0xf8 (or + // 0xfc) rather than 0x00..0xff + // Check: (0xf8 * 78 + 0xfc * 154 + 0xf8 * 29) // 256 == 255 + *luminance_data++ = (r * 78 + g * 154 + b * 29) / 256; + } + } +} + +static void write_pixels(displayio_bitmap_t *bitmap, int y, bool *data) { + if (bitmap->bits_per_value == 1) { + uint32_t *pixel_data = (uint32_t *)(bitmap->data + bitmap->stride * y); + for (int i = 0; i < bitmap->stride; i++) { + uint32_t p = 0; + for (int j = 0; j < 32; i++) { + p = (p << 1); + if (*data++) { + p |= 1; + } + } + *pixel_data++ = p; + } + } else { + uint16_t *pixel_data = (uint16_t *)(bitmap->data + bitmap->stride * y); + for (int i = 0; i < bitmap->width; i++) { + *pixel_data++ = *data++ ? 65535 : 0; + } + } +} + +void common_hal_bitmaptools_dither(displayio_bitmap_t *dest_bitmap, displayio_bitmap_t *source_bitmap, displayio_colorspace_t colorspace, bitmaptools_dither_algorithm_t algorithm) { + int height = dest_bitmap->height, width = dest_bitmap->width; + + int swap = 0; + if (colorspace == DISPLAYIO_COLORSPACE_RGB565_SWAPPED || colorspace == DISPLAYIO_COLORSPACE_BGR565_SWAPPED) { + swap |= SWAP_BYTES; + } + if (colorspace == DISPLAYIO_COLORSPACE_BGR565 || colorspace == DISPLAYIO_COLORSPACE_BGR565_SWAPPED) { + swap |= SWAP_RB; + } + + bitmaptools_dither_algorithm_info_t *info = algorithms[algorithm]; + // rowdata holds 3 rows of data. Each one is larger than the input + // bitmap's width, beacuse `mx` extra pixels are allocated at the start and + // end of the row so that no conditionals are needed when storing the error data. + int16_t rowdata[(width + 2 * info->mx) * 3]; + int16_t *rows[3] = { + rowdata + info->mx, rowdata + width + info->mx * 3, rowdata + 2 * width + info->mx * 5 + }; + // out holds one output row of pixels, and is padded to be a multiple of 32 so that the 1bpp storage loop can be simplified + bool out[(width + 31) / 32 * 32]; + + fill_row(source_bitmap, swap, rows[0], 0, info->mx); + fill_row(source_bitmap, swap, rows[1], 1, info->mx); + fill_row(source_bitmap, swap, rows[2], 2, info->mx); + + int16_t err = 0; + + for (int y = 0; y < height; y++) { + + // Serpentine dither. Going left-to-right... + for (int x = 0; x < width; x++) { + int32_t pixel_in = rows[0][x] + err; + bool pixel_out = pixel_in >= 128; + out[x] = pixel_out; + + err = pixel_in - (pixel_out ? 255 : 0); + + for (int i = 0; i < info->count; i++) { + int x1 = x + info->terms[i].dx; + int dy = info->terms[i].dy; + + rows[dy][x1] = ((info->terms[i].dl * err) >> 8) + rows[dy][x1]; + } + err = err * info->dl >> 8; + } + write_pixels(dest_bitmap, y, out); + + // Cycle the rows by shuffling pointers, this is faster than copying the data. + int16_t *tmp = rows[0]; + rows[0] = rows[1]; + rows[1] = rows[2]; + rows[2] = tmp; + + fill_row(source_bitmap, swap, rows[2], y + 2, info->mx); + + y++; + if (y == height) { + break; + } + + // Serpentine dither. Going right-to-left... + for (int x = width; x--;) { + int16_t pixel_in = rows[0][x] + err; + bool pixel_out = pixel_in >= 128; + out[x] = pixel_out; + err = pixel_in - (pixel_out ? 255 : 0); + + for (int i = 0; i < info->count; i++) { + int x1 = x - info->terms[i].dx; + int dy = info->terms[i].dy; + + rows[dy][x1] = ((info->terms[i].dl * err) >> 8) + rows[dy][x1]; + } + err = err * info->dl >> 8; + } + write_pixels(dest_bitmap, y, out); + + tmp = rows[0]; + rows[0] = rows[1]; + rows[1] = rows[2]; + rows[2] = tmp; + + fill_row(source_bitmap, swap, rows[2], y + 2, info->mx); + } + + displayio_area_t a = { 0, 0, width, height }; + displayio_bitmap_set_dirty_area(dest_bitmap, &a); +}