From 602d1b41dd8a78a2aabc2e600d3e49ad4508524f Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Fri, 10 Jun 2022 00:58:54 +0100 Subject: [PATCH] PicoGraphics: Attempt at dither and scanline interrupts. Currently both unused and both very slow. Dither may only be feasible for <=8 colour displays. JPEGDEC will dither to 4-bit greyscale quite happily. --- libraries/pico_graphics/pico_graphics.cpp | 16 ++++ libraries/pico_graphics/pico_graphics.hpp | 80 +++++++++++++++++-- micropython/modules/jpegdec/jpegdec.cpp | 13 ++- .../modules/picographics/picographics.c | 2 + .../modules/picographics/picographics.cpp | 23 ++++++ .../modules/picographics/picographics.h | 1 + 6 files changed, 124 insertions(+), 11 deletions(-) diff --git a/libraries/pico_graphics/pico_graphics.cpp b/libraries/pico_graphics/pico_graphics.cpp index 9e690a0e..abcfa1e8 100644 --- a/libraries/pico_graphics/pico_graphics.cpp +++ b/libraries/pico_graphics/pico_graphics.cpp @@ -8,6 +8,7 @@ namespace pimoroni { int PicoGraphics::reset_pen(uint8_t i) {return -1;}; int PicoGraphics::create_pen(uint8_t r, uint8_t g, uint8_t b) {return -1;}; void PicoGraphics::set_pixel(const Point &p) {}; + void PicoGraphics::set_pixel_dither(const Point &p, const RGB &c) {}; void PicoGraphics::scanline_convert(PenType type, conversion_callback_func callback) {}; void PicoGraphics::set_dimensions(int width, int height) { @@ -51,6 +52,21 @@ namespace pimoroni { clip = bounds; } + void PicoGraphics::get_dither_candidates(const RGB &col, const RGB *palette, size_t len, std::array &candidates) { + RGB error; + for(size_t i = 0; i < candidates.size(); i++) { + candidates[i] = (col + error).closest(palette, len); + error += (col - palette[candidates[i]]); + } + + // sort by a rough approximation of luminance, this ensures that neighbouring + // pixels in the dither matrix are at extreme opposites of luminence + // giving a more balanced output + std::sort(candidates.begin(), candidates.end(), [palette](int a, int b) { + return palette[a].luminance() > palette[b].luminance(); + }); + } + void PicoGraphics::clear() { rectangle(clip); } diff --git a/libraries/pico_graphics/pico_graphics.hpp b/libraries/pico_graphics/pico_graphics.hpp index c0062394..38204988 100644 --- a/libraries/pico_graphics/pico_graphics.hpp +++ b/libraries/pico_graphics/pico_graphics.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include @@ -24,7 +25,7 @@ namespace pimoroni { typedef uint8_t RGB332; typedef uint16_t RGB565; struct RGB { - uint8_t r, g, b; + int16_t r, g, b; constexpr RGB() : r(0), g(0), b(0) {} constexpr RGB(RGB332 c) : @@ -37,25 +38,31 @@ namespace pimoroni { b((__builtin_bswap16(c) & 0b0000000000011111) << 3) {} constexpr RGB(uint8_t r, uint8_t g, uint8_t b) : r(r), g(g), b(b) {} + constexpr RGB operator+ (const RGB& c) const {return RGB(r + c.r, g + c.g, b + c.b);} + constexpr RGB& operator+=(const RGB& c) {r += c.r; g += c.g; b += c.b; return *this;} + constexpr RGB operator- (const RGB& c) const {return RGB(r - c.r, g - c.g, b - c.b);} + // a rough approximation of how bright a colour is used to compare the // relative brightness of two colours - int luminance() { + int luminance() const { // weights based on https://www.johndcook.com/blog/2009/08/24/algorithms-convert-color-grayscale/ - return r * 21 + g * 72 * b * 7; + return r * 21 + g * 72 + b * 7; } // a relatively low cost approximation of how "different" two colours are // perceived which avoids expensive colour space conversions. // described in detail at https://www.compuphase.com/cmetric.htm - int distance(RGB c) { - int rmean = ((int)r + c.r) / 2; - int rx = (int)r - c.r, gx = (int)g - c.g, bx = (int)b - c.b; + int distance(const RGB& c) const { + int rmean = (r + c.r) / 2; + int rx = r - c.r; + int gx = g - c.g; + int bx = b - c.b; return abs((int)( (((512 + rmean) * rx * rx) >> 8) + 4 * gx * gx + (((767 - rmean) * bx * bx) >> 8) )); } - int closest(const RGB *palette, size_t len) { + int closest(const RGB *palette, size_t len) const { int d = INT_MAX, m = -1; for(size_t i = 0; i < len; i++) { int dc = distance(palette[i]); @@ -127,6 +134,9 @@ namespace pimoroni { Rect clip; typedef std::function conversion_callback_func; + //typedef std::function scanline_interrupt_func; + + //scanline_interrupt_func scanline_interrupt = nullptr; const bitmap::font_t *bitmap_font; const hershey::font_t *hershey_font; @@ -172,6 +182,7 @@ namespace pimoroni { virtual int reset_pen(uint8_t i); virtual int create_pen(uint8_t r, uint8_t g, uint8_t b); virtual void set_pixel(const Point &p); + virtual void set_pixel_dither(const Point &p, const RGB &c); virtual void scanline_convert(PenType type, conversion_callback_func callback); void set_font(const bitmap::font_t *font); @@ -187,6 +198,8 @@ namespace pimoroni { void set_clip(const Rect &r); void remove_clip(); + void get_dither_candidates(const RGB &col, const RGB *palette, size_t len, std::array &candidates); + void clear(); void pixel(const Point &p); void pixel_span(const Point &p, int32_t l); @@ -244,6 +257,21 @@ namespace pimoroni { *f &= m; // clear bits *f |= b; // set value } + void set_pixel_dither(const Point &p, const RGB &c) override { + if(!bounds.contains(p)) return; + static uint pattern[16] = // dither pattern + {0, 8, 2, 10, 12, 4, 14, 6, 3, 11, 1, 9, 15, 7, 13, 5}; + + static std::array candidates; + get_dither_candidates(c, palette, 256, candidates); + + // find the pattern coordinate offset + uint pattern_index = (p.x & 0b11) | ((p.y & 0b11) << 2); + + // set the pixel + color = candidates[pattern[pattern_index]]; + set_pixel(p); + } void scanline_convert(PenType type, conversion_callback_func callback) override { if(type == PEN_RGB565) { // Cache the RGB888 palette as RGB565 @@ -258,6 +286,14 @@ namespace pimoroni { // Allocate a per-row temporary buffer uint16_t row_buf[bounds.w]; for(auto y = 0; y < bounds.h; y++) { + /*if(scanline_interrupt != nullptr) { + scanline_interrupt(y); + // Cache the RGB888 palette as RGB565 + for(auto i = 0u; i < 16; i++) { + cache[i] = palette[i].to_rgb565(); + } + }*/ + for(auto x = 0; x < bounds.w; x++) { uint8_t c = src[(bounds.w * y / 2) + (x / 2)]; uint8_t o = (~x & 0b1) * 4; // bit offset within byte @@ -322,6 +358,21 @@ namespace pimoroni { uint8_t *buf = (uint8_t *)frame_buffer; buf[p.y * bounds.w + p.x] = color; } + void set_pixel_dither(const Point &p, const RGB &c) override { + if(!bounds.contains(p)) return; + static uint pattern[16] = // dither pattern + {0, 8, 2, 10, 12, 4, 14, 6, 3, 11, 1, 9, 15, 7, 13, 5}; + + static std::array candidates; + get_dither_candidates(c, palette, 256, candidates); + + // find the pattern coordinate offset + uint pattern_index = (p.x & 0b11) | ((p.y & 0b11) << 2); + + // set the pixel + color = candidates[pattern[pattern_index]]; + set_pixel(p); + } void scanline_convert(PenType type, conversion_callback_func callback) override { if(type == PEN_RGB565) { // Cache the RGB888 palette as RGB565 @@ -376,6 +427,21 @@ namespace pimoroni { uint8_t *buf = (uint8_t *)frame_buffer; buf[p.y * bounds.w + p.x] = color; } + void set_pixel_dither(const Point &p, const RGB &c) override { + if(!bounds.contains(p)) return; + static uint pattern[16] = // dither pattern + {0, 8, 2, 10, 12, 4, 14, 6, 3, 11, 1, 9, 15, 7, 13, 5}; + + static std::array candidates; + get_dither_candidates(c, palette, 256, candidates); + + // find the pattern coordinate offset + uint pattern_index = (p.x & 0b11) | ((p.y & 0b11) << 2); + + // set the pixel + color = candidates[pattern[pattern_index]]; + set_pixel(p); + } void scanline_convert(PenType type, conversion_callback_func callback) override { if(type == PEN_RGB565) { // Cache the RGB888 palette as RGB565 diff --git a/micropython/modules/jpegdec/jpegdec.cpp b/micropython/modules/jpegdec/jpegdec.cpp index bc333202..2d1136d5 100644 --- a/micropython/modules/jpegdec/jpegdec.cpp +++ b/micropython/modules/jpegdec/jpegdec.cpp @@ -29,10 +29,13 @@ typedef struct _JPEG_obj_t { PicoGraphics *current_graphics = nullptr; int JPEGDraw(JPEGDRAW *pDraw) { +#ifdef MICROPY_EVENT_POLL_HOOK +MICROPY_EVENT_POLL_HOOK +#endif // "pixel" is slow and clipped, // guaranteeing we wont draw jpeg data out of the framebuffer.. // Can we clip beforehand and make this faster? - if(pDraw->iBpp == 4) { // TODO 4-bit pixel unpacking isn't working. What's up? + if(pDraw->iBpp == 4) { uint8_t *pixels = (uint8_t *)pDraw->pPixels; for(int y = 0; y < pDraw->iHeight; y++) { for(int x = 0; x < pDraw->iWidth; x++) { @@ -58,11 +61,14 @@ int JPEGDraw(JPEGDRAW *pDraw) { for(int x = 0; x < pDraw->iWidth; x++) { int i = y * pDraw->iWidth + x; if (current_graphics->pen_type == PicoGraphics::PEN_RGB332) { - current_graphics->set_pen(PicoGraphics::rgb565_to_rgb332(pDraw->pPixels[i])); + current_graphics->set_pen(RGB((RGB565)pDraw->pPixels[i]).to_rgb332()); + current_graphics->pixel({pDraw->x + x, pDraw->y + y}); + // FIXME VERY, VERY SLOW! + //current_graphics->set_pixel_dither({pDraw->x + x, pDraw->y + y}, RGB((RGB565)(pDraw->pPixels[i]))); } else { current_graphics->set_pen(pDraw->pPixels[i]); + current_graphics->pixel({pDraw->x + x, pDraw->y + y}); } - current_graphics->pixel({pDraw->x + x, pDraw->y + y}); } } } @@ -106,7 +112,6 @@ static int _open(_JPEG_obj_t *self, void *buf, size_t len) { case PicoGraphics::PEN_P8: self->jpeg->setPixelType(EIGHT_BIT_GRAYSCALE); break; - // TODO currently uses EIGHT_BIT_GREYSCALE and shifts down should this use a 4-bit dither? case PicoGraphics::PEN_P4: self->jpeg->setPixelType(FOUR_BIT_DITHERED); break; diff --git a/micropython/modules/picographics/picographics.c b/micropython/modules/picographics/picographics.c index c42c804b..bd2253b9 100644 --- a/micropython/modules/picographics/picographics.c +++ b/micropython/modules/picographics/picographics.c @@ -36,6 +36,7 @@ MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(ModPicoGraphics_triangle_obj, 7, 7, ModPicoG MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(ModPicoGraphics_line_obj, 5, 5, ModPicoGraphics_line); // Utility +//MP_DEFINE_CONST_FUN_OBJ_2(ModPicoGraphics_set_scanline_callback_obj, ModPicoGraphics_set_scanline_callback); MP_DEFINE_CONST_FUN_OBJ_1(ModPicoGraphics_get_bounds_obj, ModPicoGraphics_get_bounds); MP_DEFINE_CONST_FUN_OBJ_2(ModPicoGraphics_set_font_obj, ModPicoGraphics_set_font); MP_DEFINE_CONST_FUN_OBJ_2(ModPicoGraphics_set_framebuffer_obj, ModPicoGraphics_set_framebuffer); @@ -65,6 +66,7 @@ STATIC const mp_rom_map_elem_t ModPicoGraphics_locals_dict_table[] = { { MP_ROM_QSTR(MP_QSTR_set_backlight), MP_ROM_PTR(&ModPicoGraphics_set_backlight_obj) }, + //{ MP_ROM_QSTR(MP_QSTR_set_scanline_callback), MP_ROM_PTR(&ModPicoGraphics_set_scanline_callback_obj) }, { MP_ROM_QSTR(MP_QSTR_get_bounds), MP_ROM_PTR(&ModPicoGraphics_get_bounds_obj) }, { MP_ROM_QSTR(MP_QSTR_set_font), MP_ROM_PTR(&ModPicoGraphics_set_font_obj) }, { MP_ROM_QSTR(MP_QSTR_set_framebuffer), MP_ROM_PTR(&ModPicoGraphics_set_framebuffer_obj) }, diff --git a/micropython/modules/picographics/picographics.cpp b/micropython/modules/picographics/picographics.cpp index d185e136..5cb9b85f 100644 --- a/micropython/modules/picographics/picographics.cpp +++ b/micropython/modules/picographics/picographics.cpp @@ -25,6 +25,7 @@ typedef struct _ModPicoGraphics_obj_t { PicoGraphics *graphics; DisplayDriver *display; void *buffer; + //mp_obj_t scanline_callback; // Not really feasible in MicroPython } ModPicoGraphics_obj_t; bool get_display_resolution(PicoGraphicsDisplay display, int &width, int &height) { @@ -166,6 +167,8 @@ mp_obj_t ModPicoGraphics_make_new(const mp_obj_type_t *type, size_t n_args, size break; } + //self->scanline_callback = mp_const_none; + // Clear the buffer self->graphics->set_pen(0); self->graphics->clear(); @@ -235,8 +238,28 @@ mp_obj_t ModPicoGraphics_get_bounds(mp_obj_t self_in) { return mp_obj_new_tuple(2, tuple); } +/* +mp_obj_t ModPicoGraphics_set_scanline_callback(mp_obj_t self_in, mp_obj_t cb_in) { + ModPicoGraphics_obj_t *self = MP_OBJ_TO_PTR2(self_in, ModPicoGraphics_obj_t); + self->scanline_callback = cb_in; + return mp_const_none; +} +*/ + mp_obj_t ModPicoGraphics_update(mp_obj_t self_in) { ModPicoGraphics_obj_t *self = MP_OBJ_TO_PTR2(self_in, ModPicoGraphics_obj_t); +/* + if(self->scanline_callback != mp_const_none) { + self->graphics->scanline_interrupt = [self](int y){ + mp_obj_t args[] = { + mp_obj_new_int(y) + }; + mp_call_function_n_kw(self->scanline_callback, MP_ARRAY_SIZE(args), 0, args); + }; + } else { + self->graphics->scanline_interrupt = nullptr; + } +*/ self->display->update(self->graphics); return mp_const_none; diff --git a/micropython/modules/picographics/picographics.h b/micropython/modules/picographics/picographics.h index 9f18fdcb..62955a96 100644 --- a/micropython/modules/picographics/picographics.h +++ b/micropython/modules/picographics/picographics.h @@ -61,6 +61,7 @@ extern mp_obj_t ModPicoGraphics_triangle(size_t n_args, const mp_obj_t *args); extern mp_obj_t ModPicoGraphics_line(size_t n_args, const mp_obj_t *args); // Utility +//extern mp_obj_t ModPicoGraphics_set_scanline_callback(mp_obj_t self_in, mp_obj_t cb_in); extern mp_obj_t ModPicoGraphics_set_font(mp_obj_t self_in, mp_obj_t font); extern mp_obj_t ModPicoGraphics_get_bounds(mp_obj_t self_in); extern mp_obj_t ModPicoGraphics_set_framebuffer(mp_obj_t self_in, mp_obj_t framebuffer);