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   263 use v5.12;
  17         65  
6 17     17   63 use warnings;
  17         21  
  17         806  
7 17     17   84 use Exporter 'import';
  17         214  
  17         819  
8 17     17   65 use Graphics::Toolkit::Color::Space qw/min max spow mult_matrix_vector_3/;
  17         22  
  17         25243  
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 55 my ($rgb) = @_;
18 25         109 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         69 @lms = map {spow($_, (1/3))} @lms;
  75         107  
22 25         72 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         63 return \@lab;
26             }
27              
28             sub oklab_to_linear_srgb {
29 79     79 0 104 my ($lab) = @_;
30 79         197 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         159 @lms = map {spow($_, 3)} @lms;
  237         269  
34 79         169 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         181 return \@rgb;
38             }
39              
40             sub compute_max_saturation {
41 44     44 0 51 my ($a, $b) = @_;
42 44         59 my (@k, @w); # @k = Polynom-Koeffizienten, @w = oklab->lms Zeile fuer Halley
43 44 100       116 if (-1.88170328 * $a - 0.80936493 * $b > 1) { # Rot-Kanal zuerst
    100          
44 9         19 @k = ( 1.19086277, 1.76576728, 0.59662641, 0.75515197, 0.56771245);
45 9         12 @w = ( 4.0767416621, -3.3077115913, 0.2309699292);
46             } elsif (1.81444104 * $a - 1.19445276 * $b > 1) { # Gruen-Kanal zuerst
47 20         37 @k = ( 0.73956515, -0.45954404, 0.08285427, 0.12541070, 0.14503204);
48 20         29 @w = (-1.2684380046, 2.6097574011, -0.3413193965);
49             } else { # Blau-Kanal zuerst
50 15         26 @k = ( 1.35733652, -0.00915799, -1.15130210, -0.50559606, 0.00692167);
51 15         24 @w = (-0.0041960863, -0.7034186147, 1.7076147010);
52             }
53              
54             # Stufe 1: Polynom-Schaetzung von S
55 44         80 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         70 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         56 my @lms = map {1 + $S * $_} @k_lms;
  132         191  
63 44         90 my @dS = map {3 * $k_lms[$_] * $lms[$_] * $lms[$_]} 0 .. 2;
  132         253  
64 44         61 my @dS2 = map {6 * $k_lms[$_] * $k_lms[$_] * $lms[$_]} 0 .. 2;
  132         169  
65 44         46 @lms = map {spow($_, 3)} @lms;
  132         169  
66              
67 44         65 my $f = $w[0]*$lms[0] + $w[1]*$lms[1] + $w[2]*$lms[2];
68 44         56 my $f1 = $w[0]*$dS[0] + $w[1]*$dS[1] + $w[2]*$dS[2];
69 44         49 my $f2 = $w[0]*$dS2[0] + $w[1]*$dS2[1] + $w[2]*$dS2[2];
70              
71 44         78 $S = $S - $f * $f1 / ($f1 * $f1 - 0.5 * $f * $f2);
72              
73 44         83 return $S;
74             }
75              
76             sub find_cusp {
77 44     44 0 59 my ($a, $b) = @_; # normalisiert: a**2 + b**2 == 1
78              
79             # buntester Punkt dieses Farbtons als Saettigung S = C/L
80 44         71 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         125 my $rgb = oklab_to_linear_srgb([1, $S_cusp * $a, $S_cusp * $b]);
85 44         92 my $max_rgb = max(@$rgb);
86 44         128 my $L_cusp = spow(1 / $max_rgb, 1/3);
87 44         60 my $C_cusp = $L_cusp * $S_cusp;
88              
89 44         81 return ($L_cusp, $C_cusp);
90             }
91              
92             sub get_ST_mid {
93 30     30 0 33 my ($a, $b) = @_; # normalisiert: a**2 + b**2 == 1
94              
95 30         61 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         49 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         45 return ($S, $T);
108             }
109              
110             sub to_ST {
111 44     44 0 54 my ($L, $C) = @_;
112 44         82 return ($C / $L, $C / (1 - $L));
113             }
114              
115             sub find_gamut_intersection {
116 30     30 0 47 my ($a, $b, $L1, $C1, $L0, $cusp_L, $cusp_C) = @_;
117 30         37 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         19 $t = $cusp_C * $L0 / ($C1 * $cusp_L + $cusp_C * ($L0 - $L1));
122             } else {
123             # oberer (gekruemmter) Schenkel: erst grober Dreiecks-Schnitt ...
124 17         35 $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         20 my $dL = $L1 - $L0;
128 17         21 my $dC = $C1;
129 17         32 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         26 my @lms_dt = map { $dL + $dC * $_ } @k_lms;
  51         62  
134 17         31 my $L = $L0 * (1 - $t) + $t * $L1;
135 17         26 my $C = $t * $C1;
136 17         16 my @lms = map { $L + $C * $_ } @k_lms; # l_, m_, s_
  51         59  
137 17         36 my @ldt = map { 3 * $lms_dt[$_] * $lms[$_] * $lms[$_] } 0 .. 2;
  51         70  
138 17         24 my @ldt2 = map { 6 * $lms_dt[$_] * $lms_dt[$_] * $lms[$_] } 0 .. 2;
  51         93  
139 17         31 @lms = map { $_ * $_ * $_ } @lms; # kubiert
  51         64  
140              
141             # Matrix = oklab_to_linear_srgb (LMS -> linear sRGB), Rand bei Kanal == 1
142 17         37 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         32 my @res = map { $_ - 1 } mult_matrix_vector_3($M, @lms);
  51         63  
147 17         30 my @res1 = mult_matrix_vector_3($M, @ldt);
148 17         30 my @res2 = mult_matrix_vector_3($M, @ldt2);
149 17         22 my $huge = 9**9**9; # ersetzt FLT_MAX
150             my @t_ch = map {
151 17         22 my $u = $res1[$_] / ($res1[$_] * $res1[$_] - 0.5 * $res[$_] * $res2[$_]);
  51         68  
152 51 100       98 $u >= 0 ? -$res[$_] * $u : $huge;
153             } 0 .. 2;
154              
155 17         28 $t += min(@t_ch);
156             }
157 30         46 return $t;
158             }
159              
160              
161             sub get_Cs { # Chroma min mid max
162 30     30 0 46 my ($L, $a, $b) = @_; # a, b normalisiert
163              
164 30         73 my ($L_cusp, $C_cusp) = find_cusp($a, $b);
165              
166             # C_max: exakter Gamutrand bei dieser Helligkeit
167 30         61 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         51 my ($S_max, $T_max) = to_ST($L_cusp, $C_cusp);
171              
172             # Skalierung der gekruemmten Gamut-Form
173 30         63 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         49 my ($S_mid, $T_mid) = get_ST_mid($a, $b);
177 30         36 my $C_a = $L * $S_mid;
178 30         39 my $C_b = (1 - $L) * $T_mid;
179 30         124 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         39 $C_a = $L * 0.4;
183 30         34 $C_b = (1 - $L) * 0.8;
184 30         69 my $C_0 = spow(1 / (1/$C_a**2 + 1/$C_b**2), 1/2);
185              
186 30         65 return ($C_0, $C_mid, $C_max);
187             }
188              
189             sub toe { # korrigiert die Helligkeit
190 32     32 0 40 my ($L) = @_;
191 32         79 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 50 my ($L) = @_;
197 35         77 return ($L*$L + $toe_k1 * $L) / ($toe_k3 * ($L + $toe_k2));
198             }
199              
200              
201             1;
202              
203             __END__