Skip to content

Commit 2e4919f

Browse files
authored
Merge pull request #299 from SolderedElectronics/dev-image-dither-fix
Dev image dither fix
2 parents 362775b + e70b9e8 commit 2e4919f

12 files changed

Lines changed: 360 additions & 312 deletions

File tree

src/graphics/Image/Image.cpp

Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -25,40 +25,51 @@
2525
Image *_imagePtrJpeg = nullptr;
2626
Image *_imagePtrPng = nullptr;
2727

28-
// uint8_t (*Image::ditherBuffer)[E_INK_WIDTH + 20] = nullptr;
29-
__attribute__((section(".ext_ram.bss"))) uint8_t Image::ditherBuffer[2][E_INK_WIDTH + 20];
30-
__attribute__((section(".ext_ram.bss"))) uint8_t Image::jpegDitherBuffer[18][18];
31-
// uint8_t (*Image::jpegDitherBuffer)[18] = nullptr;
28+
int16_t (*Image::ditherBuffer)[E_INK_WIDTH + 20] = nullptr;
3229
uint8_t *Image::pixelBuffer = nullptr;
3330
uint32_t *Image::ditherPalette = nullptr;
3431
uint8_t *Image::palette = nullptr;
3532

33+
/**
34+
* @brief begin initialises the Image subsystem.
35+
* Must be called once before any draw functions are used.
36+
*
37+
* Stores the Inkplate pointer, registers the JPEG/PNG callback
38+
* pointers, allocates all PSRAM buffers needed for dithering and
39+
* pixel staging, and sets the default dither kernel.
40+
*
41+
* @param Inkplate *inkplateptr
42+
* Pointer to the parent Inkplate instance.
43+
*/
3644
void Image::begin(Inkplate *inkplateptr)
3745
{
3846
_inkplate = inkplateptr;
47+
48+
// Register this instance as the target for JPEG and PNG decode callbacks.
3949
_imagePtrJpeg = this;
4050
_imagePtrPng = this;
4151

42-
43-
// jpegDitherBuffer = (uint8_t (*)[18])heap_caps_calloc(18, 18, MALLOC_CAP_SPIRAM);
44-
45-
46-
// ditherBuffer = (uint8_t (*)[E_INK_WIDTH + 20])heap_caps_calloc(2, (E_INK_WIDTH + 20), MALLOC_CAP_SPIRAM);
47-
48-
52+
// Allocate PSRAM buffers. All four are required; a NULL result is reported
53+
// via Serial and the caller should not attempt to draw images.
54+
ditherBuffer = (int16_t(*)[E_INK_WIDTH + 20]) heap_caps_calloc(1, ditherBufferSizeBytes, MALLOC_CAP_SPIRAM);
4955
pixelBuffer = (uint8_t *)heap_caps_calloc(1, (E_INK_WIDTH * 4 + 5), MALLOC_CAP_SPIRAM);
50-
51-
5256
ditherPalette = (uint32_t *)heap_caps_calloc(256, sizeof(uint32_t), MALLOC_CAP_SPIRAM);
53-
54-
5557
palette = (uint8_t *)heap_caps_calloc(128, sizeof(uint8_t), MALLOC_CAP_SPIRAM);
5658

57-
58-
if (!jpegDitherBuffer || !ditherBuffer || !pixelBuffer || !ditherPalette || !palette)
59+
if (!ditherBuffer || !pixelBuffer || !ditherPalette || !palette)
5960
{
6061
Serial.println(" Failed to allocate one or more buffers (SRAM/PSRAM)");
6162
}
63+
64+
setDitherKernel(FloydSteinberg);
65+
}
66+
67+
void Image::setDitherKernel(const DitherKernel kernel)
68+
{
69+
const uint8_t kernelIndex = static_cast<uint8_t>(kernel);
70+
if (kernelIndex >= DITHER_KERNEL_COUNT)
71+
return;
72+
currentKernel = &DITHER_KERNELS[kernelIndex];
6273
}
6374

