bitmaptools: add dither

This can convert a BGR565_SWAPPED bitmap to B&W in about 82ms on
esp32-s2.
This commit is contained in:
Jeff Epler 2021-10-29 14:01:47 -05:00
parent 5572876d29
commit eaf8bc0abe
4 changed files with 317 additions and 3 deletions

View File

@ -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"

View File

@ -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 <stdint.h>
#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);

View File

@ -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

View File

@ -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 <math.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
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);
}