apca-introduction

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

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)