6475
/**

src/graphics/Image/Image.h

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
#define __IMAGE_H__
2121
#if !defined(ARDUINO_INKPLATECOLOR) && !defined(ARDUINO_INKPLATE2) && !defined(ARDUINO_INKPLATE13SPECTRA)
2222
#include "../../features/SdFat/SdFat.h"
23+
#include "ImageDitherKernels.h"
2324
#include "WiFi.h"
2425

2526
class Inkplate;
@@ -64,6 +65,17 @@ class Image
6465
_npos
6566
} Position;
6667

68+
typedef enum
69+
{
70+
FloydSteinberg = 0,
71+
JarvisJudiceNinke,
72+
Atkinson,
73+
Burkes,
74+
Stucki,
75+
SierraLite,
76+
ReducedDiffusion
77+
} DitherKernel;
78+
6779

6880
Inkplate *_inkplate = NULL;
6981

@@ -105,33 +117,41 @@ class Image
105117
bool drawPngFromWeb(const char *url, int x, int y, bool dither = 0, bool invert = 0);
106118
bool drawPngFromWeb(WiFiClient *s, int x, int y, int32_t len, bool dither = 0, bool invert = 0);
107119

108-
// Should be private, but needed in a png callback :(
109-
void ditherSwap(int w);
110120
uint8_t ditherGetPixelBmp(uint32_t px, int i, int j, int w, bool paletted);
111121

122+
void setDitherKernel(const DitherKernel kernel);
123+
112124
void getPointsForPosition(const Position &position, const uint16_t imageWidth, const uint16_t imageHeight,
113125
const uint16_t screenWidth, const uint16_t screenHeight, uint16_t *posX, uint16_t *posY);
114-
uint8_t findClosestPalette(int16_t r, int16_t g, int16_t b);
115126

116127

117128
private:
118129
static uint8_t *pixelBuffer;
119-
static uint8_t jpegDitherBuffer[18][18];
120-
static uint8_t ditherBuffer[2][E_INK_WIDTH + 20];
130+
static const uint8_t ditherRowCount = 4;
131+
static const uint8_t ditherRowMask = ditherRowCount - 1;
132+
static int16_t (*ditherBuffer)[E_INK_WIDTH + 20];
133+
static constexpr size_t ditherBufferSizeBytes = ditherRowCount * (E_INK_WIDTH + 20) * sizeof(int16_t);
121134
static uint32_t *ditherPalette; // 8 bit colors, in color, 3x8 bit colors
122135
static uint8_t *palette; // 2 3 bit colors per byte, _###_###
123136

137+
const DitherKernelDef *currentKernel = &DITHER_KERNELS[0];
138+
124139
uint16_t _lastTileRowY = -1;
125140

126141
static bool drawJpegChunk(int16_t x, int16_t y, uint16_t w, uint16_t h, uint16_t *bitmap, bool dither, bool invert);
127142

128143
int16_t blockW = 0, blockH = 0;
129144
int16_t lastY = -1;
130145

146+
uint16_t *jpegRowBuffer = nullptr;
147+
uint16_t jpegImageWidth = 0;
148+
uint8_t jpegMcuH = 0;
149+
int jpegDrawX = 0, jpegDrawY = 0;
150+
bool jpegDither = false, jpegInvert = false;
151+
131152
bool legalBmp(bitmapHeader *bmpHeader);
132153

133-
uint8_t ditherGetPixelJpeg(uint8_t px, int i, int j, int x, int y, int w, int h);
134-
void ditherSwapBlockJpeg(int x);
154+
void flushJpegRow(int rowY);
135155

136156
void readBmpHeader(uint8_t *buf, bitmapHeader *_h);
137157
void readBmpHeaderSd(SdFile *_f, bitmapHeader *_h);

src/graphics/Image/ImageDither.cpp

Lines changed: 33 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -21,127 +21,59 @@
2121

2222

2323
/**
24-
* @brief ditherGetPixelBmp calculates dither for given pixel in bmp
25-
* images
24+
* @brief ditherGetPixelBmp calculates dither for given pixel using the
25+
* currently selected kernel. The dither buffer is a circular
26+
* buffer indexed by j & ditherRowMask so error propagates
27+
* naturally across row and block boundaries without any swap step.
2628
*
2729
* @param uint32_t px
28-
* pixel value with color information
30+
* pixel value (8-bit grayscale or palette index)
2931
* @param int i
30-
* ditherBuffer width plane position
32+
* absolute column position
33+
* @param int j
34+
* absolute row position
3135
* @param int w
32-
* image width
36+
* image width (used for error-diffusion boundary clamping)
3337
* @param bool paletted
34-
* 1 if paletted image, 0 if not
38+
* true if px is a palette index that should be looked up first
3539
*
36-
* @return new pixel value (dithered pixel)
40+
* @return new dithered pixel value (3-bit grayscale, 0-7)
3741
*/
3842
uint8_t Image::ditherGetPixelBmp(uint32_t px, int i, int j, int w, bool paletted)
3943
{
4044
if (paletted)
4145
px = ditherPalette[px];
4246

43-
if (_inkplate->getDisplayMode() == INKPLATE_1BIT)
44-
px = (uint16_t)px >> 1;
47+
const int rowIdx = j & ditherRowMask;
48+
int16_t *row = ditherBuffer[rowIdx];
4549

46-
uint8_t oldPixel = min((uint16_t)0xFF, (uint16_t)((uint16_t)ditherBuffer[0][i] + px));
50+
int16_t oldPixel = (int16_t)px + row[i];
51+
row[i] = 0; // clear after reading
4752

48-
uint8_t newPixel = oldPixel & (_inkplate->getDisplayMode() == INKPLATE_1BIT ? B10000000 : B11100000);
49-
uint8_t quantError = oldPixel - newPixel;
53+
oldPixel = max((int16_t)0, min((int16_t)255, oldPixel));
5054

51-
ditherBuffer[1][i + 0] += (quantError * 5) >> 4;
52-
if (i != w - 1)
53-
{
54-
ditherBuffer[0][i + 1] += (quantError * 7) >> 4;
55-
ditherBuffer[1][i + 1] += (quantError * 1) >> 4;
56-
}
57-
if (i != 0)
58-
ditherBuffer[1][i - 1] += (quantError * 3) >> 4;
55+
// 1-bit: snap to 0x00 or 0xFF so that >>5 produces 0 or 7 (full display range).
56+
// 3-bit: quantise to the top 3 bits; >>5 produces 0-7.
57+
uint8_t newPixel = (_inkplate->getDisplayMode() == INKPLATE_1BIT) ? ((oldPixel >= 128) ? 0xFF : 0x00)
58+
: ((uint8_t)oldPixel & B11100000);
59+
int16_t quantError = oldPixel - newPixel;
5960

60-
return newPixel >> 5;
61-
}
61+
const int minOffset = max(-currentKernel->x, -i);
62+
const int maxOffset = min(currentKernel->width - currentKernel->x - 1, w - 1 - i);
6263

