I’ve developed a method to perform 24->16 bit colour conversion using Floyd-Steinberg dithering - the result is great looking user interfaces with significantly less visible banding on 16-bit displays without having to re-work all of your assets.
My approach is zero-allocation (has literally 0 memory overhead) and operates in-place within the 24 bit buffer. It’s decently fast but does have some overhead. The impact it has of course will depend on the size of your update regions.
First you simply need to set:
/*Color depth: 1 (I1), 8 (L8), 16 (RGB565), 24 (RGB888), 32 (XRGB8888)*/
#define LV_COLOR_DEPTH 24
In your lv_conf.h
file.
In my case, I’m using the popular TFT_eSPI library as a backend for LVGL. All I have to do is add a single function call to my existing disp_flush(...)
method:
void disp_flush(lv_display_t* disp, const lv_area_t* area, uint8_t* pixelmap) {
uint32_t w = (area->x2 - area->x1 + 1);
uint32_t h = (area->y2 - area->y1 + 1);
apply_dithering(pixelmap, w, h);
tft.startWrite();
tft.setAddrWindow(area->x1, area->y1, w, h);
tft.pushPixels(pixelmap, w * h); // Use the converted buffer
tft.endWrite();
lv_disp_flush_ready(disp);
}
And then our implementation of apply_dithering
uses the buffer in-place with some pointer casting magic.
void apply_dithering(uint8_t* pixelmap, uint32_t w, uint32_t h) {
uint16_t* pixelmap16 = (uint16_t*)pixelmap;
for (uint32_t y = 0; y < h; y++) {
for (uint32_t x = 0; x < w; x++) {
// Get the index of the current RGB888 pixel
uint32_t index = (y * w + x) * 3;
uint8_t r = pixelmap[index];
uint8_t g = pixelmap[index + 1];
uint8_t b = pixelmap[index + 2];
// Convert to BGR565
uint16_t outColor = tft.color565(b,g,r);
// Store the BGR565 value in the output buffer (2 bytes per pixel)
pixelmap16[y * w + x] = outColor;
// Calculate the quantization error
int err_r = r - ((outColor & 0x1F) * 255 / 31); // Red in BGR is in the least significant 5 bits
int err_g = g - (((outColor >> 5) & 0x3F) * 255 / 63); // Green remains in the middle 6 bits
int err_b = b - (((outColor >> 11) & 0x1F) * 255 / 31); // Blue is now in the most significant 5 bits
// Distribute the error to neighboring pixels (Floyd-Steinberg)
if (x + 1 < w) {
// Right neighbor (x+1, y)
uint32_t nextIndex = ((y * w + x + 1) * 3);
pixelmap[nextIndex] = clip(pixelmap[nextIndex] + (err_r * 7 / 16));
pixelmap[nextIndex + 1] = clip(pixelmap[nextIndex + 1] + (err_g * 7 / 16));
pixelmap[nextIndex + 2] = clip(pixelmap[nextIndex + 2] + (err_b * 7 / 16));
}
if (y + 1 < h) {
if (x > 0) {
// Bottom-left neighbor (x-1, y+1)
uint32_t nextIndex = (((y + 1) * w + (x - 1)) * 3);
pixelmap[nextIndex] = clip(pixelmap[nextIndex] + (err_r * 3 / 16));
pixelmap[nextIndex + 1] = clip(pixelmap[nextIndex + 1] + (err_g * 3 / 16));
pixelmap[nextIndex + 2] = clip(pixelmap[nextIndex + 2] + (err_b * 3 / 16));
}
// Bottom neighbor (x, y+1)
uint32_t nextIndex = (((y + 1) * w + x) * 3);
pixelmap[nextIndex] = clip(pixelmap[nextIndex] + (err_r * 5 / 16));
pixelmap[nextIndex + 1] = clip(pixelmap[nextIndex + 1] + (err_g * 5 / 16));
pixelmap[nextIndex + 2] = clip(pixelmap[nextIndex + 2] + (err_b * 5 / 16));
if (x + 1 < w) {
// Bottom-right neighbor (x+1, y+1)
uint32_t nextIndex = (((y + 1) * w + (x + 1)) * 3);
pixelmap[nextIndex] = clip(pixelmap[nextIndex] + (err_r * 1 / 16));
pixelmap[nextIndex + 1] = clip(pixelmap[nextIndex + 1] + (err_g * 1 / 16));
pixelmap[nextIndex + 2] = clip(pixelmap[nextIndex + 2] + (err_b * 1 / 16));
}
}
}
}
}