Grey From Black and White Dots
Compare three ways to show grey with only black and white pixels, and see the page measure how much tone each one loses.
Made by Claude, an AI model. The picture is drawn by the page itself: a ramp, a fading disc, four flat greys and some text. No photos.
Each picture is pixels, enlarged without smoothing. Zoom in to see single dots.
What the page measures
It reads its own output pixels and asks two questions. Tone per tile: cut the picture into square tiles; in each, how far is the average of the black and white pixels (0 or 255) from the average of the grey original? Error after a blur: blur both pictures with a Gaussian and take the root-mean-square difference, as the eye does from a distance. Both are in grey levels out of 255; smaller is better.
| Method | Mean tile error | Worst tile | Error after blur | White share |
|---|
How each one works
Threshold: a pixel is white if its grey is 128 or more, else black. Every flat patch becomes all black or all white, so its tone is lost.
Ordered (Bayer 4 by 4): each pixel is compared with a threshold that depends on its place in a repeating 4 by 4 grid, so a flat grey becomes a fixed pattern of dots; 16 cells give 17 tones.
Floyd-Steinberg: go along each row, round the pixel to black or white, and push the rounding error onto pixels not yet visited: 7/16 to the right, 3/16 below-left, 5/16 below, 1/16 below-right. I wrote those four weights from memory. Pillow's own output showed the rest of the recipe, which matters: the error is kept in whole sixteenths, each pixel adds its share cut toward zero to a whole level, the sum is clipped to 0 to 255, and a pixel is white only if it is above 128 (not at 128). Rows all run left to right.
Self-checks
checks run when the page loads. Each compares the page's code with something computed another way, and each was made to fail once by breaking the code (test.js in the folder does that).
Not counted, because they hold by construction: the blur kernel sums to 1 (it is divided by its own sum); the Bayer matrix is the standard one typed in from memory, and the Bayer check above only catches an entry that is repeated or missing, not two entries swapped.
What I could and could not check
Second source. Pillow ran in the sandbox (version ), built from separate Python code for the same image. The grey image, the plain threshold and Floyd-Steinberg all match it pixel for pixel. Floyd-Steinberg first differed on about a quarter of the pixels; matching Pillow meant using its integer recipe (above), and I found that by trying variants against its bitmap until the difference was zero. So the match shows this page and Pillow agree, not that either is the one true Floyd-Steinberg. Pillow has no Bayer mode, so that method has no second source.
Not independent. The Python script and this page were written by the same author, and the Pillow bitmaps are stored in reference.js; the page does not run Pillow. Only the test image is a model: results depend on it, and a photo would give other numbers. The ranking can change with tile size and blur width, so try both.
Files: README.md · dither.js · checks.js · test.js · make_reference.py · reference.js