63-
/**
64-
* @brief ditherGetPixelJpeg calculates dither for given pixel in jpeg
65-
* images
66-
*
67-
* @param uint8_t px
68-
* pixel value with color information
69-
* @param int i
70-
* ditherBuffer width plane position
71-
* @param int j
72-
* ditherBuffer height plane position
73-
* @param int x
74-
* x image starting position
75-
* @param int y
76-
* y image starting position
77-
* @param int w
78-
* image width
79-
* @param int h
80-
* image height
81-
*
82-
* @return new pixel value (dithered pixel)
83-
*/
84-
uint8_t Image::ditherGetPixelJpeg(uint8_t px, int i, int j, int x, int y, int w, int h)
85-
{
86-
if (blockW == -1)
64+
for (int k = 0; k < currentKernel->height; ++k)
8765
{
88-
blockW = w;
89-
blockH = h;
66+
int16_t *nextRow = ditherBuffer[(rowIdx + k) & ditherRowMask];
67+
for (int l = minOffset; l <= maxOffset; ++l)
68+
{
69+
const int weight = currentKernel->data[k * currentKernel->width + (l + currentKernel->x)];
70+
if (!weight)
71+
continue;
72+
nextRow[i + l] += (weight * quantError) / currentKernel->coef;
73+
}
9074
}
9175

92-
if (_inkplate->getDisplayMode() == INKPLATE_1BIT)
93-
px = (uint16_t)px >> 1;
94-
95-
uint16_t oldPixel = min((uint16_t)0xFF, (uint16_t)((uint16_t)px + (uint16_t)jpegDitherBuffer[j + 1][i + 1] +
96-
(j ? (uint16_t)0 : (uint16_t)ditherBuffer[0][x + i])));
97-
98-
uint8_t newPixel = oldPixel & (_inkplate->getDisplayMode() == INKPLATE_1BIT ? B10000000 : B11100000);
99-
uint8_t quantError = oldPixel - newPixel;
100-
101-
jpegDitherBuffer[j + 1 + 1][i + 0 + 1] += (quantError * 5) >> 4;
102-
103-
jpegDitherBuffer[j + 0 + 1][i + 1 + 1] += (quantError * 7) >> 4;
104-
jpegDitherBuffer[j + 1 + 1][i + 1 + 1] += (quantError * 1) >> 4;
105-
106-
jpegDitherBuffer[j + 1 + 1][i - 1 + 1] += (quantError * 3) >> 4;
107-
10876
return newPixel >> 5;
10977
}
11078

