Move helper functions to Bitmap
This commit is contained in:
parent
54e5388e76
commit
40a92f3d34
@ -8,15 +8,22 @@
|
|||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Note: For cover images, dithering is done in JpegToBmpConverter.cpp
|
// Note: For cover images, dithering is done in JpegToBmpConverter.cpp
|
||||||
// This file handles BMP reading - use simple quantization to avoid double-dithering
|
// This file handles BMP reading - use simple quantization to avoid double-dithering
|
||||||
// constexpr bool USE_FLOYD_STEINBERG = true; // Disabled - dithering done at JPEG conversion
|
|
||||||
constexpr bool USE_NOISE_DITHERING = false; // Hash-based noise dithering
|
constexpr bool USE_NOISE_DITHERING = false; // Hash-based noise dithering
|
||||||
constexpr bool GAMMA_CORRECTION = true; // Gamma curve, only if USE_BRIGHTNESS=true
|
// Brightness/Contrast adjustments:
|
||||||
|
constexpr bool USE_BRIGHTNESS = false; // true: apply brightness/gamma adjustments
|
||||||
|
constexpr int BRIGHTNESS_BOOST = 10; // Brightness offset (0-50)
|
||||||
|
constexpr bool GAMMA_CORRECTION = false; // Gamma curve (brightens midtones)
|
||||||
|
constexpr float CONTRAST_FACTOR = 1.15f; // Contrast multiplier (1.0 = no change, >1 = more contrast)
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
// Integer approximation of gamma correction (brightens midtones)
|
// Integer approximation of gamma correction (brightens midtones)
|
||||||
|
// Uses a simple curve: out = 255 * sqrt(in/255) ≈ sqrt(in * 255)
|
||||||
static inline int applyGamma(int gray) {
|
static inline int applyGamma(int gray) {
|
||||||
if (!GAMMA_CORRECTION) return gray;
|
if (!GAMMA_CORRECTION) return gray;
|
||||||
|
// Fast integer square root approximation for gamma ~0.5 (brightening)
|
||||||
|
// This brightens dark/mid tones while preserving highlights
|
||||||
const int product = gray * 255;
|
const int product = gray * 255;
|
||||||
|
// Newton-Raphson integer sqrt (2 iterations for good accuracy)
|
||||||
int x = gray;
|
int x = gray;
|
||||||
if (x > 0) {
|
if (x > 0) {
|
||||||
x = (x + product / x) >> 1;
|
x = (x + product / x) >> 1;
|
||||||
@ -25,6 +32,7 @@ static inline int applyGamma(int gray) {
|
|||||||
return x > 255 ? 255 : x;
|
return x > 255 ? 255 : x;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: remove
|
||||||
static inline uint8_t boostBrightness(int gray, uint8_t boost) {
|
static inline uint8_t boostBrightness(int gray, uint8_t boost) {
|
||||||
if (boost > 0) {
|
if (boost > 0) {
|
||||||
gray += boost;
|
gray += boost;
|
||||||
@ -33,10 +41,34 @@ static inline uint8_t boostBrightness(int gray, uint8_t boost) {
|
|||||||
}
|
}
|
||||||
return gray;
|
return gray;
|
||||||
}
|
}
|
||||||
// Simple quantization without dithering - just divide into 4 levels
|
|
||||||
static inline uint8_t quantizeSimple(int gray) {
|
|
||||||
// return static_cast<uint8_t>(gray >> 6);
|
|
||||||
|
|
||||||
|
// Apply contrast adjustment around midpoint (128)
|
||||||
|
// factor > 1.0 increases contrast, < 1.0 decreases
|
||||||
|
static inline int applyContrast(int gray) {
|
||||||
|
// Integer-based contrast: (gray - 128) * factor + 128
|
||||||
|
// Using fixed-point: factor 1.15 ≈ 115/100
|
||||||
|
constexpr int factorNum = static_cast<int>(CONTRAST_FACTOR * 100);
|
||||||
|
int adjusted = ((gray - 128) * factorNum) / 100 + 128;
|
||||||
|
if (adjusted < 0) adjusted = 0;
|
||||||
|
if (adjusted > 255) adjusted = 255;
|
||||||
|
return adjusted;
|
||||||
|
}
|
||||||
|
// Combined brightness/contrast/gamma adjustment
|
||||||
|
int adjustPixel(int gray) {
|
||||||
|
if (!USE_BRIGHTNESS) return gray;
|
||||||
|
|
||||||
|
// Order: contrast first, then brightness, then gamma
|
||||||
|
gray = applyContrast(gray);
|
||||||
|
gray += BRIGHTNESS_BOOST;
|
||||||
|
if (gray > 255) gray = 255;
|
||||||
|
if (gray < 0) gray = 0;
|
||||||
|
gray = applyGamma(gray);
|
||||||
|
|
||||||
|
return gray;
|
||||||
|
}
|
||||||
|
// Simple quantization without dithering - divide into 4 levels
|
||||||
|
// The thresholds are fine-tuned to the X4 display
|
||||||
|
uint8_t quantizeSimple(int gray) {
|
||||||
if (gray < 50) {
|
if (gray < 50) {
|
||||||
return 0;
|
return 0;
|
||||||
} else if (gray < 70) {
|
} else if (gray < 70) {
|
||||||
@ -49,6 +81,7 @@ static inline uint8_t quantizeSimple(int gray) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Hash-based noise dithering - survives downsampling without moiré artifacts
|
// Hash-based noise dithering - survives downsampling without moiré artifacts
|
||||||
|
// Uses integer hash to generate pseudo-random threshold per pixel
|
||||||
static inline uint8_t quantizeNoise(int gray, int x, int y) {
|
static inline uint8_t quantizeNoise(int gray, int x, int y) {
|
||||||
uint32_t hash = static_cast<uint32_t>(x) * 374761393u + static_cast<uint32_t>(y) * 668265263u;
|
uint32_t hash = static_cast<uint32_t>(x) * 374761393u + static_cast<uint32_t>(y) * 668265263u;
|
||||||
hash = (hash ^ (hash >> 13)) * 1274126177u;
|
hash = (hash ^ (hash >> 13)) * 1274126177u;
|
||||||
@ -64,8 +97,8 @@ static inline uint8_t quantizeNoise(int gray, int x, int y) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Main quantization function
|
// Main quantization function - selects between methods based on config
|
||||||
static inline uint8_t quantize(int gray, int x, int y) {
|
uint8_t quantize(int gray, int x, int y) {
|
||||||
if (USE_NOISE_DITHERING) {
|
if (USE_NOISE_DITHERING) {
|
||||||
return quantizeNoise(gray, x, y);
|
return quantizeNoise(gray, x, y);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -62,3 +62,8 @@ class Bitmap {
|
|||||||
mutable int16_t* errorNextRow = nullptr;
|
mutable int16_t* errorNextRow = nullptr;
|
||||||
mutable int prevRowY = -1; // Track row progression for error propagation
|
mutable int prevRowY = -1; // Track row progression for error propagation
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Helper functions
|
||||||
|
uint8_t quantize(int gray, int x, int y);
|
||||||
|
uint8_t quantizeSimple(int gray);
|
||||||
|
int adjustPixel(int gray);
|
||||||
|
|||||||
@ -7,6 +7,8 @@
|
|||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
|
||||||
|
#include "Bitmap.h"
|
||||||
|
|
||||||
// Context structure for picojpeg callback
|
// Context structure for picojpeg callback
|
||||||
struct JpegReadContext {
|
struct JpegReadContext {
|
||||||
FsFile& file;
|
FsFile& file;
|
||||||
@ -23,97 +25,12 @@ constexpr bool USE_8BIT_OUTPUT = false; // true: 8-bit grayscale (no quantizati
|
|||||||
constexpr bool USE_ATKINSON = true; // Atkinson dithering (cleaner than F-S, less error diffusion)
|
constexpr bool USE_ATKINSON = true; // Atkinson dithering (cleaner than F-S, less error diffusion)
|
||||||
constexpr bool USE_FLOYD_STEINBERG = false; // Floyd-Steinberg error diffusion (can cause "worm" artifacts)
|
constexpr bool USE_FLOYD_STEINBERG = false; // Floyd-Steinberg error diffusion (can cause "worm" artifacts)
|
||||||
constexpr bool USE_NOISE_DITHERING = false; // Hash-based noise dithering (good for downsampling)
|
constexpr bool USE_NOISE_DITHERING = false; // Hash-based noise dithering (good for downsampling)
|
||||||
// Brightness/Contrast adjustments:
|
|
||||||
constexpr bool USE_BRIGHTNESS = false; // true: apply brightness/gamma adjustments
|
|
||||||
constexpr int BRIGHTNESS_BOOST = 10; // Brightness offset (0-50)
|
|
||||||
constexpr bool GAMMA_CORRECTION = false; // Gamma curve (brightens midtones)
|
|
||||||
constexpr float CONTRAST_FACTOR = 1.15f; // Contrast multiplier (1.0 = no change, >1 = more contrast)
|
|
||||||
// Pre-resize to target display size (CRITICAL: avoids dithering artifacts from post-downsampling)
|
// Pre-resize to target display size (CRITICAL: avoids dithering artifacts from post-downsampling)
|
||||||
constexpr bool USE_PRESCALE = true; // true: scale image to target size before dithering
|
constexpr bool USE_PRESCALE = true; // true: scale image to target size before dithering
|
||||||
constexpr int TARGET_MAX_WIDTH = 480; // Max width for cover images (portrait display width)
|
constexpr int TARGET_MAX_WIDTH = 480; // Max width for cover images (portrait display width)
|
||||||
constexpr int TARGET_MAX_HEIGHT = 800; // Max height for cover images (portrait display height)
|
constexpr int TARGET_MAX_HEIGHT = 800; // Max height for cover images (portrait display height)
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
// Integer approximation of gamma correction (brightens midtones)
|
|
||||||
// Uses a simple curve: out = 255 * sqrt(in/255) ≈ sqrt(in * 255)
|
|
||||||
static inline int applyGamma(int gray) {
|
|
||||||
if (!GAMMA_CORRECTION) return gray;
|
|
||||||
// Fast integer square root approximation for gamma ~0.5 (brightening)
|
|
||||||
// This brightens dark/mid tones while preserving highlights
|
|
||||||
const int product = gray * 255;
|
|
||||||
// Newton-Raphson integer sqrt (2 iterations for good accuracy)
|
|
||||||
int x = gray;
|
|
||||||
if (x > 0) {
|
|
||||||
x = (x + product / x) >> 1;
|
|
||||||
x = (x + product / x) >> 1;
|
|
||||||
}
|
|
||||||
return x > 255 ? 255 : x;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply contrast adjustment around midpoint (128)
|
|
||||||
// factor > 1.0 increases contrast, < 1.0 decreases
|
|
||||||
static inline int applyContrast(int gray) {
|
|
||||||
// Integer-based contrast: (gray - 128) * factor + 128
|
|
||||||
// Using fixed-point: factor 1.15 ≈ 115/100
|
|
||||||
constexpr int factorNum = static_cast<int>(CONTRAST_FACTOR * 100);
|
|
||||||
int adjusted = ((gray - 128) * factorNum) / 100 + 128;
|
|
||||||
if (adjusted < 0) adjusted = 0;
|
|
||||||
if (adjusted > 255) adjusted = 255;
|
|
||||||
return adjusted;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Combined brightness/contrast/gamma adjustment
|
|
||||||
static inline int adjustPixel(int gray) {
|
|
||||||
if (!USE_BRIGHTNESS) return gray;
|
|
||||||
|
|
||||||
// Order: contrast first, then brightness, then gamma
|
|
||||||
gray = applyContrast(gray);
|
|
||||||
gray += BRIGHTNESS_BOOST;
|
|
||||||
if (gray > 255) gray = 255;
|
|
||||||
if (gray < 0) gray = 0;
|
|
||||||
gray = applyGamma(gray);
|
|
||||||
|
|
||||||
return gray;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Simple quantization without dithering - just divide into 4 levels
|
|
||||||
static inline uint8_t quantizeSimple(int gray) {
|
|
||||||
gray = adjustPixel(gray);
|
|
||||||
// Simple 2-bit quantization: 0-63=0, 64-127=1, 128-191=2, 192-255=3
|
|
||||||
return static_cast<uint8_t>(gray >> 6);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hash-based noise dithering - survives downsampling without moiré artifacts
|
|
||||||
// Uses integer hash to generate pseudo-random threshold per pixel
|
|
||||||
static inline uint8_t quantizeNoise(int gray, int x, int y) {
|
|
||||||
gray = adjustPixel(gray);
|
|
||||||
|
|
||||||
// Generate noise threshold using integer hash (no regular pattern to alias)
|
|
||||||
uint32_t hash = static_cast<uint32_t>(x) * 374761393u + static_cast<uint32_t>(y) * 668265263u;
|
|
||||||
hash = (hash ^ (hash >> 13)) * 1274126177u;
|
|
||||||
const int threshold = static_cast<int>(hash >> 24); // 0-255
|
|
||||||
|
|
||||||
// Map gray (0-255) to 4 levels with dithering
|
|
||||||
const int scaled = gray * 3;
|
|
||||||
|
|
||||||
if (scaled < 255) {
|
|
||||||
return (scaled + threshold >= 255) ? 1 : 0;
|
|
||||||
} else if (scaled < 510) {
|
|
||||||
return ((scaled - 255) + threshold >= 255) ? 2 : 1;
|
|
||||||
} else {
|
|
||||||
return ((scaled - 510) + threshold >= 255) ? 3 : 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Main quantization function - selects between methods based on config
|
|
||||||
static inline uint8_t quantize(int gray, int x, int y) {
|
|
||||||
if (USE_NOISE_DITHERING) {
|
|
||||||
return quantizeNoise(gray, x, y);
|
|
||||||
} else {
|
|
||||||
return quantizeSimple(gray);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Atkinson dithering - distributes only 6/8 (75%) of error for cleaner results
|
// Atkinson dithering - distributes only 6/8 (75%) of error for cleaner results
|
||||||
// Error distribution pattern:
|
// Error distribution pattern:
|
||||||
// X 1/8 1/8
|
// X 1/8 1/8
|
||||||
@ -686,7 +603,7 @@ bool JpegToBmpConverter::jpegFileToBmpStream(FsFile& jpegFile, Print& bmpOut) {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for (int x = 0; x < outWidth; x++) {
|
for (int x = 0; x < outWidth; x++) {
|
||||||
const uint8_t gray = (rowCount[x] > 0) ? (rowAccum[x] / rowCount[x]) : 0;
|
const uint8_t gray = adjustPixel((rowCount[x] > 0) ? (rowAccum[x] / rowCount[x]) : 0);
|
||||||
uint8_t twoBit;
|
uint8_t twoBit;
|
||||||
if (atkinsonDitherer) {
|
if (atkinsonDitherer) {
|
||||||
twoBit = atkinsonDitherer->processPixel(gray, x);
|
twoBit = atkinsonDitherer->processPixel(gray, x);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user