Skip to main content

Dithering algorithms for e-paper/e-ink displays

Project description

epaper-dithering

PyPI Python Tests Python Lint

Dithering algorithms optimized for e-ink/e-paper displays with limited color palettes.

Installation

# With uv
uv add epaper-dithering

# With pip
pip install epaper-dithering

Features

  • Perceptually Correct: Uses linear RGB color space with gamma correction for accurate error diffusion
  • 8 Dithering Algorithms: From simple ordered dithering to high-quality Jarvis-Judice-Ninke
  • 6 Color Schemes: Support for mono, 3-color, 4-color, and 6-color e-paper displays
  • Tone Mapping: Dynamic range compression maps image luminance to the display's actual range for smoother dithering
  • Serpentine Scanning: Reduces directional artifacts in error diffusion (enabled by default)
  • RGBA Support: Automatic compositing on white background for transparent images

Quick Start

from PIL import Image
from epaper_dithering import dither_image, ColorScheme, DitherMode

# Load your image
image = Image.open("photo.jpg")

# Apply dithering for a black/white/red display
dithered = dither_image(image, ColorScheme.BWR, DitherMode.FLOYD_STEINBERG)

# Save result
dithered.save("output.png")

Supported Color Schemes

  • MONO - Black and white (1-bit)
  • BWR - Black, white, red (3-color)
  • BWY - Black, white, yellow (3-color)
  • BWRY - Black, white, red, yellow (4-color)
  • BWGBRY - Black, white, green, blue, red, yellow (6-color Spectra)
  • GRAYSCALE_4 - 4-level grayscale

Dithering Algorithms

Algorithm Quality Speed Best For
NONE Lowest Fastest Testing, simple graphics
ORDERED Low Very Fast Patterns, textures
SIERRA_LITE Medium Fast Quick results
BURKES Good Medium General purpose (default)
FLOYD_STEINBERG Good Medium Popular standard
SIERRA High Medium Balanced quality
ATKINSON Good Medium High contrast, artistic
STUCKI Very High Slow Maximum quality
JARVIS_JUDICE_NINKE Highest Slowest Smooth gradients

Usage Examples

Basic Usage

from PIL import Image
from epaper_dithering import dither_image, ColorScheme, DitherMode

# Load image
img = Image.open("photo.jpg")

# Apply Floyd-Steinberg dithering for BWR display
result = dither_image(img, ColorScheme.BWR, DitherMode.FLOYD_STEINBERG)
result.save("dithered.png")

All Color Schemes

from epaper_dithering import ColorScheme

# Black and white only
dithered = dither_image(img, ColorScheme.MONO)

# Black, white, and red (common for e-paper tags)
dithered = dither_image(img, ColorScheme.BWR)

# Grayscale (4 levels)
dithered = dither_image(img, ColorScheme.GRAYSCALE_4)

# 6-color display (Spectra)
dithered = dither_image(img, ColorScheme.BWGBRY)

Advanced Options

Serpentine Scanning

By default, error diffusion algorithms use serpentine scanning (alternating scan direction per row) to reduce directional artifacts and "worm" patterns. You can disable this for raster scanning:

# Default: serpentine scanning (recommended for best quality)
result = dither_image(img, ColorScheme.BWR, DitherMode.FLOYD_STEINBERG, serpentine=True)

# Disable serpentine for raster scanning (left-to-right only)
result = dither_image(img, ColorScheme.BWR, DitherMode.FLOYD_STEINBERG, serpentine=False)

Note: The serpentine parameter only affects error diffusion algorithms (Floyd-Steinberg, Burkes, Atkinson, Sierra, Sierra Lite, Stucki, Jarvis-Judice-Ninke). It has no effect on NONE and ORDERED modes.

Tone Compression (Dynamic Range)

E-paper displays can't reproduce the full luminance range of digital images. Pure white on a display is much darker than (255, 255, 255), and pure black is lighter than (0, 0, 0). Without tone compression, dithering tries to represent unreachable brightness levels, causing large accumulated errors and noisy output.

Tone compression remaps image luminance from [0, 1] to the display's actual [black, white] range before dithering, producing smoother results. Based on fast_compress_dynamic_range() from esp32-photoframe by aitjcize. It is enabled by default (tone_compression=1.0) and only applies when using measured ColorPalette instances:

from epaper_dithering import dither_image, SPECTRA_7_3_6COLOR, DitherMode

# Default: full tone compression (recommended)
result = dither_image(img, SPECTRA_7_3_6COLOR, DitherMode.FLOYD_STEINBERG)

# Disable tone compression
result = dither_image(img, SPECTRA_7_3_6COLOR, DitherMode.FLOYD_STEINBERG, tone_compression=0.0)

# Partial compression (blend between original and compressed)
result = dither_image(img, SPECTRA_7_3_6COLOR, DitherMode.FLOYD_STEINBERG, tone_compression=0.5)

