background
In the field of embedded development, we often encounter display devices with extremely low color digits:
- The classic SSD1306 (0.96-inch OLED) only supports black and white.
- Electronic e-paper display (E-Ink), usually only black and white, and the refresh rate is extremely low.
If 24-bit true color images are directly quantized, the result is often like burnt charcoal, with all the details lost. However, if the dithering algorithm(Dithering) is introduced, these 1-Bit screens can simulate a delicate grayscale feel.
I have tried to implement a version in Rust before, but as a blog demonstration, it is most intuitive to simulate directly in the browser using JavaScript and Canvas. This article will introduce the principles of several mainstream dithering algorithms and their JS implementation.
Laboratory console
Upload the image here, and all algorithm demonstrations below will take effect simultaneously. It is recommended to turn on Gamma correction to observe the difference in details in the dark parts of the image.
* Images will only be processed locally and will not be uploaded to the server
0x01 Linear Space
This is the most important core content of this article.
Many developers will directly make if (gray > 0.5) judgments when processing images.
For approximate physical brightness preservation, first establish whether the input is linear. Thresholding sRGB values can also be an intentional artistic choice.
Non-linear characteristics of sRGB
Mainstream image formats are typically stored in the sRGB space. In order to adapt to the sensitivity of the human eye to dark details, sRGB uses non-linear mapping (approximately Gamma 2.2).
Why do we need to go to so much trouble with gamma correction? This is the result of human evolution: our eyes are much more sensitive to changes in dark parts than in bright parts. In the physical world, doubling the number of photons doubles the energy; but in human perception, brightness does not increase linearly. The sRGB standard allocates more bit depth to dark information via a power law curve (approximately
). If we do not perform de-gamma when processing dithering, the algorithm will mistakenly treat “perceptual neutral gray” as “physical neutral gray” to share the error, so the black/white area ratio no longer approximately represents physical luminance. Appearance also depends on the display, viewing distance and intended style.
If error diffusion or addition and subtraction operations are performed directly in sRGB space, the following problems will occur:
- Brightness is not conserved: The calculated gray value does not match the physical brightness.
- Appearance changes: Using sRGB values directly generally produces more white pixels. Linear-luminance output can look darker; the Gamma switch is not a simple brightness boost.
To approximate physical luminance, apply De-gamma to each sRGB channel before combining them with luminance weights. The toggle compares this with processing in nonlinear space.
function srgbToLinear(v) {
v /= 255;
return v <= 0.04045 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
}
0x02 Thresholding (Thresholding)
This is the original and simplest solution: set a fixed threshold (usually 0.5), above which it is set to 1 (white), and below it is set to 0 (black).
Result: Severe loss of detail. Since all midtones are forcibly discarded, severe quantization noise will result, and the image will be completely different.
Demo: Threshold Clipping (Fixed Threshold 0.5)
0x03 Random Dithering (Random Dithering)
In order to alleviate the quantization error, the most intuitive method is to introduce random noise before comparison.
let newPixel = oldPixelLinear + (Math.random() - 0.5) > 0.5 ? 1.0 : 0.0;
Result: Although some detail is preserved, the image is filled with evenly distributed “snowflake spots”. This White Noise is visually very harsh because it has energy distributed across all frequencies.
Demo: Random white noise dithering
0x04 Ordered Dithering (Ordered Dithering)
Ordered dithering no longer relies on randomness, but uses a specific threshold matrix for cyclic laying. The most typical one is the Bayer matrix.
Bayer matrix principle
Bayer matrix is a fractal structure. Its core design goal is: Distribute threshold points of different levels as evenly as possible in space.
- Advantages: It is extremely efficient and does not need to process adjacent pixels. It is extremely suitable for microcontrollers with limited computing resources.
- Disadvantages: Will produce regular cross dots or stripes.
Demo: Bayer 4x4 ordered dithering
0x05 Error Diffusion
This is the current common solution for 1-Bit image processing. The core logic is: Allocate the error caused by the current pixel quantization to adjacent unprocessed pixels according to specific weights.
Floyd-Steinberg algorithm
This is the best-known error diffusion operator. It distributes the error to the four neighbors to the right and below in a ratio of 7/16, 3/16, 5/16, 1/16.
Demonstration: Floyd-Steinberg Error Diffusion
Atkinson algorithm
Introduced by Apple engineer Bill Atkinson during early Macintosh development. It only amortizes 75% of the error and spreads wider (affects 6 neighbors). This makes the image more contrasty and visually clearer.
Demo: Atkinson (Macintosh style)
0x06 Blue Noise (Blue Noise)
If you are looking for artistic quality, blue noise is the ultimate answer.
- White noise: The energy is evenly distributed and looks messy.
- Blue noise: The low-frequency components are removed, the pixels “repel” each other, and the distribution is extremely even.
Using blue noise textures as a threshold map can produce an effect similar to film grain, completely eliminating regular dots.
Demo: R2 low-discrepancy thresholds (not verified blue noise)
Demo scope and export format
The R2 threshold function is a low-discrepancy-sequence demonstration, not a spectrally verified blue-noise generator. Blue-noise threshold maps can be precomputed; applying one does not require expensive online generation.
All demos share a default image at /images/archives/ditherpunk/sample.jpg, or you can upload your own. The article’s cover shows Return of the Obra Dinn as a visual reference; the game artwork is not original site artwork. Use images you have permission to process.
Downloads contain the current algorithm’s black/white PNG output, without the hover lens. The longest edge is capped at 600 pixels and transparency is composited onto white. A PNG is not a packed 1-bit display buffer: adapt resolution, bit order and row layout for your driver. The demo uses full floating-point buffers for clarity, not a memory-optimized MCU implementation.
Summary
| Algorithm name | Core features | Visual style | Computational complexity | Suitable for scenarios |
|---|---|---|---|---|
| Threshold | Fixed threshold | High contrast, blocky | Very low | Plain text, high contrast Logo |
| Bayer | Matrix mapping | Regular grid, pixel style | Low | Dynamic UI, retro games |
| Floyd-S | Error Diffusion | Delicate, sketch-like | Medium | Photography, electronic photo frame |
| Atkinson | Local diffusion | Clean, high detail | Medium | Art scan, low resolution screen |
| R2 demo | Low-discrepancy sequence | Dispersed dots | Low | Comparison experiment, not verified blue noise |
From original threshold clipping to sophisticated error diffusion, the dithering algorithm finds the balance point of human visual aesthetics under the condition of limited display resources. With the abundance of hardware resources, these algorithms have changed from “having to choose” to “artistic style” that improves quality.
In the next article, we will discuss the actual hardware in depth: How to generate dithering output of high-resolution images in real time on an ESP32 with limited memory resources?
environment
Browser: Chrome Engine (Any)
Language: JavaScript (ES6+)
Framework: Vanilla JS / HTML5 Canvas
Target: PC / Embedded (SSD1306/EPD)