111-
/**
112-
* @brief ditherSwap swaps ditherBuffer values
113-
*
114-
* @param int w
115-
* screen width
116-
*/
117-
void Image::ditherSwap(int w)
118-
{
119-
for (int i = 0; i < w; ++i)
120-
{
121-
ditherBuffer[0][i] = ditherBuffer[1][i];
122-
ditherBuffer[1][i] = 0;
123-
}
124-
}
125-
126-
/**
127-
* @brief ditherSwapBlockJpeg swaps ditherBuffer values
128-
*
129-
* @param int x
130-
* x plane image starting point
131-
*/
132-
void Image::ditherSwapBlockJpeg(int x)
133-
{
134-
for (int i = 0; i < 18; ++i)
135-
{
136-
if (x + i)
137-
ditherBuffer[1][x + i - 1] += jpegDitherBuffer[blockH - 1 + 2][i];
138-
jpegDitherBuffer[i][0 + 1] = jpegDitherBuffer[i][blockW - 1 + 2];
139-
}
140-
for (int j = 0; j < 18; ++j)
141-
for (int i = 0; i < 18; ++i)
142-
if (i != 1)
143-
jpegDitherBuffer[j][i] = 0;
144-
145-
jpegDitherBuffer[17][1] = 0;
146-
}
147-
#endif
79+
#endif
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/**
2+
**************************************************
3+
* @file ImageDitherKernels.h
4+
* @brief Dither kernel definitions for grayscale images
5+
*
6+
* https://github.com/e-radionicacom/Inkplate-Arduino-library
7+
* For support, please reach over forums: forum.e-radionica.com/en
8+
* For more info about the product, please check: www.inkplate.io
9+
*
10+
* This code is released under the GNU Lesser General Public
11+
*License v3.0: https://www.gnu.org/licenses/lgpl-3.0.en.html Please review the
12+
*LICENSE file included with this example. If you have any questions about
13+
*licensing, please contact techsupport@e-radionica.com Distributed as-is; no
14+
*warranty is given.
15+
*
16+
* @authors Soldered
17+
***************************************************/
18+
#pragma once
19+
#if !defined(ARDUINO_INKPLATECOLOR) && !defined(ARDUINO_INKPLATE2) && !defined(ARDUINO_INKPLATE13SPECTRA)
20+
21+
struct DitherKernelDef
22+
{
23+
uint8_t width;
24+
uint8_t height;
25+
uint8_t x;
26+
uint16_t coef;
27+
const unsigned char *data;
28+
};
29+
30+
// Floyd Steinberg
31+
static const unsigned char _kernelFloydSteinberg[] = {
32+
0, 0, 7, 3, 5, 1,
33+
};
34+
35+
// J F Jarvis, C N Judice, and W H Ninke "Minimized Average Error"
36+
static const unsigned char _kernelJarvisJudiceNinke[] = {
37+
0, 0, 0, 7, 5, 3, 5, 7, 5, 3, 1, 3, 5, 3, 1,
38+
};
39+
40+
// Atkinson
41+
static const unsigned char _kernelAtkinson[] = {
42+
0, 0, 1, 1, 1, 1, 1, 0, 0, 1, 0, 0,
43+
};
44+
45+
// Burkes
46+
static const unsigned char _kernelBurkes[] = {
47+
0, 0, 0, 8, 4, 2, 4, 8, 4, 2, 0, 0, 0, 0, 0,
48+
};
49+
50+
// Stucki
51+
static const unsigned char _kernelStucki[] = {
52+
0, 0, 0, 8, 4, 2, 4, 8, 4, 2, 1, 2, 4, 2, 1,
53+
};
54+
55+
// Sierra lite
56+
static const unsigned char _kernelSierraLite[] = {
57+
0, 0, 2, 1, 1, 0, 0, 0, 0,
58+
};
59+
60+
static const unsigned char _kernelReducedDiffusion[] = {
61+
0, 0, 5, 2, 3, 1,
62+
};
63+
64+
static const DitherKernelDef DITHER_KERNELS[] = {
65+
{3, 2, 1, 16, _kernelFloydSteinberg}, {5, 3, 2, 48, _kernelJarvisJudiceNinke},
66+
{4, 3, 1, 8, _kernelAtkinson}, {5, 3, 2, 32, _kernelBurkes},
67+
{5, 3, 2, 42, _kernelStucki}, {3, 3, 1, 4, _kernelSierraLite},
68+
{3, 2, 1, 26, _kernelReducedDiffusion},
69+
};
70+
71+
static const uint8_t DITHER_KERNEL_COUNT = sizeof DITHER_KERNELS / sizeof DITHER_KERNELS[0];
72+
73+
#endif

0 commit comments

Comments
 (0)