apca-introduction

The missing introduction to APCA  https://p.ce9e.org/apca-introduction/
git clone https://git.ce9e.org/apca-introduction.git

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 ```js
   56    -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    63 
   64    -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   123 
  102    -1 In WCAG 2.x we see the same general structure, but the individual steps are
  103    -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 this
  106    -1 formula is the official formula to convert a sRGB color to luminance. Luminance
  107    -1 is a measure for the amount of light emitted from the screen. Doubling sRGB
  108    -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   134 
  114    -1 Next the [Weber contrast] of those two luminances is calculated. Weber contrast
  115    -1 has been called the ["gold standard" for text contrast]. It is usually defined
  116    -1 as `(yfg - ybg) / ybg` which is the same as `yfg / ybg - 1`. In this case, 0.05
  117    -1 is added to both values to account for ambient light. The shift by 1 is removed
  118    -1 because it has no impact on the results (as long as the thresholds are adapted
  119    -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   187 
  165    -1 Again we can see the same structure: We first convert colors to lightness, then
  166    -1 calculate the difference between them. However, in order to be able to compare
  167    -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   246 
  169    -1 -   The final steps do some scaling and shifting that only serves to get nice
  170    -1     threshold values. Just like the shift by 1 in the WCAG formula, this can
  171    -1     simply be ignored.
   -1   247   var lfg = YtoL(yfg);
   -1   248   var lbg = YtoL(ybg);
  172   249 
  173    -1 -   I will also ignore the `< 0.1` condition because it only affects contrasts
  174    -1     that are too low to be interesting anyway.
   -1   250   return lbg - lfg;
   -1   251 };
  175   252 
  176    -1 -   The contrast is calculated as a difference, not as a ratio as in WCAG. I
  177    -1     will look at the `exp()` of that difference. Since
  178    -1     `exp(a - b) == exp(a) / exp(b)`, this allows us to convert the APCA formula
  179    -1     from a difference to a ratio. Again I user the same trick: Since `exp()` is
  180    -1     monotonic, it does not change the results other than moving the
  181    -1     thresholds.
   -1   253 function normalize(c) {
   -1   254     return Math.log(c) / Math.log(21);
   -1   255 }
   -1   256 ```
  182   257 
  183    -1 With those changes. All other differences between APCA and WCAG 2.x can be
  184    -1 pushed into `sRGBtoY()`:
   -1   258 APCA becomes:
  185   259 
  186   260 ```js
  187    -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   272 
  200    -1 An interesting feature of APCA is that it uses four different exponents for
  201    -1 light foreground (0.62), dark foreground (0.57), light background (0.56), and
  202    -1 dark background (0.65). `sRGBtoY_modified()` takes that exponent as a second
  203    -1 parameter.
   -1   273 function YtoL(y) {
   -1   274     return Math.pow(y, 0.6);
   -1   275 }
  204   276 
  205    -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   290 
  207    -1 This conversion again uses sRGB coefficients. However, the non-linear part is
  208    -1 very different. The author of APCA provides some motivation for these changes
  209    -1 in the article [Regarding APCA Exponents]. The main argument seems to be that
  210    -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   295 
  212    -1 To get a better feeling for how these formulas compare, I plotted the results
  213    -1 of `sRGBtoY()`. In order to reduce colors to a single dimension, I used gray
  214    -1 `[x, x, x]`, red `[x, 0, 0]`, green `[0, x, 0]` and blue `[0, 0, x]` values.
   -1   296 ### Comparison
  215   297 
  216    -1 I also normalized the values so they are in the same range as WCAG 2.x. I used
  217    -1 factors (because they do not change the contrast ratio) and powers (because
  218    -1 they are monotonic on the contrast ratio).
   -1   298 Now that we have aligned the two formulas, what are the actual differences?
  219   299 
  220    -1 ```js
  221    -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 ![contrast comparison](plots/contrast_comparison.png)
  224   301 
  225    -1 function normalize(y) {
  226    -1   // scale the lower end to 1
  227    -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   306 
  229    -1   // scale the upper end to 21
  230    -1   // we use a power so the lower end stays at 1
  231    -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   313 
  233    -1   // scale down to the desired range
  234    -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   318 
  238    -1 ![sRGBtoY comparison](plots/sRGBtoY_comparison.png)
   -1   319 Since the conversion from luminance to lightness causes the biggest difference,
   -1   320 I took a closer look at it.
  239   321 
  240    -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 curve
  242    -1 with an ambient light value of 0.4 instead of 0.05. This one is very similar
  243    -1 to the APCA curves. The second column shows the differences between the APCA
  244    -1 curves and this modified WCAG 2.x. 0.4 was just a guess, there might be even
  245    -1 better values.
   -1   322 ![lightness comparison](plots/lightness_comparison.png)
  246   323 
  247    -1 I also wanted to see how the contrast results compare. I took a random sample
  248    -1 of color pairs and computed the normalized APCA contrast, WCAG 2.x contrast
  249    -1 (without removing the polarity) and the modified WCAG contrast with an ambient
  250    -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   326 
  252    -1 ![contrast comparison](plots/contrast_comparison.png)
   -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   333 
  254    -1 In the top row we see two scatter plots that compare APCA to both WCAG
  255    -1 variants. As we can see, they correlate in both cases, but the modified WCAG
  256    -1 2.x contrast is much closer.
  257    -1 
  258    -1 In the bottom row we see two more scatter plots. This time the X axis
  259    -1 corresponds to foreground luminance and the Y axis corresponds to background
  260    -1 luminance. The color of the dots indicated the differences between the
  261    -1 respective formulas, calculated as `log(apca / wcag)`. As we can see, the
  262    -1 biggest differences between APCA and WCAG 2.x are in areas where one color is
  263    -1 extremely light or extremely dark. For light colors, APCA predicts an even
  264    -1 higher contrast (difference is in the same direction as contrast polarity). For
  265    -1 dark colors, APCA predicts a lower contrast (difference is inverse to contrast
  266    -1 polarity).
  267    -1 
  268    -1 To sum up, the APCA contrast formula is certainly not as obvious a choice as
  269    -1 the one from WCAG 2.x. I was not able to find much information on how it was
  270    -1 derived. A closer analysis reveals that it is actually not that different from
  271    -1 WCAG 2.x, but assumes much more ambient light. More research is needed to
  272    -1 determine if this higher ambient light value is significant or just an
  273    -1 artifact of the conversion I did.
  274    -1 
  275    -1 As we have seen, using a polarity-aware difference instead of a ratio is not a
  276    -1 significant change in terms of results. However, in terms of developer
  277    -1 ergonomics, I personally feel like it is easier to work with. So I would be
  278    -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 with
  409    -1 a contrast above 3, but fail APCA with a contrast below 45 (assuming a
  410    -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   485 
  427    -1 The second table compares APCA to the modified WCAG 2.x contrast. The
  428    -1 thresholds were derived by applying the normalization steps described above to
  429    -1 the APCA thresholds. As expected, most color pairs fall into the same category
  430    -1 with both formulas. For example, only 1.7 % pass the modified WCAG 2.x with a
  431    -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   528 
  468    -1 The new contrast formula agrees with WCAG 2.x for 83.5% of color pairs. That
  469    -1 number rises to 91% for a modified WCAG 2.x formula with an ambient light value
  470    -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/SRGB
  481    -1 [Weber contrast]: https://en.wikipedia.org/wiki/Weber_contrast
  482   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 math
    2    -1 
    3     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    10 
   13    -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    14 
   16    -1 	y0 = ambient
   17    -1 	y1 = 1 + ambient
   18    -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    26 
   29    -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 - lfg
   33    -1 	c = np.exp(c)
   -1    27 def apca_l(y):
   -1    28 	return y ** 0.6
   -1    29 
   34    30 
   35    -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    -1 
   56    -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    -1 
   61    -1 	axes[0][0].set_title('APCA vs WCAG 2.x')
   62    -1 
   63    -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    -1 
   69    -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    -1 
   74    -1 	axes[0][1].set_title('APCA vs WCAG 2.x (0.4)')
   75    -1 
   76    -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    -1 
   82    -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 math
    2    -1 
    3    -1 from matplotlib import pyplot as plt
    4    -1 import numpy as np
    5    -1 
    6    -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    -1 
   10    -1 
   11    -1 def wcag(x, factor, ambient):
   12    -1 	y = x / 255
   13    -1 	y = np.where(y < 0.04045, y / 12.92, ((y + 0.055) / 1.055) ** 2.4)
   14    -1 	y *= factor
   15    -1 	y += ambient
   16    -1 
   17    -1 	y0 = ambient
   18    -1 	y1 = 1 + ambient
   19    -1 	return (y / y0) ** math.log(21, y1 / y0) / 20
   20    -1 
   21    -1 
   22    -1 def apca(x, factor, exp):
   23    -1 	y = x / 255
   24    -1 	y **= 2.4
   25    -1 	y *= factor
   26    -1 	y += np.where(y < 0.022, 0.022 - y, 0) ** 1.414
   27    -1 	y **= exp
   28    -1 	y = np.exp(y)
   29    -1 
   30    -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) / 20
   33    -1 
   34    -1 
   35    -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    -1 
   39    -1 	for i in range(4):
   40    -1 		wcag6 = wcag(x, WCAG_FACTORS[i], 0.4)
   41    -1 
   42    -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    -1 
   47    -1 		axes[i][0].plot(x, wcag(x, WCAG_FACTORS[i], 0.05))
   48    -1 		axes[i][0].plot(x, wcag6)
   49    -1 
   50    -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    -1 
   55    -1 	axes[0][0].set_title('sRGBtoY')
   56    -1 	axes[0][1].set_title('ratio APCA / WCAG 0.4')
   57    -1 
   58    -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    -1 
   67    -1 	plt.tight_layout()
   68    -1 	plt.subplots_adjust(bottom=0.1)
   69    -1 	plt.savefig('sRGBtoY_comparison.png')