1 /*
2     This file is part of darktable,
3     Copyright (C) 2020 darktable developers.
4 
5     darktable is free software: you can redistribute it and/or modify
6     it under the terms of the GNU General Public License as published by
7     the Free Software Foundation, either version 3 of the License, or
8     (at your option) any later version.
9 
10     darktable is distributed in the hope that it will be useful,
11     but WITHOUT ANY WARRANTY; without even the implied warranty of
12     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13     GNU General Public License for more details.
14 
15     You should have received a copy of the GNU General Public License
16     along with darktable.  If not, see <http://www.gnu.org/licenses/>.
17 */
18 
19 #pragma once
20 
21 #include "common/math.h"
22 
23 typedef enum dt_adaptation_t
24 {
25   DT_ADAPTATION_LINEAR_BRADFORD = 0, // $DESCRIPTION: "linear Bradford (ICC v4)"
26   DT_ADAPTATION_CAT16           = 1, // $DESCRIPTION: "CAT16 (CIECAM16)"
27   DT_ADAPTATION_FULL_BRADFORD   = 2, // $DESCRIPTION: "non-linear Bradford"
28   DT_ADAPTATION_XYZ             = 3, // $DESCRIPTION: "XYZ "
29   DT_ADAPTATION_RGB             = 4, // $DESCRIPTION: "none (bypass)"
30   DT_ADAPTATION_LAST
31 } dt_adaptation_t;
32 
33 
34 // modified LMS cone response space for Bradford transform
35 // explanation here : https://onlinelibrary.wiley.com/doi/pdf/10.1002/9781119021780.app3
36 // but coeffs are wrong in the above, so they come from :
37 // http://www2.cmp.uea.ac.uk/Research/compvis/Papers/FinSuss_COL00.pdf
38 // At any time, ensure XYZ_to_LMS is the exact matrice inverse of LMS_to_XYZ
39 const float DT_ALIGNED_ARRAY XYZ_to_Bradford_LMS[3][4] = { {  0.8951f,  0.2664f, -0.1614f, 0.f },
40                                                            { -0.7502f,  1.7135f,  0.0367f, 0.f },
41                                                            {  0.0389f, -0.0685f,  1.0296f, 0.f } };
42 
43 const float DT_ALIGNED_ARRAY Bradford_LMS_to_XYZ[3][4] = { {  0.9870f, -0.1471f,  0.1600f, 0.f },
44                                                            {  0.4323f,  0.5184f,  0.0493f, 0.f },
45                                                            { -0.0085f,  0.0400f,  0.9685f, 0.f } };
46 
47 #ifdef _OPENMP
48 #pragma omp declare simd aligned(XYZ, LMS:16)
49 #endif
convert_XYZ_to_bradford_LMS(const float XYZ[4],float LMS[4])50 static inline void convert_XYZ_to_bradford_LMS(const float XYZ[4], float LMS[4])
51 {
52   // Warning : needs XYZ normalized with Y - you need to downscale before
53   dot_product(XYZ, XYZ_to_Bradford_LMS, LMS);
54 }
55 
56 #ifdef _OPENMP
57 #pragma omp declare simd aligned(XYZ, LMS:16)
58 #endif
convert_bradford_LMS_to_XYZ(const float LMS[4],float XYZ[4])59 static inline void convert_bradford_LMS_to_XYZ(const float LMS[4], float XYZ[4])
60 {
61   // Warning : output XYZ normalized with Y - you need to upscale later
62   dot_product(LMS, Bradford_LMS_to_XYZ, XYZ);
63 }
64 
65 
66 // modified LMS cone response for CAT16, from CIECAM16
67 // reference : https://ntnuopen.ntnu.no/ntnu-xmlui/bitstream/handle/11250/2626317/CCIW-23.pdf?sequence=1
68 // At any time, ensure XYZ_to_LMS is the exact matrice inverse of LMS_to_XYZ
69 const float DT_ALIGNED_ARRAY XYZ_to_CAT16_LMS[3][4] = { {  0.401288f, 0.650173f, -0.051461f, 0.f },
70                                                         { -0.250268f, 1.204414f,  0.045854f, 0.f },
71                                                         { -0.002079f, 0.048952f,  0.953127f, 0.f } };
72 
73 const float DT_ALIGNED_ARRAY CAT16_LMS_to_XYZ[3][4] = { {  1.862068f, -1.011255f,  0.149187f, 0.f },
74                                                         {  0.38752f ,  0.621447f, -0.008974f, 0.f },
75                                                         { -0.015841f, -0.034123f,  1.049964f, 0.f } };
76 
77 #ifdef _OPENMP
78 #pragma omp declare simd aligned(XYZ, LMS:16)
79 #endif
convert_XYZ_to_CAT16_LMS(const float XYZ[4],float LMS[4])80 static inline void convert_XYZ_to_CAT16_LMS(const float XYZ[4], float LMS[4])
81 {
82   // Warning : needs XYZ normalized with Y - you need to downscale before
83   dot_product(XYZ, XYZ_to_CAT16_LMS, LMS);
84 }
85 
86 #ifdef _OPENMP
87 #pragma omp declare simd aligned(XYZ, LMS:16)
88 #endif
convert_CAT16_LMS_to_XYZ(const float LMS[4],float XYZ[4])89 static inline void convert_CAT16_LMS_to_XYZ(const float LMS[4], float XYZ[4])
90 {
91   // Warning : output XYZ normalized with Y - you need to upscale later
92   dot_product(LMS, CAT16_LMS_to_XYZ, XYZ);
93 }
94 
95 
96 #ifdef _OPENMP
97 #pragma omp declare simd aligned(XYZ, LMS:16) uniform(kind)
98 #endif
convert_any_LMS_to_XYZ(const float LMS[4],float XYZ[4],const dt_adaptation_t kind)99 static inline void convert_any_LMS_to_XYZ(const float LMS[4], float XYZ[4], const dt_adaptation_t kind)
100 {
101   // helper function switching internally to the proper conversion
102 
103   switch(kind)
104   {
105     case DT_ADAPTATION_FULL_BRADFORD:
106     case DT_ADAPTATION_LINEAR_BRADFORD:
107     {
108       convert_bradford_LMS_to_XYZ(LMS, XYZ);
109       break;
110     }
111     case DT_ADAPTATION_CAT16:
112     {
113       convert_CAT16_LMS_to_XYZ(LMS, XYZ);
114       break;
115     }
116     case DT_ADAPTATION_XYZ:
117     case DT_ADAPTATION_RGB:
118     case DT_ADAPTATION_LAST:
119     {
120       // special case : just pass through.
121       XYZ[0] = LMS[0];
122       XYZ[1] = LMS[1];
123       XYZ[2] = LMS[2];
124       break;
125     }
126   }
127 }
128 
129 
130 #ifdef _OPENMP
131 #pragma omp declare simd aligned(XYZ, LMS:16) uniform(kind)
132 #endif
convert_any_XYZ_to_LMS(const float XYZ[4],float LMS[4],dt_adaptation_t kind)133 static inline void convert_any_XYZ_to_LMS(const float XYZ[4], float LMS[4], dt_adaptation_t kind)
134 {
135   // helper function switching internally to the proper conversion
136 
137   switch(kind)
138   {
139     case DT_ADAPTATION_FULL_BRADFORD:
140     case DT_ADAPTATION_LINEAR_BRADFORD:
141     {
142       convert_XYZ_to_bradford_LMS(XYZ, LMS);
143       break;
144     }
145     case DT_ADAPTATION_CAT16:
146     {
147       convert_XYZ_to_CAT16_LMS(XYZ, LMS);
148       break;
149     }
150     case DT_ADAPTATION_XYZ:
151     case DT_ADAPTATION_RGB:
152     case DT_ADAPTATION_LAST:
153     {
154       // special case : just pass through.
155       LMS[0] = XYZ[0];
156       LMS[1] = XYZ[1];
157       LMS[2] = XYZ[2];
158       break;
159     }
160   }
161 }
162 
163 
164 #ifdef _OPENMP
165 #pragma omp declare simd aligned(RGB, LMS:16) uniform(kind)
166 #endif
convert_any_LMS_to_RGB(const float LMS[4],float RGB[4],dt_adaptation_t kind)167 static inline void convert_any_LMS_to_RGB(const float LMS[4], float RGB[4], dt_adaptation_t kind)
168 {
169   // helper function switching internally to the proper conversion
170   float DT_ALIGNED_PIXEL XYZ[4] = { 0.f };
171   convert_any_LMS_to_XYZ(LMS, XYZ, kind);
172 
173   // Fixme : convert to RGB display space instead of sRGB but first the display profile should be global in dt,
174   // not confined to colorout where it gets created/destroyed all the time.
175   dt_XYZ_to_Rec709_D65(XYZ, RGB);
176 
177   // Handle gamut clipping
178   float max_RGB = fmaxf(fmaxf(RGB[0], RGB[1]), RGB[2]);
179   for(int c = 0; c < 3; c++) RGB[c] = fmaxf(RGB[c] / max_RGB, 0.f);
180 
181 }
182 
183 
184 /* Bradford adaptations pre-computed for D50 and D65 outputs */
185 
186 #ifdef _OPENMP
187 #pragma omp declare simd uniform(origin_illuminant) \
188   aligned(lms_in, lms_out, origin_illuminant:16)
189 #endif
bradford_adapt_D65(const float lms_in[4],const float origin_illuminant[4],const float p,const int full,float lms_out[4])190 static inline void bradford_adapt_D65(const float lms_in[4],
191                                       const float origin_illuminant[4],
192                                       const float p, const int full,
193                                       float lms_out[4])
194 {
195   // Bradford chromatic adaptation from origin to target D65 illuminant in LMS space
196   // p = powf(origin_illuminant[2] / D65[2], 0.0834f) needs to be precomputed for performance,
197   // since it is independent from current pixel values
198   // origin illuminant need also to be precomputed to LMS
199 
200   // Precomputed D65 primaries in Bradford LMS for camera WB adjustment
201   const float DT_ALIGNED_PIXEL D65[4] = { 0.941238f, 1.040633f, 1.088932f, 0.f };
202 
203 
204   float DT_ALIGNED_PIXEL temp[4] = { lms_in[0] / origin_illuminant[0],
205                                      lms_in[1] / origin_illuminant[1],
206                                      lms_in[2] / origin_illuminant[2],
207                                      0.f };
208 
209   // use linear Bradford if B is negative
210   if(full) temp[2] = (temp[2] > 0.f) ? powf(temp[2], p) : temp[2];
211 
212   lms_out[0] = D65[0] * temp[0];
213   lms_out[1] = D65[1] * temp[1];
214   lms_out[2] = D65[2] * temp[2];
215 }
216 
217 
218 #ifdef _OPENMP
219 #pragma omp declare simd uniform(origin_illuminant) \
220   aligned(lms_in, lms_out, origin_illuminant:16)
221 #endif
bradford_adapt_D50(const float lms_in[4],const float origin_illuminant[4],const float p,const int full,float lms_out[4])222 static inline void bradford_adapt_D50(const float lms_in[4],
223                                       const float origin_illuminant[4],
224                                       const float p, const int full,
225                                       float lms_out[4])
226 {
227   // Bradford chromatic adaptation from origin to target D50 illuminant in LMS space
228   // p = powf(origin_illuminant[2] / D50[2], 0.0834f) needs to be precomputed for performance,
229   // since it is independent from current pixel values
230   // origin illuminant need also to be precomputed to LMS
231 
232   // Precomputed D50 primaries in Bradford LMS for ICC transforms
233   const float DT_ALIGNED_PIXEL D50[4] = { 0.996078f, 1.020646f, 0.818155f, 0.f };
234 
235 
236   float DT_ALIGNED_PIXEL temp[4] = { lms_in[0] / origin_illuminant[0],
237                                      lms_in[1] / origin_illuminant[1],
238                                      lms_in[2] / origin_illuminant[2],
239                                      0.f };
240 
241   // use linear Bradford if B is negative
242   if(full) temp[2] = (temp[2] > 0.f) ? powf(temp[2], p) : temp[2];
243 
244   lms_out[0] = D50[0] * temp[0];
245   lms_out[1] = D50[1] * temp[1];
246   lms_out[2] = D50[2] * temp[2];
247 }
248 
249 
250 /* CAT16 adaptations pre-computed for D50 and D65 outputs */
251 
252 #ifdef _OPENMP
253 #pragma omp declare simd uniform(origin_illuminant) \
254   aligned(lms_in, lms_out, origin_illuminant:16)
255 #endif
CAT16_adapt_D65(const float lms_in[4],const float origin_illuminant[4],const float D,const int full,float lms_out[4])256 static inline void CAT16_adapt_D65(const float lms_in[4],
257                                       const float origin_illuminant[4],
258                                       const float D, const int full,
259                                       float lms_out[4])
260 {
261   // CAT16 chromatic adaptation from origin to target D65 illuminant in LMS space
262   // D is the coefficient of adaptation, depending of the surround lighting
263   // origin illuminant need also to be precomputed to LMS
264 
265   // Precomputed D65 primaries in CAT16 LMS for camera WB adjustment
266   const float DT_ALIGNED_PIXEL D65[4] = { 0.97553267f, 1.01647859f, 1.0848344f, 0.f };
267 
268   if(full)
269   {
270     lms_out[0] = lms_in[0] * D65[0] / origin_illuminant[0];
271     lms_out[1] = lms_in[1] * D65[1] / origin_illuminant[1];
272     lms_out[2] = lms_in[2] * D65[2] / origin_illuminant[2];
273   }
274   else
275   {
276     lms_out[0] = lms_in[0] * (D * D65[0] / origin_illuminant[0] + 1.f - D);
277     lms_out[1] = lms_in[1] * (D * D65[1] / origin_illuminant[1] + 1.f - D);
278     lms_out[2] = lms_in[2] * (D * D65[2] / origin_illuminant[2] + 1.f - D);
279   }
280 }
281 
282 
283 #ifdef _OPENMP
284 #pragma omp declare simd uniform(origin_illuminant) \
285   aligned(lms_in, lms_out, origin_illuminant:16)
286 #endif
CAT16_adapt_D50(const float lms_in[4],const float origin_illuminant[4],const float D,const int full,float lms_out[4])287 static inline void CAT16_adapt_D50(const float lms_in[4],
288                                       const float origin_illuminant[4],
289                                       const float D, const int full,
290                                       float lms_out[4])
291 {
292   // CAT16 chromatic adaptation from origin to target D50 illuminant in LMS space
293   // D is the coefficient of adaptation, depending of the surround lighting
294   // origin illuminant need also to be precomputed to LMS
295 
296   // Precomputed D50 primaries in CAT16 LMS for ICC transforms
297   const float DT_ALIGNED_PIXEL D50[4] = { 0.994535f, 1.000997f, 0.833036f, 0.f };
298 
299   if(full)
300   {
301     lms_out[0] = lms_in[0] * D50[0] / origin_illuminant[0];
302     lms_out[1] = lms_in[1] * D50[1] / origin_illuminant[1];
303     lms_out[2] = lms_in[2] * D50[2] / origin_illuminant[2];
304   }
305   else
306   {
307     lms_out[0] = lms_in[0] * (D * D50[0] / origin_illuminant[0] + 1.f - D);
308     lms_out[1] = lms_in[1] * (D * D50[1] / origin_illuminant[1] + 1.f - D);
309     lms_out[2] = lms_in[2] * (D * D50[2] / origin_illuminant[2] + 1.f - D);
310   }
311 }
312 
313 /* XYZ adaptations pre-computed for D50 and D65 outputs */
314 
315 #ifdef _OPENMP
316 #pragma omp declare simd uniform(origin_illuminant) \
317   aligned(lms_in, lms_out, origin_illuminant:16)
318 #endif
XYZ_adapt_D65(const float lms_in[4],const float origin_illuminant[4],float lms_out[4])319 static inline void XYZ_adapt_D65(const float lms_in[4],
320                                       const float origin_illuminant[4],
321                                       float lms_out[4])
322 {
323   // XYZ chromatic adaptation from origin to target D65 illuminant in XYZ space
324   // origin illuminant need also to be precomputed to XYZ
325 
326   // Precomputed D65 primaries in XYZ for camera WB adjustment
327   const float DT_ALIGNED_PIXEL D65[4] = { 0.9504285453771807f, 1.0f, 1.0889003707981277f, 0.f };
328 
329   lms_out[0] = lms_in[0] * D65[0] / origin_illuminant[0];
330   lms_out[1] = lms_in[1] * D65[1] / origin_illuminant[1];
331   lms_out[2] = lms_in[2] * D65[2] / origin_illuminant[2];
332 }
333 
334 #ifdef _OPENMP
335 #pragma omp declare simd uniform(origin_illuminant) \
336   aligned(lms_in, lms_out, origin_illuminant:16)
337 #endif
XYZ_adapt_D50(const float lms_in[4],const float origin_illuminant[4],float lms_out[4])338 static inline void XYZ_adapt_D50(const float lms_in[4],
339                                       const float origin_illuminant[4],
340                                       float lms_out[4])
341 {
342   // XYZ chromatic adaptation from origin to target D65 illuminant in XYZ space
343   // origin illuminant need also to be precomputed to XYZ
344 
345   // Precomputed D50 primaries in XYZ for camera WB adjustment
346   const float DT_ALIGNED_PIXEL D50[4] = { 0.9642119944211994f, 1.0f, 0.8251882845188288f, 0.f };
347 
348   lms_out[0] = lms_in[0] * D50[0] / origin_illuminant[0];
349   lms_out[1] = lms_in[1] * D50[1] / origin_illuminant[1];
350   lms_out[2] = lms_in[2] * D50[2] / origin_illuminant[2];
351 }
352 
353 /* Pre-solved matrices to adjust white point for triplets in CIE XYZ 1931 2° observer */
354 
355 const float DT_ALIGNED_ARRAY XYZ_D50_to_D65_CAT16[3][4]
356     = { { 9.89466254e-01f, -4.00304626e-02f, 4.40530317e-02f, 0.f },
357         { -5.40518733e-03f, 1.00666069e+00f, -1.75551955e-03f, 0.f },
358         { -4.03920992e-04f, 1.50768030e-02f, 1.30210211e+00f, 0.f } };
359 
360 const float DT_ALIGNED_ARRAY XYZ_D50_to_D65_Bradford[3][4]
361     = { { 0.95547342f, -0.02309845f, 0.06325924f, 0.f },
362         { -0.02836971f, 1.00999540f, 0.02104144f, 0.f },
363         { 0.01231401f, -0.02050765f, 1.33036593f, 0.f } };
364 
365 const float DT_ALIGNED_ARRAY XYZ_D65_to_D50_CAT16[3][4]
366     = { { 1.01085433e+00f, 4.07086103e-02f, -3.41445825e-02f, 0.f },
367         { 5.42814201e-03f, 9.93581926e-01f, 1.15592039e-03f, 0.f },
368         { 2.50722468e-04f, -1.14918759e-02f, 7.67964947e-01f, 0.f } };
369 
370 const float DT_ALIGNED_ARRAY XYZ_D65_to_D50_Bradford[3][4]
371     = { { 1.04792979f, 0.02294687f, -0.05019227f, 0.f },
372         { 0.02962781f, 0.99043443f, -0.0170738f, 0.f },
373         { -0.00924304f, 0.01505519f, 0.75187428f, 0.f } };
374 
375 #ifdef _OPENMP
376 #pragma omp declare simd aligned(XYZ_in, XYZ_out:16)
377 #endif
XYZ_D50_to_D65(const float XYZ_in[4],float XYZ_out[4])378 static inline void XYZ_D50_to_D65(const float XYZ_in[4], float XYZ_out[4])
379 {
380   dot_product(XYZ_in, XYZ_D50_to_D65_CAT16, XYZ_out);
381 }
382 
383 #ifdef _OPENMP
384 #pragma omp declare simd aligned(XYZ_in, XYZ_out:16)
385 #endif
XYZ_D65_to_D50(const float XYZ_in[4],float XYZ_out[4])386 static inline void XYZ_D65_to_D50(const float XYZ_in[4], float XYZ_out[4])
387 {
388   dot_product(XYZ_in, XYZ_D65_to_D50_CAT16, XYZ_out);
389 }
390