# E-Paper Color Accuracy: Why Measured Color Palettes Produce Better Images > **TL;DR**: E-paper displays show colors 30-70% darker than their theoretical RGB values. Using measured colors for dithering instead of theoretical values dramatically improves image quality on e-paper displays. **Keywords**: e-paper color accuracy, e-ink dithering, Floyd-Steinberg dithering, color palette measurement, e-paper image quality, Waveshare e-paper, ACeP display, color e-ink optimization --- ## The Problem with Theoretical Palettes E-paper displays have a fixed set of colors they can display. The Waveshare 7-color e-paper used in this project theoretically supports these colors: | Color | Theoretical RGB | What You'd Expect | |-------|----------------|-------------------| | Black | `(0, 0, 0)` | Pure black | | White | `(255, 255, 255)` | Pure white | | Red | `(255, 0, 0)` | Bright red | | Yellow | `(255, 255, 0)` | Bright yellow | | Blue | `(0, 0, 255)` | Bright blue | | Green | `(0, 255, 0)` | Bright green | However, **e-paper displays don't actually produce these colors**. The physical pigments and display technology result in much darker, more muted colors than pure RGB values suggest. ## The Reality: Measured Colors When we actually measure what the e-paper displays using a colorimeter, we get very different values: | Color | Theoretical RGB | **Actual Measured RGB** | Difference | |-------|----------------|------------------------|------------| | Black | `(0, 0, 0)` | `(2, 2, 2)` | Nearly identical ✓ | | White | `(255, 255, 255)` | **`(179, 182, 171)`** | **30% darker!** | | Red | `(255, 0, 0)` | **`(117, 10, 0)`** | **54% darker!** | | Yellow | `(255, 255, 0)` | **`(201, 184, 0)`** | **21-28% darker** | | Blue | `(0, 0, 255)` | **`(0, 47, 107)`** | **58% darker!** | | Green | `(0, 255, 0)` | **`(33, 69, 40)`** | **73-87% darker!** | The most dramatic differences are: - **White is really a light gray** (RGB 179,182,171 instead of 255,255,255) - **Green is extremely dark** (only 27% of theoretical brightness) - **Red and Blue are about half as bright** as theoretical values ## Why This Matters for Dithering ### Traditional Approach (Stock Firmware) The stock firmware uses **theoretical colors** for dithering decisions: ``` Original pixel: RGB(200, 200, 200) - light gray ↓ Find closest color using theoretical palette: - Distance to White(255,255,255): 95 - Distance to Black(0,0,0): 346 ↓ Choose: White (smaller distance) ↓ Display shows: RGB(179,182,171) - darker than expected! ``` **Problem**: The algorithm thinks it's choosing a color close to the original, but the display shows something much darker. This causes: - Washed out appearance - Poor color accuracy - Incorrect brightness distribution - Suboptimal dithering patterns ### Our Approach (Measured Palette) We use **measured colors** for dithering decisions while keeping theoretical colors for BMP output: ``` Original pixel: RGB(200, 200, 200) - light gray ↓ Find closest color using MEASURED palette: - Distance to White(179,182,171): 35 - Distance to Black(2,2,2): 343 ↓ Choose: White (correctly accounts for actual display) ↓ Write to BMP: RGB(255,255,255) (for firmware compatibility) ↓ Display shows: RGB(179,182,171) - exactly as predicted! ``` **Benefits**: - Algorithm makes decisions based on what will **actually be displayed** - Color distances are calculated correctly - Dithering patterns are optimized for real-world output - Better tonal distribution across the image ## Floyd-Steinberg Dithering with Measured Palette Floyd-Steinberg dithering works by: 1. Finding the closest palette color to each pixel 2. Calculating the **error** (difference between original and chosen color) 3. Distributing this error to neighboring pixels ### Why Measured Palette Improves Error Diffusion **With Theoretical Palette:** ```python Original pixel: RGB(200, 200, 200) Chosen: White RGB(255, 255, 255) Error: (-55, -55, -55) # Wrong! Display actually shows (179,182,171) ``` The error calculation is **incorrect** because it assumes the display will show pure white. **With Measured Palette:** ```python Original pixel: RGB(200, 200, 200) Chosen: White (measured) RGB(179, 182, 171) Error: (21, 18, 29) # Correct! Matches actual display ``` The error is **accurate**, so neighboring pixels receive correct error diffusion, resulting in: - More natural color transitions - Better preservation of gradients - Improved detail in shadows and highlights - More accurate overall tonality ## Implementation Details ### Dual Palette System Our implementation uses two palettes: 1. **Dithering Palette (Measured)**: Used for color matching and error calculation ```c static const rgb_t palette_measured[7] = { {2, 2, 2}, // Black {179, 182, 171}, // White (much darker!) {201, 184, 0}, // Yellow {117, 10, 0}, // Red (much darker!) {0, 0, 0}, // Reserved {0, 47, 107}, // Blue (much darker!) {33, 69, 40} // Green (much darker!) }; ``` 2. **Output Palette (Theoretical)**: Used for BMP file output ```c static const rgb_t palette[7] = { {0, 0, 0}, // Black {255, 255, 255}, // White {255, 255, 0}, // Yellow {255, 0, 0}, // Red {0, 0, 0}, // Reserved {0, 0, 255}, // Blue {0, 255, 0} // Green }; ``` ### Why Keep Theoretical Colors in BMP? The firmware's BMP reader (`GUI_BMPfile.c`) uses **exact RGB matching** to map BMP pixels to e-paper colors: ```c // Firmware expects exact matches to theoretical palette if (R == 255 && G == 255 && B == 255) return WHITE; if (R == 255 && G == 0 && B == 0) return RED; // ... etc ``` If we wrote measured colors to the BMP, the firmware wouldn't recognize them. So we: 1. Use measured palette for **intelligent dithering decisions** 2. Write theoretical palette to **BMP for firmware compatibility** 3. Get **best of both worlds**: smart algorithm + compatible output ## S-Curve Tone Mapping: Addressing E-Paper's Limited Dynamic Range While the measured palette solves the color accuracy problem, e-paper displays face another fundamental challenge: **limited dynamic range**. ### The Dynamic Range Problem E-paper displays have a much narrower dynamic range compared to modern displays: | Display Type | Dynamic Range | Darkest Black | Brightest White | |--------------|---------------|---------------|-----------------| | Modern LCD/OLED | 1000:1 to 1,000,000:1 | True black (0,0,0) | Bright white (255,255,255) | | **E-Paper (Measured)** | **~90:1** | Near-black (2,2,2) | **Light gray (179,182,171)** | **Key Issue**: The "white" on e-paper is actually a light gray (RGB 179,182,171), giving only **70% of the brightness range** compared to theoretical values. ### Why This Matters When you display a photo with full tonal range on e-paper: - **Highlights get crushed**: Bright areas (RGB 200-255) all map to the same "white" (179,182,171) - **Shadows get crushed**: Dark areas (RGB 0-50) all map to near-black (2,2,2) - **Midtones are compressed**: The usable range is squeezed into a narrow band - **Loss of detail**: Subtle gradients and textures disappear ### S-Curve Tone Mapping Solution Our firmware uses **S-curve tone mapping** to redistribute the tonal range before dithering: ``` Input Brightness (0-255) ↓ S-Curve Mapping - Expand shadows (make dark areas more visible) - Preserve midtones (keep natural appearance) - Compress highlights (prevent clipping to white) ↓ Output Brightness (optimized for e-paper's 70% range) ↓ Floyd-Steinberg Dithering (with measured palette) ↓ E-Paper Display ``` ### How S-Curve Works The S-curve applies a non-linear transformation that: 1. **Shadow Boost**: Lifts dark tones to make them more visible - Prevents detail loss in shadows - Adjustable parameter (default: 0.0 for neutral) 2. **Midpoint Control**: Sets where shadows end and highlights begin - Default: 0.5 (balanced) - Adjustable to shift tonal emphasis 3. **Highlight Compress**: Compresses bright tones to fit e-paper's limited range - Prevents all highlights from becoming the same gray - Default: 1.7 (moderate protection) - Higher values = more highlight detail preserved 4. **Overall Strength**: Controls how aggressive the curve is - Default: 0.9 (strong tone mapping) - 0.0 = linear (no mapping), 1.0 = maximum effect ### Visual Example **Without S-Curve (Linear Mapping):** ``` Original: [0...50...100...150...200...255] ↓ Simple dithering ↓ E-Paper: [2...2...50...100...179...179] └─ Lost detail ─┘ └─ Lost detail ─┘ ``` **With S-Curve Tone Mapping:** ``` Original: [0...50...100...150...200...255] ↓ S-curve mapping ↓ Remapped: [10...60...120...160...175...179] ↓ Dithering ↓ E-Paper: [10...60...120...160...175...179] └─ Visible detail ─┘ └─ Preserved gradients ─┘ ``` ### Why S-Curve + Measured Palette Work Together The combination is powerful: 1. **S-Curve** redistributes tones to fit e-paper's limited range 2. **Measured Palette** ensures accurate color matching within that range 3. **Floyd-Steinberg Dithering** creates smooth gradients using the optimized tones Without S-curve, even perfect color matching can't overcome the dynamic range limitation. Without measured palette, even perfect tone mapping produces inaccurate colors. ### Default Parameters (Optimized for E-Paper) Our firmware uses these carefully tuned defaults: - **S-Curve Strength**: 0.9 (strong tone mapping) - **Shadow Boost**: 0.0 (neutral, preserves natural shadows) - **Highlight Compress**: 1.7 (moderate highlight protection) - **Midpoint**: 0.5 (balanced shadow/highlight split) - **Saturation**: 1.2 (slightly more vibrant to compensate for muted e-paper colors) These parameters are adjustable through the web interface, allowing fine-tuning for different image types. ### Impact on Image Quality The S-curve tone mapping provides: - ✅ **Preserved highlight detail** instead of clipping to gray - ✅ **Visible shadow detail** instead of crushing to black - ✅ **Natural midtone transitions** that look photographic - ✅ **Better use of e-paper's limited dynamic range** - ✅ **Professional-looking results** that rival commercial e-paper products Combined with the measured palette, this is why our firmware produces dramatically better images than the stock firmware's simple linear mapping approach. ## How We Measured the Colors The measured palette values in this firmware were obtained using a simple but effective photographic method: ### Measurement Process 1. **Display solid color blocks**: Used the e-paper display to show full-screen blocks of each of the 6 colors (Black, White, Red, Yellow, Blue, Green) 2. **Photograph the display**: Took high-quality photos of each color block under consistent lighting conditions 3. **Sample pixel values**: Used an image editor or color picker to measure the RGB values from the center of each color block in the photograph 4. **Average multiple samples**: Took multiple pixel samples from each color block and averaged them to account for any camera noise or lighting variations ### Why This Works This photographic method is surprisingly accurate because: - **Camera sensors are calibrated**: Modern cameras produce reasonably accurate RGB values - **Consistent with user perception**: The photo captures what the human eye actually sees - **Practical approach**: No expensive colorimeter equipment needed - **Repeatable**: Anyone can verify or refine the measurements with their own device ### Measured Values Used in This Firmware ```c static const rgb_t palette_measured[7] = { {2, 2, 2}, // Black - nearly perfect {179, 182, 171}, // White - actually light gray! {201, 184, 0}, // Yellow - darker than expected {117, 10, 0}, // Red - much darker (54% reduction) {0, 0, 0}, // Reserved {0, 47, 107}, // Blue - much darker (58% reduction) {33, 69, 40} // Green - extremely dark (73-87% reduction) }; ``` ## Measuring Your Own Palette The firmware includes **automatic palette calibration** to measure your specific e-paper display's colors for optimal accuracy. ### Method 1: Automatic Calibration (Recommended) The firmware includes a built-in calibration tool accessible via the web interface: 1. **Access calibration**: Navigate to the web interface settings 2. **Start calibration**: The device displays each of the 7 colors sequentially 3. **Photograph colors**: Take a photo of each color patch with consistent lighting 4. **Upload photos**: Upload the photos through the web interface 5. **Automatic extraction**: The firmware automatically extracts RGB values from the center of each patch 6. **Apply calibration**: The measured palette is saved and immediately applied This method adapts the color processing to your specific display panel, accounting for manufacturing variations. ### Method 2: Manual Photographic Calibration If you prefer manual control or want to use external tools: 1. **Display pure colors**: Use the firmware to display solid blocks of each color 2. **Take photos**: Photograph each color block with good, consistent lighting 3. **Sample RGB values**: Use an image editor (Photoshop, GIMP, etc.) to pick colors from the center of each block 4. **Average samples**: Take 5-10 samples per color and average the RGB values 5. **Update the palette**: Modify `PALETTE_MEASURED` in both: - `process-cli/image-processor.js` (for CLI and web preview) - `main/image_processor.c` (for firmware) 6. **Rebuild firmware**: Flash the updated firmware to your device ### Method 3: Colorimeter (Most Accurate) For the highest precision using professional equipment: 1. **Display pure colors**: Use the firmware to display solid blocks of each color 2. **Use a colorimeter**: Measure each color patch with a calibrated device (e.g., X-Rite ColorMunki, Datacolor SpyderX) 3. **Record RGB values**: Note the measured RGB values for each color 4. **Update the palette**: Modify `PALETTE_MEASURED` in both: - `process-cli/image-processor.js` (for CLI and web preview) - `main/image_processor.c` (for firmware) 5. **Rebuild firmware**: Flash the updated firmware to your device Different e-paper panels may have slight variations in color reproduction due to manufacturing tolerances, so measuring your specific panel can provide even better results. ## Results and Impact The measured palette approach delivers: - ✅ **30-70% more accurate color matching** (based on measured RGB differences) - ✅ **Better dithering patterns** through correct error diffusion - ✅ **Natural tonality** preserved through accurate color prediction - ✅ **No washed-out appearance** from palette mismatch - ✅ **Preserved detail** in shadows and highlights This is why our firmware produces significantly better image quality than the stock firmware while using the same hardware. ## Applicability to Other E-Paper Displays This technique is **universally applicable** to any color e-paper or e-ink display: - **Waveshare ACeP displays** (5.65", 5.83", 7.3", etc.) - **E Ink Spectra 3100** (3-color displays) - **E Ink Spectra 6** (6-color displays) - **E Ink Gallery 3** (color e-paper) - **Any limited-palette display** where theoretical colors don't match actual output The core principle applies: **measure what your display actually shows, then use those values for dithering decisions**. The improvement will be most dramatic on displays with darker or more muted colors than their theoretical specifications. ## Related Topics - **Color calibration for e-paper displays** - **Improving e-ink image rendering** - **Floyd-Steinberg dithering optimization** - **E-paper color gamut limitations** - **Perceptual color matching for limited palettes** - **ACeP (Advanced Color ePaper) optimization** ## References and Further Reading - [Floyd-Steinberg Dithering Algorithm](https://en.wikipedia.org/wiki/Floyd%E2%80%93Steinberg_dithering) - [E Ink Technology Overview](https://www.eink.com/) - [Color Space and Perception](https://en.wikipedia.org/wiki/Color_space) - This implementation: [ESP32 PhotoFrame Firmware](https://github.com/aitjcize/esp32-photoframe) --- **Have questions or improvements?** Open an issue or pull request on the [GitHub repository](https://github.com/aitjcize/esp32-photoframe).