Skip to content

Commit ee1b10c

Browse files
committed
rework analysis based on Weber/Stevens distinction
1 parent 3370f94 commit ee1b10c

File tree

5 files changed

+198
-224
lines changed

5 files changed

+198
-224
lines changed

analysis.md

Lines changed: 169 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Detailed analysis of APCA (2022-07-16)
1+
# Detailed analysis of APCA (2022-08-05)
22

33
I am a regular web developer with a bachelor's degree in math, but without any
44
training in the science around visual perception. That's why I cannot evaluate
@@ -53,15 +53,15 @@ correct 100% of the time.
5353
### A naive approach
5454

5555
```js
56-
function sRGBtoY(srgb) {
56+
function sRGBtoL(srgb) {
5757
return (srgb[0] + srgb[1] + srgb[2]) / 3;
5858
}
5959

6060
function contrast(fg, bg) {
61-
var yfg = sRGBtoY(fg);
62-
var ybg = sRGBtoY(bg);
61+
var lfg = sRGBtoL(fg);
62+
var lbg = sRGBtoL(bg);
6363

64-
return ybg - yfg;
64+
return lbg - lfg;
6565
};
6666
```
6767

@@ -71,6 +71,28 @@ features the basic structure: We first transform each color to a value that
7171
represents lightness. Then we calculate a difference between the two lightness
7272
values.
7373

74+
### Historical context
75+
76+
Lightness (L) is a measure for the perceived amount of light. Luminance (Y) is
77+
a measure for the physical amount of light. In order to understand perceived
78+
contrast, we first have to understand the relationship between luminance and
79+
lightness.
80+
81+
In the nineteenth century, E. H. Weber found that human perception works in
82+
relative terms, i.e. the difference between 100 g and 110 g is perceived the
83+
same as the difference between 1000 g and 1100 g. Applied to vision this means
84+
that a contrast between two color pairs is perceived the same if `(Y1 - Y2) /
85+
Y2` has the same value. This is known as Weber contrast and has been called the
86+
["gold standard" for text contrast].
87+
88+
Fechner concluded that the relation between a physical measure `Y` and a
89+
perceived measure `L` can be expressed as `L = a * log(Y) + b`. This is called
90+
the Weber-Fechner law.
91+
92+
In 1961 Stevens published a different model that was found to more accurately
93+
predict human vision. It has the form `L = a * pow(Y, alpha) + b`. The exponent
94+
`alpha` has a value of approximately 1/3.
95+
7496
### WCAG 2.x
7597

7698
```js
@@ -99,24 +121,25 @@ function contrast(fg, bg) {
99121
};
100122
```
101123

102-
In WCAG 2.x we see the same general structure, but the individual steps are
103-
more complicated:
124+
In WCAG 2.x we see the same general structure as in the naive approach, but the
125+
individual steps are more complicated:
104126

105127
Colors on the web are defined in the [sRGB color space]. The first part of this
106-
formula is the official formula to convert a sRGB color to luminance. Luminance
107-
is a measure for the amount of light emitted from the screen. Doubling sRGB
108-
values (e.g. from `#444` to `#888`) does not actually double the physical
128+
formula is the official formula to convert a sRGB color to luminance. Doubling
129+
sRGB values (e.g. from `#444` to `#888`) does not actually double the physical
109130
amount of light, so the first step is a non-linear "gamma decoding". Then the
110131
red, green, and blue channels are weighted to sum to the final luminance. The
111132
weights result from different sensitivities in the human eye: Yellow light has
112133
a much bigger response than the same amount of blue light.
113134

