Add Bayer 4x4 ordered dithering for EPUB cover images
Replace simple threshold-based grayscale quantization with ordered dithering using a 4x4 Bayer matrix. This eliminates color banding artifacts and produces smoother gradients on e-ink display. - Add 4x4 Bayer dithering matrix for 16-level threshold patterns - Modify grayscaleTo2Bit() to use pixel position for dithering - Simulates smooth grayscale transitions in BW mode without flicker
This commit is contained in:
parent
1107590b56
commit
9aa305700c
@ -13,14 +13,40 @@ struct JpegReadContext {
|
|||||||
size_t bufferFilled;
|
size_t bufferFilled;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Helper function: Convert 8-bit grayscale to 2-bit (0-3)
|
// 4x4 Bayer ordered dithering matrix (normalized to 0-255 range for 16 levels)
|
||||||
uint8_t JpegToBmpConverter::grayscaleTo2Bit(const uint8_t grayscale) {
|
// This creates a pattern that distributes quantization error spatially
|
||||||
// Simple threshold mapping:
|
// Reference: https://surma.dev/things/ditherpunk/
|
||||||
// 0-63 -> 0 (black)
|
static const uint8_t bayerMatrix4x4[4][4] = {
|
||||||
// 64-127 -> 1 (dark gray)
|
{0, 128, 32, 160}, // 0/16, 8/16, 2/16, 10/16
|
||||||
// 128-191 -> 2 (light gray)
|
{192, 64, 224, 96}, // 12/16, 4/16, 14/16, 6/16
|
||||||
// 192-255 -> 3 (white)
|
{48, 176, 16, 144}, // 3/16, 11/16, 1/16, 9/16
|
||||||
return grayscale >> 6;
|
{240, 112, 208, 80} // 15/16, 7/16, 13/16, 5/16
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper function: Convert 8-bit grayscale to 2-bit (0-3) using ordered dithering
|
||||||
|
uint8_t JpegToBmpConverter::grayscaleTo2Bit(const uint8_t grayscale, const int x, const int y) {
|
||||||
|
// Get the threshold from Bayer matrix based on pixel position
|
||||||
|
const uint8_t threshold = bayerMatrix4x4[y & 3][x & 3];
|
||||||
|
|
||||||
|
// For 4-level output (2-bit), we need to map grayscale to one of 4 levels
|
||||||
|
// Each level spans ~85 values (255/3 ≈ 85)
|
||||||
|
// We use the Bayer threshold to decide between adjacent levels
|
||||||
|
|
||||||
|
// Scale grayscale to 0-765 range (3 * 255) for finer comparison
|
||||||
|
const int scaled = grayscale * 3;
|
||||||
|
|
||||||
|
// Determine which level pair we're between, then use dithering to pick one
|
||||||
|
if (scaled < 255) {
|
||||||
|
// Between level 0 (black) and level 1 (dark gray)
|
||||||
|
// Use threshold to decide: if scaled value + dither > 255, go to level 1
|
||||||
|
return (scaled + threshold >= 255) ? 1 : 0;
|
||||||
|
} else if (scaled < 510) {
|
||||||
|
// Between level 1 (dark gray) and level 2 (light gray)
|
||||||
|
return ((scaled - 255) + threshold >= 255) ? 2 : 1;
|
||||||
|
} else {
|
||||||
|
// Between level 2 (light gray) and level 3 (white)
|
||||||
|
return ((scaled - 510) + threshold >= 255) ? 3 : 2;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inline void write16(Print& out, const uint16_t value) {
|
inline void write16(Print& out, const uint16_t value) {
|
||||||
@ -223,7 +249,7 @@ bool JpegToBmpConverter::jpegFileToBmpStream(File& jpegFile, Print& bmpOut) {
|
|||||||
for (int x = 0; x < imageInfo.m_width; x++) {
|
for (int x = 0; x < imageInfo.m_width; x++) {
|
||||||
const int bufferY = y - startRow;
|
const int bufferY = y - startRow;
|
||||||
const uint8_t gray = mcuRowBuffer[bufferY * imageInfo.m_width + x];
|
const uint8_t gray = mcuRowBuffer[bufferY * imageInfo.m_width + x];
|
||||||
const uint8_t twoBit = grayscaleTo2Bit(gray);
|
const uint8_t twoBit = grayscaleTo2Bit(gray, x, y);
|
||||||
|
|
||||||
const int byteIndex = (x * 2) / 8;
|
const int byteIndex = (x * 2) / 8;
|
||||||
const int bitOffset = 6 - ((x * 2) % 8); // 6, 4, 2, 0
|
const int bitOffset = 6 - ((x * 2) % 8); // 6, 4, 2, 0
|
||||||
|
|||||||
@ -6,7 +6,7 @@ class ZipFile;
|
|||||||
|
|
||||||
class JpegToBmpConverter {
|
class JpegToBmpConverter {
|
||||||
static void writeBmpHeader(Print& bmpOut, int width, int height);
|
static void writeBmpHeader(Print& bmpOut, int width, int height);
|
||||||
static uint8_t grayscaleTo2Bit(uint8_t grayscale);
|
static uint8_t grayscaleTo2Bit(uint8_t grayscale, int x, int y);
|
||||||
static unsigned char jpegReadCallback(unsigned char* pBuf, unsigned char buf_size,
|
static unsigned char jpegReadCallback(unsigned char* pBuf, unsigned char buf_size,
|
||||||
unsigned char* pBytes_actually_read, void* pCallback_data);
|
unsigned char* pBytes_actually_read, void* pCallback_data);
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user