Ansel 0.0
A darktable fork - bloat + design vision
Loading...
Searching...
No Matches
chromatic_adaptation.h
Go to the documentation of this file.
1/*
2 This file is part of darktable,
3 Copyright (C) 2020-2021, 2025 Aurélien PIERRE.
4 Copyright (C) 2021 Pascal Obry.
5 Copyright (C) 2021 Ralf Brown.
6 Copyright (C) 2022 Martin Bařinka.
7 Copyright (C) 2022 Sakari Kapanen.
8 Copyright (C) 2023 Luca Zulberti.
9
10 darktable is free software: you can redistribute it and/or modify
11 it under the terms of the GNU General Public License as published by
12 the Free Software Foundation, either version 3 of the License, or
13 (at your option) any later version.
14
15 darktable is distributed in the hope that it will be useful,
16 but WITHOUT ANY WARRANTY; without even the implied warranty of
17 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 GNU General Public License for more details.
19
20 You should have received a copy of the GNU General Public License
21 along with darktable. If not, see <http://www.gnu.org/licenses/>.
22*/
23
24#ifndef DT_PIXEL_CHROMATIC_ADAPTATION_H
25#define DT_PIXEL_CHROMATIC_ADAPTATION_H
26
27#include "math/math.h"
29
30typedef enum dt_adaptation_t
31{
32 DT_ADAPTATION_LINEAR_BRADFORD = 0, // $DESCRIPTION: "linear Bradford (ICC v4)"
33 DT_ADAPTATION_CAT16 = 1, // $DESCRIPTION: "CAT16 (CIECAM16)"
34 DT_ADAPTATION_FULL_BRADFORD = 2, // $DESCRIPTION: "non-linear Bradford"
35 DT_ADAPTATION_XYZ = 3, // $DESCRIPTION: "XYZ"
36 DT_ADAPTATION_RGB = 4, // $DESCRIPTION: "none (bypass)"
39
40
41// modified LMS cone response space for Bradford transform
42// explanation here : https://onlinelibrary.wiley.com/doi/pdf/10.1002/9781119021780.app3
43// but coeffs are wrong in the above, so they come from :
44// http://www2.cmp.uea.ac.uk/Research/compvis/Papers/FinSuss_COL00.pdf
45// At any time, ensure XYZ_to_LMS is the exact matrice inverse of LMS_to_XYZ
46static const dt_colormatrix_t XYZ_to_Bradford_LMS = { { 0.8951f, 0.2664f, -0.1614f, 0.f },
47 { -0.7502f, 1.7135f, 0.0367f, 0.f },
48 { 0.0389f, -0.0685f, 1.0296f, 0.f } };
49
50static const dt_colormatrix_t Bradford_LMS_to_XYZ = { { 0.9870f, -0.1471f, 0.1600f, 0.f },
51 { 0.4323f, 0.5184f, 0.0493f, 0.f },
52 { -0.0085f, 0.0400f, 0.9685f, 0.f } };
53
54static const dt_colormatrix_t XYZ_to_Bradford_LMS_transposed = { { 0.8951f, -0.7502f, 0.0389f, 0.f },
55 { 0.2664f, 1.7135f, -0.0685f, 0.f },
56 { -0.1614f, 0.0367f, 1.0296f, 0.f } };
57
58static const dt_colormatrix_t Bradford_LMS_to_XYZ_transposed = { { 0.9870f, 0.4323f, -0.0085f, 0.f },
59 { -0.1471f, 0.5184f, 0.0400f, 0.f },
60 { 0.1600f, 0.0493f, 0.9685f, 0.f } };
61
62static inline __attribute__((always_inline)) dt_aligned_pixel_simd_t
63convert_XYZ_to_bradford_LMS(const dt_aligned_pixel_simd_t XYZ)
64{
65 // Warning : needs XYZ normalized with Y - you need to downscale before
66 return dt_mat3x4_mul_vec4(XYZ,
67 dt_colormatrix_row_to_simd(XYZ_to_Bradford_LMS_transposed, 0),
68 dt_colormatrix_row_to_simd(XYZ_to_Bradford_LMS_transposed, 1),
69 dt_colormatrix_row_to_simd(XYZ_to_Bradford_LMS_transposed, 2));
70}
71
72static inline __attribute__((always_inline)) dt_aligned_pixel_simd_t
73convert_bradford_LMS_to_XYZ(const dt_aligned_pixel_simd_t LMS)
74{
75 // Warning : output XYZ normalized with Y - you need to upscale later
76 return dt_mat3x4_mul_vec4(LMS,
77 dt_colormatrix_row_to_simd(Bradford_LMS_to_XYZ_transposed, 0),
78 dt_colormatrix_row_to_simd(Bradford_LMS_to_XYZ_transposed, 1),
79 dt_colormatrix_row_to_simd(Bradford_LMS_to_XYZ_transposed, 2));
80}
81
82
83// modified LMS cone response for CAT16, from CIECAM16
84// reference : https://ntnuopen.ntnu.no/ntnu-xmlui/bitstream/handle/11250/2626317/CCIW-23.pdf?sequence=1
85// At any time, ensure XYZ_to_LMS is the exact matrice inverse of LMS_to_XYZ
86static const dt_colormatrix_t XYZ_to_CAT16_LMS = { { 0.401288f, 0.650173f, -0.051461f, 0.f },
87 { -0.250268f, 1.204414f, 0.045854f, 0.f },
88 { -0.002079f, 0.048952f, 0.953127f, 0.f } };
89
90static const dt_colormatrix_t CAT16_LMS_to_XYZ = { { 1.862068f, -1.011255f, 0.149187f, 0.f },
91 { 0.38752f , 0.621447f, -0.008974f, 0.f },
92 { -0.015841f, -0.034123f, 1.049964f, 0.f } };
93
94static const dt_colormatrix_t XYZ_to_CAT16_LMS_transposed = { { 0.401288f, -0.250268f, -0.002079f, 0.f },
95 { 0.650173f, 1.204414f, 0.048952f, 0.f },
96 { -0.051461f, 0.045854f, 0.953127f, 0.f } };
97
98static const dt_colormatrix_t CAT16_LMS_to_XYZ_transposed = { { 1.862068f, 0.38752f , -0.015841f, 0.f },
99 { -1.011255f, 0.621447f, -0.034123f, 0.f },
100 { 0.149187f, -0.008974f, 1.049964f, 0.f } };
101
102static inline __attribute__((always_inline)) dt_aligned_pixel_simd_t
103convert_XYZ_to_CAT16_LMS(const dt_aligned_pixel_simd_t XYZ)
104{
105 // Warning : needs XYZ normalized with Y - you need to downscale before
106 return dt_mat3x4_mul_vec4(XYZ,
107 dt_colormatrix_row_to_simd(XYZ_to_CAT16_LMS_transposed, 0),
108 dt_colormatrix_row_to_simd(XYZ_to_CAT16_LMS_transposed, 1),
109 dt_colormatrix_row_to_simd(XYZ_to_CAT16_LMS_transposed, 2));
110}
111
112static inline __attribute__((always_inline)) dt_aligned_pixel_simd_t
113convert_CAT16_LMS_to_XYZ(const dt_aligned_pixel_simd_t LMS)
114{
115 // Warning : output XYZ normalized with Y - you need to upscale later
116 return dt_mat3x4_mul_vec4(LMS,
117 dt_colormatrix_row_to_simd(CAT16_LMS_to_XYZ_transposed, 0),
118 dt_colormatrix_row_to_simd(CAT16_LMS_to_XYZ_transposed, 1),
119 dt_colormatrix_row_to_simd(CAT16_LMS_to_XYZ_transposed, 2));
120}
121
122
123static inline __attribute__((always_inline)) dt_aligned_pixel_simd_t
124convert_any_LMS_to_XYZ(const dt_aligned_pixel_simd_t LMS, const dt_adaptation_t kind)
125{
126 switch(kind)
127 {
136 default:
137 return LMS;
138 }
139}
140
141
142static inline __attribute__((always_inline)) dt_aligned_pixel_simd_t
143convert_any_XYZ_to_LMS(const dt_aligned_pixel_simd_t XYZ, const dt_adaptation_t kind)
144{
145 switch(kind)
146 {
155 default:
156 return XYZ;
157 }
158}
159
160
161__OMP_DECLARE_SIMD__(aligned(RGB, LMS:16) uniform(kind))
162static inline void convert_any_LMS_to_RGB(const dt_aligned_pixel_t LMS, dt_aligned_pixel_t RGB, dt_adaptation_t kind)
163{
164 // helper function switching internally to the proper conversion
165 dt_aligned_pixel_t XYZ = { 0.f };
166 dt_store_simd_aligned(XYZ, convert_any_LMS_to_XYZ(dt_load_simd_aligned(LMS), kind));
167
168 // Fixme : convert to RGB display space instead of sRGB but first the display profile should be global in dt,
169 // not confined to colorout where it gets created/destroyed all the time.
170 dt_XYZ_to_Rec709_D65(XYZ, RGB);
171
172 // Handle gamut clipping
173 float max_RGB = fmaxf(fmaxf(RGB[0], RGB[1]), RGB[2]);
174 for(int c = 0; c < 3; c++) RGB[c] = fmaxf(RGB[c] / max_RGB, 0.f);
175
176}
177
178
179/* Bradford adaptations pre-computed for D50 and D65 outputs */
180
181static inline __attribute__((always_inline)) dt_aligned_pixel_simd_t
182bradford_adapt_D65(const dt_aligned_pixel_simd_t lms_in,
183 const dt_aligned_pixel_simd_t origin_illuminant,
184 const float p, const int full)
185{
186 static const dt_aligned_pixel_simd_t D65 = { 0.941238f, 1.040633f, 1.088932f, 0.f };
187 dt_aligned_pixel_simd_t temp = lms_in / origin_illuminant;
188 if(full) temp[2] = (temp[2] > 0.f) ? powf(temp[2], p) : temp[2];
189 return D65 * temp;
190}
191
192
193static inline __attribute__((always_inline)) dt_aligned_pixel_simd_t
194bradford_adapt_D50(const dt_aligned_pixel_simd_t lms_in,
195 const dt_aligned_pixel_simd_t origin_illuminant,
196 const float p, const int full)
197{
198 static const dt_aligned_pixel_simd_t D50 = { 0.996078f, 1.020646f, 0.818155f, 0.f };
199 dt_aligned_pixel_simd_t temp = lms_in / origin_illuminant;
200 if(full) temp[2] = (temp[2] > 0.f) ? powf(temp[2], p) : temp[2];
201 return D50 * temp;
202}
203
204
205/* CAT16 adaptations pre-computed for D50 and D65 outputs */
206
207static inline __attribute__((always_inline)) dt_aligned_pixel_simd_t
208CAT16_adapt_D65(const dt_aligned_pixel_simd_t lms_in,
209 const dt_aligned_pixel_simd_t origin_illuminant,
210 const float D, const int full)
211{
212 static const dt_aligned_pixel_simd_t D65 = { 0.97553267f, 1.01647859f, 1.0848344f, 0.f };
213 return full ? lms_in * D65 / origin_illuminant
214 : lms_in * (dt_simd_set1(D) * D65 / origin_illuminant + dt_simd_set1(1.f - D));
215}
216
217
218static inline __attribute__((always_inline)) dt_aligned_pixel_simd_t
219CAT16_adapt_D50(const dt_aligned_pixel_simd_t lms_in,
220 const dt_aligned_pixel_simd_t origin_illuminant,
221 const float D, const int full)
222{
223 static const dt_aligned_pixel_simd_t D50 = { 0.994535f, 1.000997f, 0.833036f, 0.f };
224 return full ? lms_in * D50 / origin_illuminant
225 : lms_in * (dt_simd_set1(D) * D50 / origin_illuminant + dt_simd_set1(1.f - D));
226}
227
228/* XYZ adaptations pre-computed for D50 and D65 outputs */
229
230static inline __attribute__((always_inline)) dt_aligned_pixel_simd_t
231XYZ_adapt_D65(const dt_aligned_pixel_simd_t lms_in,
232 const dt_aligned_pixel_simd_t origin_illuminant)
233{
234 static const dt_aligned_pixel_simd_t D65 = { 0.9504285453771807f, 1.0f, 1.0889003707981277f, 0.f };
235 return lms_in * D65 / origin_illuminant;
236}
237
238static inline __attribute__((always_inline)) dt_aligned_pixel_simd_t
239XYZ_adapt_D50(const dt_aligned_pixel_simd_t lms_in,
240 const dt_aligned_pixel_simd_t origin_illuminant)
241{
242 static const dt_aligned_pixel_simd_t D50 = { 0.9642119944211994f, 1.0f, 0.8251882845188288f, 0.f };
243 return lms_in * D50 / origin_illuminant;
244}
245
246/* Pre-solved matrices to adjust white point for triplets in CIE XYZ 1931 2° observer */
247
249 = { { 9.89466254e-01f, -4.00304626e-02f, 4.40530317e-02f, 0.f },
250 { -5.40518733e-03f, 1.00666069e+00f, -1.75551955e-03f, 0.f },
251 { -4.03920992e-04f, 1.50768030e-02f, 1.30210211e+00f, 0.f } };
252
254 = { { 0.95547342f, -0.02309845f, 0.06325924f, 0.f },
255 { -0.02836971f, 1.00999540f, 0.02104144f, 0.f },
256 { 0.01231401f, -0.02050765f, 1.33036593f, 0.f } };
257
259 = { { 1.01085433e+00f, 4.07086103e-02f, -3.41445825e-02f, 0.f },
260 { 5.42814201e-03f, 9.93581926e-01f, 1.15592039e-03f, 0.f },
261 { 2.50722468e-04f, -1.14918759e-02f, 7.67964947e-01f, 0.f } };
262
264 = { { 1.04792979f, 0.02294687f, -0.05019227f, 0.f },
265 { 0.02962781f, 0.99043443f, -0.0170738f, 0.f },
266 { -0.00924304f, 0.01505519f, 0.75187428f, 0.f } };
267
269 = { { 9.89466254e-01f, -5.40518733e-03f, -4.03920992e-04f, 0.f },
270 { -4.00304626e-02f, 1.00666069e+00f, 1.50768030e-02f, 0.f },
271 { 4.40530317e-02f, -1.75551955e-03f, 1.30210211e+00f, 0.f } };
272
274 = { { 1.01085433e+00f, 5.42814201e-03f, 2.50722468e-04f, 0.f },
275 { 4.07086103e-02f, 9.93581926e-01f, -1.14918759e-02f, 0.f },
276 { -3.41445825e-02f, 1.15592039e-03f, 7.67964947e-01f, 0.f } };
277
278__OMP_DECLARE_SIMD__(aligned(XYZ_in, XYZ_out:16))
283
284__OMP_DECLARE_SIMD__(aligned(XYZ_in, XYZ_out:16))
289
290/* Helper function to directly chroma-adapt a pixel in CIE XYZ 1931 2° */
291
292static inline __attribute__((always_inline)) dt_aligned_pixel_simd_t
293_downscale_vector_simd(const dt_aligned_pixel_simd_t vector, const float scaling)
294{
295 const int valid = (scaling > NORM_MIN) && !isnan(scaling);
296 return vector / dt_simd_set1(valid ? (scaling + NORM_MIN) : NORM_MIN);
297}
298
299static inline __attribute__((always_inline)) dt_aligned_pixel_simd_t
300_upscale_vector_simd(const dt_aligned_pixel_simd_t vector, const float scaling)
301{
302 const int valid = (scaling > NORM_MIN) && !isnan(scaling);
303 return vector * dt_simd_set1(valid ? (scaling + NORM_MIN) : NORM_MIN);
304}
305
306static inline __attribute__((always_inline)) dt_aligned_pixel_simd_t
307chroma_adapt_pixel(const dt_aligned_pixel_simd_t in,
308 const dt_aligned_pixel_simd_t illuminant,
309 const dt_adaptation_t adaptation, const float p)
310{
311
312 /* WE START IN XYZ */
313 const float Y = in[1];
314
316 {
319 _upscale_vector_simd(bradford_adapt_D50(
320 _downscale_vector_simd(convert_XYZ_to_bradford_LMS(in), Y),
321 illuminant, p, TRUE),
322 Y));
325 _upscale_vector_simd(bradford_adapt_D50(
326 _downscale_vector_simd(convert_XYZ_to_bradford_LMS(in), Y),
327 illuminant, p, FALSE),
328 Y));
331 _upscale_vector_simd(CAT16_adapt_D50(
332 _downscale_vector_simd(convert_XYZ_to_CAT16_LMS(in), Y),
333 illuminant, 1.0f, TRUE),
334 Y));
336 return _upscale_vector_simd(XYZ_adapt_D50(_downscale_vector_simd(in, Y), illuminant), Y);
339 default:
340 return in;
341 }
342}
343
344/* Helper to get the D50 white point coordinates in LMS spaces */
346{
347 switch(adaptation)
348 {
351 {
352 dt_store_simd_aligned(D50, (dt_aligned_pixel_simd_t){ 0.996078f, 1.020646f, 0.818155f, 0.f });
353 break;
354 }
356 {
357 dt_store_simd_aligned(D50, (dt_aligned_pixel_simd_t){ 0.994535f, 1.000997f, 0.833036f, 0.f });
358 break;
359 }
361 {
362 dt_store_simd_aligned(D50, (dt_aligned_pixel_simd_t){ 0.9642119944211994f, 1.0f, 0.8251882845188288f, 0.f });
363 break;
364 }
367 default:
368 {
369 dt_store_simd_aligned(D50, (dt_aligned_pixel_simd_t){ 1.f, 1.f, 1.f, 0.f });
370 break;
371 }
372 }
373}
374
375#endif // DT_PIXEL_CHROMATIC_ADAPTATION_H
376
377// clang-format off
378// modelines: These editor modelines have been set for all relevant files by tools/update_modelines.py
379// vim: shiftwidth=2 expandtab tabstop=2 cindent
380// kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified;
381// clang-format on
#define TRUE
Definition ashift_lsd.c:162
#define FALSE
Definition ashift_lsd.c:158
typedef void((*dt_cache_allocate_t)(void *userdata, dt_cache_entry_t *entry))
static void XYZ_D50_to_D65(const dt_aligned_pixel_t XYZ_in, dt_aligned_pixel_t XYZ_out)
static const dt_aligned_pixel_simd_t const dt_adaptation_t adaptation
static const dt_colormatrix_t XYZ_D50_to_D65_Bradford
static const dt_colormatrix_t CAT16_LMS_to_XYZ
static void XYZ_D65_to_D50(const dt_aligned_pixel_t XYZ_in, dt_aligned_pixel_t XYZ_out)
static const dt_adaptation_t kind
static const dt_colormatrix_t XYZ_D65_to_D50_CAT16
static const dt_colormatrix_t XYZ_to_CAT16_LMS
static void convert_D50_to_LMS(const dt_adaptation_t adaptation, dt_aligned_pixel_t D50)
static const dt_colormatrix_t XYZ_to_CAT16_LMS_transposed
@ DT_ADAPTATION_LAST
@ DT_ADAPTATION_FULL_BRADFORD
@ DT_ADAPTATION_XYZ
@ DT_ADAPTATION_CAT16
@ DT_ADAPTATION_RGB
@ DT_ADAPTATION_LINEAR_BRADFORD
static const dt_colormatrix_t XYZ_to_Bradford_LMS_transposed
static const dt_colormatrix_t XYZ_D65_to_D50_CAT16_transposed
static const dt_colormatrix_t XYZ_D50_to_D65_CAT16_transposed
static const dt_aligned_pixel_simd_t const dt_adaptation_t const float p
static const dt_colormatrix_t XYZ_to_Bradford_LMS
static const dt_colormatrix_t Bradford_LMS_to_XYZ
static const float scaling
static const dt_colormatrix_t CAT16_LMS_to_XYZ_transposed
static const dt_colormatrix_t XYZ_D65_to_D50_Bradford
static const dt_colormatrix_t Bradford_LMS_to_XYZ_transposed
return vector dt_simd_set1(valid ?(scaling+NORM_MIN) :NORM_MIN)
static const dt_aligned_pixel_simd_t illuminant
static void CAT16_adapt_D50(float4 *lms_in, const float4 origin_illuminant, const float D, const int full)
Definition colorspace.h:722
static float4 convert_XYZ_to_bradford_LMS(const float4 XYZ)
Definition colorspace.h:657
static float4 convert_XYZ_to_CAT16_LMS(const float4 XYZ)
Definition colorspace.h:677
static void bradford_adapt_D50(float4 *lms_in, const float4 origin_illuminant, const float p, const int full)
Definition colorspace.h:697
static void XYZ_adapt_D50(float4 *lms_in, const float4 origin_illuminant)
Definition colorspace.h:737
static float4 convert_CAT16_LMS_to_XYZ(const float4 LMS)
Definition colorspace.h:687
static float4 convert_bradford_LMS_to_XYZ(const float4 LMS)
Definition colorspace.h:667
dt_aligned_pixel_t LMS
dt_apply_transposed_color_matrix(XYZ, xyz_to_srgb_matrix_transposed, sRGB)
static dt_aligned_pixel_t XYZ
dt_store_simd_aligned(out, dt_mat3x4_mul_vec4(vin, dt_colormatrix_row_to_simd(matrix, 0), dt_colormatrix_row_to_simd(matrix, 1), dt_colormatrix_row_to_simd(matrix, 2)))
static dt_aligned_pixel_t RGB
#define NORM_MIN
Definition math.h:37
float DT_ALIGNED_ARRAY dt_colormatrix_t[4][4]
Definition matrices.h:34
#define __OMP_DECLARE_SIMD__(...)
Definition openmp.h:100
DT_ALIGNED_PIXEL float dt_aligned_pixel_t[4]
Definition simd.h:53
float dt_aligned_pixel_simd_t __attribute__((vector_size(16), aligned(16)))
Apply one channel's tone curve to each of the three colour channels, or pass the channel through unto...
Definition simd.h:55