Note: tone_compression has no effect when using theoretical ColorScheme palettes (e.g., ColorScheme.BWR), since their black/white values already span the full range.

RGBA Images

Images with transparency (RGBA mode) are automatically composited on a white background, matching the typical appearance of e-paper displays:

# RGBA images are handled automatically
rgba_img = Image.open("transparent.png")  # Has alpha channel
result = dither_image(rgba_img, ColorScheme.BWR)
# Transparent areas become white

Measured Display Colors

For the most accurate dithering, use measured RGB values from your specific e-paper display instead of theoretical pure RGB colors.

Why Measure?

E-paper displays use reflective technology, making colors 30-87% darker than pure RGB:

  • Pure RGB White: (255, 255, 255) → Real display: ~(180-200, 180-200, 180-200)
  • Pure RGB Red: (255, 0, 0) → Real display: ~(115-125, 10-20, 0-10)

Using measured values ensures dithered images match your display's actual appearance.

Using Pre-defined Measured Palettes

The library includes measured palettes for common displays:

from epaper_dithering import dither_image, SPECTRA_7_3_6COLOR, DitherMode

# Use measured palette for Spectra 7.3" 6-color display
result = dither_image(img, SPECTRA_7_3_6COLOR, DitherMode.FLOYD_STEINBERG)

Available measured palettes:

  • SPECTRA_7_3_6COLOR - 7.3" Spectra™ 6-color (BWGBRY)
  • MONO_4_26 - 4.26" Monochrome
  • BWRY_4_2 - 4.2" BWRY
  • SOLUM_BWR - Solum BWR
  • HANSHOW_BWR - Hanshow BWR
  • HANSHOW_BWY - Hanshow BWY

Note: Pre-defined palettes start with theoretical values. See CALIBRATION.md for measuring your specific display.

Creating Custom Measured Palettes

Measure your display and create a custom palette:

from epaper_dithering import dither_image, ColorPalette, DitherMode

# Your measured RGB values
my_display = ColorPalette(
    colors={
        'black': (5, 5, 5),           # Measured from your display
        'white': (185, 190, 180),     # Much darker than (255,255,255)
        'red': (120, 15, 5),          # Much darker than (255,0,0)
    },
    accent='red'
)

# Use it directly
result = dither_image(img, my_display, DitherMode.FLOYD_STEINBERG)

Measurement Quick Start

  1. Display full-screen color patches on your e-paper
  2. Photograph in consistent lighting (avoid shadows/reflections)
  3. Sample RGB values from center using photo editor
  4. Average 5+ samples per color
  5. Create ColorPalette with measured values

See docs/CALIBRATION.md for detailed measurement procedures, including camera calibration, colorimeter usage, and validation techniques.

Development

# Install with dev dependencies
uv sync --all-extras

# Run tests
uv run pytest tests/ -v

# Run tests with coverage
uv run pytest tests/ --cov=src/epaper_dithering

# Lint
uv run ruff check src/ tests/

# Type check
uv run mypy src/epaper_dithering

Credits

Measured color calibration techniques and reference measurements inspired by:

  • esp32-photoframe by aitjcize - Measured palette methodology, dynamic range compression algorithm, and reference values for Waveshare 7.3" displays

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

epaper_dithering-0.5.3.tar.gz (28.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

epaper_dithering-0.5.3-py3-none-any.whl (18.7 kB view details)

Uploaded Python 3

File details

Details for the file epaper_dithering-0.5.3.tar.gz.

File metadata

  • Download URL: epaper_dithering-0.5.3.tar.gz
  • Upload date:
  • Size: 28.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for epaper_dithering-0.5.3.tar.gz
Algorithm Hash digest
SHA256 4c939f54adf8638612d02793d825a91e5d1c59f89b9807fe11336858553b1979
MD5 8ca524700a0cda3820b2a009783c6f2a
BLAKE2b-256 e44878ba920bb16c6f8b82e0098b295391b3bca80dd3b60312d2a6878e5c0b9a

See more details on using hashes here.

Provenance

The following attestation bundles were made for epaper_dithering-0.5.3.tar.gz:

Publisher: release.yml on OpenDisplay-org/epaper-dithering

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file epaper_dithering-0.5.3-py3-none-any.whl.

File metadata

File hashes

Hashes for epaper_dithering-0.5.3-py3-none-any.whl
Algorithm Hash digest
SHA256 9966951e08bc598fdecb4d9b6db4eb95b49f852297491abdaede32fb68acc830
MD5 32b42e72084f0598f6450faf014cb314
BLAKE2b-256 f03340ed5d7be6e8963b6e5279d9f712875c7ada7173433590cf4ee9dac3b399

See more details on using hashes here.

Provenance

The following attestation bundles were made for epaper_dithering-0.5.3-py3-none-any.whl:

Publisher: release.yml on OpenDisplay-org/epaper-dithering

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page