File Coverage

lib/Graphics/Toolkit/Color/Space/Instance/Helper/OK.pm
Criterion Covered Total %
statement 108 108 100.0
branch 8 8 100.0
condition n/a
subroutine 14 14 100.0
pod 0 10 0.0
total 130 140 92.8


line stmt bran cond sub pod time code
1              
2             # OKHSL and OKHSV converter helper Copyright (c) 2021 Björn Ottosson, see LICENSE.OK
3              
4             package Graphics::Toolkit::Color::Space::Instance::Helper::OK;
5 17     17   217 use v5.12;
  17         52  
6 17     17   66 use warnings;
  17         22  
  17         820  
7 17     17   96 use Exporter 'import';
  17         275  
  17         705  
8 17     17   522 use Graphics::Toolkit::Color::Space qw/min max spow mult_matrix_vector_3/;
  17         25  
  17         24477  
9             our @EXPORT_OK = qw/toe toe_inv find_cusp to_ST get_Cs oklab_to_linear_srgb linear_srgb_to_oklab/;
10             our %EXPORT_TAGS = (all => [@EXPORT_OK]);
11              
12             my $toe_k1 = 0.206;
13             my $toe_k2 = 0.03;
14             my $toe_k3 = (1 + $toe_k1) / (1 + $toe_k2);
15              
16             sub linear_srgb_to_oklab {
17 25     25 0 34 my ($rgb) = @_;
18 25         106 my @lms = mult_matrix_vector_3([[ 0.4122214708, 0.5363325363, 0.0514459929],
19             [ 0.2119034982, 0.6806995451, 0.1073969566],
20             [ 0.0883024619, 0.2817188376, 0.6299787005]], @$rgb);
21 25         77 @lms = map {spow($_, (1/3))} @lms;
  75         101  
22 25         75 my @lab = mult_matrix_vector_3([[ 0.2104542553, 0.7936177850, -0.0040720468],
23             [ 1.9779984951, -2.4285922050, 0.4505937099],
24             [ 0.0259040371, 0.7827717662, -0.8086757660]], @lms);
25 25         60 return \@lab;
26             }
27              
28             sub oklab_to_linear_srgb {
29 79     79 0 93 my ($lab) = @_;
30 79         205 my @lms = mult_matrix_vector_3([[ 1, 0.3963377774, 0.2158037573],
31             [ 1, -0.1055613458, -0.0638541728],
32             [ 1, -0.0894841775, -1.2914855480]], @$lab);
33 79         151 @lms = map {spow($_, 3)} @lms;
  237         281  
34 79         160 my @rgb = mult_matrix_vector_3([[ 4.0767416621, -3.3077115913, 0.2309699292],
35             [ -1.2684380046, 2.6097574011, -0.3413193965],
36             [ -0.0041960863, -0.7034186147, 1.7076147010]], @lms);
37 79         182 return \@rgb;
38             }
39              
40             sub compute_max_saturation {
41 44     44 0 53 my ($a, $b) = @_;
42 44         77 my (@k, @w); # @k = Polynom-Koeffizienten, @w = oklab->lms Zeile fuer Halley
43 44 100       118 if (-1.88170328 * $a - 0.80936493 * $b > 1) { # Rot-Kanal zuerst
    100          
44 9         15 @k = ( 1.19086277, 1.76576728, 0.59662641, 0.75515197, 0.56771245);
45 9         14 @w = ( 4.0767416621, -3.3077115913, 0.2309699292);
46             } elsif (1.81444104 * $a - 1.19445276 * $b > 1) { # Gruen-Kanal zuerst
47 20         41 @k = ( 0.73956515, -0.45954404, 0.08285427, 0.12541070, 0.14503204);
48 20         59 @w = (-1.2684380046, 2.6097574011, -0.3413193965);
49             } else { # Blau-Kanal zuerst
50 15         29 @k = ( 1.35733652, -0.00915799, -1.15130210, -0.50559606, 0.00692167);
51 15         21 @w = (-0.0041960863, -0.7034186147, 1.7076147010);
52             }
53              
54             # Stufe 1: Polynom-Schaetzung von S
55 44         120 my $S = $k[0] + $k[1]*$a + $k[2]*$b + $k[3]*$a*$a + $k[4]*$a*$b;
56              
57             # Stufe 2: ein Halley-Schritt zur Verfeinerung
58 44         101 my @k_lms = (0.3963377774 * $a + 0.2158037573 * $b,
59             -0.1055613458 * $a - 0.0638541728 * $b,
60             -0.0894841775 * $a - 1.2914855480 * $b);
61              
62 44         74 my @lms = map {1 + $S * $_} @k_lms;
  132         173  
63 44         68 my @dS = map {3 * $k_lms[$_] * $lms[$_] * $lms[$_]} 0 .. 2;
  132         201  
64 44         68 my @dS2 = map {6 * $k_lms[$_] * $k_lms[$_] * $lms[$_]} 0 .. 2;
  132         179  
65 44         52 @lms = map {spow($_, 3)} @lms;
  132         195  
66              
67 44         65 my $f = $w[0]*$lms[0] + $w[1]*$lms[1] + $w[2]*$lms[2];
68 44         63 my $f1 = $w[0]*$dS[0] + $w[1]*$dS[1] + $w[2]*$dS[2];
69 44         55 my $f2 = $w[0]*$dS2[0] + $w[1]*$dS2[1] + $w[2]*$dS2[2];
70              
71 44         65 $S = $S - $f * $f1 / ($f1 * $f1 - 0.5 * $f * $f2);
72              
73 44         80 return $S;
74             }
75              
76             sub find_cusp {
77 44     44 0 61 my ($a, $b) = @_; # normalisiert: a**2 + b**2 == 1
78              
79             # buntester Punkt dieses Farbtons als Saettigung S = C/L
80 44         81 my $S_cusp = compute_max_saturation($a, $b);
81              
82             # diese Oklab-Richtung nach linearem RGB, dann so skalieren,
83             # dass der groesste Kanal genau 1 erreicht -> Gamut-Wand
84 44         96 my $rgb = oklab_to_linear_srgb([1, $S_cusp * $a, $S_cusp * $b]);
85 44         82 my $max_rgb = max(@$rgb);
86 44         93 my $L_cusp = spow(1 / $max_rgb, 1/3);
87 44         51 my $C_cusp = $L_cusp * $S_cusp;
88              
89 44         82 return ($L_cusp, $C_cusp);
90             }
91              
92             sub get_ST_mid {
93 30     30 0 41 my ($a, $b) = @_; # normalisiert: a**2 + b**2 == 1
94              
95 30         66 my $S = 0.11516993 + 1 / (
96             + 7.44778970 + 4.15901240 * $b
97             + $a * (-2.19557347 + 1.75198401 * $b
98             + $a * (-2.13704948 - 10.02301043 * $b
99             + $a * (-4.24894561 + 5.38770819 * $b + 4.69891013 * $a))));
100              
101 30         59 my $T = 0.11239642 + 1 / (
102             + 1.61320320 - 0.68124379 * $b
103             + $a * ( 0.40370612 + 0.90148123 * $b
104             + $a * (-0.27087943 + 0.61223990 * $b
105             + $a * ( 0.00299215 - 0.45399568 * $b - 0.14661872 * $a))));
106              
107 30         40 return ($S, $T);
108             }
109              
110             sub to_ST {
111 44     44 0 62 my ($L, $C) = @_;
112 44         86 return ($C / $L, $C / (1 - $L));
113             }
114              
115             sub find_gamut_intersection {
116 30     30 0 52 my ($a, $b, $L1, $C1, $L0, $cusp_L, $cusp_C) = @_;
117 30         29 my $t;
118              
119 30 100       72 if (($L1 - $L0) * $cusp_C - ($cusp_L - $L0) * $C1 <= 0) {
120             # unterer (gerader) Schenkel: direkter Schnitt, keine Verfeinerung noetig
121 13         21 $t = $cusp_C * $L0 / ($C1 * $cusp_L + $cusp_C * ($L0 - $L1));
122             } else {
123             # oberer (gekruemmter) Schenkel: erst grober Dreiecks-Schnitt ...
124 17         30 $t = $cusp_C * ($L0 - 1) / ($C1 * ($cusp_L - 1) + $cusp_C * ($L0 - $L1));
125              
126             # ... dann ein Halley-Schritt auf den echten Gamutrand
127 17         19 my $dL = $L1 - $L0;
128 17         19 my $dC = $C1;
129 17         35 my @k_lms = ( 0.3963377774 * $a + 0.2158037573 * $b,
130             -0.1055613458 * $a - 0.0638541728 * $b,
131             -0.0894841775 * $a - 1.2914855480 * $b);
132              
133 17         20 my @lms_dt = map { $dL + $dC * $_ } @k_lms;
  51         69  
134 17         45 my $L = $L0 * (1 - $t) + $t * $L1;
135 17         23 my $C = $t * $C1;
136 17         20 my @lms = map { $L + $C * $_ } @k_lms; # l_, m_, s_
  51         52  
137 17         22 my @ldt = map { 3 * $lms_dt[$_] * $lms[$_] * $lms[$_] } 0 .. 2;
  51         84  
138 17         24 my @ldt2 = map { 6 * $lms_dt[$_] * $lms_dt[$_] * $lms[$_] } 0 .. 2;
  51         60  
139 17         19 @lms = map { $_ * $_ * $_ } @lms; # kubiert
  51         81  
140              
141             # Matrix = oklab_to_linear_srgb (LMS -> linear sRGB), Rand bei Kanal == 1
142 17         54 my $M = [[ 4.0767416621, -3.3077115913, 0.2309699292],
143             [-1.2684380046, 2.6097574011, -0.3413193965],
144             [-0.0041960863, -0.7034186147, 1.7076147010]];
145              
146 17         31 my @res = map { $_ - 1 } mult_matrix_vector_3($M, @lms);
  51         64  
147 17         30 my @res1 = mult_matrix_vector_3($M, @ldt);
148 17         25 my @res2 = mult_matrix_vector_3($M, @ldt2);
149 17         18 my $huge = 9**9**9; # ersetzt FLT_MAX
150             my @t_ch = map {
151 17         42 my $u = $res1[$_] / ($res1[$_] * $res1[$_] - 0.5 * $res[$_] * $res2[$_]);
  51         65  
152 51 100       79 $u >= 0 ? -$res[$_] * $u : $huge;
153             } 0 .. 2;
154              
155 17         33 $t += min(@t_ch);
156             }
157 30         41 return $t;
158             }
159              
160              
161             sub get_Cs { # Chroma min mid max
162 30     30 0 45 my ($L, $a, $b) = @_; # a, b normalisiert
163              
164 30         59 my ($L_cusp, $C_cusp) = find_cusp($a, $b);
165              
166             # C_max: exakter Gamutrand bei dieser Helligkeit
167 30         62 my $C_max = find_gamut_intersection($a, $b, $L, 1, $L, $L_cusp, $C_cusp);
168              
169             # Cusp als Steigungen S = C/L (von Schwarz) und T = C/(1-L) (von Weiss)
170 30         46 my ($S_max, $T_max) = to_ST($L_cusp, $C_cusp);
171              
172             # Skalierung der gekruemmten Gamut-Form
173 30         74 my $k = $C_max / min($L * $S_max, (1 - $L) * $T_max);
174              
175             # C_mid: glatte mittlere Chroma aus der genaeherten Cusp-Lage
176 30         52 my ($S_mid, $T_mid) = get_ST_mid($a, $b);
177 30         54 my $C_a = $L * $S_mid;
178 30         36 my $C_b = (1 - $L) * $T_mid;
179 30         127 my $C_mid = 0.9 * $k * spow(1 / (1/$C_a**4 + 1/$C_b**4), 1/4);
180              
181             # C_0: innere Referenz, hue-unabhaengige Anker
182 30         34 $C_a = $L * 0.4;
183 30         35 $C_b = (1 - $L) * 0.8;
184 30         62 my $C_0 = spow(1 / (1/$C_a**2 + 1/$C_b**2), 1/2);
185              
186 30         70 return ($C_0, $C_mid, $C_max);
187             }
188              
189             sub toe { # korrigiert die Helligkeit
190 32     32 0 39 my ($L) = @_;
191 32         84 return 0.5 * ($toe_k3 * $L - $toe_k1
192             + spow(($toe_k3 * $L - $toe_k1)**2 + 4 * $toe_k2 * $toe_k3 * $L, 1/2));
193             }
194              
195             sub toe_inv {
196 35     35 0 45 my ($L) = @_;
197 35         75 return ($L*$L + $toe_k1 * $L) / ($toe_k3 * ($L + $toe_k2));
198             }
199              
200              
201             1;
202              
203             __END__