- commit
- 81cf96cc89c2333a42ca2e716a6c6c99f84c3765
- parent
- 3370f941f5fdbdf75ac5b11e374895970eff3adb
- Author
- Tobias Bengfort <tobias.bengfort@posteo.de>
- Date
- 2022-08-05 05:07
rework analysis based on Weber/Stevens distinction
Diffstat
| M | analysis.md | 287 | +++++++++++++++++++++++++++++++++++++------------------------ |
| M | plots/contrast_comparison.png | 0 | |
| M | plots/contrast_comparison.py | 76 | +++++++++++++++++++++++-------------------------------------- |
| D | plots/sRGBtoY_comparison.png | 0 | |
| D | plots/sRGBtoY_comparison.py | 69 | ------------------------------------------------------------ |
5 files changed, 203 insertions, 229 deletions
diff --git a/analysis.md b/analysis.md
@@ -1,4 +1,4 @@1 -1 # Detailed analysis of APCA (2022-07-16)-1 1 # Detailed analysis of APCA (2022-08-05) 2 2 3 3 I am a regular web developer with a bachelor's degree in math, but without any 4 4 training in the science around visual perception. That's why I cannot evaluate @@ -53,15 +53,15 @@ correct 100% of the time. 53 53 ### A naive approach 54 54 55 55 ```js56 -1 function sRGBtoY(srgb) {-1 56 function sRGBtoL(srgb) { 57 57 return (srgb[0] + srgb[1] + srgb[2]) / 3; 58 58 } 59 59 60 60 function contrast(fg, bg) {61 -1 var yfg = sRGBtoY(fg);62 -1 var ybg = sRGBtoY(bg);-1 61 var lfg = sRGBtoL(fg); -1 62 var lbg = sRGBtoL(bg); 63 6364 -1 return ybg - yfg;-1 64 return lbg - lfg; 65 65 }; 66 66 ``` 67 67 @@ -71,6 +71,28 @@ features the basic structure: We first transform each color to a value that 71 71 represents lightness. Then we calculate a difference between the two lightness 72 72 values. 73 73 -1 74 ### Historical context -1 75 -1 76 Lightness (L) is a measure for the perceived amount of light. Luminance (Y) is -1 77 a measure for the physical amount of light. In order to understand perceived -1 78 contrast, we first have to understand the relationship between luminance and -1 79 lightness. -1 80 -1 81 In the nineteenth century, E. H. Weber found that human perception works in -1 82 relative terms, i.e. the difference between 100 g and 110 g is perceived the -1 83 same as the difference between 1000 g and 1100 g. Applied to vision this means -1 84 that a contrast between two color pairs is perceived the same if `(Y1 - Y2) / -1 85 Y2` has the same value. This is known as Weber contrast and has been called the -1 86 ["gold standard" for text contrast]. -1 87 -1 88 Fechner concluded that the relation between a physical measure `Y` and a -1 89 perceived measure `L` can be expressed as `L = a * log(Y) + b`. This is called -1 90 the Weber-Fechner law. -1 91 -1 92 In 1961 Stevens published a different model that was found to more accurately -1 93 predict human vision. It has the form `L = a * pow(Y, alpha) + b`. The exponent -1 94 `alpha` has a value of approximately 1/3. -1 95 74 96 ### WCAG 2.x 75 97 76 98 ```js @@ -99,24 +121,25 @@ function contrast(fg, bg) { 99 121 }; 100 122 ``` 101 123102 -1 In WCAG 2.x we see the same general structure, but the individual steps are103 -1 more complicated:-1 124 In WCAG 2.x we see the same general structure as in the naive approach, but the -1 125 individual steps are more complicated: 104 126 105 127 Colors on the web are defined in the [sRGB color space]. The first part of this106 -1 formula is the official formula to convert a sRGB color to luminance. Luminance107 -1 is a measure for the amount of light emitted from the screen. Doubling sRGB108 -1 values (e.g. from `#444` to `#888`) does not actually double the physical-1 128 formula is the official formula to convert a sRGB color to luminance. Doubling -1 129 sRGB values (e.g. from `#444` to `#888`) does not actually double the physical 109 130 amount of light, so the first step is a non-linear "gamma decoding". Then the 110 131 red, green, and blue channels are weighted to sum to the final luminance. The 111 132 weights result from different sensitivities in the human eye: Yellow light has 112 133 a much bigger response than the same amount of blue light. 113 134114 -1 Next the [Weber contrast] of those two luminances is calculated. Weber contrast115 -1 has been called the ["gold standard" for text contrast]. It is usually defined116 -1 as `(yfg - ybg) / ybg` which is the same as `yfg / ybg - 1`. In this case, 0.05117 -1 is added to both values to account for ambient light. The shift by 1 is removed118 -1 because it has no impact on the results (as long as the thresholds are adapted119 -1 accordingly).-1 135 Next, 0.05 is added to both values to account for ambient light that is -1 136 reflected on the screen (flare). Since we are in the domain of physical light, -1 137 we can just add these values. 0.05 mean that we assume that the flare amounts -1 138 to 5% of the white of the screen. -1 139 -1 140 Then the Weber contrast is calculated. Note that `(Y1 - Y2) / Y2` is the same -1 141 as `Y1 / Y2 - 1`. The shift by 1 is removed because it has no impact on the -1 142 results (as long as the thresholds are adapted accordingly). 120 143 121 144 Finally, the polarity is removed so that the formula has the same results when 122 145 the two colors are switched. @@ -162,29 +185,80 @@ function contrast(fg, bg) { 162 185 }; 163 186 ``` 164 187165 -1 Again we can see the same structure: We first convert colors to lightness, then166 -1 calculate the difference between them. However, in order to be able to compare167 -1 APCA to WCAG 2.x, I will make some modifications:-1 188 The conversion from sRGB to luminance uses similar coefficients, but the -1 189 non-linear part is very different. The author of APCA provides some motivation -1 190 for these changes in the article [Regarding APCA Exponents]. The main argument -1 191 seems to be that this is supposed to more closely model real-world computer -1 192 screens. This document also explains that this step incorporates flare. -1 193 -1 194 Next, the contrast is calculated based on the Stevens model. Interestingly, -1 195 APCA uses four different exponents for light foreground (0.62), dark foreground -1 196 (0.57), light background (0.56), and dark background (0.65). -1 197 -1 198 The final steps do some scaling and shifting that only serves to get nice -1 199 threshold values. Just like the shift by 1 in the WCAG formula, this does not -1 200 effect results as long as the thresholds are adapted accordingly. Note that the -1 201 `< 0.1` condition only affects contrasts that are below the lowest threshold -1 202 anyway. -1 203 -1 204 This formula is based on the more modern Stevens model, but also has some -1 205 unusual parts. The non-standard `sRGBtoY` is hard to evaluate without further -1 206 information on how it was derived. All of the exponents are significantly -1 207 higher than the common 1/3. Analysis is also complicated by the fact that the -1 208 three levels of exponents (gamma, alpha, different exponents for light/dark -1 209 foreground/background) are not clearly separated. -1 210 -1 211 ### Normalization -1 212 -1 213 To make it easier to compare the two formulas, I will normalize them: -1 214 -1 215 - clearly seperate the individual steps of the calculation -1 216 - calculate a difference of lightnesses -1 217 - preserve polarity -1 218 - scale to a range of -1 to 1 -1 219 -1 220 WCAG 2.x therefore becomes: -1 221 -1 222 ```js -1 223 function gamma(x) { -1 224 if (x < 0.04045) { -1 225 return x / 12.92; -1 226 } else { -1 227 return Math.pow((x + 0.055) / 1.055, 2.4); -1 228 } -1 229 } -1 230 -1 231 function sRGBtoY(srgb) { -1 232 var r = gamma(srgb[0] / 255); -1 233 var g = gamma(srgb[1] / 255); -1 234 var b = gamma(srgb[2] / 255); -1 235 -1 236 return 0.2126 * r + 0.7152 * g + 0.0722 * b; -1 237 } -1 238 -1 239 function YtoL(y) { -1 240 return (Math.log(y + 0.05) - Math.log(0.05)) / Math.log(21); -1 241 } -1 242 -1 243 function contrast(fg, bg) { -1 244 var yfg = sRGBtoY(fg); -1 245 var ybg = sRGBtoY(bg); 168 246169 -1 - The final steps do some scaling and shifting that only serves to get nice170 -1 threshold values. Just like the shift by 1 in the WCAG formula, this can171 -1 simply be ignored.-1 247 var lfg = YtoL(yfg); -1 248 var lbg = YtoL(ybg); 172 249173 -1 - I will also ignore the `< 0.1` condition because it only affects contrasts174 -1 that are too low to be interesting anyway.-1 250 return lbg - lfg; -1 251 }; 175 252176 -1 - The contrast is calculated as a difference, not as a ratio as in WCAG. I177 -1 will look at the `exp()` of that difference. Since178 -1 `exp(a - b) == exp(a) / exp(b)`, this allows us to convert the APCA formula179 -1 from a difference to a ratio. Again I user the same trick: Since `exp()` is180 -1 monotonic, it does not change the results other than moving the181 -1 thresholds.-1 253 function normalize(c) { -1 254 return Math.log(c) / Math.log(21); -1 255 } -1 256 ``` 182 257183 -1 With those changes. All other differences between APCA and WCAG 2.x can be184 -1 pushed into `sRGBtoY()`:-1 258 APCA becomes: 185 259 186 260 ```js187 -1 function sRGBtoY_modified(srgb, exponent) {-1 261 function sRGBtoY(srgb) { 188 262 var r = Math.pow(srgb[0] / 255, 2.4); 189 263 var g = Math.pow(srgb[1] / 255, 2.4); 190 264 var b = Math.pow(srgb[2] / 255, 2.4); @@ -193,89 +267,75 @@ function sRGBtoY_modified(srgb, exponent) { 193 267 if (y < 0.022) { 194 268 y += Math.pow(0.022 - y, 1.414); 195 269 }196 -1 return Math.exp(Math.pow(y, exponent));-1 270 return y; 197 271 }198 -1 ```199 272200 -1 An interesting feature of APCA is that it uses four different exponents for201 -1 light foreground (0.62), dark foreground (0.57), light background (0.56), and202 -1 dark background (0.65). `sRGBtoY_modified()` takes that exponent as a second203 -1 parameter.-1 273 function YtoL(y) { -1 274 return Math.pow(y, 0.6); -1 275 } 204 276205 -1 Now that we have aligned the two formulas, what are the actual differences?-1 277 function contrast(fg, bg) { -1 278 var yfg = sRGBtoY(fg); -1 279 var ybg = sRGBtoY(bg); -1 280 -1 281 var lfg = YtoL(yfg); -1 282 var lbg = YtoL(ybg); -1 283 -1 284 if (ybg > yfg) { -1 285 return Math.pow(lbg, 0.56 / 0.6) - Math.pow(yfg, 0.57 / 0.6); -1 286 } else { -1 287 return Math.pow(ybg, 0.65 / 0.6) - Math.pow(yfg, 0.62 / 0.6); -1 288 } -1 289 }; 206 290207 -1 This conversion again uses sRGB coefficients. However, the non-linear part is208 -1 very different. The author of APCA provides some motivation for these changes209 -1 in the article [Regarding APCA Exponents]. The main argument seems to be that210 -1 this more closely models real-world computer screens.-1 291 function normalize(c) { -1 292 return (c / 100 + 0.027) / 1.14; -1 293 } -1 294 ``` 211 295212 -1 To get a better feeling for how these formulas compare, I plotted the results213 -1 of `sRGBtoY()`. In order to reduce colors to a single dimension, I used gray214 -1 `[x, x, x]`, red `[x, 0, 0]`, green `[0, x, 0]` and blue `[0, 0, x]` values.-1 296 ### Comparison 215 297216 -1 I also normalized the values so they are in the same range as WCAG 2.x. I used217 -1 factors (because they do not change the contrast ratio) and powers (because218 -1 they are monotonic on the contrast ratio).-1 298 Now that we have aligned the two formulas, what are the actual differences? 219 299220 -1 ```js221 -1 var average_exponent = 0.6;222 -1 var y0 = Math.exp(Math.pow(0.022, 1.414 * average_exponent));223 -1 var y1 = Math.exp(1);-1 300  224 301225 -1 function normalize(y) {226 -1 // scale the lower end to 1227 -1 y /= y0;-1 302 These are scatter plots based on a random sample of color pairs. The x-axis -1 303 corresponds to background luminance, the y-axis corresponds to foreground -1 304 luminance (both using the APCA formula). The color of the dots indicated the -1 305 differences between the respective formulas. 228 306229 -1 // scale the upper end to 21230 -1 // we use a power so the lower end stays at 1231 -1 y = Math.pow(y, Math.log(21) / Math.log(y1 / y0));-1 307 The plot on the bottom right compares APCA to WCAG 2.x. As we can see, the -1 308 biggest differences are in areas where one color is extremely light or -1 309 extremely dark. For light colors, APCA predicts an even higher contrast -1 310 (difference is in the same direction as contrast polarity). For dark colors, -1 311 APCA predicts a lower contrast (difference is inverse to contrast polarity). -1 312 The difference goes up to 20%. 232 313233 -1 // scale down to the desired range234 -1 return y / 20;235 -1 }236 -1 ```-1 314 The other three plots compare APCA to a modified version of APCA where one of -1 315 the steps has been replaced by the corresponding step from WCAG 2.x. This way -1 316 we can see that `sRGBtoY` contributes 4% to the difference, `YtoL` contributes -1 317 15%, and `contrast` contributes 3%. 237 318238 -1 -1 319 Since the conversion from luminance to lightness causes the biggest difference, -1 320 I took a closer look at it. 239 321240 -1 The four curves for APCA are very similar. Despite the very different formula,241 -1 the WCAG 2.x curve also has a similar shape. I added a modified WCAG 2.x curve242 -1 with an ambient light value of 0.4 instead of 0.05. This one is very similar243 -1 to the APCA curves. The second column shows the differences between the APCA244 -1 curves and this modified WCAG 2.x. 0.4 was just a guess, there might be even245 -1 better values.-1 322  246 323247 -1 I also wanted to see how the contrast results compare. I took a random sample248 -1 of color pairs and computed the normalized APCA contrast, WCAG 2.x contrast249 -1 (without removing the polarity) and the modified WCAG contrast with an ambient250 -1 light value of 0.4.-1 324 I plotted curves for both the Weber-Fechner model (log) and the Stevens model -1 325 (pow) with different parameters. 251 326252 -1 -1 327 - The log curve with a flare of 0.05 (WCAG 2) is closer to the pow curve with -1 328 an exponent of 1/3 -1 329 - The log curve with a flare of 0.4 is closer to the pow curves with -1 330 exponents 0.56 and 0.68 (similar to APCA) -1 331 - The pow curve with an exponent of 1/3 **and** a flare of 0.025 is somewhere -1 332 in the middle. 253 333254 -1 In the top row we see two scatter plots that compare APCA to both WCAG255 -1 variants. As we can see, they correlate in both cases, but the modified WCAG256 -1 2.x contrast is much closer.257 -1258 -1 In the bottom row we see two more scatter plots. This time the X axis259 -1 corresponds to foreground luminance and the Y axis corresponds to background260 -1 luminance. The color of the dots indicated the differences between the261 -1 respective formulas, calculated as `log(apca / wcag)`. As we can see, the262 -1 biggest differences between APCA and WCAG 2.x are in areas where one color is263 -1 extremely light or extremely dark. For light colors, APCA predicts an even264 -1 higher contrast (difference is in the same direction as contrast polarity). For265 -1 dark colors, APCA predicts a lower contrast (difference is inverse to contrast266 -1 polarity).267 -1268 -1 To sum up, the APCA contrast formula is certainly not as obvious a choice as269 -1 the one from WCAG 2.x. I was not able to find much information on how it was270 -1 derived. A closer analysis reveals that it is actually not that different from271 -1 WCAG 2.x, but assumes much more ambient light. More research is needed to272 -1 determine if this higher ambient light value is significant or just an273 -1 artifact of the conversion I did.274 -1275 -1 As we have seen, using a polarity-aware difference instead of a ratio is not a276 -1 significant change in terms of results. However, in terms of developer277 -1 ergonomics, I personally feel like it is easier to work with. So I would be278 -1 happy if this idea sticks.-1 334 This shows that a big part of the different results between WCAG 2.x and APCA -1 335 are caused by a different choice in parameters. If we were to change the flare -1 336 value in WCAG 2.x to 0.4 we would get results much closer to APCA. And if we -1 337 were to change the exponents in APCA to 1/3 we would get results much closer to -1 338 WCAG 2.x. 279 339 280 340 ## Spatial frequency 281 341 @@ -406,8 +466,7 @@ Again I generated random color pairs and used them to compare APCA to WCAG 2.x: 406 466 407 467 The columns correspond to APCA thresholds, the rows correspond to WCAG 2.x 408 468 thresholds. For example, 6.2 % of the generated color pairs pass WCAG 2.x with409 -1 a contrast above 3, but fail APCA with a contrast below 45 (assuming a410 -1 conventional spatial frequency).-1 469 a contrast above 3, but fail APCA with a contrast below 45. 411 470 412 471 The \* indicate cases where both a algorithms agree on a threshold level. The 413 472 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. 424 483 | > 13.2 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.1\* | 0.2 | 425 484 | total | 35.3 | 25.8 | 18.3 | 12.3 | 6.4 | 1.8 | 0.1 | 91.0\* | 426 485427 -1 The second table compares APCA to the modified WCAG 2.x contrast. The428 -1 thresholds were derived by applying the normalization steps described above to429 -1 the APCA thresholds. As expected, most color pairs fall into the same category430 -1 with both formulas. For example, only 1.7 % pass the modified WCAG 2.x with a431 -1 contrast above 3.8, but fail APCA with a contrast below 45.-1 486 The second table compares APCA to a modified WCAG 2.x contrast with a flare -1 487 value of 0.4. The thresholds were derived by applying the normalization steps -1 488 described above to the APCA thresholds. As expected, the difference is reduced -1 489 significantly, though there is still a considerable difference left. 432 490 433 491 | | < 15 | 15-30 | 30-45 | 45-60 | 60-75 | 75-90 | > 90 | total | 434 492 | -------:| -------:| -------:| -------:| -------:| -------:| -------:| -------:| -------:| @@ -459,15 +517,19 @@ algorithm in many key aspects: 459 517 460 518 - It uses a different luminance calculation that deviates from the standards 461 519 but is supposed to be closer to real world usage.462 -1 - It uses a different way of calculating a contrast from luminances.-1 520 - It uses a more accurate model and significantly different parameters for -1 521 converting luminance to perceptual lightness. -1 522 - It adds an additional step where different exponents are applied to -1 523 foreground and background. 463 524 - It uses different scaling. Crucially, this scaling is based on a difference 464 525 rather than a ratio. 465 526 - It uses a more sophisticated link between spatial frequency and minimum 466 527 color contrast that might allow for more nuanced thresholds. 467 528468 -1 The new contrast formula agrees with WCAG 2.x for 83.5% of color pairs. That469 -1 number rises to 91% for a modified WCAG 2.x formula with an ambient light value470 -1 of 0.4. This could indicate that APCA assumes more ambient light. It would also-1 529 The new contrast formula agrees with WCAG 2.x for 83.5% of randomly picked -1 530 color pairs. That number rises to 91% for a modified WCAG 2.x formula with a -1 531 flare value of 0.4. As far as I understand, this is not a realistic value for -1 532 flare. So the physical interpretation might be incorrect. This would however 471 533 explain why APCA reports lower contrast for darker colors. 472 534 473 535 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. 478 540 479 541 [Web Content Accessibility Guidelines]: https://www.w3.org/TR/WCAG21/ 480 542 [sRGB color space]: https://en.wikipedia.org/wiki/SRGB481 -1 [Weber contrast]: https://en.wikipedia.org/wiki/Weber_contrast482 543 ["gold standard" for text contrast]: https://github.com/w3c/wcag/issues/695#issuecomment-483805436 483 544 [Regarding APCA Exponents]: https://git.apcacontrast.com/documentation/regardingexponents 484 545 [Studies have shown]: https://en.wikipedia.org/wiki/Contrast_(vision)#Contrast_sensitivity_and_visual_acuity
diff --git a/plots/contrast_comparison.png b/plots/contrast_comparison.png
Binary files differ.diff --git a/plots/contrast_comparison.py b/plots/contrast_comparison.py
@@ -1,5 +1,3 @@1 -1 import math2 -13 1 from matplotlib import pyplot as plt 4 2 import numpy as np 5 3 @@ -10,12 +8,12 @@ def wcag_y(color): 10 8 return 0.2126 * c[:, 0] + 0.7152 * c[:, 1] + 0.0722 * c[:, 2] 11 9 12 1013 -1 def wcag_contrast(yfg, ybg, ambient=0.05):14 -1 c = (ybg + ambient) / (yfg + ambient)-1 11 def wcag_l(y, flare=0.05): -1 12 return np.log(y / flare + 1) / np.log(1 / flare + 1) -1 13 15 1416 -1 y0 = ambient17 -1 y1 = 1 + ambient18 -1 return c ** math.log(21, y1 / y0)-1 15 def wcag_contrast(lfg, lbg): -1 16 return lbg - lfg 19 17 20 18 21 19 def apca_y(color): @@ -26,15 +24,14 @@ def apca_y(color): 26 24 return y 27 25 28 2629 -1 def apca_contrast(yfg, ybg):30 -1 lfg = yfg ** np.where(ybg > yfg, 0.57, 0.62)31 -1 lbg = ybg ** np.where(ybg > yfg, 0.56, 0.65)32 -1 c = lbg - lfg33 -1 c = np.exp(c)-1 27 def apca_l(y): -1 28 return y ** 0.6 -1 29 34 3035 -1 y0 = math.exp((0.022 ** 1.414) ** 0.6)36 -1 y1 = math.exp(1)37 -1 return c ** math.log(21, y1 / y0)-1 31 def apca_contrast(lfg, lbg): -1 32 _lfg = lfg ** (np.where(lbg > lfg, 0.57, 0.62) / 0.6) -1 33 _lbg = lbg ** (np.where(lbg > lfg, 0.56, 0.65) / 0.6) -1 34 return _lbg - _lfg 38 35 39 36 40 37 if __name__ == '__main__': @@ -51,38 +48,23 @@ if __name__ == '__main__': 51 48 52 49 apca_yfg = apca_y(fg) 53 50 apca_ybg = apca_y(bg)54 -1 apca = apca_contrast(apca_yfg, apca_ybg)55 -156 -1 wcag_yfg = wcag_y(fg)57 -1 wcag_ybg = wcag_y(bg)58 -1 wcag = wcag_contrast(wcag_yfg, wcag_ybg)59 -1 wcag4 = wcag_contrast(wcag_yfg, wcag_ybg, 0.4)60 -161 -1 axes[0][0].set_title('APCA vs WCAG 2.x')62 -163 -1 axes[0][0].scatter(wcag, apca, **options)64 -1 axes[0][0].set_xlabel('WCAG 2.x')65 -1 axes[0][0].set_ylabel('APCA')66 -1 axes[0][0].set_xscale('log')67 -1 axes[0][0].set_yscale('log')68 -169 -1 p2 = axes[1][0].scatter(wcag_yfg, wcag_ybg, c=np.log(apca / wcag), **options)70 -1 axes[1][0].set_xlabel('Yfg')71 -1 axes[1][0].set_ylabel('Ybg')72 -1 plt.colorbar(p2, ax=axes[1][0])73 -174 -1 axes[0][1].set_title('APCA vs WCAG 2.x (0.4)')75 -176 -1 axes[0][1].scatter(wcag4, apca, **options)77 -1 axes[0][1].set_xlabel('WCAG 2.x (0.4)')78 -1 axes[0][1].set_ylabel('APCA')79 -1 axes[0][1].set_xscale('log')80 -1 axes[0][1].set_yscale('log')81 -182 -1 p4 = axes[1][1].scatter(wcag_yfg, wcag_ybg, c=np.log(apca / wcag4), **options)83 -1 axes[1][1].set_xlabel('Yfg')84 -1 axes[1][1].set_ylabel('Ybg')85 -1 plt.colorbar(p4, ax=axes[1][1])-1 51 apca_lfg = apca_l(apca_yfg) -1 52 apca_lbg = apca_l(apca_ybg) -1 53 apca = apca_contrast(apca_lfg, apca_lbg) -1 54 -1 55 for a, b, y, l, contrast, title in [ -1 56 (0, 0, wcag_y, apca_l, apca_contrast, 'sRGBtoY'), -1 57 (0, 1, apca_y, wcag_l, apca_contrast, 'YtoL'), -1 58 (1, 0, apca_y, apca_l, wcag_contrast, 'contrast'), -1 59 (1, 1, wcag_y, wcag_l, wcag_contrast, 'all'), -1 60 ]: -1 61 values = apca - contrast(l(y(fg)), l(y(bg))) -1 62 vmax = np.max(np.abs(values)) -1 63 p = axes[a][b].scatter( -1 64 apca_ybg, apca_yfg, c=values, vmin=-vmax, vmax=vmax, **options -1 65 ) -1 66 plt.colorbar(p, ax=axes[a][b]) -1 67 axes[a][b].set_title(title) 86 68 87 69 plt.tight_layout() 88 70 plt.savefig('contrast_comparison.png')
diff --git a/plots/sRGBtoY_comparison.png b/plots/sRGBtoY_comparison.png
Binary files differ.diff --git a/plots/sRGBtoY_comparison.py b/plots/sRGBtoY_comparison.py
@@ -1,69 +0,0 @@1 -1 import math2 -13 -1 from matplotlib import pyplot as plt4 -1 import numpy as np5 -16 -1 WCAG_FACTORS = [1, 0.2126, 0.7152, 0.0722]7 -1 APCA_FACTORS = [1, 0.2126729, 0.7151522, 0.0721750]8 -1 APCA_EXPONENTS = [0.56, 0.57, 0.62, 0.65]9 -110 -111 -1 def wcag(x, factor, ambient):12 -1 y = x / 25513 -1 y = np.where(y < 0.04045, y / 12.92, ((y + 0.055) / 1.055) ** 2.4)14 -1 y *= factor15 -1 y += ambient16 -117 -1 y0 = ambient18 -1 y1 = 1 + ambient19 -1 return (y / y0) ** math.log(21, y1 / y0) / 2020 -121 -122 -1 def apca(x, factor, exp):23 -1 y = x / 25524 -1 y **= 2.425 -1 y *= factor26 -1 y += np.where(y < 0.022, 0.022 - y, 0) ** 1.41427 -1 y **= exp28 -1 y = np.exp(y)29 -130 -1 y0 = math.exp((0.022 ** 1.414) ** 0.6)31 -1 y1 = math.exp(1)32 -1 return (y / y0) ** math.log(21, y1 / y0) / 2033 -134 -135 -1 if __name__ == '__main__':36 -1 x = np.linspace(0, 255, 256)37 -1 fig, axes = plt.subplots(4, 2, sharex=True, sharey='col', figsize=(6.4, 8))38 -139 -1 for i in range(4):40 -1 wcag6 = wcag(x, WCAG_FACTORS[i], 0.4)41 -142 -1 for exp in APCA_EXPONENTS:43 -1 y = apca(x, APCA_FACTORS[i], exp)44 -1 axes[i][0].plot(x, y)45 -1 axes[i][1].plot(x, y / wcag6)46 -147 -1 axes[i][0].plot(x, wcag(x, WCAG_FACTORS[i], 0.05))48 -1 axes[i][0].plot(x, wcag6)49 -150 -1 axes[0][0].set_ylabel('gray')51 -1 axes[1][0].set_ylabel('red')52 -1 axes[2][0].set_ylabel('green')53 -1 axes[3][0].set_ylabel('blue')54 -155 -1 axes[0][0].set_title('sRGBtoY')56 -1 axes[0][1].set_title('ratio APCA / WCAG 0.4')57 -158 -1 fig.legend([59 -1 'APCA 0.56',60 -1 'APCA 0.57',61 -1 'APCA 0.62',62 -1 'APCA 0.65',63 -1 'WCAG 0.05',64 -1 'WCAG 0.4',65 -1 ], ncol=3, loc='lower center')66 -167 -1 plt.tight_layout()68 -1 plt.subplots_adjust(bottom=0.1)69 -1 plt.savefig('sRGBtoY_comparison.png')