Subject: bug#41544: 26.3; Possible incorrect results from color-distance

From: Mattias Engdegård <[email-masked]>
Date: Mon, 8 Jun 2020 15:11:36 +0200

6 juni 2020 kl. 20.27 skrev Eli Zaretskii <[email-masked]>:

> What practical problem is being solved here?

The current predicates for determining whether a colour is light or dark are just bad. We can and should do better, and that's what this is all about.

Let's consider the three saturated colours #ff0000 (red), #00ff00 (green) and #0000ff (blue). Black text looks terrible on blue, as does white on green; black on red isn't good either. This comes as no surprise: the human eye is more sensitive to brightness levels of green than blue, with red somewhere in-between.

(These three colours just serve as illustrative examples; large chunks of the RGB space will share the same properties.)

How do the existing predicates do? Let's call them MAX, AVG and DIST.

MAX: dark iff max(r,g,b) < 0.5

This classifies saturated red and blue as light, which is clearly terrible.

AVG: dark iff (r+g+b) / 3 < 0.6

This classifies saturated green as dark, which is also wrong.

DIST: dark iff (color-distance c "black") ≤ 292485

This one also thinks saturated green is dark. While the approach here looks reasonable at first blush, it's really not: color-distance uses a simplified expression for how dissimilar two colours are, but doesn't do a very good job at determining brightness. For instance, its heavy blue weight makes no sense when used in this way:

 (color-distance "#ff0000" "black") => 162308
 (color-distance "#00ff00" "black") => 260100
 (color-distance "#0000ff" "black") => 194820

This means that we cannot fix DIST by tweaking its cut-off value; it's fundamentally unfit for this purpose.

For a proper solution, we have theory to guide us: determine the perceived brightness of a colour, and classify everything below a cut-off value as dark, others as light. The patch uses a standard expression for relative luminance: Y = 0.2126R + 0.7152G + 0.0722B, where R, G and B are linear colour components. We assume a gamma of 2.2; it is nearly identical to the sRGB gamma curve and the results are almost the same.

With a cut-off of 0.6, this predicate turns out to be quite good: much better than MAX, AVG or DIST at any rate. While not perfect, it's good enough in the sense that for colours where it seems to make the wrong decision, it's a fairly close call, and the colour is quite readable with both black and write as contrast.

I have tested it on several platforms and monitors, including 80x25 VGA hardware text mode, and have yet to find a case where it fails, which definitely cannot be said about the old predicates.

We can still tweak it: mainly the cut-off value (which is just experimentally determined) but also the coefficients and the gamma correction. Although technically valid, they aren't holy.

> This code's algorithm and rationale should be explained in the
> comments before we can discuss whether it's an improvement and why.

Thanks, I have improved this in the new patch.

This patch should also use the right coefficients (see above); I think I got them wrong the first time.