Blending for Dithering
What’s the correct color space to compute color blends in for dithering for display on a typical monitor? At one point in my palette experiments I had written some dithering code and naively assumed CIELAB since that’s designed to be perceptually uniform. But I had noticed that I was getting some odd color casts. Initially I’d thought I’d had a bug, but I eventually realized that it was because CIELAB is the wrong color space. Linearized RGB with sRGB primaries is the way to go. Here’s why.
Consider a yellow pixel above a blue pixel. The yellow pixel has the red and green elements fully lit, while the blue pixel has only the blue element. On many displays it might look something like this from close up:
This is exactly the same set of elements as a single white pixel would have, just spread out over twice the differential area. Consequently, when these are smaller than the resolution at which the eye can tell them apart they should look exactly like a white pixel would, only with half the apparent brightness. They should look like a 50% gray. This is precisely what linearized RGB gives us.
On the other hand, the midpoint between pure yellow and blue in CIELAB is a pale pinkish color.
In other words, for dithering, even though it is the human visual system that is blending the colors of the adjacent pixels, we don’t just want a color space that models the perception of human eye – we want a color space that models the device! I’ll admit that I found this somewhat counterintuitive.
Here’s a little widget to try for yourself. Drag the sliders to select a pair of colors. The swatch on the bottom shows a checkerboard alternating pixels of those colors in the middle, their 50% blend in linearized RGB space on the left, and their 50% blend in CIELAB on the right. While still not perfect, I think you’ll agree that the linear RGB gives a much more accurate prediction for how the dither in the middle appears. (Note: the dither may not look quite right due to scaling on hiDPI devices.)
Edited Sep 23, 2017 - Added styling to canvas to try to force nearest neighbor interpolation for better hiDPI display. Thanks to Alexey Lebedev.