feat: add dizzy dithering (DitherMode 9)#68
Merged
Conversation
Adds an example comparing dizzy dithering against Burkes and Floyd-Steinberg via block-averaged OKLab dE, and extends the error_diffusion criterion benchmark group with a dizzy entry at the same sizes as the other kernels.
Implemented from the written description in the linked article; the article contains no code listing, so this is an independent implementation rather than a port. Credits the two design choices taken directly from it: the stateless multiply-and-xor permutation, and the 10:1 orthogonal:diagonal error ratio.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds dizzy dithering — error diffusion that visits pixels in pseudo-random order instead of a raster scan, spreading each pixel's error only to neighbours not yet quantized. No scan direction means no directional artifacts.
Algorithm by Liam Appelbe. Implemented from the written description — the article has no code listing, so this is an independent implementation, not a port.
opendisplay_structs.h:187states DitherMode ids are owned by this repo, so no protocol change is needed; 9 was the next free value.Evaluation — read this before merging
The article demonstrates the technique on continuous-tone greyscale. This library targets 2–7 ink measured palettes where hue error dominates, so the result does not transfer cleanly. Mean OKLab ΔE on 4×4-block-averaged output, 8 fixture images:
Overall it is a wash — the spread is within noise. Split by palette it is not:
Metric sanity check:
DitherMode::Nonescores 0.1843, ~21% worse than every diffusion mode — so the metric is measuring what we think it is.Cost: 1.09× Burkes at 100×100, 1.25× at 400×300. The gap widens with size, consistent with random access over a multi-hundred-KB f64 buffer being cache-hostile. This matters because Home Assistant renders on this path.
Verdict: an opt-in mode for multi-ink palettes, not a new default. Burkes stays the default.
Correctness
The load-bearing property is that the traversal visits every pixel exactly once. If it didn't, pixels would be silently skipped or quantized twice and the output would still look entirely plausible — no image-quality test would catch it.
Multiplication by an odd number is invertible mod 2^k and XOR is self-inverse, so five rounds compose to a bijection. Verified exhaustively for all n from 1 to 3000 plus real panel sizes (384,000 / 524,288 / 1024×576): zero failures.
Also pinned: error-conservation over all 255 non-empty neighbour subsets, determinism, canonical-ink pinning, and a frozen 16-value vector proving Rust, Python and TypeScript agree byte for byte. 16 new regression references (8 images × 2 tags) bring the suite to 40; zero existing references changed.
Notes
serpentineis ignored — there is no scan direction to reverse. Documented alongside NONE and ORDERED.benches/dithering.rs'sbench_real_imagesunconditionally loads a missingkatzi.pngat group-construction time, so an unfilteredcargo benchpanics. Confirmed present on main. Worth a separate PR.odl-rendererandpy-opendisplaymust adopt id 9 before this is reachable from Home Assistant.