Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ yarn.lock
# Project specific
/fixtures/expected/
*.png
!docs/examples/*.png
*.jpg
*.jpeg
*.gif
Expand Down
16 changes: 14 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ Dithering algorithms for e-paper/e-ink displays. Rust core with Python and JavaS

![All color schemes compared](docs/examples/color_schemes_grid.png)

**DBS refinement** (Spectra 6-color, auto tone + gamut). Direct Binary Search optimizes the dithered pixels against a model of the eye at a given viewing distance: here it recovers the blue sky that plain error diffusion washes out to grey. The zoomed crops show the trade-off up close, including a few colored specks where it mixes inks. It is opt-in and slower (about 0.3 s per 800×480 panel on a multi-core machine, vs. ~30 ms).

![Original vs Burkes vs Burkes + DBS, full image and 4× crops](docs/examples/dbs_comparison.png)

## Packages

### Python (`packages/python/`)
Expand All @@ -44,6 +48,9 @@ dithered = dither_image(img, SPECTRA_7_3_6COLOR_V2)

# Opt in to automatic tone + gamut compression for photos
dithered = dither_image(img, SPECTRA_7_3_6COLOR_V2, tone="auto", gamut="auto")

# Opt in to DBS refinement (slower; optimizes for how the image is perceived)
dithered = dither_image(img, SPECTRA_7_3_6COLOR_V2, dbs=True)
```

See [`packages/python/README.md`](packages/python/README.md) for full documentation.
Expand All @@ -67,6 +74,9 @@ const dithered = ditherImage(imageBuffer, SPECTRA_7_3_6COLOR_V2);

// Opt in to automatic tone + gamut compression for photos
const compressed = ditherImage(imageBuffer, SPECTRA_7_3_6COLOR_V2, { tone: 'auto', gamut: 'auto' });

// Opt in to DBS refinement (slower; run it in a Web Worker in the browser)
const refined = ditherImage(imageBuffer, SPECTRA_7_3_6COLOR_V2, { dbs: true });
```

See [`packages/javascript/README.md`](packages/javascript/README.md) for full documentation.
Expand All @@ -78,7 +88,8 @@ See [`packages/javascript/README.md`](packages/javascript/README.md) for full do
- **10 Color Schemes**: MONO, BWR, BWY, BWRY, BWGBRY (Spectra 6), GRAYSCALE_4, GRAYSCALE_16, SEVEN_COLOR (Spectra/ACeP 7), BWGBRY_SPLIT, GRAYSCALE_8 (library-local, e.g. Inkplate 10 — not a firmware wire value)
- **Measured Palettes**: Calibrated RGB values for real displays, linked to their canonical firmware palette
- **OKLab Color Matching**: Weighted Cartesian OKLab — preserves hue without the achromatic-attractor bug of LCH-weighted approaches
- **Pre-dither Knobs**: Per-image exposure, saturation, shadows, highlights, and gamut compression — all orthogonal
- **Pre-dither Knobs**: Per-image exposure, saturation, shadows, highlights, dynamic-range (tone) compression, and gamut compression — all orthogonal
- **DBS Refinement**: Optional Direct Binary Search pass that optimizes the dithered pixels for how they are perceived at a given viewing distance and panel resolution — opt-in, slower

## Repository Structure

Expand All @@ -103,7 +114,8 @@ uv sync --all-extras
uv run maturin develop --release
uv run pytest tests/ -v

# JavaScript
# JavaScript (requires wasm-pack and the wasm32-unknown-unknown target)
wasm-pack build packages/rust/wasm --target web --out-dir ../../javascript/src/wasm-core
cd packages/javascript
bun install
bun run test
Expand Down
Binary file modified docs/examples/algorithms_grid.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/examples/color_schemes_grid.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/examples/dbs_comparison.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/examples/frankfurt_before_after.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
57 changes: 49 additions & 8 deletions docs/examples/generate.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,32 @@ def label(img: Image.Image, text: str, size: int = 20) -> Image.Image:
return out


def hstack(images: list[Image.Image], gap: int = 4) -> Image.Image:
"""Place images side by side with a dark gutter."""
out = Image.new("RGB", (sum(i.width for i in images) + gap * (len(images) - 1), max(i.height for i in images)), (15, 15, 15))
x = 0
for im in images:
out.paste(im, (x, 0))
x += im.width + gap
return out


def vstack(images: list[Image.Image], gap: int = 4) -> Image.Image:
"""Stack images vertically with a dark gutter."""
out = Image.new("RGB", (max(i.width for i in images), sum(i.height for i in images) + gap * (len(images) - 1)), (15, 15, 15))
y = 0
for im in images:
out.paste(im, (0, y))
y += im.height + gap
return out


def zoom(img: Image.Image, box: tuple[int, int, int, int], factor: int = 4) -> Image.Image:
"""Crop and upscale with nearest-neighbour so individual pixels stay visible."""
crop = img.crop(box)
return crop.resize((crop.width * factor, crop.height * factor), Image.Resampling.NEAREST)


def grid(cells: list[list[Image.Image]]) -> Image.Image:
rows = len(cells)
cols = max(len(r) for r in cells)
Expand All @@ -48,17 +74,13 @@ def grid(cells: list[list[Image.Image]]) -> Image.Image:
frankfurt_no_pre = dither_image(frankfurt_orig, SPECTRA_7_3_6COLOR_V2,
mode=DitherMode.BURKES, tone=0.0, gamut=0.0).convert("RGB")
frankfurt_auto = dither_image(frankfurt_orig, SPECTRA_7_3_6COLOR_V2,
mode=DitherMode.BURKES).convert("RGB")
mode=DitherMode.BURKES, tone="auto", gamut="auto").convert("RGB")

p1 = label(frankfurt_orig, "Original")
p2 = label(frankfurt_no_pre, "Spectra 6-color · Burkes · no preprocessing")
p3 = label(frankfurt_auto, "Spectra 6-color · Burkes · auto tone + gamut")

ba = Image.new("RGB", (p1.width + p2.width + p3.width + 8, p1.height), (15, 15, 15))
ba.paste(p1, (0, 0))
ba.paste(p2, (p1.width + 4, 0))
ba.paste(p3, (p1.width + p2.width + 8, 0))
ba.save(OUT / "frankfurt_before_after.png")
hstack([p1, p2, p3]).save(OUT / "frankfurt_before_after.png")

# ── 2. All algorithms grid ────────────────────────────────────────────────────

Expand All @@ -75,12 +97,14 @@ def grid(cells: list[list[Image.Image]]) -> Image.Image:
(DitherMode.SIERRA, "Sierra"),
(DitherMode.STUCKI, "Stucki"),
(DitherMode.JARVIS_JUDICE_NINKE,"Jarvis-Judice-Ninke"),
(DitherMode.DIZZY, "Dizzy"),
]

# Two columns keep each cell large enough to judge once GitHub scales the image down.
algo_cells = []
for i in range(0, len(ALGOS), 3):
for i in range(0, len(ALGOS), 2):
row = []
for mode, name in ALGOS[i:i+3]:
for mode, name in ALGOS[i:i+2]:
cell = dithered_rgb(src, SPECTRA_7_3_6COLOR_V2, mode)
row.append(label(cell, name))
algo_cells.append(row)
Expand Down Expand Up @@ -113,4 +137,21 @@ def grid(cells: list[list[Image.Image]]) -> Image.Image:

grid(scheme_cells).save(OUT / "color_schemes_grid.png")

# ── 4. DBS refinement ─────────────────────────────────────────────────────────

print("Generating DBS comparison...")
src3 = load("river.png")
dbs_kwargs = {"mode": DitherMode.BURKES, "tone": "auto", "gamut": "auto"}
burkes = dither_image(src3, SPECTRA_7_3_6COLOR_V2, **dbs_kwargs).convert("RGB")
refined = dither_image(src3, SPECTRA_7_3_6COLOR_V2, dbs=True, **dbs_kwargs).convert("RGB")

# Sky, clouds and treeline: smooth gradients plus edges.
CROP = (230, 60, 430, 180)
dbs_rows = [
[label(src3, "Original"), label(burkes, "Burkes"), label(refined, "Burkes + DBS")],
[label(zoom(im, CROP), f"{name} · 4× crop") for im, name in
[(src3, "Original"), (burkes, "Burkes"), (refined, "Burkes + DBS")]],
]
vstack([hstack(row) for row in dbs_rows]).save(OUT / "dbs_comparison.png")

print("Done. Output in", OUT)
Loading