mirror of
https://github.com/daveallie/crosspoint-reader.git
synced 2026-02-07 08:07:40 +03:00
Moving color and dithering related helpers into dedicated file. In case of 2bpp (i.e. file generated from JPG), we don't apply any color adjustments. In other cases, they are applied based on selected algorithm.
223 lines
6.0 KiB
C++
223 lines
6.0 KiB
C++
#pragma once
|
|
|
|
#include <cstring>
|
|
|
|
// Helper functions
|
|
uint8_t quantize(int gray, int x, int y);
|
|
uint8_t quantizeSimple(int gray);
|
|
int adjustPixel(int gray);
|
|
|
|
// Atkinson dithering - distributes only 6/8 (75%) of error for cleaner results
|
|
// Error distribution pattern:
|
|
// X 1/8 1/8
|
|
// 1/8 1/8 1/8
|
|
// 1/8
|
|
// Less error buildup = fewer artifacts than Floyd-Steinberg
|
|
class AtkinsonDitherer {
|
|
public:
|
|
AtkinsonDitherer(int width) : width(width) {
|
|
errorRow0 = new int16_t[width + 4](); // Current row
|
|
errorRow1 = new int16_t[width + 4](); // Next row
|
|
errorRow2 = new int16_t[width + 4](); // Row after next
|
|
}
|
|
|
|
~AtkinsonDitherer() {
|
|
delete[] errorRow0;
|
|
delete[] errorRow1;
|
|
delete[] errorRow2;
|
|
}
|
|
|
|
uint8_t processPixel(int gray, int x) {
|
|
// Add accumulated error
|
|
int adjusted = gray + errorRow0[x + 2];
|
|
if (adjusted < 0) adjusted = 0;
|
|
if (adjusted > 255) adjusted = 255;
|
|
|
|
// Quantize to 4 levels
|
|
uint8_t quantized;
|
|
int quantizedValue;
|
|
if (false) { // original thresholds
|
|
if (adjusted < 43) {
|
|
quantized = 0;
|
|
quantizedValue = 0;
|
|
} else if (adjusted < 128) {
|
|
quantized = 1;
|
|
quantizedValue = 85;
|
|
} else if (adjusted < 213) {
|
|
quantized = 2;
|
|
quantizedValue = 170;
|
|
} else {
|
|
quantized = 3;
|
|
quantizedValue = 255;
|
|
}
|
|
} else { // fine-tuned to X4 eink display
|
|
if (adjusted < 30) {
|
|
quantized = 0;
|
|
quantizedValue = 15;
|
|
} else if (adjusted < 50) {
|
|
quantized = 1;
|
|
quantizedValue = 30;
|
|
} else if (adjusted < 140) {
|
|
quantized = 2;
|
|
quantizedValue = 80;
|
|
} else {
|
|
quantized = 3;
|
|
quantizedValue = 210;
|
|
}
|
|
}
|
|
|
|
// Calculate error (only distribute 6/8 = 75%)
|
|
int error = (adjusted - quantizedValue) >> 3; // error/8
|
|
|
|
// Distribute 1/8 to each of 6 neighbors
|
|
errorRow0[x + 3] += error; // Right
|
|
errorRow0[x + 4] += error; // Right+1
|
|
errorRow1[x + 1] += error; // Bottom-left
|
|
errorRow1[x + 2] += error; // Bottom
|
|
errorRow1[x + 3] += error; // Bottom-right
|
|
errorRow2[x + 2] += error; // Two rows down
|
|
|
|
return quantized;
|
|
}
|
|
|
|
void nextRow() {
|
|
int16_t* temp = errorRow0;
|
|
errorRow0 = errorRow1;
|
|
errorRow1 = errorRow2;
|
|
errorRow2 = temp;
|
|
memset(errorRow2, 0, (width + 4) * sizeof(int16_t));
|
|
}
|
|
|
|
void reset() {
|
|
memset(errorRow0, 0, (width + 4) * sizeof(int16_t));
|
|
memset(errorRow1, 0, (width + 4) * sizeof(int16_t));
|
|
memset(errorRow2, 0, (width + 4) * sizeof(int16_t));
|
|
}
|
|
|
|
private:
|
|
int width;
|
|
int16_t* errorRow0;
|
|
int16_t* errorRow1;
|
|
int16_t* errorRow2;
|
|
};
|
|
|
|
// Floyd-Steinberg error diffusion dithering with serpentine scanning
|
|
// Serpentine scanning alternates direction each row to reduce "worm" artifacts
|
|
// Error distribution pattern (left-to-right):
|
|
// X 7/16
|
|
// 3/16 5/16 1/16
|
|
// Error distribution pattern (right-to-left, mirrored):
|
|
// 1/16 5/16 3/16
|
|
// 7/16 X
|
|
class FloydSteinbergDitherer {
|
|
public:
|
|
FloydSteinbergDitherer(int width) : width(width), rowCount(0) {
|
|
errorCurRow = new int16_t[width + 2](); // +2 for boundary handling
|
|
errorNextRow = new int16_t[width + 2]();
|
|
}
|
|
|
|
~FloydSteinbergDitherer() {
|
|
delete[] errorCurRow;
|
|
delete[] errorNextRow;
|
|
}
|
|
|
|
// Process a single pixel and return quantized 2-bit value
|
|
// x is the logical x position (0 to width-1), direction handled internally
|
|
uint8_t processPixel(int gray, int x, bool reverseDirection) {
|
|
// Add accumulated error to this pixel
|
|
int adjusted = gray + errorCurRow[x + 1];
|
|
|
|
// Clamp to valid range
|
|
if (adjusted < 0) adjusted = 0;
|
|
if (adjusted > 255) adjusted = 255;
|
|
|
|
// Quantize to 4 levels (0, 85, 170, 255)
|
|
uint8_t quantized;
|
|
int quantizedValue;
|
|
if (false) { // original thresholds
|
|
if (adjusted < 43) {
|
|
quantized = 0;
|
|
quantizedValue = 0;
|
|
} else if (adjusted < 128) {
|
|
quantized = 1;
|
|
quantizedValue = 85;
|
|
} else if (adjusted < 213) {
|
|
quantized = 2;
|
|
quantizedValue = 170;
|
|
} else {
|
|
quantized = 3;
|
|
quantizedValue = 255;
|
|
}
|
|
} else { // fine-tuned to X4 eink display
|
|
if (adjusted < 30) {
|
|
quantized = 0;
|
|
quantizedValue = 15;
|
|
} else if (adjusted < 50) {
|
|
quantized = 1;
|
|
quantizedValue = 30;
|
|
} else if (adjusted < 140) {
|
|
quantized = 2;
|
|
quantizedValue = 80;
|
|
} else {
|
|
quantized = 3;
|
|
quantizedValue = 210;
|
|
}
|
|
}
|
|
|
|
// Calculate error
|
|
int error = adjusted - quantizedValue;
|
|
|
|
// Distribute error to neighbors (serpentine: direction-aware)
|
|
if (!reverseDirection) {
|
|
// Left to right: standard distribution
|
|
// Right: 7/16
|
|
errorCurRow[x + 2] += (error * 7) >> 4;
|
|
// Bottom-left: 3/16
|
|
errorNextRow[x] += (error * 3) >> 4;
|
|
// Bottom: 5/16
|
|
errorNextRow[x + 1] += (error * 5) >> 4;
|
|
// Bottom-right: 1/16
|
|
errorNextRow[x + 2] += (error) >> 4;
|
|
} else {
|
|
// Right to left: mirrored distribution
|
|
// Left: 7/16
|
|
errorCurRow[x] += (error * 7) >> 4;
|
|
// Bottom-right: 3/16
|
|
errorNextRow[x + 2] += (error * 3) >> 4;
|
|
// Bottom: 5/16
|
|
errorNextRow[x + 1] += (error * 5) >> 4;
|
|
// Bottom-left: 1/16
|
|
errorNextRow[x] += (error) >> 4;
|
|
}
|
|
|
|
return quantized;
|
|
}
|
|
|
|
// Call at the end of each row to swap buffers
|
|
void nextRow() {
|
|
// Swap buffers
|
|
int16_t* temp = errorCurRow;
|
|
errorCurRow = errorNextRow;
|
|
errorNextRow = temp;
|
|
// Clear the next row buffer
|
|
memset(errorNextRow, 0, (width + 2) * sizeof(int16_t));
|
|
rowCount++;
|
|
}
|
|
|
|
// Check if current row should be processed in reverse
|
|
bool isReverseRow() const { return (rowCount & 1) != 0; }
|
|
|
|
// Reset for a new image or MCU block
|
|
void reset() {
|
|
memset(errorCurRow, 0, (width + 2) * sizeof(int16_t));
|
|
memset(errorNextRow, 0, (width + 2) * sizeof(int16_t));
|
|
rowCount = 0;
|
|
}
|
|
|
|
private:
|
|
int width;
|
|
int rowCount;
|
|
int16_t* errorCurRow;
|
|
int16_t* errorNextRow;
|
|
};
|