- commit
- e2709fa10cccb02162888f0fab910203d70fbdd8
- parent
- 28922b6cf7e90a3d81c12acd9813735d8f8019eb
- Author
- Tobias Bengfort <tobias.bengfort@posteo.de>
- Date
- 2022-07-18 20:22
add coverage tables
Diffstat
| M | analysis.md | 33 | +++++++++++++++++++++++++++++++++ |
| A | plots/coverage.py | 94 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
2 files changed, 127 insertions, 0 deletions
diff --git a/analysis.md b/analysis.md
@@ -381,6 +381,39 @@ APCA defines 6 thresholds: 15, 30, 45, 60, 75, 90. 381 381 The required threshold depends on the spatial frequency (see above). 45, 60, 382 382 and 75 loosely correspond to 3, 4.5, and 7 in WCAG 2.x. 383 383 -1 384 Again I generated random color pairs and used them to compare APCA to WCAG 2.x: -1 385 -1 386 -1 387 | | < 15 | 15-30 | 30-45 | 45-60 | 60-75 | 75-90 | > 90 | total | -1 388 | ------:| -----:| -----:| -----:| -----:| -----:| -----:| -----:| -----:| -1 389 | < 3 | 35.3 | 25.0 | 11.3 | 1.7 | 0.0 | 0.0 | 0.0 | 73.3 | -1 390 | 3-4.5 | 0.0 | 0.8 | 6.2 | 6.5 | 0.8 | 0.0 | 0.0 | 14.3 | -1 391 | 4.5-7 | 0.0 | 0.0 | 0.8 | 3.8 | 3.7 | 0.2 | 0.0 | 8.6 | -1 392 | > 7 | 0.0 | 0.0 | 0.0 | 0.2 | 1.8 | 1.6 | 0.1 | 3.8 | -1 393 | total | 35.3 | 25.8 | 18.3 | 12.3 | 6.4 | 1.8 | 0.1 | | -1 394 -1 395 The columns correspond to APCA thresholds, the rows correspond to WCAG 2.x -1 396 thresholds. For example, 6.2 % of the generated color pairs pass WCAG 2.x with -1 397 a contrast above 3, but fail APCA with a contrast below 45 (assuming a -1 398 conventional spatial frequency). -1 399 -1 400 | | < 15 | 15-30 | 30-45 | 45-60 | 60-75 | 75-90 | > 90 | total | -1 401 | --------:| -----:| -----:| -----:| -----:| -----:| -----:| -----:| -----:| -1 402 | < 1.6 | 33.7 | 0.7 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 34.5 | -1 403 | 1.6-2.5 | 1.5 | 23.5 | 0.8 | 0.0 | 0.0 | 0.0 | 0.0 | 25.9 | -1 404 | 2.5-3.8 | 0.0 | 1.5 | 15.8 | 0.2 | 0.0 | 0.0 | 0.0 | 17.5 | -1 405 | 3.8-5.7 | 0.0 | 0.0 | 1.7 | 10.4 | 0.0 | 0.0 | 0.0 | 12.2 | -1 406 | 5.7-8.7 | 0.0 | 0.0 | 0.0 | 1.7 | 5.7 | 0.0 | 0.0 | 7.4 | -1 407 | 8.7-13.2 | 0.0 | 0.0 | 0.0 | 0.0 | 0.6 | 1.8 | 0.0 | 2.4 | -1 408 | > 13.2 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.1 | 0.2 | -1 409 | total | 35.3 | 25.8 | 18.3 | 12.3 | 6.4 | 1.8 | 0.1 | | -1 410 -1 411 The second table compares APCA to the modified WCAG 2.x contrast. The -1 412 thresholds were derived by applying the normalization steps described above to -1 413 the APCA thresholds. As expected, most color pairs fall into the same category -1 414 with both formulas. For example, only 1.7 % pass the modified WCAG 2.x with a -1 415 contrast above 3.8, but fail APCA with a contrast below 45. -1 416 384 417 ## Conclusion 385 418 386 419 In this analysis I took a deeper look at the Accessible Perceptual Contrast
diff --git a/plots/coverage.py b/plots/coverage.py
@@ -0,0 +1,94 @@
-1 1 import math
-1 2
-1 3 import numpy as np
-1 4
-1 5 WCAG_LEVELS = [3, 4.5, 7]
-1 6 WCAG4_LEVELS = [1.6, 2.5, 3.8, 5.7, 8.7, 13.2]
-1 7 APCA_LEVELS = [15, 30, 45, 60, 75, 90]
-1 8
-1 9
-1 10 def wcag_y(color):
-1 11 c = color / 255
-1 12 c = np.where(c < 0.04045, c / 12.92, ((c + 0.055) / 1.055) ** 2.4)
-1 13 return 0.2126 * c[:, 0] + 0.7152 * c[:, 1] + 0.0722 * c[:, 2]
-1 14
-1 15
-1 16 def wcag_contrast(yfg, ybg, ambient=0.05):
-1 17 c = (ybg + ambient) / (yfg + ambient)
-1 18 c = np.where(c < 1, 1 / c, c)
-1 19
-1 20 y0 = ambient
-1 21 y1 = 1 + ambient
-1 22 return c ** math.log(21, y1 / y0)
-1 23
-1 24
-1 25 def apca_y(color):
-1 26 c = color / 255
-1 27 c **= 2.4
-1 28 y = 0.2126729 * c[:, 0] + 0.7151522 * c[:, 1] + 0.0721750 * c[:, 2]
-1 29 y += np.where(y < 0.022, 0.022 - y, 0) ** 1.414
-1 30 return y
-1 31
-1 32
-1 33 def apca_contrast(yfg, ybg):
-1 34 _yfg = yfg ** np.where(ybg > yfg, 0.57, 0.62)
-1 35 _ybg = ybg ** np.where(ybg > yfg, 0.56, 0.65)
-1 36 c = (_ybg - _yfg) * 1.14
-1 37 c = np.where(np.abs(c) < 0.1, 0, np.where(c > 0, c - 0.027, c + 0.027))
-1 38 return np.abs(c) * 100
-1 39
-1 40
-1 41 def count(a, b):
-1 42 return sum(a * b)
-1 43
-1 44
-1 45 def iter_levels(levels):
-1 46 for i in range(len(levels) + 1):
-1 47 if i == 0:
-1 48 yield -math.inf, levels[i]
-1 49 elif i == len(levels):
-1 50 yield levels[i - 1], math.inf
-1 51 else:
-1 52 yield levels[i - 1], levels[i]
-1 53
-1 54
-1 55 def print_row(row, sep=' | ', end=['']):
-1 56 _row = [''] + [f'{x:.1f}' for x in row] + end
-1 57 _row = [f'{s: >5}' for s in _row]
-1 58 print(sep.join(_row).strip())
-1 59
-1 60
-1 61 def print_table(rows):
-1 62 for row in rows:
-1 63 print_row(row + [sum(row)])
-1 64
-1 65 r = np.array(rows)
-1 66 totals = [sum(r[:, i]) for i in range(r.shape[1])]
-1 67 print_row(totals, end=['', ''])
-1 68 print()
-1 69
-1 70
-1 71 if __name__ == '__main__':
-1 72 size = 20_000
-1 73 fg = np.random.randint(0, 256, size=(size, 3))
-1 74 bg = np.random.randint(0, 256, size=(size, 3))
-1 75
-1 76 apca_yfg = apca_y(fg)
-1 77 apca_ybg = apca_y(bg)
-1 78 apca = apca_contrast(apca_yfg, apca_ybg)
-1 79
-1 80 wcag_yfg = wcag_y(fg)
-1 81 wcag_ybg = wcag_y(bg)
-1 82 wcag = wcag_contrast(wcag_yfg, wcag_ybg)
-1 83 wcag4 = wcag_contrast(wcag_yfg, wcag_ybg, 0.4)
-1 84
-1 85 for _wcag, wcag_levels in [(wcag, WCAG_LEVELS), (wcag4, WCAG4_LEVELS)]:
-1 86 rows = []
-1 87 for wcag_lower, wcag_upper in iter_levels(wcag_levels):
-1 88 rows.append([])
-1 89 a = (wcag_lower <= _wcag) * (_wcag < wcag_upper)
-1 90 for apca_lower, apca_upper in iter_levels(APCA_LEVELS):
-1 91 b = (apca_lower <= apca) * (apca < apca_upper)
-1 92 v = sum(a * b) / size * 100
-1 93 rows[-1].append(v)
-1 94 print_table(rows)