114-
Next the [Weber contrast] of those two luminances is calculated. Weber contrast
115-
has been called the ["gold standard" for text contrast]. It is usually defined
116-
as `(yfg - ybg) / ybg` which is the same as `yfg / ybg - 1`. In this case, 0.05
117-
is added to both values to account for ambient light. The shift by 1 is removed
118-
because it has no impact on the results (as long as the thresholds are adapted
119-
accordingly).
135+
Next, 0.05 is added to both values to account for ambient light that is
136+
reflected on the screen (flare). Since we are in the domain of physical light,
137+
we can just add these values. 0.05 mean that we assume that the flare amounts
138+
to 5% of the white of the screen.
139+
140+
Then the Weber contrast is calculated. Note that `(Y1 - Y2) / Y2` is the same
141+
as `Y1 / Y2 - 1`. The shift by 1 is removed because it has no impact on the
142+
results (as long as the thresholds are adapted accordingly).
120143

121144
Finally, the polarity is removed so that the formula has the same results when
122145
the two colors are switched.
@@ -162,29 +185,80 @@ function contrast(fg, bg) {
162185
};
163186
```
164187

165-
Again we can see the same structure: We first convert colors to lightness, then
166-
calculate the difference between them. However, in order to be able to compare
167-
APCA to WCAG 2.x, I will make some modifications:
188+
The conversion from sRGB to luminance uses similar coefficients, but the
189+
non-linear part is very different. The author of APCA provides some motivation
190+
for these changes in the article [Regarding APCA Exponents]. The main argument
191+
seems to be that this is supposed to more closely model real-world computer
192+
screens. This document also explains that this step incorporates flare.
168193

169-
- The final steps do some scaling and shifting that only serves to get nice
170-
threshold values. Just like the shift by 1 in the WCAG formula, this can
171-
simply be ignored.
194+
Next, the contrast is calculated based on the Stevens model. Interestingly,
195+
APCA uses four different exponents for light foreground (0.62), dark foreground
196+
(0.57), light background (0.56), and dark background (0.65).
172197

173-
- I will also ignore the `< 0.1` condition because it only affects contrasts
174-
that are too low to be interesting anyway.
198+
The final steps do some scaling and shifting that only serves to get nice
199+
threshold values. Just like the shift by 1 in the WCAG formula, this does not
200+
effect results as long as the thresholds are adapted accordingly. Note that the
201+
`< 0.1` condition only affects contrasts that are below the lowest threshold
202+
anyway.
175203

176-
- The contrast is calculated as a difference, not as a ratio as in WCAG. I
177-
will look at the `exp()` of that difference. Since
178-
`exp(a - b) == exp(a) / exp(b)`, this allows us to convert the APCA formula
179-
from a difference to a ratio. Again I user the same trick: Since `exp()` is
180-
monotonic, it does not change the results other than moving the
181-
thresholds.
204+
This formula is based on the more modern Stevens model, but also has some
205+
unusual parts. The non-standard `sRGBtoY` is hard to evaluate without further
206+
information on how it was derived. All of the exponents are significantly
207+
higher than the common 1/3. Analysis is also complicated by the fact that the
208+
three levels of exponents (gamma, alpha, different exponents for light/dark
209+
foreground/background) are not clearly separated.
182210

183-
With those changes. All other differences between APCA and WCAG 2.x can be
184-
pushed into `sRGBtoY()`:
211+
### Normalization
212+
213+
To make it easier to compare the two formulas, I will normalize them:
214+
215+
- clearly seperate the individual steps of the calculation
216+
- calculate a difference of lightnesses
217+
- preserve polarity
218+
- scale to a range of -1 to 1
219+
220+
WCAG 2.x therefore becomes:
185221

186222
```js
187-
function sRGBtoY_modified(srgb, exponent) {
223+
function gamma(x) {
224+
if (x < 0.04045) {
225+
return x / 12.92;
226+
} else {
227+
return Math.pow((x + 0.055) / 1.055, 2.4);
228+
}
229+
}
230+
231+
function sRGBtoY(srgb) {
232+
var r = gamma(srgb[0] / 255);
233+
var g = gamma(srgb[1] / 255);
234+
var b = gamma(srgb[2] / 255);
235+
236+
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
237+
}
238+
239+
function YtoL(y) {
240+
return (Math.log(y + 0.05) - Math.log(0.05)) / Math.log(21);
241+
}
242+
243+
function contrast(fg, bg) {
244+
var yfg = sRGBtoY(fg);
245+
var ybg = sRGBtoY(bg);
246+
247+
var lfg = YtoL(yfg);
248+
var lbg = YtoL(ybg);
249+
250+
return lbg - lfg;
251+
};
252+
253+
function normalize(c) {
254+
return Math.log(c) / Math.log(21);
255+
}
256+
```
257+
258+
APCA becomes:
259+
260+
```js
261+
function sRGBtoY(srgb) {
188262
var r = Math.pow(srgb[0] / 255, 2.4);
189263
var g = Math.pow(srgb[1] / 255, 2.4);
190264
var b = Math.pow(srgb[2] / 255, 2.4);
@@ -193,89 +267,75 @@ function sRGBtoY_modified(srgb, exponent) {
193267
if (y < 0.022) {
194268
y += Math.pow(0.022 - y, 1.414);
195269
}
196-
return Math.exp(Math.pow(y, exponent));
270+
return y;
197271
}
198-
```
199-
200-
An interesting feature of APCA is that it uses four different exponents for
201-
light foreground (0.62), dark foreground (0.57), light background (0.56), and
202-
dark background (0.65). `sRGBtoY_modified()` takes that exponent as a second
203-
parameter.
204-
205-
Now that we have aligned the two formulas, what are the actual differences?
206-
207-
This conversion again uses sRGB coefficients. However, the non-linear part is
208-
very different. The author of APCA provides some motivation for these changes
209-
in the article [Regarding APCA Exponents]. The main argument seems to be that
210-
this more closely models real-world computer screens.
211272

212-
To get a better feeling for how these formulas compare, I plotted the results
213-
of `sRGBtoY()`. In order to reduce colors to a single dimension, I used gray
214-
`[x, x, x]`, red `[x, 0, 0]`, green `[0, x, 0]` and blue `[0, 0, x]` values.
215-
216-
I also normalized the values so they are in the same range as WCAG 2.x. I used
217-
factors (because they do not change the contrast ratio) and powers (because
218-
they are monotonic on the contrast ratio).
273+
function YtoL(y) {
274+
return Math.pow(y, 0.6);
275+
}
219276

220-
```js
221-
var average_exponent = 0.6;
222-
var y0 = Math.exp(Math.pow(0.022, 1.414 * average_exponent));
223-
var y1 = Math.exp(1);
277+
function contrast(fg, bg) {
278+
var yfg = sRGBtoY(fg);
279+
var ybg = sRGBtoY(bg);
224280

225-
function normalize(y) {
226-
// scale the lower end to 1
227-
y /= y0;
281+
var lfg = YtoL(yfg);
282+
var lbg = YtoL(ybg);
228283

229-
// scale the upper end to 21
230-
// we use a power so the lower end stays at 1
231-
y = Math.pow(y, Math.log(21) / Math.log(y1 / y0));
284+
if (ybg > yfg) {
285+
return Math.pow(lbg, 0.56 / 0.6) - Math.pow(yfg, 0.57 / 0.6);
286+
} else {
287+
return Math.pow(ybg, 0.65 / 0.6) - Math.pow(yfg, 0.62 / 0.6);
288+
}
289+
};
232290

233-
// scale down to the desired range
234-
return y / 20;
291+
function normalize(c) {
292+
return (c / 100 + 0.027) / 1.14;
235293
}
236294
```
237295

238-
![sRGBtoY comparison](plots/sRGBtoY_comparison.png)
296+
### Comparison
239297

240-
The four curves for APCA are very similar. Despite the very different formula,
241-
the WCAG 2.x curve also has a similar shape. I added a modified WCAG 2.x curve
242-
with an ambient light value of 0.4 instead of 0.05. This one is very similar
243-
to the APCA curves. The second column shows the differences between the APCA
244-
curves and this modified WCAG 2.x. 0.4 was just a guess, there might be even
245-
better values.
246-
247-
I also wanted to see how the contrast results compare. I took a random sample
248-
of color pairs and computed the normalized APCA contrast, WCAG 2.x contrast
249-
(without removing the polarity) and the modified WCAG contrast with an ambient
250-
light value of 0.4.
298+
Now that we have aligned the two formulas, what are the actual differences?
251299

252300
![contrast comparison](plots/contrast_comparison.png)
253301

254-
In the top row we see two scatter plots that compare APCA to both WCAG
255-
variants. As we can see, they correlate in both cases, but the modified WCAG
256-
2.x contrast is much closer.
302+
These are scatter plots based on a random sample of color pairs. The x-axis
303+
corresponds to background luminance, the y-axis corresponds to foreground
304+
luminance (both using the APCA formula). The color of the dots indicated the
305+
differences between the respective formulas.
257306

258-
In the bottom row we see two more scatter plots. This time the X axis
259-
corresponds to foreground luminance and the Y axis corresponds to background
260-
luminance. The color of the dots indicated the differences between the
261-
respective formulas, calculated as `log(apca / wcag)`. As we can see, the
307+
The plot on the bottom right compares the APCA to WCAG 2.x. As we can see, the
262308
biggest differences between APCA and WCAG 2.x are in areas where one color is
263309
extremely light or extremely dark. For light colors, APCA predicts an even
264310
higher contrast (difference is in the same direction as contrast polarity). For
265311
dark colors, APCA predicts a lower contrast (difference is inverse to contrast
266-
polarity).
312+
polarity). The difference goes up to 20%.
313+
314+
The other three plots compare APCA to a modified version of APCA where one of
315+
the steps has been replaced by the corresponding step from WCAG 2.x. This way
316+
we can see that `sRGBtoY` contributes 4% to the difference, `YtoL` contributes
317+
15%, and `contrast` contributes 3%.
318+
319+
Since the conversion from luminance to lightness causes the biggest difference,
320+
I took a closer look at it.
321+
322+
![lightness comparison](plots/lightness_comparison.png)
323+
324+
I plotted curves for both the Weber-Fechner model (log) and the Stevens model
325+
(pow) with different parameters.
267326

268-
To sum up, the APCA contrast formula is certainly not as obvious a choice as
269-
the one from WCAG 2.x. I was not able to find much information on how it was
270-
derived. A closer analysis reveals that it is actually not that different from
271-
WCAG 2.x, but assumes much more ambient light. More research is needed to
272-
determine if this higher ambient light value is significant or just an
273-
artifact of the conversion I did.
327+
- The log curve with a flare of 0.05 (WCAG 2) is closer to the pow curve with
328+
an exponent of 1/3
329+
- The log curve with a flare of 0.4 is closer to the pow curves with
330+
exponents 0.56 and 0.68 (similar to APCA)
331+
- The pow curve with an exponent of 1/3 **and** a flare of 0.025 is somewhere
332+
in the middle.
274333

275-
As we have seen, using a polarity-aware difference instead of a ratio is not a
276-
significant change in terms of results. However, in terms of developer
277-
ergonomics, I personally feel like it is easier to work with. So I would be
278-
happy if this idea sticks.
334+
This shows that a big part of the different results between WCAG 2.x and APCA
335+
are caused by a different choice in parameters. If we were to change the flare
336+
value in WCAG 2.x to 0.4 we would get results much closer to APCA. And if we
337+
were to change the exponents in APCA to 1/3 we would get results much closer to
338+
WCAG 2.x.
279339

280340
## Spatial frequency
281341

@@ -406,8 +466,7 @@ Again I generated random color pairs and used them to compare APCA to WCAG 2.x:
406466

407467
The columns correspond to APCA thresholds, the rows correspond to WCAG 2.x
408468
thresholds. For example, 6.2 % of the generated color pairs pass WCAG 2.x with
409-
a contrast above 3, but fail APCA with a contrast below 45 (assuming a
410-
conventional spatial frequency).
469+
a contrast above 3, but fail APCA with a contrast below 45.
411470

412471
The \* indicate cases where both a algorithms agree on a threshold level. The
413472
cell in the bottom right is the total number of cases where both algorithms
@@ -424,11 +483,10 @@ agree, so it can be seen as an indicator of how similar the algorithms are.
424483
| > 13.2 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.1\* | 0.2 |
425484
| total | 35.3 | 25.8 | 18.3 | 12.3 | 6.4 | 1.8 | 0.1 | 91.0\* |
426485

427-
The second table compares APCA to the modified WCAG 2.x contrast. The
428-
thresholds were derived by applying the normalization steps described above to
429-
the APCA thresholds. As expected, most color pairs fall into the same category
430-
with both formulas. For example, only 1.7 % pass the modified WCAG 2.x with a
431-
contrast above 3.8, but fail APCA with a contrast below 45.
486+
The second table compares APCA to a modified WCAG 2.x contrast with a flare
487+
value of 0.4. The thresholds were derived by applying the normalization steps
488+
described above to the APCA thresholds. As expected, the difference is reduced
489+
significantly, though there is still a considerable difference left.
432490

433491
| | < 15 | 15-30 | 30-45 | 45-60 | 60-75 | 75-90 | > 90 | total |
434492
| -------:| -------:| -------:| -------:| -------:| -------:| -------:| -------:| -------:|
@@ -459,15 +517,19 @@ algorithm in many key aspects:
459517

460518
- It uses a different luminance calculation that deviates from the standards
461519
but is supposed to be closer to real world usage.
462-
- It uses a different way of calculating a contrast from luminances.
520+
- It uses a more accurate model and significantly different parameters for
521+
converting luminance to perceptual lightness.
522+
- It adds an additional step where different exponents are applied to
523+
foreground and background.
463524
- It uses different scaling. Crucially, this scaling is based on a difference
464525
rather than a ratio.
465526
- It uses a more sophisticated link between spatial frequency and minimum
466527
color contrast that might allow for more nuanced thresholds.
467528

468-
The new contrast formula agrees with WCAG 2.x for 83.5% of color pairs. That
469-
number rises to 91% for a modified WCAG 2.x formula with an ambient light value
470-
of 0.4. This could indicate that APCA assumes more ambient light. It would also
529+
The new contrast formula agrees with WCAG 2.x for 83.5% of randomly picked
530+
color pairs. That number rises to 91% for a modified WCAG 2.x formula with a
531+
flare value of 0.4. As far as I understand, this is not a realistic value for
532+
flare. So the physical interpretation might be incorrect. This would however
471533
explain why APCA reports lower contrast for darker colors.
472534

473535
So far I like many of the ideas of APCA, but I am concerned by the [lack of
@@ -478,7 +540,6 @@ figuring out what questions need to be answered.
478540

479541
[Web Content Accessibility Guidelines]: https://www.w3.org/TR/WCAG21/
480542
[sRGB color space]: https://en.wikipedia.org/wiki/SRGB
481-
[Weber contrast]: https://en.wikipedia.org/wiki/Weber_contrast
482543
["gold standard" for text contrast]: https://github.com/w3c/wcag/issues/695#issuecomment-483805436
483544
[Regarding APCA Exponents]: https://git.apcacontrast.com/documentation/regardingexponents
484545
[Studies have shown]: https://en.wikipedia.org/wiki/Contrast_(vision)#Contrast_sensitivity_and_visual_acuity

plots/contrast_comparison.png

112 KB
Loading

0 commit comments

Comments
 (0)