1 /*
2 This file is part of darktable,
3 Copyright (C) 2010-2021 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 #ifdef HAVE_CONFIG_H
20 #include "config.h"
21 #endif
22 #include "bauhaus/bauhaus.h"
23 #include "chart/common.h"
24 #include "develop/imageop_gui.h"
25 #include "dtgtk/drawingarea.h"
26 #include "common/chromatic_adaptation.h"
27 #include "common/colorspaces_inline_conversions.h"
28 #include "common/colorchecker.h"
29 #include "common/opencl.h"
30 #include "common/illuminants.h"
31 #include "common/imagebuf.h"
32 #include "common/iop_profile.h"
33 #include "develop/imageop_math.h"
34 #include "develop/openmp_maths.h"
35 #include "gui/accelerators.h"
36 #include "gui/color_picker_proxy.h"
37 #include "gui/gtk.h"
38 #include "gui/presets.h"
39 #include "iop/iop_api.h"
40 #include "gaussian_elimination.h"
41
42 #include <assert.h>
43 #include <gtk/gtk.h>
44 #include <inttypes.h>
45 #include <math.h>
46 #include <stdlib.h>
47 #include <string.h>
48 #include <time.h>
49
50 DT_MODULE_INTROSPECTION(3, dt_iop_channelmixer_rgb_params_t)
51
52 /** Note :
53 * we use finite-math-only and fast-math because divisions by zero are manually avoided in the code
54 * fp-contract=fast enables hardware-accelerated Fused Multiply-Add
55 * the rest is loop reorganization and vectorization optimization
56 **/
57 #if defined(__GNUC__)
58 #pragma GCC optimize ("unroll-loops", "tree-loop-if-convert", \
59 "tree-loop-distribution", "no-strict-aliasing", \
60 "loop-interchange", "tree-loop-im", \
61 "unswitch-loops", "tree-loop-ivcanon", "ira-loop-pressure", \
62 "split-ivs-in-unroller", "variable-expansion-in-unroller", \
63 "split-loops", "ivopts", "predictive-commoning",\
64 \
65 "finite-math-only", "fp-contract=fast", "fast-math", \
66 "tree-vectorize", "no-math-errno")
67 #endif
68
69
70 #define CHANNEL_SIZE 4
71 #define NORM_MIN 1e-6f
72 #define INVERSE_SQRT_3 0.5773502691896258f
73
74 typedef enum dt_iop_channelmixer_rgb_version_t
75 {
76 CHANNELMIXERRGB_V_1 = 0, // $DESCRIPTION: "version 1 (2020)"
77 CHANNELMIXERRGB_V_2 = 1, // $DESCRIPTION: "version 2 (2021)"
78 CHANNELMIXERRGB_V_3 = 2, // $DESCRIPTION: "version 3 (Apr 2021)"
79 } dt_iop_channelmixer_rgb_version_t;
80
81 typedef struct dt_iop_channelmixer_rgb_params_t
82 {
83 /* params of v1 and v2 */
84 float red[CHANNEL_SIZE]; // $MIN: -2.0 $MAX: 2.0
85 float green[CHANNEL_SIZE]; // $MIN: -2.0 $MAX: 2.0
86 float blue[CHANNEL_SIZE]; // $MIN: -2.0 $MAX: 2.0
87 float saturation[CHANNEL_SIZE]; // $MIN: -1.0 $MAX: 1.0
88 float lightness[CHANNEL_SIZE]; // $MIN: -1.0 $MAX: 1.0
89 float grey[CHANNEL_SIZE]; // $MIN: 0.0 $MAX: 1.0
90 gboolean normalize_R, normalize_G, normalize_B, normalize_sat, normalize_light, normalize_grey; // $DESCRIPTION: "normalize channels"
91 dt_illuminant_t illuminant; // $DEFAULT: DT_ILLUMINANT_D
92 dt_illuminant_fluo_t illum_fluo; // $DEFAULT: DT_ILLUMINANT_FLUO_F3 $DESCRIPTION: "F source"
93 dt_illuminant_led_t illum_led; // $DEFAULT: DT_ILLUMINANT_LED_B5 $DESCRIPTION: "LED source"
94 dt_adaptation_t adaptation; // $DEFAULT: DT_ADAPTATION_CAT16
95 float x, y; // $DEFAULT: 0.333
96 float temperature; // $MIN: 1667. $MAX: 25000. $DEFAULT: 5003.
97 float gamut; // $MIN: 0.0 $MAX: 4.0 $DEFAULT: 1.0 $DESCRIPTION: "gamut compression"
98 gboolean clip; // $DEFAULT: TRUE $DESCRIPTION: "clip negative RGB from gamut"
99
100 /* params of v3 */
101 dt_iop_channelmixer_rgb_version_t version; // $DEFAULT: CHANNELMIXERRGB_V_3 $DESCRIPTION: "saturation algorithm"
102
103 /* always add new params after this so we can import legacy params with memcpy on the common part of the struct */
104
105 } dt_iop_channelmixer_rgb_params_t;
106
107
108 typedef enum dt_solving_strategy_t
109 {
110 DT_SOLVE_OPTIMIZE_NONE = 0,
111 DT_SOLVE_OPTIMIZE_LOW_SAT = 1,
112 DT_SOLVE_OPTIMIZE_HIGH_SAT = 2,
113 DT_SOLVE_OPTIMIZE_SKIN = 3,
114 DT_SOLVE_OPTIMIZE_FOLIAGE = 4,
115 DT_SOLVE_OPTIMIZE_SKY = 5,
116 DT_SOLVE_OPTIMIZE_AVG_DELTA_E = 6,
117 DT_SOLVE_OPTIMIZE_MAX_DELTA_E = 7,
118 } dt_solving_strategy_t;
119
120
121 typedef struct dt_iop_channelmixer_rgb_gui_data_t
122 {
123 GtkNotebook *notebook;
124 GtkWidget *illuminant, *temperature, *adaptation, *gamut, *clip;
125 GtkWidget *illum_fluo, *illum_led, *illum_x, *illum_y, *approx_cct, *illum_color;
126 GtkWidget *scale_red_R, *scale_red_G, *scale_red_B;
127 GtkWidget *scale_green_R, *scale_green_G, *scale_green_B;
128 GtkWidget *scale_blue_R, *scale_blue_G, *scale_blue_B;
129 GtkWidget *scale_saturation_R, *scale_saturation_G, *scale_saturation_B, *saturation_version;
130 GtkWidget *scale_lightness_R, *scale_lightness_G, *scale_lightness_B;
131 GtkWidget *scale_grey_R, *scale_grey_G, *scale_grey_B;
132 GtkWidget *normalize_R, *normalize_G, *normalize_B, *normalize_sat, *normalize_light, *normalize_grey;
133 GtkWidget *color_picker;
134 float xy[2];
135 float XYZ[4];
136
137 point_t box[4]; // the current coordinates, possibly non rectangle, of the bounding box for the color checker
138 point_t ideal_box[4]; // the desired coordinates of the perfect rectangle bounding box for the color checker
139 point_t center_box; // the barycenter of both boxes
140 gboolean active_node[4]; // true if the cursor is close to a node (node = corner of the bounding box)
141 gboolean is_cursor_close; // do we have the cursor close to a node ?
142 gboolean drag_drop; // are we currently dragging and dropping a node ?
143 point_t click_start; // the coordinates where the drag and drop started
144 point_t click_end; // the coordinates where the drag and drop started
145 dt_color_checker_t *checker;
146 dt_solving_strategy_t optimization;
147 float safety_margin;
148
149 float homography[9]; // the perspective correction matrix
150 float inverse_homography[9]; // The inverse perspective correction matrix
151 gboolean run_profile; // order a profiling at next pipeline recompute
152 gboolean run_validation; // order a profile validation at next pipeline recompute
153 gboolean profile_ready; // notify that a profile is ready to be applied
154 gboolean checker_ready; // notify that a checker bounding box is ready to be used
155 dt_colormatrix_t mix;
156
157 GtkWidget *start_profiling;
158 gboolean is_profiling_started;
159 GtkWidget *collapsible;
160 GtkWidget *checkers_list, *optimize, *safety, *label_delta_E, *button_profile, *button_validate, *button_commit;
161
162 float *delta_E_in;
163
164 gchar *delta_E_label_text;
165 } dt_iop_channelmixer_rgb_gui_data_t;
166
167 typedef struct dt_iop_channelmixer_rbg_data_t
168 {
169 dt_colormatrix_t MIX;
170 float DT_ALIGNED_PIXEL saturation[CHANNEL_SIZE];
171 float DT_ALIGNED_PIXEL lightness[CHANNEL_SIZE];
172 float DT_ALIGNED_PIXEL grey[CHANNEL_SIZE];
173 dt_aligned_pixel_t illuminant; // XYZ coordinates of illuminant
174 float p, gamut;
175 int apply_grey;
176 int clip;
177 dt_adaptation_t adaptation;
178 dt_illuminant_t illuminant_type;
179 dt_iop_channelmixer_rgb_version_t version;
180 } dt_iop_channelmixer_rbg_data_t;
181
182 typedef struct dt_iop_channelmixer_rgb_global_data_t
183 {
184 int kernel_channelmixer_rgb_xyz;
185 int kernel_channelmixer_rgb_cat16;
186 int kernel_channelmixer_rgb_bradford_full;
187 int kernel_channelmixer_rgb_bradford_linear;
188 int kernel_channelmixer_rgb_rgb;
189 } dt_iop_channelmixer_rgb_global_data_t;
190
191
192 const char *
name()193 name()
194 {
195 return _("color calibration");
196 }
197
aliases()198 const char *aliases()
199 {
200 return _("channel mixer|white balance|monochrome");
201 }
202
description(struct dt_iop_module_t * self)203 const char *description(struct dt_iop_module_t *self)
204 {
205 return dt_iop_set_description(self, _("perform color space corrections\n"
206 "such as white balance, channels mixing\n"
207 "and conversions to monochrome emulating film"),
208 _("corrective or creative"),
209 _("linear, RGB, scene-referred"),
210 _("linear, RGB or XYZ"),
211 _("linear, RGB, scene-referred"));
212 }
213
flags()214 int flags()
215 {
216 return IOP_FLAGS_INCLUDE_IN_STYLES | IOP_FLAGS_SUPPORTS_BLENDING | IOP_FLAGS_ALLOW_TILING;
217 }
218
default_group()219 int default_group()
220 {
221 return IOP_GROUP_COLOR;
222 }
223
default_colorspace(dt_iop_module_t * self,dt_dev_pixelpipe_t * pipe,dt_dev_pixelpipe_iop_t * piece)224 int default_colorspace(dt_iop_module_t *self, dt_dev_pixelpipe_t *pipe, dt_dev_pixelpipe_iop_t *piece)
225 {
226 return iop_cs_rgb;
227 }
228
legacy_params(dt_iop_module_t * self,const void * const old_params,const int old_version,void * new_params,const int new_version)229 int legacy_params(dt_iop_module_t *self, const void *const old_params, const int old_version, void *new_params,
230 const int new_version)
231 {
232 if(old_version == 1 && new_version == 3)
233 {
234 // V1 and V2 use the same param structure but the normalize_grey param had no effect since commit_params
235 // forced normalization no matter what. So we re-import the params and force the param to TRUE to keep edits.
236 memcpy(new_params, old_params, sizeof(dt_iop_channelmixer_rgb_params_t));
237 dt_iop_channelmixer_rgb_params_t *n = (dt_iop_channelmixer_rgb_params_t *)new_params;
238 n->normalize_grey = TRUE;
239
240 // V2 and V3 use the same param structure but these :
241
242 // swap the saturation parameters for R and B to put them in natural order
243 const float R = n->saturation[0];
244 const float B = n->saturation[2];
245 n->saturation[0] = B;
246 n->saturation[2] = R;
247
248 // say that these params were created with legacy code
249 n->version = CHANNELMIXERRGB_V_1;
250
251 return 0;
252 }
253 if(old_version == 2 && new_version == 3)
254 {
255 typedef struct dt_iop_channelmixer_rgb_params_v2_t
256 {
257 float red[CHANNEL_SIZE]; // $MIN: -2.0 $MAX: 2.0
258 float green[CHANNEL_SIZE]; // $MIN: -2.0 $MAX: 2.0
259 float blue[CHANNEL_SIZE]; // $MIN: -2.0 $MAX: 2.0
260 float saturation[CHANNEL_SIZE]; // $MIN: -1.0 $MAX: 1.0
261 float lightness[CHANNEL_SIZE]; // $MIN: -1.0 $MAX: 1.0
262 float grey[CHANNEL_SIZE]; // $MIN: 0.0 $MAX: 1.0
263 gboolean normalize_R, normalize_G, normalize_B, normalize_sat, normalize_light, normalize_grey; // $DESCRIPTION: "normalize channels"
264 dt_illuminant_t illuminant; // $DEFAULT: DT_ILLUMINANT_D
265 dt_illuminant_fluo_t illum_fluo; // $DEFAULT: DT_ILLUMINANT_FLUO_F3 $DESCRIPTION: "F source"
266 dt_illuminant_led_t illum_led; // $DEFAULT: DT_ILLUMINANT_LED_B5 $DESCRIPTION: "LED source"
267 dt_adaptation_t adaptation; // $DEFAULT: DT_ADAPTATION_LINEAR_BRADFORD
268 float x, y; // $DEFAULT: 0.333
269 float temperature; // $MIN: 1667. $MAX: 25000. $DEFAULT: 5003.
270 float gamut; // $MIN: 0.0 $MAX: 4.0 $DEFAULT: 1.0 $DESCRIPTION: "gamut compression"
271 gboolean clip; // $DEFAULT: TRUE $DESCRIPTION: "clip negative RGB from gamut"
272 } dt_iop_channelmixer_rgb_params_v2_t;
273
274 memcpy(new_params, old_params, sizeof(dt_iop_channelmixer_rgb_params_v2_t));
275 dt_iop_channelmixer_rgb_params_t *n = (dt_iop_channelmixer_rgb_params_t *)new_params;
276
277 // swap the saturation parameters for R and B to put them in natural order
278 const float R = n->saturation[0];
279 const float B = n->saturation[2];
280 n->saturation[0] = B;
281 n->saturation[2] = R;
282
283 // say that these params were created with legacy code
284 n->version = CHANNELMIXERRGB_V_1;
285
286 return 0;
287 }
288 return 1;
289 }
290
init_presets(dt_iop_module_so_t * self)291 void init_presets(dt_iop_module_so_t *self)
292 {
293 dt_iop_channelmixer_rgb_params_t p;
294 memset(&p, 0, sizeof(p));
295
296 p.version = CHANNELMIXERRGB_V_3;
297
298 // bypass adaptation
299 p.illuminant = DT_ILLUMINANT_PIPE;
300 p.adaptation = DT_ADAPTATION_XYZ;
301
302 // set everything to no-op
303 p.gamut = 0.f;
304 p.clip = FALSE;
305 p.illum_fluo = DT_ILLUMINANT_FLUO_F3;
306 p.illum_led = DT_ILLUMINANT_LED_B5;
307 p.temperature = 5003.f;
308 illuminant_to_xy(DT_ILLUMINANT_PIPE, NULL, NULL, &p.x, &p.y, p.temperature, DT_ILLUMINANT_FLUO_LAST, DT_ILLUMINANT_LED_LAST);
309
310 p.red[0] = 1.f;
311 p.red[1] = 0.f;
312 p.red[2] = 0.f;
313 p.green[0] = 0.f;
314 p.green[1] = 1.f;
315 p.green[2] = 0.f;
316 p.blue[0] = 0.f;
317 p.blue[1] = 0.f;
318 p.blue[2] = 1.f;
319
320 p.saturation[0] = 0.f;
321 p.saturation[1] = 0.f;
322 p.saturation[2] = 0.f;
323 p.lightness[0] = 0.f;
324 p.lightness[1] = 0.f;
325 p.lightness[2] = 0.f;
326 p.grey[0] = 0.f;
327 p.grey[1] = 0.f;
328 p.grey[2] = 0.f;
329
330 p.normalize_R = FALSE;
331 p.normalize_G = FALSE;
332 p.normalize_B = FALSE;
333 p.normalize_sat = FALSE;
334 p.normalize_light = FALSE;
335 p.normalize_grey = TRUE;
336
337 // Create B&W presets
338 p.clip = TRUE;
339 p.grey[0] = 0.f;
340 p.grey[1] = 1.f;
341 p.grey[2] = 0.f;
342
343 dt_gui_presets_add_generic(_("B&W : luminance-based"), self->op,
344 self->version(), &p, sizeof(p), 1, DEVELOP_BLEND_CS_RGB_SCENE);
345
346 // film emulations
347
348 /* These emulations are built using spectral sensitivies provided by film manufacturers for tungsten light,
349 * corrected in spectral domain for D50 illuminant, and integrated in spectral space against CIE 2° 1931 XYZ
350 * color matching functions in the Python lib Colour, with the following code :
351 *
352 import colour
353 import numpy as np
354
355 XYZ = np.zeros((3))
356
357 for l in range(360, 830):
358 XYZ += film_CMF[l] * colour.colorimetry.STANDARD_OBSERVERS_CMFS['CIE 1931 2 Degree Standard Observer'][l] / colour.ILLUMINANTS_SDS['A'][l] * colour.ILLUMINANTS_SDS['D50'][l]
359
360 XYZ / np.sum(XYZ)
361 *
362 * The film CMF is visually approximated from the graph. It is still more accurate than bullshit factors
363 * in legacy channel mixer that don't even say in which RGB space they are supposed to be applied.
364 */
365
366 // ILFORD HP5 +
367 // https://www.ilfordphoto.com/amfile/file/download/file/1903/product/695/
368 p.grey[0] = 0.25304098f;
369 p.grey[1] = 0.25958747f;
370 p.grey[2] = 0.48737156f;
371
372 dt_gui_presets_add_generic(_("B&W : ILFORD HP5+"), self->op,
373 self->version(), &p, sizeof(p), 1, DEVELOP_BLEND_CS_RGB_SCENE);
374
375 // ILFORD Delta 100
376 // https://www.ilfordphoto.com/amfile/file/download/file/3/product/681/
377 p.grey[0] = 0.24552374f;
378 p.grey[1] = 0.25366007f;
379 p.grey[2] = 0.50081619f;
380
381 dt_gui_presets_add_generic(_("B&W : ILFORD DELTA 100"), self->op,
382 self->version(), &p, sizeof(p), 1, DEVELOP_BLEND_CS_RGB_SCENE);
383
384 // ILFORD Delta 400 and 3200 - they have the same curve
385 // https://www.ilfordphoto.com/amfile/file/download/file/1915/product/685/
386 // https://www.ilfordphoto.com/amfile/file/download/file/1913/product/683/
387 p.grey[0] = 0.24376712f;
388 p.grey[1] = 0.23613559f;
389 p.grey[2] = 0.52009729f;
390
391 dt_gui_presets_add_generic(_("B&W : ILFORD DELTA 400 - 3200"), self->op,
392 self->version(), &p, sizeof(p), 1, DEVELOP_BLEND_CS_RGB_SCENE);
393
394 // ILFORD FP4+
395 // https://www.ilfordphoto.com/amfile/file/download/file/1919/product/690/
396 p.grey[0] = 0.24149085f;
397 p.grey[1] = 0.22149272f;
398 p.grey[2] = 0.53701643f;
399
400 dt_gui_presets_add_generic(_("B&W : ILFORD FP4+"), self->op,
401 self->version(), &p, sizeof(p), 1, DEVELOP_BLEND_CS_RGB_SCENE);
402
403 // Fuji Acros 100
404 // https://dacnard.wordpress.com/2013/02/15/the-real-shades-of-gray-bw-film-is-a-matter-of-heart-pt-1/
405 p.grey[0] = 0.333f;
406 p.grey[1] = 0.313f;
407 p.grey[2] = 0.353f;
408
409 dt_gui_presets_add_generic(_("B&W : Fuji Acros 100"), self->op,
410 self->version(), &p, sizeof(p), 1, DEVELOP_BLEND_CS_RGB_SCENE);
411
412 // Kodak ?
413 // can't find spectral sensitivity curves and the illuminant under which they are produced,
414 // so ¯\_(ツ)_/¯
415
416 // basic channel-mixer
417 p.adaptation = DT_ADAPTATION_RGB; // bypass adaptation
418 p.grey[0] = 0.f;
419 p.grey[1] = 0.f;
420 p.grey[2] = 0.f;
421 p.normalize_R = TRUE;
422 p.normalize_G = TRUE;
423 p.normalize_B = TRUE;
424 p.normalize_grey = FALSE;
425 p.clip = FALSE;
426 dt_gui_presets_add_generic(_("basic channel mixer"), self->op,
427 self->version(), &p, sizeof(p), 1, DEVELOP_BLEND_CS_RGB_SCENE);
428
429 // swap G-B
430 p.red[0] = 1.f;
431 p.red[1] = 0.f;
432 p.red[2] = 0.f;
433 p.green[0] = 0.f;
434 p.green[1] = 0.f;
435 p.green[2] = 1.f;
436 p.blue[0] = 0.f;
437 p.blue[1] = 1.f;
438 p.blue[2] = 0.f;
439 dt_gui_presets_add_generic(_("swap G and B"), self->op,
440 self->version(), &p, sizeof(p), 1, DEVELOP_BLEND_CS_RGB_SCENE);
441
442 // swap G-R
443 p.red[0] = 0.f;
444 p.red[1] = 1.f;
445 p.red[2] = 0.f;
446 p.green[0] = 1.f;
447 p.green[1] = 0.f;
448 p.green[2] = 0.f;
449 p.blue[0] = 0.f;
450 p.blue[1] = 0.f;
451 p.blue[2] = 1.f;
452 dt_gui_presets_add_generic(_("swap G and R"), self->op,
453 self->version(), &p, sizeof(p), 1, DEVELOP_BLEND_CS_RGB_SCENE);
454
455 // swap R-B
456 p.red[0] = 0.f;
457 p.red[1] = 0.f;
458 p.red[2] = 1.f;
459 p.green[0] = 0.f;
460 p.green[1] = 1.f;
461 p.green[2] = 0.f;
462 p.blue[0] = 1.f;
463 p.blue[1] = 0.f;
464 p.blue[2] = 0.f;
465 dt_gui_presets_add_generic(_("swap R and B"), self->op,
466 self->version(), &p, sizeof(p), 1, DEVELOP_BLEND_CS_RGB_SCENE);
467 }
468
469
get_white_balance_coeff(struct dt_iop_module_t * self,dt_aligned_pixel_t custom_wb)470 static int get_white_balance_coeff(struct dt_iop_module_t *self, dt_aligned_pixel_t custom_wb)
471 {
472 // Init output with a no-op
473 for(size_t k = 0; k < 4; k++) custom_wb[k] = 1.f;
474
475 if(!dt_image_is_matrix_correction_supported(&self->dev->image_storage)) return 1;
476
477 // First, get the D65-ish coeffs from the input matrix
478 // keep this in synch with calculate_bogus_daylight_wb from temperature.c !
479 // predicts the bogus D65 that temperature.c will compute for the camera input matrix
480 double bwb[4];
481
482 if(dt_colorspaces_conversion_matrices_rgb(self->dev->image_storage.camera_makermodel, NULL, NULL, self->dev->image_storage.d65_color_matrix, bwb))
483 {
484 // normalize green:
485 bwb[0] /= bwb[1];
486 bwb[2] /= bwb[1];
487 bwb[3] /= bwb[1];
488 bwb[1] = 1.0;
489 }
490 else
491 {
492 return 1;
493 }
494
495 // Second, if the temperature module is not using these, for example because they are wrong
496 // and user made a correct preset, find the WB adaptation ratio
497 if(self->dev->proxy.wb_coeffs[0] != 0.f)
498 {
499 for(size_t k = 0; k < 4; k++) custom_wb[k] = bwb[k] / self->dev->proxy.wb_coeffs[k];
500 }
501
502 return 0;
503 }
504
505
506 #ifdef _OPENMP
507 #pragma omp declare simd aligned(vector:16)
508 #endif
euclidean_norm(const dt_aligned_pixel_t vector)509 static inline float euclidean_norm(const dt_aligned_pixel_t vector)
510 {
511 return fmaxf(sqrtf(sqf(vector[0]) + sqf(vector[1]) + sqf(vector[2])), NORM_MIN);
512 }
513
514
515 #ifdef _OPENMP
516 #pragma omp declare simd aligned(vector:16)
517 #endif
downscale_vector(dt_aligned_pixel_t vector,const float scaling)518 static inline void downscale_vector(dt_aligned_pixel_t vector, const float scaling)
519 {
520 // check zero or NaN
521 const int valid = (scaling > NORM_MIN) && !isnan(scaling);
522 for(size_t c = 0; c < 3; c++) vector[c] = (valid) ? vector[c] / (scaling + NORM_MIN) : vector[c] / NORM_MIN;
523 }
524
525
526 #ifdef _OPENMP
527 #pragma omp declare simd aligned(vector:16)
528 #endif
upscale_vector(dt_aligned_pixel_t vector,const float scaling)529 static inline void upscale_vector(dt_aligned_pixel_t vector, const float scaling)
530 {
531 const int valid = (scaling > NORM_MIN) && !isnan(scaling);
532 for(size_t c = 0; c < 3; c++) vector[c] = (valid) ? vector[c] * (scaling + NORM_MIN) : vector[c] * NORM_MIN;
533 }
534
535
536 #ifdef _OPENMP
537 #pragma omp declare simd aligned(input, output:16) uniform(compression, clip)
538 #endif
gamut_mapping(const dt_aligned_pixel_t input,const float compression,const int clip,dt_aligned_pixel_t output)539 static inline void gamut_mapping(const dt_aligned_pixel_t input, const float compression, const int clip,
540 dt_aligned_pixel_t output)
541 {
542 // Get the sum XYZ
543 const float sum = input[0] + input[1] + input[2];
544 const float Y = input[1];
545
546 if(sum > 0.f && Y > 0.f)
547 {
548 // Convert to xyY
549 dt_aligned_pixel_t xyY = { input[0] / sum, input[1] / sum , Y, 0.0f };
550
551 // Convert to uvY
552 dt_aligned_pixel_t uvY;
553 dt_xyY_to_uvY(xyY, uvY);
554
555 // Get the chromaticity difference with white point uv
556 const float D50[2] DT_ALIGNED_PIXEL = { 0.20915914598542354f, 0.488075320769787f };
557 const float delta[2] DT_ALIGNED_PIXEL = { D50[0] - uvY[0], D50[1] - uvY[1] };
558 const float Delta = Y * (sqf(delta[0]) + sqf(delta[1]));
559
560 // Compress chromaticity (move toward white point)
561 const float correction = (compression == 0.0f) ? 0.f : powf(Delta, compression);
562 for(size_t c = 0; c < 2; c++)
563 {
564 // Ensure the correction does not bring our uyY vector the other side of D50
565 // that would switch to the opposite color, so we clip at D50
566 const float tmp = DT_FMA(correction, delta[c], uvY[c]); // correction * delta[c] + uvY[c]
567 uvY[c] = (uvY[c] > D50[c]) ? fmaxf(tmp, D50[c])
568 : fminf(tmp, D50[c]);
569 }
570
571 // Convert back to xyY
572 dt_uvY_to_xyY(uvY, xyY);
573
574 // Clip upon request
575 if(clip) for(size_t c = 0; c < 2; c++) xyY[c] = fmaxf(xyY[c], 0.0f);
576
577 // Check sanity of y
578 // since we later divide by y, it can't be zero
579 xyY[1] = fmaxf(xyY[1], NORM_MIN);
580
581 // Check sanity of x and y :
582 // since Z = Y (1 - x - y) / y, if x + y >= 1, Z will be negative
583 const float scale = xyY[0] + xyY[1];
584 const int sanitize = (scale >= 1.f);
585 for(size_t c = 0; c < 2; c++) xyY[c] = (sanitize) ? xyY[c] / scale : xyY[c];
586
587 // Convert back to XYZ
588 dt_xyY_to_XYZ(xyY, output);
589 }
590 else
591 {
592 // sum of channels == 0, and/or Y == 0 so we have black
593 for(size_t c = 0; c < 3; c++) output[c] = 0.f;
594 }
595 }
596
597
598 #ifdef _OPENMP
599 #pragma omp declare simd aligned(input, output, saturation, lightness:16) uniform(saturation, lightness)
600 #endif
luma_chroma(const dt_aligned_pixel_t input,const dt_aligned_pixel_t saturation,const dt_aligned_pixel_t lightness,dt_aligned_pixel_t output,const dt_iop_channelmixer_rgb_version_t version)601 static inline void luma_chroma(const dt_aligned_pixel_t input, const dt_aligned_pixel_t saturation,
602 const dt_aligned_pixel_t lightness, dt_aligned_pixel_t output,
603 const dt_iop_channelmixer_rgb_version_t version)
604 {
605 // Compute euclidean norm
606 float norm = euclidean_norm(input);
607 const float avg = fmaxf((input[0] + input[1] + input[2]) / 3.0f, NORM_MIN);
608
609 if(norm > 0.f && avg > 0.f)
610 {
611 // Compute flat lightness adjustment
612 const float mix = scalar_product(input, lightness);
613
614 // Compensate the norm to get color ratios (R, G, B) = (1, 1, 1) for grey (colorless) pixels.
615 if(version == CHANNELMIXERRGB_V_3) norm *= INVERSE_SQRT_3;
616
617 // Ratios
618 for(size_t c = 0; c < 3; c++) output[c] = input[c] / norm;
619
620 // Compute ratios and a flat colorfulness adjustment for the whole pixel
621 float coeff_ratio = 0.f;
622
623 if(version == CHANNELMIXERRGB_V_1)
624 {
625 for(size_t c = 0; c < 3; c++)
626 coeff_ratio += sqf(1.0f - output[c]) * saturation[c];
627 }
628 else
629 coeff_ratio = scalar_product(output, saturation) / 3.f;
630
631 // Adjust the RGB ratios with the pixel correction
632 for(size_t c = 0; c < 3; c++)
633 {
634 // if the ratio was already invalid (negative), we accept the result to be invalid too
635 // otherwise bright saturated blues end up solid black
636 const float min_ratio = (output[c] < 0.0f) ? output[c] : 0.0f;
637 const float output_inverse = 1.0f - output[c];
638 output[c] = fmaxf(DT_FMA(output_inverse, coeff_ratio, output[c]),
639 min_ratio); // output_inverse * coeff_ratio + output
640 }
641
642 // The above interpolation between original pixel ratios and (1, 1, 1) might change the norm of the
643 // ratios. Compensate for that.
644 if(version == CHANNELMIXERRGB_V_3) norm /= euclidean_norm(output) * INVERSE_SQRT_3;
645
646 // Apply colorfulness adjustment channel-wise and repack with lightness to get LMS back
647 norm *= fmaxf(1.f + mix / avg, 0.f);
648 for(size_t c = 0; c < 3; c++) output[c] *= norm;
649 }
650 else
651 {
652 // we have black, 0 stays 0, no luminance = no color
653 for(size_t c = 0; c < 3; c++) output[c] = input[c];
654 }
655 }
656
657 #ifdef _OPENMP
658 #pragma omp declare simd aligned(in, out, XYZ_to_RGB, RGB_to_XYZ, MIX : 64) aligned(illuminant, saturation, lightness, grey:16)
659 #endif
loop_switch(const float * const restrict in,float * const restrict out,const size_t width,const size_t height,const size_t ch,const dt_colormatrix_t XYZ_to_RGB,const dt_colormatrix_t RGB_to_XYZ,const dt_colormatrix_t MIX,const dt_aligned_pixel_t illuminant,const dt_aligned_pixel_t saturation,const dt_aligned_pixel_t lightness,const dt_aligned_pixel_t grey,const float p,const float gamut,const int clip,const int apply_grey,const dt_adaptation_t kind,const dt_iop_channelmixer_rgb_version_t version)660 static inline void loop_switch(const float *const restrict in, float *const restrict out,
661 const size_t width, const size_t height, const size_t ch, const dt_colormatrix_t XYZ_to_RGB,
662 const dt_colormatrix_t RGB_to_XYZ, const dt_colormatrix_t MIX,
663 const dt_aligned_pixel_t illuminant, const dt_aligned_pixel_t saturation,
664 const dt_aligned_pixel_t lightness, const dt_aligned_pixel_t grey,
665 const float p, const float gamut, const int clip, const int apply_grey,
666 const dt_adaptation_t kind,
667 const dt_iop_channelmixer_rgb_version_t version)
668 {
669 #ifdef _OPENMP
670 #pragma omp parallel for default(none) \
671 dt_omp_firstprivate(width, height, ch, in, out, XYZ_to_RGB, RGB_to_XYZ, MIX, illuminant, saturation, lightness, grey, p, gamut, clip, apply_grey, kind, version) \
672 schedule(simd:static)
673 #endif
674 for(size_t k = 0; k < height * width * 4; k += 4)
675 {
676 // intermediate temp buffers
677 dt_aligned_pixel_t temp_one;
678 dt_aligned_pixel_t temp_two;
679
680 for(size_t c = 0; c < DT_PIXEL_SIMD_CHANNELS; c++)
681 temp_two[c] = (clip) ? fmaxf(in[k + c], 0.0f) : in[k + c];
682
683 /* WE START IN PIPELINE RGB */
684
685 switch(kind)
686 {
687 case DT_ADAPTATION_FULL_BRADFORD:
688 {
689 // Convert from RGB to XYZ
690 dot_product(temp_two, RGB_to_XYZ, temp_one);
691 const float Y = temp_one[1];
692
693 // Convert to LMS
694 convert_XYZ_to_bradford_LMS(temp_one, temp_two);
695 {
696 // Do white balance
697 downscale_vector(temp_two, Y);
698 bradford_adapt_D50(temp_two, illuminant, p, TRUE, temp_one);
699 upscale_vector(temp_one, Y);
700
701 // Compute the 3D mix - this is a rotation + homothety of the vector base
702 dot_product(temp_one, MIX, temp_two);
703 }
704 convert_bradford_LMS_to_XYZ(temp_two, temp_one);
705
706 break;
707 }
708 case DT_ADAPTATION_LINEAR_BRADFORD:
709 {
710 // Convert from RGB to XYZ
711 dot_product(temp_two, RGB_to_XYZ, temp_one);
712 const float Y = temp_one[1];
713
714 // Convert to LMS
715 convert_XYZ_to_bradford_LMS(temp_one, temp_two);
716 {
717 // Do white balance
718 downscale_vector(temp_two, Y);
719 bradford_adapt_D50(temp_two, illuminant, p, FALSE, temp_one);
720 upscale_vector(temp_one, Y);
721
722 // Compute the 3D mix - this is a rotation + homothety of the vector base
723 dot_product(temp_one, MIX, temp_two);
724 }
725 convert_bradford_LMS_to_XYZ(temp_two, temp_one);
726
727 break;
728 }
729 case DT_ADAPTATION_CAT16:
730 {
731 // Convert from RGB to XYZ
732 dot_product(temp_two, RGB_to_XYZ, temp_one);
733 const float Y = temp_one[1];
734
735 // Convert to LMS
736 convert_XYZ_to_CAT16_LMS(temp_one, temp_two);
737 {
738 // Do white balance
739 downscale_vector(temp_two, Y);
740 CAT16_adapt_D50(temp_two, illuminant, 1.0f, TRUE, temp_one); // force full-adaptation
741 upscale_vector(temp_one, Y);
742
743 // Compute the 3D mix - this is a rotation + homothety of the vector base
744 dot_product(temp_one, MIX, temp_two);
745 }
746 convert_CAT16_LMS_to_XYZ(temp_two, temp_one);
747
748 break;
749 }
750 case DT_ADAPTATION_XYZ:
751 {
752 // Convert from RGB to XYZ
753 dot_product(temp_two, RGB_to_XYZ, temp_one);
754 const float Y = temp_one[1];
755
756 // Do white balance in XYZ
757 downscale_vector(temp_one, Y);
758 XYZ_adapt_D50(temp_one, illuminant, temp_two);
759 upscale_vector(temp_two, Y);
760
761 // Compute the 3D mix in XYZ - this is a rotation + homothety of the vector base
762 dot_product(temp_two, MIX, temp_one);
763
764 break;
765 }
766 case DT_ADAPTATION_RGB:
767 case DT_ADAPTATION_LAST:
768 {
769 // No white balance.
770
771 // Compute the 3D mix in RGB - this is a rotation + homothety of the vector base
772 dot_product(temp_two, MIX, temp_one);
773
774 // Convert from RGB to XYZ
775 dot_product(temp_one, RGB_to_XYZ, temp_two);
776
777 for(size_t c = 0; c < DT_PIXEL_SIMD_CHANNELS; ++c) temp_one[c] = temp_two[c];
778 break;
779 }
780 default:
781 {
782 for(size_t c = 0; c < DT_PIXEL_SIMD_CHANNELS; ++c) temp_one[c] = temp_two[c];
783 break;
784 }
785 }
786
787 /* FROM HERE WE ARE MANDATORILY IN XYZ - DATA IS IN temp_one */
788
789 // Gamut mapping happens in XYZ space no matter what
790 gamut_mapping(temp_one, gamut, clip, temp_two);
791
792 // convert to LMS, XYZ or pipeline RGB
793 switch(kind)
794 {
795 case DT_ADAPTATION_FULL_BRADFORD:
796 case DT_ADAPTATION_LINEAR_BRADFORD:
797 case DT_ADAPTATION_CAT16:
798 case DT_ADAPTATION_XYZ:
799 {
800 convert_any_XYZ_to_LMS(temp_two, temp_one, kind);
801 break;
802 }
803 case DT_ADAPTATION_RGB:
804 case DT_ADAPTATION_LAST:
805 {
806 // Convert from XYZ to RGB
807 dot_product(temp_two, XYZ_to_RGB, temp_one);
808 break;
809 }
810 }
811
812 /* FROM HERE WE ARE IN LMS, XYZ OR PIPELINE RGB depending on user param - DATA IS IN temp_one */
813
814 // Clip in LMS
815 if(clip) for(size_t c = 0; c < DT_PIXEL_SIMD_CHANNELS; c++) temp_one[c] = fmaxf(temp_one[c], 0.0f);
816
817 // Apply lightness / saturation adjustment
818 luma_chroma(temp_one, saturation, lightness, temp_two, version);
819
820 // Clip in LMS
821 if(clip) for(size_t c = 0; c < DT_PIXEL_SIMD_CHANNELS; c++) temp_two[c] = fmaxf(temp_two[c], 0.0f);
822
823 // Save
824 if(apply_grey)
825 {
826 // Turn LMS, XYZ or pipeline RGB into monochrome
827 const float grey_mix = fmaxf(scalar_product(temp_two, grey), 0.0f);
828
829 out[k] = out[k + 1] = out[k + 2] = grey_mix;
830 out[k + 3] = in[k + 3]; // alpha mask
831 }
832 else
833 {
834 // Convert back to XYZ
835 switch(kind)
836 {
837 case DT_ADAPTATION_FULL_BRADFORD:
838 case DT_ADAPTATION_LINEAR_BRADFORD:
839 case DT_ADAPTATION_CAT16:
840 case DT_ADAPTATION_XYZ:
841 {
842 convert_any_LMS_to_XYZ(temp_two, temp_one, kind);
843 break;
844 }
845 case DT_ADAPTATION_RGB:
846 case DT_ADAPTATION_LAST:
847 {
848 // Convert from RBG to XYZ
849 dot_product(temp_two, RGB_to_XYZ, temp_one);
850 break;
851 }
852 }
853
854 /* FROM HERE WE ARE MANDATORILY IN XYZ - DATA IS IN temp_one */
855
856 // Clip in XYZ
857 if(clip) for(size_t c = 0; c < DT_PIXEL_SIMD_CHANNELS; c++) temp_one[c] = fmaxf(temp_one[c], 0.0f);
858
859 // Convert back to RGB
860 dot_product(temp_one, XYZ_to_RGB, temp_two);
861
862 if(clip)
863 for(size_t c = 0; c < DT_PIXEL_SIMD_CHANNELS; c++) out[k + c] = fmaxf(temp_two[c], 0.0f);
864 else
865 for(size_t c = 0; c < DT_PIXEL_SIMD_CHANNELS; c++) out[k + c] = temp_two[c];
866
867 out[k + 3] = in[k + 3]; // alpha mask
868 }
869 }
870 }
871
872 // util to shift pixel index without headache
873 #define SHF(ii, jj, c) ((i + ii) * width + j + jj) * ch + c
874 #define OFF 4
875
auto_detect_WB(const float * const restrict in,dt_illuminant_t illuminant,const size_t width,const size_t height,const size_t ch,const dt_colormatrix_t RGB_to_XYZ,dt_aligned_pixel_t xyz)876 static inline void auto_detect_WB(const float *const restrict in, dt_illuminant_t illuminant,
877 const size_t width, const size_t height, const size_t ch,
878 const dt_colormatrix_t RGB_to_XYZ, dt_aligned_pixel_t xyz)
879 {
880 /**
881 * Detect the chromaticity of the illuminant based on the grey edges hypothesis.
882 * So we compute a laplacian filter and get the weighted average of its chromaticities
883 *
884 * Inspired by :
885 * A Fast White Balance Algorithm Based on Pixel Greyness, Ba Thai·Guang Deng·Robert Ross
886 * https://www.researchgate.net/profile/Ba_Son_Thai/publication/308692177_A_Fast_White_Balance_Algorithm_Based_on_Pixel_Greyness/
887 *
888 * Edge-Based Color Constancy, Joost van de Weijer, Theo Gevers, Arjan Gijsenij
889 * https://hal.inria.fr/inria-00548686/document
890 *
891 */
892
893 float *const restrict temp = dt_alloc_sse_ps(width * height * ch);
894
895 // Convert RGB to xy
896 #ifdef _OPENMP
897 #pragma omp parallel for default(none) \
898 dt_omp_firstprivate(width, height, ch, in, temp, RGB_to_XYZ) \
899 collapse(2) schedule(simd:static)
900 #endif
901 for(size_t i = 0; i < height; i++)
902 for(size_t j = 0; j < width; j++)
903 {
904 const size_t index = (i * width + j) * ch;
905 dt_aligned_pixel_t RGB;
906 dt_aligned_pixel_t XYZ;
907
908 // Clip negatives
909 for_each_channel(c,aligned(in))
910 RGB[c] = fmaxf(in[index + c], 0.0f);
911
912 // Convert to XYZ
913 dot_product(RGB, RGB_to_XYZ, XYZ);
914
915 // Convert to xyY
916 const float sum = fmaxf(XYZ[0] + XYZ[1] + XYZ[2], NORM_MIN);
917 XYZ[0] /= sum; // x
918 XYZ[2] = XYZ[1]; // Y
919 XYZ[1] /= sum; // y
920
921 // Shift the chromaticity plane so the D50 point (target) becomes the origin
922 const float D50[2] = { 0.34567f, 0.35850f };
923 const float norm = dt_fast_hypotf(D50[0], D50[1]);
924
925 temp[index ] = (XYZ[0] - D50[0]) / norm;
926 temp[index + 1] = (XYZ[1] - D50[1]) / norm;
927 temp[index + 2] = XYZ[2];
928 }
929
930 float elements = 0.f;
931 dt_aligned_pixel_t xyY = { 0.f };
932
933 if(illuminant == DT_ILLUMINANT_DETECT_SURFACES)
934 {
935 #ifdef _OPENMP
936 #pragma omp parallel for default(none) reduction(+:xyY, elements) \
937 dt_omp_firstprivate(width, height, temp, ch) \
938 schedule(simd:static)
939 #endif
940 for(size_t i = 2 * OFF; i < height - 4 * OFF; i += OFF)
941 for(size_t j = 2 * OFF; j < width - 4 * OFF; j += OFF)
942 {
943 float DT_ALIGNED_PIXEL central_average[2];
944
945 #pragma unroll
946 for(size_t c = 0; c < 2; c++)
947 {
948 // B-spline local average / blur
949 central_average[c] = ( temp[SHF(-OFF, -OFF, c)] + 2.f * temp[SHF(-OFF, 0, c)] + temp[SHF(-OFF, +OFF, c)] +
950 2.f * temp[SHF( 0, -OFF, c)] + 4.f * temp[SHF( 0, 0, c)] + 2.f * temp[SHF( 0, +OFF, c)] +
951 temp[SHF(+OFF, -OFF, c)] + 2.f * temp[SHF(+OFF, 0, c)] + temp[SHF(+OFF, +OFF, c)]) / 16.0f;
952 central_average[c] = fmaxf(central_average[c], 0.0f);
953 }
954
955 dt_aligned_pixel_t var = { 0.f };
956
957 // compute patch-wise variance
958 // If variance = 0, we are on a flat surface and want to discard that patch.
959 #pragma unroll
960 for(size_t c = 0; c < 2; c++)
961 {
962 var[c] = (
963 sqf(temp[SHF(-OFF, -OFF, c)] - central_average[c]) +
964 sqf(temp[SHF(-OFF, 0, c)] - central_average[c]) +
965 sqf(temp[SHF(-OFF, +OFF, c)] - central_average[c]) +
966 sqf(temp[SHF(0, -OFF, c)] - central_average[c]) +
967 sqf(temp[SHF(0, 0, c)] - central_average[c]) +
968 sqf(temp[SHF(0, +OFF, c)] - central_average[c]) +
969 sqf(temp[SHF(+OFF, -OFF, c)] - central_average[c]) +
970 sqf(temp[SHF(+OFF, 0, c)] - central_average[c]) +
971 sqf(temp[SHF(+OFF, +OFF, c)] - central_average[c])
972 ) / 9.0f;
973 }
974
975 // Compute the patch-wise chroma covariance.
976 // If covariance = 0, chroma channels are not correlated and we either have noise or chromatic aberrations.
977 // Both ways, we want to discard that patch from the chroma average.
978 var[2] = (
979 (temp[SHF(-OFF, -OFF, 0)] - central_average[0]) * (temp[SHF(-OFF, -OFF, 1)] - central_average[1]) +
980 (temp[SHF(-OFF, 0, 0)] - central_average[0]) * (temp[SHF(-OFF, 0, 1)] - central_average[1]) +
981 (temp[SHF(-OFF, +OFF, 0)] - central_average[0]) * (temp[SHF(-OFF, +OFF, 1)] - central_average[1]) +
982 (temp[SHF( 0, -OFF, 0)] - central_average[0]) * (temp[SHF( 0, -OFF, 1)] - central_average[1]) +
983 (temp[SHF( 0, 0, 0)] - central_average[0]) * (temp[SHF( 0, 0, 1)] - central_average[1]) +
984 (temp[SHF( 0, +OFF, 0)] - central_average[0]) * (temp[SHF( 0, +OFF, 1)] - central_average[1]) +
985 (temp[SHF(+OFF, -OFF, 0)] - central_average[0]) * (temp[SHF(+OFF, -OFF, 1)] - central_average[1]) +
986 (temp[SHF(+OFF, 0, 0)] - central_average[0]) * (temp[SHF(+OFF, 0, 1)] - central_average[1]) +
987 (temp[SHF(+OFF, +OFF, 0)] - central_average[0]) * (temp[SHF(+OFF, +OFF, 1)] - central_average[1])
988 ) / 9.0f;
989
990 // Compute the Minkowski p-norm for regularization
991 const float p = 8.f;
992 const float p_norm
993 = powf(powf(fabsf(central_average[0]), p) + powf(fabsf(central_average[1]), p), 1.f / p) + NORM_MIN;
994 const float weight = var[0] * var[1] * var[2];
995
996 #pragma unroll
997 for(size_t c = 0; c < 2; c++) xyY[c] += central_average[c] * weight / p_norm;
998 elements += weight / p_norm;
999 }
1000 }
1001 else if(illuminant == DT_ILLUMINANT_DETECT_EDGES)
1002 {
1003 #ifdef _OPENMP
1004 #pragma omp parallel for default(none) reduction(+:xyY, elements) \
1005 dt_omp_firstprivate(width, height, temp, ch) \
1006 schedule(simd:static)
1007 #endif
1008 for(size_t i = 2 * OFF; i < height - 4 * OFF; i += OFF)
1009 for(size_t j = 2 * OFF; j < width - 4 * OFF; j += OFF)
1010 {
1011 float DT_ALIGNED_PIXEL dd[2];
1012 float DT_ALIGNED_PIXEL central_average[2];
1013
1014 #pragma unroll
1015 for(size_t c = 0; c < 2; c++)
1016 {
1017 // B-spline local average / blur
1018 central_average[c] = ( temp[SHF(-OFF, -OFF, c)] + 2.f * temp[SHF(-OFF, 0, c)] + temp[SHF(-OFF, +OFF, c)] +
1019 2.f * temp[SHF( 0, -OFF, c)] + 4.f * temp[SHF( 0, 0, c)] + 2.f * temp[SHF( 0, +OFF, c)] +
1020 temp[SHF(+OFF, -OFF, c)] + 2.f * temp[SHF(+OFF, 0, c)] + temp[SHF(+OFF, +OFF, c)]) / 16.0f;
1021
1022 // image - blur = laplacian = edges
1023 dd[c] = temp[SHF(0, 0, c)] - central_average[c];
1024 }
1025
1026 // Compute the Minkowski p-norm for regularization
1027 const float p = 8.f;
1028 const float p_norm = powf(powf(fabsf(dd[0]), p) + powf(fabsf(dd[1]), p), 1.f / p) + NORM_MIN;
1029
1030 #pragma unroll
1031 for(size_t c = 0; c < 2; c++) xyY[c] -= dd[c] / p_norm;
1032 elements += 1.f;
1033 }
1034 }
1035
1036 const float D50[2] = { 0.34567f, 0.35850 };
1037 const float norm_D50 = dt_fast_hypotf(D50[0], D50[1]);
1038
1039 for(size_t c = 0; c < 2; c++)
1040 xyz[c] = norm_D50 * (xyY[c] / elements) + D50[c];
1041
1042 dt_free_align(temp);
1043 }
1044
declare_cat_on_pipe(struct dt_iop_module_t * self,gboolean preset)1045 static void declare_cat_on_pipe(struct dt_iop_module_t *self, gboolean preset)
1046 {
1047 // Advertise to the pipeline that we are doing chromatic adaptation here
1048 // preset = TRUE allows to capture the CAT a priori at init time
1049 dt_iop_channelmixer_rgb_params_t *p = (dt_iop_channelmixer_rgb_params_t *)self->params;
1050
1051 if((self->enabled && !(p->adaptation == DT_ADAPTATION_RGB || p->illuminant == DT_ILLUMINANT_PIPE)) || preset)
1052 {
1053 // We do CAT here so we need to register this instance as CAT-handler.
1054 if(self->dev->proxy.chroma_adaptation == NULL)
1055 {
1056 // We are the first to try to register, let's go !
1057 self->dev->proxy.chroma_adaptation = self;
1058 }
1059 else if(self->dev->proxy.chroma_adaptation == self)
1060 {
1061 }
1062 else
1063 {
1064 // Another instance already registered.
1065 // If we are lower in the pipe than it, register in its place.
1066 if(dt_iop_is_first_instance(self->dev->iop, self))
1067 self->dev->proxy.chroma_adaptation = self;
1068 }
1069 }
1070 else
1071 {
1072 if(self->dev->proxy.chroma_adaptation != NULL)
1073 {
1074 // We do NOT do CAT here.
1075 // Deregister this instance as CAT-handler if it previously registered
1076 if(self->dev->proxy.chroma_adaptation == self)
1077 self->dev->proxy.chroma_adaptation = NULL;
1078 }
1079 }
1080 }
1081
_is_another_module_cat_on_pipe(struct dt_iop_module_t * self)1082 static inline gboolean _is_another_module_cat_on_pipe(struct dt_iop_module_t *self)
1083 {
1084 dt_iop_channelmixer_rgb_gui_data_t *g = (dt_iop_channelmixer_rgb_gui_data_t *)self->gui_data;
1085 if(!g) return FALSE;
1086 return self->dev->proxy.chroma_adaptation && self->dev->proxy.chroma_adaptation != self;
1087 }
1088
1089
1090 static void update_illuminants(struct dt_iop_module_t *self);
1091 static void update_approx_cct(struct dt_iop_module_t *self);
1092 static void update_illuminant_color(struct dt_iop_module_t *self);
1093 static void paint_temperature_background(struct dt_iop_module_t *self);
1094
1095
check_if_close_to_daylight(const float x,const float y,float * temperature,dt_illuminant_t * illuminant,dt_adaptation_t * adaptation)1096 static void check_if_close_to_daylight(const float x, const float y, float *temperature,
1097 dt_illuminant_t *illuminant, dt_adaptation_t *adaptation)
1098 {
1099 /* Check if a chromaticity x, y is close to daylight within 2.5 % error margin.
1100 * If so, we enable the daylight GUI for better ergonomics
1101 * Otherwise, we default to direct x, y control for better accuracy
1102 *
1103 * Note : The use of CCT is discouraged if dE > 5 % in CIE 1960 Yuv space
1104 * reference : https://onlinelibrary.wiley.com/doi/abs/10.1002/9780470175637.ch3
1105 */
1106
1107 // Get the correlated color temperature (CCT)
1108 float t = xy_to_CCT(x, y);
1109
1110 // xy_to_CCT is valid only in 3000 - 25000 K. We need another model below
1111 if(t < 3000.f && t > 1667.f)
1112 t = CCT_reverse_lookup(x, y);
1113
1114 if(temperature)
1115 *temperature = t;
1116
1117 // Convert to CIE 1960 Yuv space
1118 float xy_ref[2] = { x, y };
1119 float uv_ref[2];
1120 xy_to_uv(xy_ref, uv_ref);
1121
1122 float xy_test[2] = { 0.f };
1123 float uv_test[2];
1124
1125 // Compute the test chromaticity from the daylight model
1126 illuminant_to_xy(DT_ILLUMINANT_D, NULL, NULL, &xy_test[0], &xy_test[1], t, DT_ILLUMINANT_FLUO_LAST, DT_ILLUMINANT_LED_LAST);
1127 xy_to_uv(xy_test, uv_test);
1128
1129 // Compute the error between the reference illuminant and the test illuminant derivated from the CCT with daylight model
1130 const float delta_daylight = dt_fast_hypotf((uv_test[0] - uv_ref[0]), (uv_test[1] - uv_ref[1]));
1131
1132 // Compute the test chromaticity from the blackbody model
1133 illuminant_to_xy(DT_ILLUMINANT_BB, NULL, NULL, &xy_test[0], &xy_test[1], t, DT_ILLUMINANT_FLUO_LAST, DT_ILLUMINANT_LED_LAST);
1134 xy_to_uv(xy_test, uv_test);
1135
1136 // Compute the error between the reference illuminant and the test illuminant derivated from the CCT with black body model
1137 const float delta_bb = dt_fast_hypotf((uv_test[0] - uv_ref[0]), (uv_test[1] - uv_ref[1]));
1138
1139 // Check the error between original and test chromaticity
1140 if(delta_bb < 0.005f || delta_daylight < 0.005f)
1141 {
1142 if(illuminant)
1143 {
1144 if(delta_bb < delta_daylight)
1145 *illuminant = DT_ILLUMINANT_BB;
1146 else
1147 *illuminant = DT_ILLUMINANT_D;
1148 }
1149 }
1150 else
1151 {
1152 // error is too big to use a CCT-based model, we fall back to a custom/freestyle chroma selection for the illuminant
1153 if(illuminant) *illuminant = DT_ILLUMINANT_CUSTOM;
1154 }
1155
1156 // CAT16 is more accurate no matter the illuminant
1157 if(adaptation) *adaptation = DT_ADAPTATION_CAT16;
1158 }
1159
1160 #define DEG_TO_RAD(x) (x * M_PI / 180.f)
1161 #define RAD_TO_DEG(x) (x * 180.f / M_PI)
1162
compute_patches_delta_E(const float * const restrict patches,const dt_color_checker_t * const checker,float * const restrict delta_E,float * const restrict avg_delta_E,float * const restrict max_delta_E)1163 static inline void compute_patches_delta_E(const float *const restrict patches,
1164 const dt_color_checker_t *const checker,
1165 float *const restrict delta_E, float *const restrict avg_delta_E, float *const restrict max_delta_E)
1166 {
1167 // Compute the delta E
1168
1169 float dE = 0.f;
1170 float max_dE = 0.f;
1171
1172 for(size_t k = 0; k < checker->patches; k++)
1173 {
1174 // Convert to Lab
1175 dt_aligned_pixel_t Lab_test;
1176 dt_aligned_pixel_t XYZ_test;
1177
1178 // If exposure was normalized, denormalized it before
1179 for(size_t c = 0; c < 4; c++) XYZ_test[c] = patches[k * 4 + c];
1180 dt_XYZ_to_Lab(XYZ_test, Lab_test);
1181
1182 const float *const restrict Lab_ref = checker->values[k].Lab;
1183
1184 // Compute delta E 2000 to make your computer heat
1185 // ref: https://en.wikipedia.org/wiki/Color_difference#CIEDE2000
1186 // note : it will only be luck if I didn't mess-up the computation somewhere
1187 const float DL = Lab_ref[0] - Lab_test[0];
1188 const float L_avg = (Lab_ref[0] + Lab_test[0]) / 2.f;
1189 const float C_ref = dt_fast_hypotf(Lab_ref[1], Lab_ref[2]);
1190 const float C_test = dt_fast_hypotf(Lab_test[1], Lab_test[2]);
1191 const float C_avg = (C_ref + C_test) / 2.f;
1192 float C_avg_7 = C_avg * C_avg; // C_avg²
1193 C_avg_7 *= C_avg_7; // C_avg⁴
1194 C_avg_7 *= C_avg_7; // C_avg⁸
1195 C_avg_7 /= C_avg; // C_avg⁷
1196 const float C_avg_7_ratio_sqrt = sqrtf(C_avg_7 / (C_avg_7 + 6103515625.f)); // 25⁷ = 6103515625
1197 const float a_ref_prime = Lab_ref[1] * (1.f + 0.5f * (1.f - C_avg_7_ratio_sqrt));
1198 const float a_test_prime = Lab_test[1] * (1.f + 0.5f * (1.f - C_avg_7_ratio_sqrt));
1199 const float C_ref_prime = dt_fast_hypotf(a_ref_prime, Lab_ref[2]);
1200 const float C_test_prime = dt_fast_hypotf(a_test_prime, Lab_test[2]);
1201 const float DC_prime = C_ref_prime - C_test_prime;
1202 const float C_avg_prime = (C_ref_prime + C_test_prime) / 2.f;
1203 float h_ref_prime = atan2f(Lab_ref[2], a_ref_prime);
1204 float h_test_prime = atan2f(Lab_test[2], a_test_prime);
1205
1206 // Comply with recommendations, h = 0° where C = 0 by convention
1207 if(C_ref_prime == 0.f) h_ref_prime = 0.f;
1208 if(C_test_prime == 0.f) h_test_prime = 0.f;
1209
1210 // Get the hue angles from [-pi ; pi] back to [0 ; 2 pi],
1211 // again, to comply with specifications
1212 if(h_ref_prime < 0.f) h_ref_prime = 2.f * M_PI - h_ref_prime;
1213 if(h_test_prime < 0.f) h_test_prime = 2.f * M_PI - h_test_prime;
1214
1215 // Convert to degrees, again to comply with specs
1216 h_ref_prime = RAD_TO_DEG(h_ref_prime);
1217 h_test_prime = RAD_TO_DEG(h_test_prime);
1218
1219 float Dh_prime = h_test_prime - h_ref_prime;
1220 float Dh_prime_abs = fabsf(Dh_prime);
1221 if(C_test_prime == 0.f || C_ref_prime == 0.f)
1222 Dh_prime = 0.f;
1223 else if(Dh_prime_abs <= 180.f)
1224 ;
1225 else if(Dh_prime_abs > 180.f && (h_test_prime <= h_ref_prime))
1226 Dh_prime += 360.f;
1227 else if(Dh_prime_abs > 180.f && (h_test_prime > h_ref_prime))
1228 Dh_prime -= 360.f;
1229
1230 // update abs(Dh_prime) for later
1231 Dh_prime_abs = fabsf(Dh_prime);
1232
1233 const float DH_prime = 2.f * sqrtf(C_test_prime * C_ref_prime) * sinf(DEG_TO_RAD(Dh_prime) / 2.f);
1234 float H_avg_prime = h_ref_prime + h_test_prime;
1235 if(C_test_prime == 0.f || C_ref_prime == 0.f)
1236 ;
1237 else if(Dh_prime_abs <= 180.f)
1238 H_avg_prime /= 2.f;
1239 else if(Dh_prime_abs > 180.f && (H_avg_prime < 360.f))
1240 H_avg_prime = (H_avg_prime + 360.f) / 2.f;
1241 else if(Dh_prime_abs > 180.f && (H_avg_prime >= 360.f))
1242 H_avg_prime = (H_avg_prime - 360.f) / 2.f;
1243
1244 const float T = 1.f
1245 - 0.17f * cosf(DEG_TO_RAD(H_avg_prime) - DEG_TO_RAD(30.f))
1246 + 0.24f * cosf(2.f * DEG_TO_RAD(H_avg_prime))
1247 + 0.32f * cosf(3.f * DEG_TO_RAD(H_avg_prime) + DEG_TO_RAD(6.f))
1248 - 0.20f * cosf(4.f * DEG_TO_RAD(H_avg_prime) - DEG_TO_RAD(63.f));
1249
1250 const float S_L = 1.f + (0.015f * sqf(L_avg - 50.f)) / sqrtf(20.f + sqf(L_avg - 50.f));
1251 const float S_C = 1.f + 0.045f * C_avg_prime;
1252 const float S_H = 1.f + 0.015f * C_avg_prime * T;
1253 const float R_T = -2.f * C_avg_7_ratio_sqrt
1254 * sinf(DEG_TO_RAD(60.f) * expf(-sqf((H_avg_prime - 275.f) / 25.f)));
1255
1256 // roll the drum, here goes the Delta E, finally…
1257 const float DE = sqrtf(sqf(DL / S_L) + sqf(DC_prime / S_C) + sqf(DH_prime / S_H)
1258 + R_T * (DC_prime / S_C) * (DH_prime / S_H));
1259
1260 // Delta E 1976 for reference :
1261 //float DE = sqrtf(sqf(Lab_test[0] - Lab_ref[0]) + sqf(Lab_test[1] - Lab_ref[1]) + sqf(Lab_test[2] - Lab_ref[2]));
1262
1263 //fprintf(stdout, "patch %s : Lab ref \t= \t%.3f \t%.3f \t%.3f \n", checker->values[k].name, Lab_ref[0], Lab_ref[1], Lab_ref[2]);
1264 //fprintf(stdout, "patch %s : Lab mes \t= \t%.3f \t%.3f \t%.3f \n", checker->values[k].name, Lab_test[0], Lab_test[1], Lab_test[2]);
1265 //fprintf(stdout, "patch %s : dE mes \t= \t%.3f \n", checker->values[k].name, DE);
1266
1267 delta_E[k] = DE;
1268 dE += DE / (float)checker->patches;
1269 if(DE > max_dE) max_dE = DE;
1270 }
1271
1272 *avg_delta_E = dE;
1273 *max_delta_E = max_dE;
1274 }
1275
1276 #define GET_WEIGHT \
1277 float hue = atan2f(reference[2], reference[1]); \
1278 const float chroma = hypotf(reference[2], reference[1]); \
1279 float delta_hue = hue - ref_hue; \
1280 if(chroma == 0.f) \
1281 delta_hue = 0.f; \
1282 else if(fabsf(delta_hue) <= M_PI) \
1283 ; \
1284 else if(fabsf(delta_hue) > M_PI && (hue <= ref_hue)) \
1285 delta_hue += 2.f * M_PI; \
1286 else if(fabsf(delta_hue) > M_PI && (hue > ref_hue)) \
1287 delta_hue -= 2.f * M_PI; \
1288 w = sqrtf(expf(-sqf(delta_hue) / 2.f));
1289
1290
1291 typedef struct {
1292 float black;
1293 float exposure;
1294 } extraction_result_t;
1295
_extract_patches(const float * const restrict in,const dt_iop_roi_t * const roi_in,dt_iop_channelmixer_rgb_gui_data_t * g,const dt_colormatrix_t RGB_to_XYZ,const dt_colormatrix_t XYZ_to_CAM,float * const restrict patches,const gboolean normalize_exposure)1296 static const extraction_result_t _extract_patches(const float *const restrict in, const dt_iop_roi_t *const roi_in,
1297 dt_iop_channelmixer_rgb_gui_data_t *g,
1298 const dt_colormatrix_t RGB_to_XYZ, const dt_colormatrix_t XYZ_to_CAM,
1299 float *const restrict patches,
1300 const gboolean normalize_exposure)
1301 {
1302 const size_t width = roi_in->width;
1303 const size_t height = roi_in->height;
1304 const float radius_x = g->checker->radius * hypotf(1.f, g->checker->ratio) * g->safety_margin;
1305 const float radius_y = radius_x / g->checker->ratio;
1306
1307 if(g->delta_E_in == NULL)
1308 g->delta_E_in = dt_alloc_sse_ps(g->checker->patches);
1309
1310 /* Get the average color over each patch */
1311 for(size_t k = 0; k < g->checker->patches; k++)
1312 {
1313 // center of the patch in the ideal reference
1314 const point_t center = { g->checker->values[k].x, g->checker->values[k].y };
1315
1316 // corners of the patch in the ideal reference
1317 const point_t corners[4] = { {center.x - radius_x, center.y - radius_y},
1318 {center.x + radius_x, center.y - radius_y},
1319 {center.x + radius_x, center.y + radius_y},
1320 {center.x - radius_x, center.y + radius_y} };
1321
1322 // apply patch coordinates transform depending on perspective
1323 point_t new_corners[4];
1324 // find the bounding box of the patch at the same time
1325 size_t x_min = width - 1;
1326 size_t x_max = 0;
1327 size_t y_min = height - 1;
1328 size_t y_max = 0;
1329 for(size_t c = 0; c < 4; c++) {
1330 new_corners[c] = apply_homography(corners[c], g->homography);
1331 x_min = fminf(new_corners[c].x, x_min);
1332 x_max = fmaxf(new_corners[c].x, x_max);
1333 y_min = fminf(new_corners[c].y, y_min);
1334 y_max = fmaxf(new_corners[c].y, y_max);
1335 }
1336
1337 x_min = CLAMP((size_t)floorf(x_min), 0, width - 1);
1338 x_max = CLAMP((size_t)ceilf(x_max), 0, width - 1);
1339 y_min = CLAMP((size_t)floorf(y_min), 0, height - 1);
1340 y_max = CLAMP((size_t)ceilf(y_max), 0, height - 1);
1341
1342 // Get the average color on the patch
1343 patches[k * 4] = patches[k * 4 + 1] = patches[k * 4 + 2] = patches[k * 4 + 3] = 0.f;
1344 size_t num_elem = 0;
1345
1346 // Loop through the rectangular bounding box
1347 for(size_t j = y_min; j < y_max; j++)
1348 for(size_t i = x_min; i < x_max; i++)
1349 {
1350 // Check if this pixel lies inside the sampling area and sample if it does
1351 point_t current_point = { i + 0.5f, j + 0.5f };
1352 current_point = apply_homography(current_point, g->inverse_homography);
1353 current_point.x -= center.x;
1354 current_point.y -= center.y;
1355
1356 if(current_point.x < radius_x && current_point.x > -radius_x &&
1357 current_point.y < radius_y && current_point.y > -radius_y)
1358 {
1359 for(size_t c = 0; c < 3; c++)
1360 {
1361 patches[k * 4 + c] += in[(j * width + i) * 4 + c];
1362
1363 // Debug : inpaint a black square in the preview to ensure the coordanites of
1364 // overlay drawings and actual pixel processing match
1365 // out[(j * width + i) * 4 + c] = 0.f;
1366 }
1367 num_elem++;
1368 }
1369 }
1370
1371 for(size_t c = 0; c < 3; c++) patches[k * 4 + c] /= (float)num_elem;
1372
1373 // Convert to XYZ
1374 dt_aligned_pixel_t XYZ = { 0 };
1375 dot_product(patches + k * 4, RGB_to_XYZ, XYZ);
1376 for(size_t c = 0; c < 3; c++) patches[k * 4 + c] = XYZ[c];
1377 }
1378
1379 // find reference white patch
1380 dt_aligned_pixel_t XYZ_white_ref;
1381 dt_Lab_to_XYZ(g->checker->values[g->checker->white].Lab, XYZ_white_ref);
1382 const float white_ref_norm = euclidean_norm(XYZ_white_ref);
1383
1384 // find test white patch
1385 dt_aligned_pixel_t XYZ_white_test;
1386 for(size_t c = 0; c < 3; c++) XYZ_white_test[c] = patches[g->checker->white * 4 + c];
1387 const float white_test_norm = euclidean_norm(XYZ_white_test);
1388
1389 /* match global exposure */
1390 // white exposure depends on camera settings and raw white point,
1391 // we want our profile to be independent from that
1392 float exposure = white_ref_norm / white_test_norm;
1393
1394 /* Exposure compensation */
1395 // Ensure the relative luminance of the test patch (compared to white patch)
1396 // is the same as the relative luminance of the reference patch.
1397 // This compensate for lighting fall-off and unevenness
1398 if(normalize_exposure)
1399 {
1400 for(size_t k = 0; k < g->checker->patches; k++)
1401 {
1402 float *const sample = patches + k * 4;
1403
1404 dt_aligned_pixel_t XYZ_ref;
1405 dt_Lab_to_XYZ(g->checker->values[k].Lab, XYZ_ref);
1406
1407 const float sample_norm = euclidean_norm(sample);
1408 const float ref_norm = euclidean_norm(XYZ_ref);
1409
1410 const float relative_luminance_test = sample_norm / white_test_norm;
1411 const float relative_luminance_ref = ref_norm / white_ref_norm;
1412
1413 const float luma_correction = relative_luminance_ref / relative_luminance_test;
1414 for(size_t c = 0; c < 3; ++c) sample[c] *= luma_correction * exposure;
1415 }
1416 }
1417
1418 // black point is evaluated by rawspeed on each picture using the dark pixels
1419 // we want our profile to be also independent from its discrepancies
1420 // so we convert back the patches to camera RGB space and search the best fit of
1421 // RGB_ref = exposure * (RGB_test - offset) for offset.
1422 float black = 0.f;
1423 const float user_exposure = exp2f(dt_dev_exposure_get_exposure(darktable.develop));
1424 const float user_black = dt_dev_exposure_get_black(darktable.develop);
1425
1426 if(XYZ_to_CAM)
1427 {
1428 float mean_ref = 0.f;
1429 float mean_test = 0.f;
1430
1431 for(size_t k = 0; k < g->checker->patches; k++)
1432 {
1433 dt_aligned_pixel_t XYZ_ref, RGB_ref;
1434 dt_aligned_pixel_t XYZ_test, RGB_test;
1435
1436 for(size_t c = 0; c < 3; c++) XYZ_test[c] = patches[k * 4 + c];
1437 dt_Lab_to_XYZ(g->checker->values[k].Lab, XYZ_ref);
1438
1439 dot_product(XYZ_test, XYZ_to_CAM, RGB_test);
1440 dot_product(XYZ_ref, XYZ_to_CAM, RGB_ref);
1441
1442 // Undo exposure module settings
1443 for(int c = 0; c < 3; c++)
1444 {
1445 RGB_test[c] = RGB_test[c] / user_exposure / exposure + user_black;
1446 }
1447
1448 // From now on, we have all the reference and test data in camera RGB space
1449 // where exposure and black level are applied
1450
1451 for(int c = 0; c < 3; c++)
1452 {
1453 mean_test += RGB_test[c];
1454 mean_ref += RGB_ref[c];
1455 }
1456 }
1457 mean_test /= 3.f * g->checker->patches;
1458 mean_ref /= 3.f * g->checker->patches;
1459
1460 float variance = 0.f;
1461 float covariance = 0.f;
1462
1463 for(size_t k = 0; k < g->checker->patches; k++)
1464 {
1465 dt_aligned_pixel_t XYZ_ref, RGB_ref;
1466 dt_aligned_pixel_t XYZ_test, RGB_test;
1467
1468 for(size_t c = 0; c < 3; c++) XYZ_test[c] = patches[k * 4 + c];
1469 dt_Lab_to_XYZ(g->checker->values[k].Lab, XYZ_ref);
1470
1471 dot_product(XYZ_test, XYZ_to_CAM, RGB_test);
1472 dot_product(XYZ_ref, XYZ_to_CAM, RGB_ref);
1473
1474 // Undo exposure module settings
1475 for(int c = 0; c < 3; c++)
1476 {
1477 RGB_test[c] = RGB_test[c] / user_exposure / exposure + user_black;
1478 }
1479
1480 for(int c = 0; c < 3; c++)
1481 {
1482 variance += sqf(RGB_test[c] - mean_test);
1483 covariance += (RGB_ref[c] - mean_ref) * (RGB_test[c] - mean_ref);
1484 }
1485 }
1486 variance /= 3.f * g->checker->patches;
1487 covariance /= 3.f * g->checker->patches;
1488
1489 // Here, we solve the least-squares problem RGB_ref = exposure * RGB_test + offset
1490 // using :
1491 // exposure = covariance(RGB_test, RGB_ref) / variance(RGB_test)
1492 // offset = mean(RGB_ref) - exposure * mean(RGB_test)
1493 exposure = covariance / variance;
1494 black = mean_ref - exposure * mean_test;
1495 }
1496
1497 // the exposure module applies output = (input - offset) * exposure
1498 // but we compute output = input * exposure + offset
1499 // so, rescale offset to adapt our offset to exposure module GUI
1500 black /= -exposure;
1501
1502 const extraction_result_t result = { black, exposure };
1503 return result;
1504 }
1505
extract_color_checker(const float * const restrict in,float * const restrict out,const dt_iop_roi_t * const roi_in,dt_iop_channelmixer_rgb_gui_data_t * g,const dt_colormatrix_t RGB_to_XYZ,const dt_colormatrix_t XYZ_to_RGB,const dt_colormatrix_t XYZ_to_CAM,const dt_adaptation_t kind)1506 void extract_color_checker(const float *const restrict in, float *const restrict out,
1507 const dt_iop_roi_t *const roi_in, dt_iop_channelmixer_rgb_gui_data_t *g,
1508 const dt_colormatrix_t RGB_to_XYZ, const dt_colormatrix_t XYZ_to_RGB,
1509 const dt_colormatrix_t XYZ_to_CAM,
1510 const dt_adaptation_t kind)
1511 {
1512 float *const restrict patches = dt_alloc_sse_ps(g->checker->patches * 4);
1513
1514 dt_simd_memcpy(in, out, (size_t)roi_in->width * roi_in->height * 4);
1515
1516 extraction_result_t extraction_result = _extract_patches(out, roi_in, g, RGB_to_XYZ, XYZ_to_CAM,
1517 patches, TRUE);
1518
1519 // Compute the delta E
1520 float pre_wb_delta_E = 0.f;
1521 float pre_wb_max_delta_E = 0.f;
1522 compute_patches_delta_E(patches, g->checker, g->delta_E_in, &pre_wb_delta_E, &pre_wb_max_delta_E);
1523
1524 /* find the scene illuminant */
1525
1526 // find reference grey patch
1527 dt_aligned_pixel_t XYZ_grey_ref;
1528 dt_Lab_to_XYZ(g->checker->values[g->checker->middle_grey].Lab, XYZ_grey_ref);
1529
1530 // find test grey patch
1531 dt_aligned_pixel_t XYZ_grey_test;
1532 for(size_t c = 0; c < 3; c++) XYZ_grey_test[c] = patches[g->checker->middle_grey * 4 + c];
1533
1534 // compute reference illuminant
1535 dt_aligned_pixel_t D50_XYZ;
1536 illuminant_xy_to_XYZ(0.34567f, 0.35850f, D50_XYZ);
1537
1538 // normalize luminances - note : illuminant is normalized by definition
1539 const float Y_test = XYZ_grey_test[1];
1540 const float Y_ref = XYZ_grey_ref[1];
1541 for(size_t c = 0; c < 3; c++)
1542 {
1543 XYZ_grey_ref[c] /= Y_ref;
1544 XYZ_grey_test[c] /= Y_test;
1545 }
1546
1547 // convert XYZ to LMS
1548 dt_aligned_pixel_t LMS_grey_ref, LMS_grey_test, D50_LMS;
1549 convert_any_XYZ_to_LMS(XYZ_grey_ref, LMS_grey_ref, kind);
1550 convert_any_XYZ_to_LMS(XYZ_grey_test, LMS_grey_test, kind);
1551 convert_any_XYZ_to_LMS(D50_XYZ, D50_LMS, kind);
1552
1553 // solve the equation to find the scene illuminant
1554 dt_aligned_pixel_t illuminant = { 0.0f };
1555 for(size_t c = 0; c < 3; c++) illuminant[c] = D50_LMS[c] * LMS_grey_test[c] / LMS_grey_ref[c];
1556
1557 // convert back the illuminant to XYZ then xyY
1558 dt_aligned_pixel_t illuminant_XYZ, illuminant_xyY = { .0f };
1559 convert_any_LMS_to_XYZ(illuminant, illuminant_XYZ, kind);
1560 const float Y_illu = illuminant_XYZ[1];
1561 for(size_t c = 0; c < 3; c++) illuminant_XYZ[c] /= Y_illu;
1562 dt_XYZ_to_xyY(illuminant_XYZ, illuminant_xyY);
1563
1564 // save the illuminant in GUI struct for commit
1565 g->xy[0] = illuminant_xyY[0];
1566 g->xy[1] = illuminant_xyY[1];
1567
1568 // and recompute back the LMS to be sure we use the parameters that will be computed later
1569 illuminant_xy_to_XYZ(illuminant_xyY[0], illuminant_xyY[1], illuminant_XYZ);
1570 convert_any_XYZ_to_LMS(illuminant_XYZ, illuminant, kind);
1571 const float p = powf(0.818155f / illuminant[2], 0.0834f);
1572
1573 /* White-balance the patches */
1574 for(size_t k = 0; k < g->checker->patches; k++)
1575 {
1576 // keep in synch with loop_switch() from process()
1577 float *const sample = patches + k * 4;
1578 const float Y = sample[1];
1579 downscale_vector(sample, Y);
1580
1581 dt_aligned_pixel_t LMS;
1582 convert_any_XYZ_to_LMS(sample, LMS, kind);
1583
1584 dt_aligned_pixel_t temp;
1585
1586 switch(kind)
1587 {
1588 case DT_ADAPTATION_FULL_BRADFORD:
1589 {
1590 bradford_adapt_D50(LMS, illuminant, p, TRUE, temp);
1591 break;
1592 }
1593 case DT_ADAPTATION_LINEAR_BRADFORD:
1594 {
1595 bradford_adapt_D50(LMS, illuminant, 1.f, FALSE, temp);
1596 break;
1597 }
1598 case DT_ADAPTATION_CAT16:
1599 {
1600 CAT16_adapt_D50(LMS, illuminant, 1.f, TRUE, temp); // force full-adaptation
1601 break;
1602 }
1603 case DT_ADAPTATION_XYZ:
1604 {
1605 XYZ_adapt_D50(LMS, illuminant, temp);
1606 break;
1607 }
1608 case DT_ADAPTATION_RGB:
1609 case DT_ADAPTATION_LAST:
1610 {
1611 // No white balance.
1612 for(size_t c = 0; c < 3; ++c) temp[c] = LMS[c];
1613 break;
1614 }
1615 }
1616
1617 convert_any_LMS_to_XYZ(temp, sample, kind);
1618 upscale_vector(sample, Y);
1619 }
1620
1621 // Compute the delta E
1622 float post_wb_delta_E = 0.f;
1623 float post_wb_max_delta_E = 0.f;
1624 compute_patches_delta_E(patches, g->checker, g->delta_E_in, &post_wb_delta_E, &post_wb_max_delta_E);
1625
1626 /* Compute the matrix of mix */
1627 double *const restrict Y = dt_alloc_align(64, g->checker->patches * 3 * sizeof(double));
1628 double *const restrict A = dt_alloc_align(64, g->checker->patches * 3 * 9 * sizeof(double));
1629
1630 for(size_t k = 0; k < g->checker->patches; k++)
1631 {
1632 float *const sample = patches + k * 4;
1633 dt_aligned_pixel_t LMS_test;
1634 convert_any_XYZ_to_LMS(sample, LMS_test, kind);
1635
1636 float *const reference = g->checker->values[k].Lab;
1637 dt_aligned_pixel_t XYZ_ref, LMS_ref;
1638 dt_Lab_to_XYZ(reference, XYZ_ref);
1639 convert_any_XYZ_to_LMS(XYZ_ref, LMS_ref, kind);
1640
1641 // get the optimization weights
1642 float w = 1.f;
1643 if(g->optimization == DT_SOLVE_OPTIMIZE_NONE)
1644 w = sqrtf(1.f / (float)g->checker->patches);
1645 else if(g->optimization == DT_SOLVE_OPTIMIZE_HIGH_SAT)
1646 w = sqrtf(hypotf(reference[1] / 128.f, reference[2] / 128.f));
1647 else if(g->optimization == DT_SOLVE_OPTIMIZE_LOW_SAT)
1648 w = sqrtf(1.f - hypotf(reference[1] / 128.f, reference[2] / 128.f));
1649 else if(g->optimization == DT_SOLVE_OPTIMIZE_SKIN)
1650 {
1651 // average skin hue angle is 1.0 rad, hue range is [0.75 ; 1.25]
1652 const float ref_hue = 1.f;
1653 GET_WEIGHT;
1654 }
1655 else if(g->optimization == DT_SOLVE_OPTIMIZE_FOLIAGE)
1656 {
1657 // average foliage hue angle is 2.23 rad, hue range is [1.94 ; 2.44]
1658 const float ref_hue = 2.23f;
1659 GET_WEIGHT;
1660 }
1661 else if(g->optimization == DT_SOLVE_OPTIMIZE_SKY)
1662 {
1663 // average sky/water hue angle is -1.93 rad, hue range is [-1.64 ; -2.41]
1664 const float ref_hue = -1.93f;
1665 GET_WEIGHT;
1666 }
1667 else if(g->optimization == DT_SOLVE_OPTIMIZE_AVG_DELTA_E)
1668 w = sqrtf(sqrtf(1.f / g->delta_E_in[k]));
1669 else if(g->optimization == DT_SOLVE_OPTIMIZE_MAX_DELTA_E)
1670 w = sqrtf(sqrtf(g->delta_E_in[k]));
1671
1672 // fill 3 rows of the y column vector
1673 for(size_t c = 0; c < 3; c++) Y[k * 3 + c] = w * LMS_ref[c];
1674
1675 // fill line one of the A matrix
1676 A[k * 3 * 9 + 0] = w * LMS_test[0];
1677 A[k * 3 * 9 + 1] = w * LMS_test[1];
1678 A[k * 3 * 9 + 2] = w * LMS_test[2];
1679 A[k * 3 * 9 + 3] = 0.f;
1680 A[k * 3 * 9 + 4] = 0.f;
1681 A[k * 3 * 9 + 5] = 0.f;
1682 A[k * 3 * 9 + 6] = 0.f;
1683 A[k * 3 * 9 + 7] = 0.f;
1684 A[k * 3 * 9 + 8] = 0.f;
1685
1686 // fill line two of the A matrix
1687 A[k * 3 * 9 + 9 + 0] = 0.f;
1688 A[k * 3 * 9 + 9 + 1] = 0.f;
1689 A[k * 3 * 9 + 9 + 2] = 0.f;
1690 A[k * 3 * 9 + 9 + 3] = w * LMS_test[0];
1691 A[k * 3 * 9 + 9 + 4] = w * LMS_test[1];
1692 A[k * 3 * 9 + 9 + 5] = w * LMS_test[2];
1693 A[k * 3 * 9 + 9 + 6] = 0.f;
1694 A[k * 3 * 9 + 9 + 7] = 0.f;
1695 A[k * 3 * 9 + 9 + 8] = 0.f;
1696
1697 // fill line three of the A matrix
1698 A[k * 3 * 9 + 18 + 0] = 0.f;
1699 A[k * 3 * 9 + 18 + 1] = 0.f;
1700 A[k * 3 * 9 + 18 + 2] = 0.f;
1701 A[k * 3 * 9 + 18 + 3] = 0.f;
1702 A[k * 3 * 9 + 18 + 4] = 0.f;
1703 A[k * 3 * 9 + 18 + 5] = 0.f;
1704 A[k * 3 * 9 + 18 + 6] = w * LMS_test[0];
1705 A[k * 3 * 9 + 18 + 7] = w * LMS_test[1];
1706 A[k * 3 * 9 + 18 + 8] = w * LMS_test[2];
1707 }
1708
1709 pseudo_solve_gaussian(A, Y, g->checker->patches * 3, 9, TRUE);
1710
1711 // repack the matrix
1712 repack_double3x3_to_3xSSE(Y, g->mix);
1713
1714 dt_free_align(Y);
1715 dt_free_align(A);
1716
1717 // apply the matrix mix
1718 for(size_t k = 0; k < g->checker->patches; k++)
1719 {
1720 float *const sample = patches + k * 4;
1721 dt_aligned_pixel_t LMS_test;
1722 dt_aligned_pixel_t temp = { 0.f };
1723
1724 // Restore the original exposure of the patch
1725 for(size_t c = 0; c < 3; c++) temp[c] = sample[c];
1726
1727 convert_any_XYZ_to_LMS(temp, LMS_test, kind);
1728 dot_product(LMS_test, g->mix, temp);
1729 convert_any_LMS_to_XYZ(temp, sample, kind);
1730 }
1731
1732 // Compute the delta E
1733 float post_mix_delta_E = 0.f;
1734 float post_mix_max_delta_E = 0.f;
1735 compute_patches_delta_E(patches, g->checker, g->delta_E_in, &post_mix_delta_E, &post_mix_max_delta_E);
1736
1737 // get the temperature
1738 float temperature;
1739 dt_illuminant_t test_illuminant;
1740 float x = illuminant_xyY[0];
1741 float y = illuminant_xyY[1];
1742 check_if_close_to_daylight(x, y, &temperature, &test_illuminant, NULL);
1743 gchar *string;
1744 if(test_illuminant == DT_ILLUMINANT_D)
1745 string = _("(daylight)");
1746 else if(test_illuminant == DT_ILLUMINANT_BB)
1747 string = _("(black body)");
1748 else
1749 string = _("(invalid)");
1750
1751 gchar *diagnostic;
1752 if(post_mix_delta_E <= 1.2f)
1753 diagnostic = _("very good");
1754 else if(post_mix_delta_E <= 2.3f)
1755 diagnostic = _("good");
1756 else if(post_mix_delta_E <= 3.4f)
1757 diagnostic = _("passable");
1758 else
1759 diagnostic = _("bad");
1760
1761 g->profile_ready = TRUE;
1762
1763 // Update GUI label
1764 g_free(g->delta_E_label_text);
1765 g->delta_E_label_text
1766 = g_strdup_printf(_("\n<b>Profile quality report : %s</b>\n"
1767 "input ΔE : \tavg. %.2f ; \tmax. %.2f\n"
1768 "WB ΔE : \tavg. %.2f ; \tmax. %.2f\n"
1769 "output ΔE : \tavg. %.2f ; \tmax. %.2f\n\n"
1770 "<b>Profile data</b>\n"
1771 "illuminant : \t%.0f K \t%s \n"
1772 "matrix in adaptation space:\n"
1773 "<tt>%+.4f \t%+.4f \t%+.4f\n"
1774 "%+.4f \t%+.4f \t%+.4f\n"
1775 "%+.4f \t%+.4f \t%+.4f</tt>\n\n"
1776 "<b>Normalization values</b>\n"
1777 "exposure compensation : \t%+.2f EV\n"
1778 "black offset : \t%+.4f"
1779 ),
1780 diagnostic, pre_wb_delta_E, pre_wb_max_delta_E, post_wb_delta_E, post_wb_max_delta_E,
1781 post_mix_delta_E, post_mix_max_delta_E, temperature, string, g->mix[0][0], g->mix[0][1],
1782 g->mix[0][2], g->mix[1][0], g->mix[1][1], g->mix[1][2], g->mix[2][0], g->mix[2][1],
1783 g->mix[2][2], log2f(extraction_result.exposure), extraction_result.black);
1784
1785 dt_free_align(patches);
1786 }
1787
validate_color_checker(const float * const restrict in,const dt_iop_roi_t * const roi_in,dt_iop_channelmixer_rgb_gui_data_t * g,const dt_colormatrix_t RGB_to_XYZ,const dt_colormatrix_t XYZ_to_RGB,const dt_colormatrix_t XYZ_to_CAM)1788 void validate_color_checker(const float *const restrict in,
1789 const dt_iop_roi_t *const roi_in, dt_iop_channelmixer_rgb_gui_data_t *g,
1790 const dt_colormatrix_t RGB_to_XYZ, const dt_colormatrix_t XYZ_to_RGB, const dt_colormatrix_t XYZ_to_CAM)
1791 {
1792 float *const restrict patches = dt_alloc_sse_ps(4 * g->checker->patches);
1793 extraction_result_t extraction_result = _extract_patches(in, roi_in, g, RGB_to_XYZ, XYZ_to_CAM, patches, FALSE);
1794
1795 // Compute the delta E
1796 float pre_wb_delta_E = 0.f;
1797 float pre_wb_max_delta_E = 0.f;
1798 compute_patches_delta_E(patches, g->checker, g->delta_E_in, &pre_wb_delta_E, &pre_wb_max_delta_E);
1799
1800 gchar *diagnostic;
1801 if(pre_wb_delta_E <= 1.2f)
1802 diagnostic = _("very good");
1803 else if(pre_wb_delta_E <= 2.3f)
1804 diagnostic = _("good");
1805 else if(pre_wb_delta_E <= 3.4f)
1806 diagnostic = _("passable");
1807 else
1808 diagnostic = _("bad");
1809
1810 // Update GUI label
1811 g_free(g->delta_E_label_text);
1812 g->delta_E_label_text = g_strdup_printf(_("\n<b>Profile quality report : %s</b>\n"
1813 "output ΔE : \tavg. %.2f ; \tmax. %.2f\n\n"
1814 "<b>Normalization values</b>\n"
1815 "exposure compensation : \t%+.2f EV\n"
1816 "black offset : \t%+.4f"),
1817 diagnostic, pre_wb_delta_E, pre_wb_max_delta_E, log2f(extraction_result.exposure),
1818 extraction_result.black);
1819
1820 dt_free_align(patches);
1821 }
1822
_check_for_wb_issue_and_set_trouble_message(struct dt_iop_module_t * self)1823 static void _check_for_wb_issue_and_set_trouble_message(struct dt_iop_module_t *self)
1824 {
1825 dt_iop_channelmixer_rgb_params_t *p = (dt_iop_channelmixer_rgb_params_t *)self->params;
1826 if(self->enabled
1827 && !(p->illuminant == DT_ILLUMINANT_PIPE || p->adaptation == DT_ADAPTATION_RGB))
1828 {
1829 // this module instance is doing chromatic adaptation
1830 if(_is_another_module_cat_on_pipe(self))
1831 {
1832 // our second biggest problem : another channelmixerrgb instance is doing CAT
1833 // earlier in the pipe.
1834 dt_iop_set_module_trouble_message(self, _("double CAT applied"),
1835 _("you have 2 instances or more of color calibration,\n"
1836 "all performing chromatic adaptation.\n"
1837 "this can lead to inconsistencies, unless you\n"
1838 "use them with masks or know what you are doing."),
1839 "double CAT applied");
1840 return;
1841 }
1842 else if(!self->dev->proxy.wb_is_D65)
1843 {
1844 // our first and biggest problem : white balance module is being clever with WB coeffs
1845 dt_iop_set_module_trouble_message(self, _("white balance module error"),
1846 _("the white balance module is not using the camera\n"
1847 "reference illuminant, which will cause issues here\n"
1848 "with chromatic adaptation. either set it to reference\n"
1849 "or disable chromatic adaptation here."),
1850 "white balance error");
1851 return;
1852 }
1853 }
1854
1855 dt_iop_set_module_trouble_message(self, NULL, NULL, NULL);
1856 }
1857
process(struct dt_iop_module_t * self,dt_dev_pixelpipe_iop_t * piece,const void * const restrict ivoid,void * const restrict ovoid,const dt_iop_roi_t * const roi_in,const dt_iop_roi_t * const roi_out)1858 void process(struct dt_iop_module_t *self, dt_dev_pixelpipe_iop_t *piece,
1859 const void *const restrict ivoid, void *const restrict ovoid,
1860 const dt_iop_roi_t *const roi_in, const dt_iop_roi_t *const roi_out)
1861 {
1862 dt_iop_channelmixer_rbg_data_t *data = (dt_iop_channelmixer_rbg_data_t *)piece->data;
1863 const struct dt_iop_order_iccprofile_info_t *const work_profile = dt_ioppr_get_pipe_current_profile_info(self, piece->pipe);
1864 const struct dt_iop_order_iccprofile_info_t *const input_profile = dt_ioppr_get_pipe_input_profile_info(piece->pipe);
1865 dt_iop_channelmixer_rgb_gui_data_t *g = (dt_iop_channelmixer_rgb_gui_data_t *)self->gui_data;
1866
1867 if (!dt_iop_have_required_input_format(4 /*we need full-color pixels*/, self, piece->colors,
1868 ivoid, ovoid, roi_in, roi_out))
1869 return; // image has been copied through to output and module's trouble flag has been updated
1870
1871 declare_cat_on_pipe(self, FALSE);
1872
1873 // dt_iop_have_required_input_format() has reset the trouble message.
1874 // we must set it again in case of any trouble.
1875 _check_for_wb_issue_and_set_trouble_message(self);
1876
1877 dt_colormatrix_t RGB_to_XYZ;
1878 dt_colormatrix_t XYZ_to_RGB;
1879 dt_colormatrix_t XYZ_to_CAM;
1880
1881 // repack the matrices as flat AVX2-compliant matrice
1882 if(work_profile)
1883 {
1884 // work profile can't be fetched in commit_params since it is not yet initialised
1885 memcpy(RGB_to_XYZ, work_profile->matrix_in, sizeof(RGB_to_XYZ));
1886 memcpy(XYZ_to_RGB, work_profile->matrix_out, sizeof(XYZ_to_RGB));
1887 memcpy(XYZ_to_CAM, input_profile->matrix_out, sizeof(XYZ_to_CAM));
1888 }
1889
1890 assert(piece->colors == 4);
1891 const size_t ch = 4;
1892
1893 const float *const restrict in = (const float *const restrict)ivoid;
1894 float *const restrict out = (float *const restrict)ovoid;
1895
1896 // auto-detect WB upon request
1897 if(self->dev->gui_attached && g)
1898 {
1899 gboolean exit = FALSE;
1900
1901 if(g->run_profile && piece->pipe->type == DT_DEV_PIXELPIPE_PREVIEW)
1902 {
1903 dt_iop_gui_enter_critical_section(self);
1904 extract_color_checker(in, out, roi_in, g, RGB_to_XYZ, XYZ_to_RGB, XYZ_to_CAM, data->adaptation);
1905 g->run_profile = FALSE;
1906 dt_iop_gui_leave_critical_section(self);
1907 }
1908
1909 if(data->illuminant_type == DT_ILLUMINANT_DETECT_EDGES || data->illuminant_type == DT_ILLUMINANT_DETECT_SURFACES)
1910 {
1911 if(piece->pipe->type == DT_DEV_PIXELPIPE_FULL)
1912 {
1913 // detection on full image only
1914 dt_iop_gui_enter_critical_section(self);
1915 auto_detect_WB(in, data->illuminant_type, roi_in->width, roi_in->height, ch, RGB_to_XYZ, g->XYZ);
1916 dt_iop_gui_leave_critical_section(self);
1917 }
1918
1919 // passthrough pixels
1920 dt_iop_image_copy_by_size(out, in, roi_in->width, roi_in->height, ch);
1921
1922 dt_control_log(_("auto-detection of white balance completed"));
1923
1924 exit = TRUE;
1925 }
1926
1927 if(exit) return;
1928 }
1929
1930 if(data->illuminant_type == DT_ILLUMINANT_CAMERA)
1931 {
1932 // The camera illuminant is a behaviour rather than a preset of values:
1933 // it uses whatever is in the RAW EXIF. But it depends on what temperature.c is doing
1934 // and needs to be updated accordingly, to give a consistent result.
1935 // We initialise the CAT defaults using the temperature coeffs at startup, but if temperature
1936 // is changed later, we get no notification of the change here, so we can't update the defaults.
1937 // So we need to re-run the detection at runtime…
1938 float x, y;
1939 dt_aligned_pixel_t custom_wb;
1940 get_white_balance_coeff(self, custom_wb);
1941
1942 if(find_temperature_from_raw_coeffs(&(self->dev->image_storage), custom_wb, &(x), &(y)))
1943 {
1944 // Convert illuminant from xyY to XYZ
1945 dt_aligned_pixel_t XYZ;
1946 illuminant_xy_to_XYZ(x, y, XYZ);
1947
1948 // Convert illuminant from XYZ to Bradford modified LMS
1949 convert_any_XYZ_to_LMS(XYZ, data->illuminant, data->adaptation);
1950 data->illuminant[3] = 0.f;
1951 }
1952 else
1953 {
1954 // just use whatever was defined in commit_params hoping the defaults work…
1955 }
1956 }
1957
1958 // force loop unswitching in a controlled way
1959 switch(data->adaptation)
1960 {
1961 case DT_ADAPTATION_FULL_BRADFORD:
1962 {
1963 loop_switch(in, out, roi_out->width, roi_out->height, ch,
1964 XYZ_to_RGB, RGB_to_XYZ, data->MIX,
1965 data->illuminant, data->saturation, data->lightness, data->grey,
1966 data->p, data->gamut, data->clip, data->apply_grey, DT_ADAPTATION_FULL_BRADFORD, data->version);
1967 break;
1968 }
1969 case DT_ADAPTATION_LINEAR_BRADFORD:
1970 {
1971 loop_switch(in, out, roi_out->width, roi_out->height, ch,
1972 XYZ_to_RGB, RGB_to_XYZ, data->MIX,
1973 data->illuminant, data->saturation, data->lightness, data->grey,
1974 data->p, data->gamut, data->clip, data->apply_grey, DT_ADAPTATION_LINEAR_BRADFORD, data->version);
1975 break;
1976 }
1977 case DT_ADAPTATION_CAT16:
1978 {
1979 loop_switch(in, out, roi_out->width, roi_out->height, ch,
1980 XYZ_to_RGB, RGB_to_XYZ, data->MIX,
1981 data->illuminant, data->saturation, data->lightness, data->grey,
1982 data->p, data->gamut, data->clip, data->apply_grey, DT_ADAPTATION_CAT16, data->version);
1983 break;
1984 }
1985 case DT_ADAPTATION_XYZ:
1986 {
1987 loop_switch(in, out, roi_out->width, roi_out->height, ch,
1988 XYZ_to_RGB, RGB_to_XYZ, data->MIX,
1989 data->illuminant, data->saturation, data->lightness, data->grey,
1990 data->p, data->gamut, data->clip, data->apply_grey, DT_ADAPTATION_XYZ, data->version);
1991 break;
1992 }
1993 case DT_ADAPTATION_RGB:
1994 {
1995 loop_switch(in, out, roi_out->width, roi_out->height, ch,
1996 XYZ_to_RGB, RGB_to_XYZ, data->MIX,
1997 data->illuminant, data->saturation, data->lightness, data->grey,
1998 data->p, data->gamut, data->clip, data->apply_grey, DT_ADAPTATION_RGB, data->version);
1999 break;
2000 }
2001 case DT_ADAPTATION_LAST:
2002 {
2003 break;
2004 }
2005 }
2006
2007 // run dE validation at output
2008 if(self->dev->gui_attached && g)
2009 if(g->run_validation && piece->pipe->type == DT_DEV_PIXELPIPE_PREVIEW)
2010 {
2011 validate_color_checker(out, roi_out, g, RGB_to_XYZ, XYZ_to_RGB, XYZ_to_CAM);
2012 g->run_validation = FALSE;
2013 }
2014 }
2015
2016 #if HAVE_OPENCL
process_cl(struct dt_iop_module_t * self,dt_dev_pixelpipe_iop_t * piece,cl_mem dev_in,cl_mem dev_out,const dt_iop_roi_t * const roi_in,const dt_iop_roi_t * const roi_out)2017 int process_cl(struct dt_iop_module_t *self, dt_dev_pixelpipe_iop_t *piece, cl_mem dev_in, cl_mem dev_out,
2018 const dt_iop_roi_t *const roi_in, const dt_iop_roi_t *const roi_out)
2019 {
2020 dt_iop_channelmixer_rbg_data_t *const d = (dt_iop_channelmixer_rbg_data_t *)piece->data;
2021 dt_iop_channelmixer_rgb_global_data_t *const gd = (dt_iop_channelmixer_rgb_global_data_t *)self->global_data;
2022 const struct dt_iop_order_iccprofile_info_t *const work_profile = dt_ioppr_get_pipe_current_profile_info(self, piece->pipe);
2023 //dt_iop_channelmixer_rgb_gui_data_t *g = (dt_iop_channelmixer_rgb_gui_data_t *)self->gui_data;
2024
2025 declare_cat_on_pipe(self, FALSE);
2026
2027 // dt_iop_have_required_input_format() has reset the trouble message.
2028 // we must set it again in case of any trouble.
2029 _check_for_wb_issue_and_set_trouble_message(self);
2030
2031 if(d->illuminant_type == DT_ILLUMINANT_CAMERA)
2032 {
2033 // The camera illuminant is a behaviour rather than a preset of values:
2034 // it uses whatever is in the RAW EXIF. But it depends on what temperature.c is doing
2035 // and needs to be updated accordingly, to give a consistent result.
2036 // We initialise the CAT defaults using the temperature coeffs at startup, but if temperature
2037 // is changed later, we get no notification of the change here, so we can't update the defaults.
2038 // So we need to re-run the detection at runtime…
2039 float x, y;
2040 dt_aligned_pixel_t custom_wb;
2041 get_white_balance_coeff(self, custom_wb);
2042
2043 if(find_temperature_from_raw_coeffs(&(self->dev->image_storage), custom_wb, &(x), &(y)))
2044 {
2045 // Convert illuminant from xyY to XYZ
2046 dt_aligned_pixel_t XYZ;
2047 illuminant_xy_to_XYZ(x, y, XYZ);
2048
2049 // Convert illuminant from XYZ to Bradford modified LMS
2050 convert_any_XYZ_to_LMS(XYZ, d->illuminant, d->adaptation);
2051 d->illuminant[3] = 0.f;
2052 }
2053 }
2054
2055 cl_int err = -999;
2056
2057 if(piece->colors != 4)
2058 {
2059 dt_control_log(_("channelmixerrgb works only on RGB input"));
2060 return err;
2061 }
2062
2063 const int devid = piece->pipe->devid;
2064 const int width = roi_in->width;
2065 const int height = roi_in->height;
2066
2067 size_t sizes[] = { ROUNDUPWD(width), ROUNDUPHT(height), 1 };
2068
2069 cl_mem input_matrix_cl = NULL;
2070 cl_mem output_matrix_cl = NULL;
2071
2072 input_matrix_cl = dt_opencl_copy_host_to_device_constant(devid, 12 * sizeof(float), (float*)work_profile->matrix_in);
2073 output_matrix_cl = dt_opencl_copy_host_to_device_constant(devid, 12 * sizeof(float), (float*)work_profile->matrix_out);
2074 cl_mem MIX_cl = dt_opencl_copy_host_to_device_constant(devid, 12 * sizeof(float), d->MIX);
2075
2076 // select the right kernel for the current LMS space
2077 int kernel = gd->kernel_channelmixer_rgb_rgb;
2078
2079 switch(d->adaptation)
2080 {
2081 case DT_ADAPTATION_FULL_BRADFORD:
2082 {
2083 kernel = gd->kernel_channelmixer_rgb_bradford_full;
2084 break;
2085 }
2086 case DT_ADAPTATION_LINEAR_BRADFORD:
2087 {
2088 kernel = gd->kernel_channelmixer_rgb_bradford_linear;
2089 break;
2090 }
2091 case DT_ADAPTATION_CAT16:
2092 {
2093 kernel = gd->kernel_channelmixer_rgb_cat16;
2094 break;
2095 }
2096 case DT_ADAPTATION_XYZ:
2097 {
2098 kernel = gd->kernel_channelmixer_rgb_xyz;
2099 break;
2100 }
2101 case DT_ADAPTATION_RGB:
2102 case DT_ADAPTATION_LAST:
2103 {
2104 kernel = gd->kernel_channelmixer_rgb_rgb;
2105 break;
2106 }
2107 }
2108
2109 dt_opencl_set_kernel_arg(devid, kernel, 0, sizeof(cl_mem), (void *)&dev_in);
2110 dt_opencl_set_kernel_arg(devid, kernel, 1, sizeof(cl_mem), (void *)&dev_out);
2111 dt_opencl_set_kernel_arg(devid, kernel, 2, sizeof(int), (void *)&width);
2112 dt_opencl_set_kernel_arg(devid, kernel, 3, sizeof(int), (void *)&height);
2113 dt_opencl_set_kernel_arg(devid, kernel, 4, sizeof(cl_mem), (void *)&input_matrix_cl);
2114 dt_opencl_set_kernel_arg(devid, kernel, 5, sizeof(cl_mem), (void *)&output_matrix_cl);
2115 dt_opencl_set_kernel_arg(devid, kernel, 6, sizeof(cl_mem), (void *)&MIX_cl);
2116 dt_opencl_set_kernel_arg(devid, kernel, 7, 4 * sizeof(float), (void *)&d->illuminant);
2117 dt_opencl_set_kernel_arg(devid, kernel, 8, 4 * sizeof(float), (void *)&d->saturation);
2118 dt_opencl_set_kernel_arg(devid, kernel, 9, 4 * sizeof(float), (void *)&d->lightness);
2119 dt_opencl_set_kernel_arg(devid, kernel, 10, 4 * sizeof(float), (void *)&d->grey);
2120 dt_opencl_set_kernel_arg(devid, kernel, 11, sizeof(float), (void *)&d->p);
2121 dt_opencl_set_kernel_arg(devid, kernel, 12, sizeof(float), (void *)&d->gamut);
2122 dt_opencl_set_kernel_arg(devid, kernel, 13, sizeof(int), (void *)&d->clip);
2123 dt_opencl_set_kernel_arg(devid, kernel, 14, sizeof(int), (void *)&d->apply_grey);
2124 dt_opencl_set_kernel_arg(devid, kernel, 15, sizeof(int), (void *)&d->version);
2125 err = dt_opencl_enqueue_kernel_2d(devid, kernel, sizes);
2126 if(err != CL_SUCCESS) goto error;
2127
2128 dt_opencl_release_mem_object(input_matrix_cl);
2129 dt_opencl_release_mem_object(output_matrix_cl);
2130 dt_opencl_release_mem_object(MIX_cl);
2131 return TRUE;
2132
2133 error:
2134 if(input_matrix_cl) dt_opencl_release_mem_object(input_matrix_cl);
2135 if(output_matrix_cl) dt_opencl_release_mem_object(output_matrix_cl);
2136 if(MIX_cl) dt_opencl_release_mem_object(MIX_cl);
2137 dt_print(DT_DEBUG_OPENCL, "[opencl_channelmixerrgb] couldn't enqueue kernel! %d\n", err);
2138 return FALSE;
2139 }
2140
init_global(dt_iop_module_so_t * module)2141 void init_global(dt_iop_module_so_t *module)
2142 {
2143 const int program = 32; // extended.cl in programs.conf
2144 dt_iop_channelmixer_rgb_global_data_t *gd
2145 = (dt_iop_channelmixer_rgb_global_data_t *)malloc(sizeof(dt_iop_channelmixer_rgb_global_data_t));
2146
2147 module->data = gd;
2148 gd->kernel_channelmixer_rgb_cat16 = dt_opencl_create_kernel(program, "channelmixerrgb_CAT16");
2149 gd->kernel_channelmixer_rgb_bradford_full = dt_opencl_create_kernel(program, "channelmixerrgb_bradford_full");
2150 gd->kernel_channelmixer_rgb_bradford_linear = dt_opencl_create_kernel(program, "channelmixerrgb_bradford_linear");
2151 gd->kernel_channelmixer_rgb_xyz = dt_opencl_create_kernel(program, "channelmixerrgb_XYZ");
2152 gd->kernel_channelmixer_rgb_rgb = dt_opencl_create_kernel(program, "channelmixerrgb_RGB");
2153 }
2154
2155
cleanup_global(dt_iop_module_so_t * module)2156 void cleanup_global(dt_iop_module_so_t *module)
2157 {
2158 dt_iop_channelmixer_rgb_global_data_t *gd = (dt_iop_channelmixer_rgb_global_data_t *)module->data;
2159 dt_opencl_free_kernel(gd->kernel_channelmixer_rgb_cat16);
2160 dt_opencl_free_kernel(gd->kernel_channelmixer_rgb_bradford_full);
2161 dt_opencl_free_kernel(gd->kernel_channelmixer_rgb_bradford_linear);
2162 dt_opencl_free_kernel(gd->kernel_channelmixer_rgb_xyz);
2163 dt_opencl_free_kernel(gd->kernel_channelmixer_rgb_rgb);
2164 free(module->data);
2165 module->data = NULL;
2166 }
2167 #endif
2168
2169
update_bounding_box(dt_iop_channelmixer_rgb_gui_data_t * g,const float x_increment,const float y_increment)2170 static inline void update_bounding_box(dt_iop_channelmixer_rgb_gui_data_t *g,
2171 const float x_increment, const float y_increment)
2172 {
2173 // update box nodes
2174 for(size_t k = 0; k < 4; k++)
2175 {
2176 if(g->active_node[k])
2177 {
2178 g->box[k].x += x_increment;
2179 g->box[k].y += y_increment;
2180 }
2181 }
2182
2183 // update the homography
2184 get_homography(g->ideal_box, g->box, g->homography);
2185 get_homography(g->box, g->ideal_box, g->inverse_homography);
2186 }
2187
init_bounding_box(dt_iop_channelmixer_rgb_gui_data_t * g,const float width,const float height)2188 static inline void init_bounding_box(dt_iop_channelmixer_rgb_gui_data_t *g, const float width, const float height)
2189 {
2190 if(!g->checker_ready)
2191 {
2192 // top left
2193 g->box[0].x = g->box[0].y = 10.;
2194
2195 // top right
2196 g->box[1].x = (width - 10.);
2197 g->box[1].y = g->box[0].y;
2198
2199 // bottom right
2200 g->box[2].x = g->box[1].x;
2201 g->box[2].y = (width - 10.) * g->checker->ratio;
2202
2203 // bottom left
2204 g->box[3].x = g->box[0].x;
2205 g->box[3].y = g->box[2].y;
2206
2207 g->checker_ready = TRUE;
2208 }
2209
2210 g->center_box.x = 0.5f;
2211 g->center_box.y = 0.5f;
2212
2213 g->ideal_box[0].x = 0.f;
2214 g->ideal_box[0].y = 0.f;
2215 g->ideal_box[1].x = 1.f;
2216 g->ideal_box[1].y = 0.f;
2217 g->ideal_box[2].x = 1.f;
2218 g->ideal_box[2].y = 1.f;
2219 g->ideal_box[3].x = 0.f;
2220 g->ideal_box[3].y = 1.f;
2221
2222 update_bounding_box(g, 0.f, 0.f);
2223 }
2224
2225
2226
mouse_moved(struct dt_iop_module_t * self,double x,double y,double pressure,int which)2227 int mouse_moved(struct dt_iop_module_t *self, double x, double y, double pressure, int which)
2228 {
2229 if(!self->enabled) return 0;
2230
2231 dt_iop_channelmixer_rgb_gui_data_t *g = (dt_iop_channelmixer_rgb_gui_data_t *)self->gui_data;
2232 if(g == NULL || !g->is_profiling_started) return 0;
2233 if(g->box[0].x == -1.0f || g->box[1].y == -1.0f) return 0;
2234
2235 dt_develop_t *dev = self->dev;
2236 const float wd = dev->preview_pipe->backbuf_width;
2237 const float ht = dev->preview_pipe->backbuf_height;
2238 if(wd == 0.f || ht == 0.f) return 0;
2239
2240 float pzx, pzy;
2241 dt_dev_get_pointer_zoom_pos(dev, x, y, &pzx, &pzy);
2242 pzx += 0.5f;
2243 pzy += 0.5f;
2244 pzx *= wd;
2245 pzy *= ht;
2246
2247 // if dragging and dropping, don't update active nodes,
2248 // just update cursor coordinates then redraw
2249 // this ensure smooth updates
2250 if(g->drag_drop)
2251 {
2252 dt_iop_gui_enter_critical_section(self);
2253 g->click_end.x = pzx;
2254 g->click_end.y = pzy;
2255
2256 update_bounding_box(g, g->click_end.x - g->click_start.x, g->click_end.y - g->click_start.y);
2257
2258 g->click_start.x = pzx;
2259 g->click_start.y = pzy;
2260 dt_iop_gui_leave_critical_section(self);
2261
2262 dt_control_queue_redraw_center();
2263 return 1;
2264 }
2265
2266 // Find out if we are close to a node
2267 dt_iop_gui_enter_critical_section(self);
2268 g->is_cursor_close = FALSE;
2269
2270 for(size_t k = 0; k < 4; k++)
2271 {
2272 if(hypotf(pzx - g->box[k].x, pzy - g->box[k].y) < 15.f)
2273 {
2274 g->active_node[k] = TRUE;
2275 g->is_cursor_close = TRUE;
2276 }
2277 else
2278 g->active_node[k] = FALSE;
2279 }
2280 dt_iop_gui_leave_critical_section(self);
2281
2282 // if cursor is close from a node, remove the system pointer arrow to prevent hiding the spot behind it
2283 if(g->is_cursor_close)
2284 {
2285 dt_control_change_cursor(GDK_BLANK_CURSOR);
2286 }
2287 else
2288 {
2289 // fall back to default cursor
2290 GdkCursor *const cursor = gdk_cursor_new_from_name(gdk_display_get_default(), "default");
2291 gdk_window_set_cursor(gtk_widget_get_window(dt_ui_main_window(darktable.gui->ui)), cursor);
2292 g_object_unref(cursor);
2293 }
2294
2295 dt_control_queue_redraw_center();
2296
2297 return 1;
2298 }
2299
button_pressed(struct dt_iop_module_t * self,double x,double y,double pressure,int which,int type,uint32_t state)2300 int button_pressed(struct dt_iop_module_t *self, double x, double y, double pressure, int which, int type,
2301 uint32_t state)
2302 {
2303 if(!self->enabled) return 0;
2304
2305 dt_iop_channelmixer_rgb_gui_data_t *g = (dt_iop_channelmixer_rgb_gui_data_t *)self->gui_data;
2306 if(g == NULL || !g->is_profiling_started) return 0;
2307
2308 dt_develop_t *dev = self->dev;
2309 const float wd = dev->preview_pipe->backbuf_width;
2310 const float ht = dev->preview_pipe->backbuf_height;
2311 if(wd == 0.f || ht == 0.f) return 0;
2312
2313 // double click : reset the perspective correction
2314 if(type == GDK_DOUBLE_BUTTON_PRESS)
2315 {
2316 dt_iop_gui_enter_critical_section(self);
2317 g->checker_ready = FALSE;
2318 g->profile_ready = FALSE;
2319 init_bounding_box(g, wd, ht);
2320 dt_iop_gui_leave_critical_section(self);
2321
2322 dt_control_queue_redraw_center();
2323 return 1;
2324 }
2325
2326 // bounded box not inited, abort
2327 if(g->box[0].x == -1.0f || g->box[1].y == -1.0f) return 0;
2328
2329 // cursor is not on a node, abort
2330 if(!g->is_cursor_close) return 0;
2331
2332 float pzx, pzy;
2333 dt_dev_get_pointer_zoom_pos(dev, x, y, &pzx, &pzy);
2334 pzx += 0.5f;
2335 pzy += 0.5f;
2336 pzx *= wd;
2337 pzy *= ht;
2338
2339 dt_iop_gui_enter_critical_section(self);
2340 g->drag_drop = TRUE;
2341 g->click_start.x = pzx;
2342 g->click_start.y = pzy;
2343 dt_iop_gui_leave_critical_section(self);
2344
2345 dt_control_queue_redraw_center();
2346
2347 return 1;
2348 }
2349
button_released(struct dt_iop_module_t * self,double x,double y,int which,uint32_t state)2350 int button_released(struct dt_iop_module_t *self, double x, double y, int which, uint32_t state)
2351 {
2352 if(!self->enabled) return 0;
2353
2354 dt_iop_channelmixer_rgb_gui_data_t *g = (dt_iop_channelmixer_rgb_gui_data_t *)self->gui_data;
2355 if(g == NULL || !g->is_profiling_started) return 0;
2356 if(g->box[0].x == -1.0f || g->box[1].y == -1.0f) return 0;
2357 if(!g->is_cursor_close || !g->drag_drop) return 0;
2358
2359 dt_develop_t *dev = self->dev;
2360 const float wd = dev->preview_pipe->backbuf_width;
2361 const float ht = dev->preview_pipe->backbuf_height;
2362 if(wd == 0.f || ht == 0.f) return 0;
2363
2364 float pzx, pzy;
2365 dt_dev_get_pointer_zoom_pos(dev, x, y, &pzx, &pzy);
2366 pzx += 0.5f;
2367 pzy += 0.5f;
2368 pzx *= wd;
2369 pzy *= ht;
2370
2371 dt_iop_gui_enter_critical_section(self);
2372 g->drag_drop = FALSE;
2373 g->click_end.x = pzx;
2374 g->click_end.y = pzy;
2375 update_bounding_box(g, g->click_end.x - g->click_start.x, g->click_end.y - g->click_start.y);
2376 dt_iop_gui_leave_critical_section(self);
2377
2378 dt_control_queue_redraw_center();
2379
2380 return 1;
2381 }
2382
gui_post_expose(struct dt_iop_module_t * self,cairo_t * cr,int32_t width,int32_t height,int32_t pointerx,int32_t pointery)2383 void gui_post_expose(struct dt_iop_module_t *self, cairo_t *cr, int32_t width, int32_t height,
2384 int32_t pointerx, int32_t pointery)
2385 {
2386 const dt_iop_order_iccprofile_info_t *const work_profile = dt_ioppr_get_pipe_output_profile_info(self->dev->pipe);
2387 if(work_profile == NULL) return;
2388
2389 dt_iop_channelmixer_rgb_gui_data_t *g = (dt_iop_channelmixer_rgb_gui_data_t *)self->gui_data;
2390 if(!g->is_profiling_started) return;
2391
2392 // Rescale and shift Cairo drawing coordinates
2393 dt_develop_t *dev = self->dev;
2394 const float wd = dev->preview_pipe->backbuf_width;
2395 const float ht = dev->preview_pipe->backbuf_height;
2396 if(wd == 0.f || ht == 0.f) return;
2397
2398 const float zoom_y = dt_control_get_dev_zoom_y();
2399 const float zoom_x = dt_control_get_dev_zoom_x();
2400 const dt_dev_zoom_t zoom = dt_control_get_dev_zoom();
2401 const int closeup = dt_control_get_dev_closeup();
2402 const float zoom_scale = dt_dev_get_zoom_scale(dev, zoom, 1<<closeup, 1);
2403 cairo_translate(cr, width / 2.0, height / 2.0);
2404 cairo_scale(cr, zoom_scale, zoom_scale);
2405 cairo_translate(cr, -.5f * wd - zoom_x * wd, -.5f * ht - zoom_y * ht);
2406
2407 cairo_set_line_width(cr, 2.0 / zoom_scale);
2408 const double origin = 9. / zoom_scale;
2409 const double destination = 18. / zoom_scale;
2410
2411 for(size_t k = 0; k < 4; k++)
2412 {
2413 if(g->active_node[k])
2414 {
2415 // draw cross hair
2416 cairo_set_source_rgba(cr, 1., 1., 1., 1.);
2417
2418 cairo_move_to(cr, g->box[k].x - origin, g->box[k].y);
2419 cairo_line_to(cr, g->box[k].x - destination, g->box[k].y);
2420
2421 cairo_move_to(cr, g->box[k].x + origin, g->box[k].y);
2422 cairo_line_to(cr, g->box[k].x + destination, g->box[k].y);
2423
2424 cairo_move_to(cr, g->box[k].x, g->box[k].y - origin);
2425 cairo_line_to(cr, g->box[k].x, g->box[k].y - destination);
2426
2427 cairo_move_to(cr, g->box[k].x, g->box[k].y + origin);
2428 cairo_line_to(cr, g->box[k].x, g->box[k].y + destination);
2429
2430 cairo_stroke(cr);
2431 }
2432
2433 // draw outline circle
2434 cairo_set_source_rgba(cr, 1., 1., 1., 1.);
2435 cairo_arc(cr, g->box[k].x, g->box[k].y, 8. / zoom_scale, 0, 2. * M_PI);
2436 cairo_stroke(cr);
2437
2438 // draw black dot
2439 cairo_set_source_rgba(cr, 0., 0., 0., 1.);
2440 cairo_arc(cr, g->box[k].x, g->box[k].y, 1.5 / zoom_scale, 0, 2. * M_PI);
2441 cairo_fill(cr);
2442 }
2443
2444 // draw symmetry axes
2445 cairo_set_line_width(cr, 1.5 / zoom_scale);
2446 cairo_set_source_rgba(cr, 1., 1., 1., 1.);
2447 const point_t top_ideal = { 0.5f, 1.f };
2448 const point_t top = apply_homography(top_ideal, g->homography);
2449 const point_t bottom_ideal = { 0.5f, 0.f };
2450 const point_t bottom = apply_homography(bottom_ideal, g->homography);
2451 cairo_move_to(cr, top.x, top.y);
2452 cairo_line_to(cr, bottom.x, bottom.y);
2453 cairo_stroke(cr);
2454
2455 const point_t left_ideal = { 0.f, 0.5f };
2456 const point_t left = apply_homography(left_ideal, g->homography);
2457 const point_t right_ideal = { 1.f, 0.5f };
2458 const point_t right = apply_homography(right_ideal, g->homography);
2459 cairo_move_to(cr, left.x, left.y);
2460 cairo_line_to(cr, right.x, right.y);
2461 cairo_stroke(cr);
2462
2463 /* For debug : display center of the image and center of the ideal target
2464 point_t new_target_center = apply_homography(target_center, g->homography);
2465 cairo_set_source_rgba(cr, 1., 1., 1., 1.);
2466 cairo_arc(cr, new_target_center.x, new_target_center.y, 7., 0, 2. * M_PI);
2467 cairo_stroke(cr);
2468
2469 cairo_set_source_rgba(cr, 0., 1., 1., 1.);
2470 cairo_arc(cr, 0.5 * wd, 0.5 * ht, 7., 0, 2. * M_PI);
2471 cairo_stroke(cr);
2472 */
2473
2474 const float radius_x = g->checker->radius * hypotf(1.f, g->checker->ratio) * g->safety_margin;
2475 const float radius_y = radius_x / g->checker->ratio;
2476
2477 for(size_t k = 0; k < g->checker->patches; k++)
2478 {
2479 // center of the patch in the ideal reference
2480 const point_t center = { g->checker->values[k].x, g->checker->values[k].y };
2481
2482 // corners of the patch in the ideal reference
2483 const point_t corners[4] = { {center.x - radius_x, center.y - radius_y},
2484 {center.x + radius_x, center.y - radius_y},
2485 {center.x + radius_x, center.y + radius_y},
2486 {center.x - radius_x, center.y + radius_y} };
2487
2488 // apply patch coordinates transform depending on perspective
2489 const point_t new_center = apply_homography(center, g->homography);
2490 // apply_homography_scaling gives a scaling of areas. we need to scale the
2491 // radius of the center circle so take a square root.
2492 const float scaling = sqrtf(apply_homography_scaling(center, g->homography));
2493 point_t new_corners[4];
2494 for(size_t c = 0; c < 4; c++) new_corners[c] = apply_homography(corners[c], g->homography);
2495
2496 cairo_set_line_cap(cr, CAIRO_LINE_CAP_SQUARE);
2497 cairo_set_source_rgba(cr, 0., 0., 0., 1.);
2498 cairo_move_to(cr, new_corners[0].x, new_corners[0].y);
2499 cairo_line_to(cr, new_corners[1].x, new_corners[1].y);
2500 cairo_line_to(cr, new_corners[2].x, new_corners[2].y);
2501 cairo_line_to(cr, new_corners[3].x, new_corners[3].y);
2502 cairo_line_to(cr, new_corners[0].x, new_corners[0].y);
2503
2504 if(g->delta_E_in)
2505 {
2506 // draw delta E feedback
2507 if(g->delta_E_in[k] > 2.3f)
2508 {
2509 // one diagonal if delta E > 3
2510 cairo_move_to(cr, new_corners[0].x, new_corners[0].y);
2511 cairo_line_to(cr, new_corners[2].x, new_corners[2].y);
2512 }
2513 if(g->delta_E_in[k] > 4.6f)
2514 {
2515 // the other diagonal if delta E > 6
2516 cairo_move_to(cr, new_corners[1].x, new_corners[1].y);
2517 cairo_line_to(cr, new_corners[3].x, new_corners[3].y);
2518 }
2519 }
2520
2521 cairo_set_line_width(cr, 5.0 / zoom_scale);
2522 cairo_stroke_preserve(cr);
2523 cairo_set_line_width(cr, 2.0 / zoom_scale);
2524 cairo_set_source_rgba(cr, 1., 1., 1., 1.);
2525 cairo_stroke(cr);
2526
2527 cairo_set_line_cap(cr, CAIRO_LINE_CAP_BUTT);
2528
2529 dt_aligned_pixel_t RGB;
2530 dt_ioppr_lab_to_rgb_matrix(g->checker->values[k].Lab, RGB, work_profile->matrix_out_transposed, work_profile->lut_out,
2531 work_profile->unbounded_coeffs_out, work_profile->lutsize,
2532 work_profile->nonlinearlut);
2533
2534 cairo_set_source_rgba(cr, RGB[0], RGB[1], RGB[2], 1.);
2535 cairo_arc(cr, new_center.x, new_center.y, 0.25 * (radius_x + radius_y) * scaling, 0, 2. * M_PI);
2536 cairo_fill(cr);
2537 }
2538 }
2539
optimize_changed_callback(GtkWidget * widget,gpointer user_data)2540 static void optimize_changed_callback(GtkWidget *widget, gpointer user_data)
2541 {
2542 if(darktable.gui->reset) return;
2543 dt_iop_module_t *self = (dt_iop_module_t *)user_data;
2544 dt_iop_channelmixer_rgb_gui_data_t *g = (dt_iop_channelmixer_rgb_gui_data_t *)self->gui_data;
2545
2546 const int i = dt_bauhaus_combobox_get(widget);
2547 dt_conf_set_int("darkroom/modules/channelmixerrgb/optimization", i);
2548
2549 dt_iop_gui_enter_critical_section(self);
2550 g->optimization = i;
2551 dt_iop_gui_leave_critical_section(self);
2552 }
2553
checker_changed_callback(GtkWidget * widget,gpointer user_data)2554 static void checker_changed_callback(GtkWidget *widget, gpointer user_data)
2555 {
2556 if(darktable.gui->reset) return;
2557 dt_iop_module_t *self = (dt_iop_module_t *)user_data;
2558 dt_iop_channelmixer_rgb_gui_data_t *g = (dt_iop_channelmixer_rgb_gui_data_t *)self->gui_data;
2559
2560 const int i = dt_bauhaus_combobox_get(widget);
2561 dt_conf_set_int("darkroom/modules/channelmixerrgb/colorchecker", i);
2562 g->checker = dt_get_color_checker(i);
2563
2564 dt_develop_t *dev = self->dev;
2565 const float wd = dev->preview_pipe->backbuf_width;
2566 const float ht = dev->preview_pipe->backbuf_height;
2567 if(wd == 0.f || ht == 0.f) return;
2568
2569 dt_iop_gui_enter_critical_section(self);
2570 g->profile_ready = FALSE;
2571 init_bounding_box(g, wd, ht);
2572 dt_iop_gui_leave_critical_section(self);
2573
2574 dt_control_queue_redraw_center();
2575 }
2576
safety_changed_callback(GtkWidget * widget,gpointer user_data)2577 static void safety_changed_callback(GtkWidget *widget, gpointer user_data)
2578 {
2579 if(darktable.gui->reset) return;
2580 dt_iop_module_t *self = (dt_iop_module_t *)user_data;
2581 dt_iop_channelmixer_rgb_gui_data_t *g = (dt_iop_channelmixer_rgb_gui_data_t *)self->gui_data;
2582
2583 dt_iop_gui_enter_critical_section(self);
2584 g->safety_margin = dt_bauhaus_slider_get(widget);
2585 dt_iop_gui_leave_critical_section(self);
2586
2587 dt_conf_set_float("darkroom/modules/channelmixerrgb/safety", g->safety_margin);
2588 dt_control_queue_redraw_center();
2589 }
2590
2591
start_profiling_callback(GtkWidget * togglebutton,dt_iop_module_t * self)2592 static void start_profiling_callback(GtkWidget *togglebutton, dt_iop_module_t *self)
2593 {
2594 if(darktable.gui->reset) return;
2595 dt_iop_request_focus(self);
2596 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(self->off), TRUE);
2597
2598 dt_develop_t *dev = self->dev;
2599 const float wd = dev->preview_pipe->backbuf_width;
2600 const float ht = dev->preview_pipe->backbuf_height;
2601 if(wd == 0.f || ht == 0.f) return;
2602
2603 dt_iop_channelmixer_rgb_gui_data_t *g = (dt_iop_channelmixer_rgb_gui_data_t *)self->gui_data;
2604 g->is_profiling_started = !g->is_profiling_started;
2605 gtk_widget_set_visible(g->collapsible, g->is_profiling_started);
2606
2607 if(g->is_profiling_started)
2608 dt_bauhaus_widget_set_quad_paint(g->start_profiling, dtgtk_cairo_paint_solid_arrow, CPF_STYLE_BOX | CPF_DIRECTION_DOWN, NULL);
2609 else
2610 dt_bauhaus_widget_set_quad_paint(g->start_profiling, dtgtk_cairo_paint_solid_arrow, CPF_STYLE_BOX | CPF_DIRECTION_LEFT, NULL);
2611
2612 // init bounding box
2613 dt_iop_gui_enter_critical_section(self);
2614 init_bounding_box(g, wd, ht);
2615 dt_iop_gui_leave_critical_section(self);
2616
2617 dt_control_queue_redraw_center();
2618 }
2619
run_profile_callback(GtkWidget * widget,GdkEventButton * event,gpointer user_data)2620 static void run_profile_callback(GtkWidget *widget, GdkEventButton *event, gpointer user_data)
2621 {
2622 if(darktable.gui->reset) return;
2623 dt_iop_module_t *self = (dt_iop_module_t *)user_data;
2624 dt_iop_channelmixer_rgb_gui_data_t *g = (dt_iop_channelmixer_rgb_gui_data_t *)self->gui_data;
2625
2626 dt_iop_gui_enter_critical_section(self);
2627 g->run_profile = TRUE;
2628 dt_iop_gui_leave_critical_section(self);
2629
2630 dt_dev_reprocess_preview(self->dev);
2631 }
2632
run_validation_callback(GtkWidget * widget,GdkEventButton * event,gpointer user_data)2633 static void run_validation_callback(GtkWidget *widget, GdkEventButton *event, gpointer user_data)
2634 {
2635 if(darktable.gui->reset) return;
2636 dt_iop_module_t *self = (dt_iop_module_t *)user_data;
2637 dt_iop_channelmixer_rgb_gui_data_t *g = (dt_iop_channelmixer_rgb_gui_data_t *)self->gui_data;
2638
2639 dt_iop_gui_enter_critical_section(self);
2640 g->run_validation = TRUE;
2641 dt_iop_gui_leave_critical_section(self);
2642
2643 dt_dev_reprocess_preview(self->dev);
2644 }
2645
commit_profile_callback(GtkWidget * widget,GdkEventButton * event,gpointer user_data)2646 static void commit_profile_callback(GtkWidget *widget, GdkEventButton *event, gpointer user_data)
2647 {
2648 if(darktable.gui->reset) return;
2649 dt_iop_module_t *self = (dt_iop_module_t *)user_data;
2650 dt_iop_channelmixer_rgb_gui_data_t *g = (dt_iop_channelmixer_rgb_gui_data_t *)self->gui_data;
2651 dt_iop_channelmixer_rgb_params_t *p = (dt_iop_channelmixer_rgb_params_t *)self->params;
2652
2653 if(!g->profile_ready) return;
2654
2655 dt_iop_gui_enter_critical_section(self);
2656
2657 p->x = g->xy[0];
2658 p->y = g->xy[1];
2659 p->illuminant = DT_ILLUMINANT_CUSTOM;
2660 check_if_close_to_daylight(p->x, p->y, &p->temperature, NULL, NULL);
2661
2662 p->red[0] = g->mix[0][0];
2663 p->red[1] = g->mix[0][1];
2664 p->red[2] = g->mix[0][2];
2665
2666 p->green[0] = g->mix[1][0];
2667 p->green[1] = g->mix[1][1];
2668 p->green[2] = g->mix[1][2];
2669
2670 p->blue[0] = g->mix[2][0];
2671 p->blue[1] = g->mix[2][1];
2672 p->blue[2] = g->mix[2][2];
2673
2674 dt_iop_gui_leave_critical_section(self);
2675
2676 ++darktable.gui->reset;
2677 dt_bauhaus_combobox_set(g->illuminant, p->illuminant);
2678 dt_bauhaus_slider_set_soft(g->temperature, p->temperature);
2679
2680 dt_aligned_pixel_t xyY = { p->x, p->y, 1.f };
2681 dt_aligned_pixel_t Lch = { 0 };
2682 dt_xyY_to_Lch(xyY, Lch);
2683 dt_bauhaus_slider_set(g->illum_x, Lch[2] / M_PI * 180.f);
2684 dt_bauhaus_slider_set_soft(g->illum_y, Lch[1]);
2685
2686 dt_bauhaus_slider_set_soft(g->scale_red_R, p->red[0]);
2687 dt_bauhaus_slider_set_soft(g->scale_red_G, p->red[1]);
2688 dt_bauhaus_slider_set_soft(g->scale_red_B, p->red[2]);
2689
2690 dt_bauhaus_slider_set_soft(g->scale_green_R, p->green[0]);
2691 dt_bauhaus_slider_set_soft(g->scale_green_G, p->green[1]);
2692 dt_bauhaus_slider_set_soft(g->scale_green_B, p->green[2]);
2693
2694 dt_bauhaus_slider_set_soft(g->scale_blue_R, p->blue[0]);
2695 dt_bauhaus_slider_set_soft(g->scale_blue_G, p->blue[1]);
2696 dt_bauhaus_slider_set_soft(g->scale_blue_B, p->blue[2]);
2697
2698 --darktable.gui->reset;
2699
2700 gui_changed(self, NULL, NULL);
2701
2702 dt_dev_add_history_item(darktable.develop, self, TRUE);
2703 }
2704
_develop_ui_pipe_finished_callback(gpointer instance,gpointer user_data)2705 static void _develop_ui_pipe_finished_callback(gpointer instance, gpointer user_data)
2706 {
2707 dt_iop_module_t *self = (dt_iop_module_t *)user_data;
2708 dt_iop_channelmixer_rgb_gui_data_t *g = (dt_iop_channelmixer_rgb_gui_data_t *)self->gui_data;
2709 dt_iop_channelmixer_rgb_params_t *p = (dt_iop_channelmixer_rgb_params_t *)self->params;
2710
2711 if(g == NULL) return;
2712 if(p->illuminant != DT_ILLUMINANT_DETECT_EDGES && p->illuminant != DT_ILLUMINANT_DETECT_SURFACES)
2713 {
2714 gui_changed(self, NULL, NULL);
2715 return;
2716 }
2717
2718 dt_iop_gui_enter_critical_section(self);
2719 p->x = g->XYZ[0];
2720 p->y = g->XYZ[1];
2721 dt_iop_gui_leave_critical_section(self);
2722
2723 check_if_close_to_daylight(p->x, p->y, &p->temperature, &p->illuminant, &p->adaptation);
2724
2725 ++darktable.gui->reset;
2726
2727 dt_bauhaus_slider_set_soft(g->temperature, p->temperature);
2728 dt_bauhaus_combobox_set(g->illuminant, p->illuminant);
2729 dt_bauhaus_combobox_set(g->adaptation, p->adaptation);
2730
2731 const dt_aligned_pixel_t xyY = { p->x, p->y, 1.f };
2732 dt_aligned_pixel_t Lch;
2733 dt_xyY_to_Lch(xyY, Lch);
2734 dt_bauhaus_slider_set(g->illum_x, Lch[2] / M_PI * 180.f);
2735 dt_bauhaus_slider_set_soft(g->illum_y, Lch[1]);
2736
2737 update_illuminants(self);
2738 update_approx_cct(self);
2739 update_illuminant_color(self);
2740 paint_temperature_background(self);
2741
2742 --darktable.gui->reset;
2743
2744 gui_changed(self, NULL, NULL);
2745
2746 dt_dev_add_history_item(darktable.develop, self, TRUE);
2747 }
2748
_preview_pipe_finished_callback(gpointer instance,gpointer user_data)2749 static void _preview_pipe_finished_callback(gpointer instance, gpointer user_data)
2750 {
2751 dt_iop_module_t *self = (dt_iop_module_t *)user_data;
2752 dt_iop_channelmixer_rgb_gui_data_t *g = (dt_iop_channelmixer_rgb_gui_data_t *)self->gui_data;
2753
2754 dt_iop_gui_enter_critical_section(self);
2755 gtk_label_set_markup(GTK_LABEL(g->label_delta_E), g->delta_E_label_text);
2756 dt_iop_gui_leave_critical_section(self);
2757 }
2758
commit_params(struct dt_iop_module_t * self,dt_iop_params_t * p1,dt_dev_pixelpipe_t * pipe,dt_dev_pixelpipe_iop_t * piece)2759 void commit_params(struct dt_iop_module_t *self, dt_iop_params_t *p1, dt_dev_pixelpipe_t *pipe,
2760 dt_dev_pixelpipe_iop_t *piece)
2761 {
2762 dt_iop_channelmixer_rgb_params_t *p = (dt_iop_channelmixer_rgb_params_t *)p1;
2763 dt_iop_channelmixer_rbg_data_t *d = (dt_iop_channelmixer_rbg_data_t *)piece->data;
2764 dt_iop_channelmixer_rgb_gui_data_t *g = (dt_iop_channelmixer_rgb_gui_data_t *)self->gui_data;
2765
2766 d->version = p->version;
2767
2768 float norm_R = 1.0f;
2769 if(p->normalize_R) norm_R = p->red[0] + p->red[1] + p->red[2];
2770
2771 float norm_G = 1.0f;
2772 if(p->normalize_G) norm_G = p->green[0] + p->green[1] + p->green[2];
2773
2774 float norm_B = 1.0f;
2775 if(p->normalize_B) norm_B = p->blue[0] + p->blue[1] + p->blue[2];
2776
2777 float norm_sat = 0.0f;
2778 if(p->normalize_sat) norm_sat = (p->saturation[0] + p->saturation[1] + p->saturation[2]) / 3.f;
2779
2780 float norm_light = 0.0f;
2781 if(p->normalize_light) norm_light = (p->lightness[0] + p->lightness[1] + p->lightness[2]) / 3.f;
2782
2783 float norm_grey = p->grey[0] + p->grey[1] + p->grey[2];
2784 d->apply_grey = (p->grey[0] != 0.f) || (p->grey[1] != 0.f) || (p->grey[2] != 0.f);
2785 if(!p->normalize_grey || norm_grey == 0.f) norm_grey = 1.f;
2786
2787 for(int i = 0; i < 3; i++)
2788 {
2789 d->MIX[0][i] = p->red[i] / norm_R;
2790 d->MIX[1][i] = p->green[i] / norm_G;
2791 d->MIX[2][i] = p->blue[i] / norm_B;
2792 d->saturation[i] = -p->saturation[i] + norm_sat;
2793 d->lightness[i] = p->lightness[i] - norm_light;
2794 d->grey[i] = p->grey[i] / norm_grey; // = NaN if (norm_grey == 0.f) but we don't care since (d->apply_grey == FALSE)
2795 }
2796
2797 if(p->version == CHANNELMIXERRGB_V_1)
2798 {
2799 // for the v1 saturation algo, the effect of R and B coeffs is reversed
2800 d->saturation[0] = -p->saturation[2] + norm_sat;
2801 d->saturation[2] = -p->saturation[0] + norm_sat;
2802 }
2803
2804 // just in case compiler feels clever and uses SSE 4×1 dot product
2805 d->saturation[CHANNEL_SIZE - 1] = 0.0f;
2806 d->lightness[CHANNEL_SIZE - 1] = 0.0f;
2807 d->grey[CHANNEL_SIZE - 1] = 0.0f;
2808
2809 d->adaptation = p->adaptation;
2810 d->clip = p->clip;
2811 d->gamut = (p->gamut == 0.f) ? p->gamut : 1.f / p->gamut;
2812
2813 // find x y coordinates of illuminant for CIE 1931 2° observer
2814 float x = p->x;
2815 float y = p->y;
2816 dt_aligned_pixel_t custom_wb;
2817 get_white_balance_coeff(self, custom_wb);
2818 illuminant_to_xy(p->illuminant, &(self->dev->image_storage), custom_wb, &x, &y, p->temperature, p->illum_fluo, p->illum_led);
2819
2820 // if illuminant is set as camera, x and y are set on-the-fly at commit time, so we need to set adaptation too
2821 if(p->illuminant == DT_ILLUMINANT_CAMERA)
2822 check_if_close_to_daylight(x, y, NULL, NULL, &(d->adaptation));
2823
2824 d->illuminant_type = p->illuminant;
2825
2826 // Convert illuminant from xyY to XYZ
2827 dt_aligned_pixel_t XYZ;
2828 illuminant_xy_to_XYZ(x, y, XYZ);
2829
2830 // Convert illuminant from XYZ to Bradford modified LMS
2831 convert_any_XYZ_to_LMS(XYZ, d->illuminant, d->adaptation);
2832 d->illuminant[3] = 0.f;
2833
2834 //fprintf(stdout, "illuminant: %i\n", p->illuminant);
2835 //fprintf(stdout, "x: %f, y: %f\n", x, y);
2836 //fprintf(stdout, "X: %f - Y: %f - Z: %f\n", XYZ[0], XYZ[1], XYZ[2]);
2837 //fprintf(stdout, "L: %f - M: %f - S: %f\n", d->illuminant[0], d->illuminant[1], d->illuminant[2]);
2838
2839 // blue compensation for Bradford transform = (test illuminant blue / reference illuminant blue)^0.0834
2840 // reference illuminant is hard-set D50 for darktable's pipeline
2841 // test illuminant is user params
2842 d->p = powf(0.818155f / d->illuminant[2], 0.0834f);
2843
2844 // Disable OpenCL path if we are in any kind of diagnose mode (only C path has diagnostics)
2845 if(self->dev->gui_attached && g)
2846 {
2847 if( (g->run_profile && piece->pipe->type == DT_DEV_PIXELPIPE_PREVIEW) || // color checker extraction mode
2848 (g->run_validation && piece->pipe->type == DT_DEV_PIXELPIPE_PREVIEW) || // delta E validation
2849 ( (d->illuminant_type == DT_ILLUMINANT_DETECT_EDGES ||
2850 d->illuminant_type == DT_ILLUMINANT_DETECT_SURFACES ) && // WB extraction mode
2851 piece->pipe->type == DT_DEV_PIXELPIPE_FULL ) )
2852 {
2853 piece->process_cl_ready = 0;
2854 }
2855 }
2856 }
2857
2858
update_illuminants(dt_iop_module_t * self)2859 static void update_illuminants(dt_iop_module_t *self)
2860 {
2861 dt_iop_channelmixer_rgb_params_t *p = (dt_iop_channelmixer_rgb_params_t *)self->params;
2862 dt_iop_channelmixer_rgb_gui_data_t *g = (dt_iop_channelmixer_rgb_gui_data_t *)self->gui_data;
2863
2864 if(p->adaptation == DT_ADAPTATION_RGB || p->adaptation == DT_ADAPTATION_LAST)
2865 {
2866 // user disabled CAT at all, hide everything and exit
2867 gtk_widget_set_visible(g->illuminant, FALSE);
2868 gtk_widget_set_visible(g->illum_color, FALSE);
2869 gtk_widget_set_visible(g->approx_cct, FALSE);
2870 gtk_widget_set_visible(g->color_picker, FALSE);
2871 gtk_widget_set_visible(g->temperature, FALSE);
2872 gtk_widget_set_visible(g->illum_fluo, FALSE);
2873 gtk_widget_set_visible(g->illum_led, FALSE);
2874 gtk_widget_set_visible(g->illum_x, FALSE);
2875 gtk_widget_set_visible(g->illum_y, FALSE);
2876 return;
2877 }
2878 else
2879 {
2880 // set everything visible again and carry on
2881 gtk_widget_set_visible(g->illuminant, TRUE);
2882 gtk_widget_set_visible(g->illum_color, TRUE);
2883 gtk_widget_set_visible(g->approx_cct, TRUE);
2884 gtk_widget_set_visible(g->color_picker, TRUE);
2885 gtk_widget_set_visible(g->temperature, TRUE);
2886 gtk_widget_set_visible(g->illum_fluo, TRUE);
2887 gtk_widget_set_visible(g->illum_led, TRUE);
2888 gtk_widget_set_visible(g->illum_x, TRUE);
2889 }
2890
2891 // Display only the relevant sliders
2892 switch(p->illuminant)
2893 {
2894 case DT_ILLUMINANT_PIPE:
2895 case DT_ILLUMINANT_A:
2896 case DT_ILLUMINANT_E:
2897 {
2898 gtk_widget_set_visible(g->adaptation, TRUE);
2899 gtk_widget_set_visible(g->temperature, FALSE);
2900 gtk_widget_set_visible(g->illum_fluo, FALSE);
2901 gtk_widget_set_visible(g->illum_led, FALSE);
2902 gtk_widget_set_visible(g->illum_x, FALSE);
2903 gtk_widget_set_visible(g->illum_y, FALSE);
2904 break;
2905 }
2906 case DT_ILLUMINANT_D:
2907 case DT_ILLUMINANT_BB:
2908 {
2909 gtk_widget_set_visible(g->adaptation, TRUE);
2910 gtk_widget_set_visible(g->temperature, TRUE);
2911 gtk_widget_set_visible(g->illum_fluo, FALSE);
2912 gtk_widget_set_visible(g->illum_led, FALSE);
2913 gtk_widget_set_visible(g->illum_x, FALSE);
2914 gtk_widget_set_visible(g->illum_y, FALSE);
2915 break;
2916 }
2917 case DT_ILLUMINANT_F:
2918 {
2919 gtk_widget_set_visible(g->adaptation, TRUE);
2920 gtk_widget_set_visible(g->temperature, FALSE);
2921 gtk_widget_set_visible(g->illum_fluo, TRUE);
2922 gtk_widget_set_visible(g->illum_led, FALSE);
2923 gtk_widget_set_visible(g->illum_x, FALSE);
2924 gtk_widget_set_visible(g->illum_y, FALSE);
2925 break;
2926 }
2927 case DT_ILLUMINANT_LED:
2928 {
2929 gtk_widget_set_visible(g->adaptation, TRUE);
2930 gtk_widget_set_visible(g->temperature, FALSE);
2931 gtk_widget_set_visible(g->illum_fluo, FALSE);
2932 gtk_widget_set_visible(g->illum_led, TRUE);
2933 gtk_widget_set_visible(g->illum_x, FALSE);
2934 gtk_widget_set_visible(g->illum_y, FALSE);
2935 break;
2936 }
2937 case DT_ILLUMINANT_CUSTOM:
2938 {
2939 gtk_widget_set_visible(g->adaptation, TRUE);
2940 gtk_widget_set_visible(g->temperature, FALSE);
2941 gtk_widget_set_visible(g->illum_fluo, FALSE);
2942 gtk_widget_set_visible(g->illum_led, FALSE);
2943 gtk_widget_set_visible(g->illum_x, TRUE);
2944 gtk_widget_set_visible(g->illum_y, TRUE);
2945 break;
2946 }
2947 case DT_ILLUMINANT_CAMERA:
2948 {
2949 gtk_widget_set_visible(g->adaptation, TRUE);
2950 gtk_widget_set_visible(g->temperature, FALSE);
2951 gtk_widget_set_visible(g->illum_fluo, FALSE);
2952 gtk_widget_set_visible(g->illum_led, FALSE);
2953 gtk_widget_set_visible(g->illum_x, FALSE);
2954 gtk_widget_set_visible(g->illum_y, FALSE);
2955 break;
2956 }
2957 case DT_ILLUMINANT_DETECT_EDGES:
2958 case DT_ILLUMINANT_DETECT_SURFACES:
2959 {
2960 gtk_widget_set_visible(g->adaptation, FALSE);
2961 gtk_widget_set_visible(g->temperature, FALSE);
2962 gtk_widget_set_visible(g->illum_fluo, FALSE);
2963 gtk_widget_set_visible(g->illum_led, FALSE);
2964 gtk_widget_set_visible(g->illum_x, FALSE);
2965 gtk_widget_set_visible(g->illum_y, FALSE);
2966 break;
2967 }
2968 case DT_ILLUMINANT_LAST:
2969 {
2970 break;
2971 }
2972 }
2973 }
2974
2975 /**
2976 * DOCUMENTATION
2977 *
2978 * The illuminant is stored in params as a set of x and y coordinates, describing its chrominance in xyY color space.
2979 * xyY is a normalized XYZ space, derivated from the retina cone sensors. By definition, for an illuminant, Y = 1,
2980 * so we only really care about (x, y).
2981 *
2982 * Using (x, y) is a robust and interoperable way to describe an illuminant, since it is all the actual pixel code needs
2983 * to perform the chromatic adaptation. This (x, y) can be computed in many different ways or taken from databases,
2984 * and possibly from other software, so storing only the result let us room to improve the computation in the future,
2985 * without losing compatibility with older versions.
2986 *
2987 * However, it's not a great GUI since x and y are not perceptually scaled. So the `g->illum_x` and `g->illum_y`
2988 * actually display respectively hue and chroma, in LCh color space, which is designed for illuminants
2989 * and preceptually spaced. This gives UI controls which effect feels more even to the user.
2990 *
2991 * But that makes things a bit tricky, API-wise, since a set of (x, y) depends on a set of (hue, chroma),
2992 * so they always need to be handled together, but also because the back-and-forth computations
2993 * Lch <-> xyY need to be done anytime we read or write from/to params from/to GUI.
2994 *
2995 * Also, the R, G, B sliders have a background color gradient that shows the actual R, G, B sensors
2996 * used by the selected chromatic adaptation. Each chromatic adaptation method uses a different RGB space,
2997 * called LMS in the literature (but it's only a special-purpose RGB space for all we care here),
2998 * which primaries are projected to sRGB colors, to be displayed in the GUI, so users may get a feeling
2999 * of what colors they will get.
3000 **/
3001
update_xy_color(dt_iop_module_t * self)3002 static void update_xy_color(dt_iop_module_t *self)
3003 {
3004 // update the fill background color of x, y sliders
3005 dt_iop_channelmixer_rgb_gui_data_t *g = (dt_iop_channelmixer_rgb_gui_data_t *)self->gui_data;
3006 dt_iop_channelmixer_rgb_params_t *p = (dt_iop_channelmixer_rgb_params_t *)self->params;
3007
3008 // Get the current values bound of the slider, taking into account the possible soft rescaling
3009 const float x_min = DT_BAUHAUS_WIDGET(g->illum_x)->data.slider.soft_min;
3010 const float x_max = DT_BAUHAUS_WIDGET(g->illum_x)->data.slider.soft_max;
3011 const float y_min = DT_BAUHAUS_WIDGET(g->illum_y)->data.slider.soft_min;
3012 const float y_max = DT_BAUHAUS_WIDGET(g->illum_y)->data.slider.soft_max;
3013 const float x_range = x_max - x_min;
3014 const float y_range = y_max - y_min;
3015
3016 // Varies x in range around current y param
3017 for(int i = 0; i < DT_BAUHAUS_SLIDER_MAX_STOPS; i++)
3018 {
3019 const float stop = ((float)i / (float)(DT_BAUHAUS_SLIDER_MAX_STOPS - 1));
3020 const float x = x_min + stop * x_range;
3021 dt_aligned_pixel_t RGB;
3022
3023 const dt_aligned_pixel_t Lch = { 100.f, 50.f, x / 180.f * M_PI };
3024 dt_aligned_pixel_t xyY = { 0 };
3025 dt_Lch_to_xyY(Lch, xyY);
3026 illuminant_xy_to_RGB(xyY[0], xyY[1], RGB);
3027 dt_bauhaus_slider_set_stop(g->illum_x, stop, RGB[0], RGB[1], RGB[2]);
3028 }
3029
3030 // Varies y in range around current x params
3031 for(int i = 0; i < DT_BAUHAUS_SLIDER_MAX_STOPS; i++)
3032 {
3033 const float stop = ((float)i / (float)(DT_BAUHAUS_SLIDER_MAX_STOPS - 1));
3034 const float y = (y_min + stop * y_range) / 2.0f;
3035 dt_aligned_pixel_t RGB = { 0 };
3036
3037 // Find current hue
3038 dt_aligned_pixel_t xyY = { p->x, p->y, 1.f };
3039 dt_aligned_pixel_t Lch = { 0 };
3040 dt_xyY_to_Lch(xyY, Lch);
3041
3042 // Replace chroma by current step
3043 Lch[0] = 75.f;
3044 Lch[1] = y;
3045
3046 // Go back to xyY
3047 dt_Lch_to_xyY(Lch, xyY);
3048 illuminant_xy_to_RGB(xyY[0], xyY[1], RGB);
3049 dt_bauhaus_slider_set_stop(g->illum_y, stop, RGB[0], RGB[1], RGB[2]);
3050 }
3051
3052 gtk_widget_queue_draw(g->illum_x);
3053 gtk_widget_queue_draw(g->illum_y);
3054 }
3055
_convert_GUI_colors(dt_iop_channelmixer_rgb_params_t * p,const struct dt_iop_order_iccprofile_info_t * const work_profile,const dt_aligned_pixel_t LMS,dt_aligned_pixel_t RGB)3056 static void _convert_GUI_colors(dt_iop_channelmixer_rgb_params_t *p,
3057 const struct dt_iop_order_iccprofile_info_t *const work_profile,
3058 const dt_aligned_pixel_t LMS, dt_aligned_pixel_t RGB)
3059 {
3060 if(p->adaptation != DT_ADAPTATION_RGB)
3061 {
3062 convert_any_LMS_to_RGB(LMS, RGB, p->adaptation);
3063 // RGB vector is normalized with max(RGB)
3064 }
3065 else
3066 {
3067 dt_aligned_pixel_t XYZ;
3068 if(work_profile)
3069 {
3070 dt_ioppr_rgb_matrix_to_xyz(LMS, XYZ, work_profile->matrix_in_transposed, work_profile->lut_in,
3071 work_profile->unbounded_coeffs_in, work_profile->lutsize,
3072 work_profile->nonlinearlut);
3073 dt_XYZ_to_Rec709_D65(XYZ, RGB);
3074
3075 // normalize with hue-preserving method (sort-of) to prevent gamut-clipping in sRGB
3076 const float max_RGB = fmaxf(fmaxf(RGB[0], RGB[1]), RGB[2]);
3077 for(size_t c = 0; c < 3; c++) RGB[c] = fmaxf(RGB[c] / max_RGB, 0.f);
3078 }
3079 else
3080 {
3081 // work profile not available yet - default to grey
3082 for(size_t c = 0; c < 3; c++) RGB[c] = 0.5f;
3083 }
3084 }
3085 }
3086
3087
update_R_colors(dt_iop_module_t * self)3088 static void update_R_colors(dt_iop_module_t *self)
3089 {
3090 // update the fill background color of x, y sliders
3091 dt_iop_channelmixer_rgb_gui_data_t *g = (dt_iop_channelmixer_rgb_gui_data_t *)self->gui_data;
3092 dt_iop_channelmixer_rgb_params_t *p = (dt_iop_channelmixer_rgb_params_t *)self->params;
3093 const struct dt_iop_order_iccprofile_info_t *const work_profile = dt_ioppr_get_pipe_current_profile_info(self, self->dev->pipe);
3094
3095 // scale params if needed
3096 dt_aligned_pixel_t RGB = { p->red[0], p->red[1], p->red[2] };
3097
3098 if(p->normalize_R)
3099 {
3100 const float sum = RGB[0] + RGB[1] + RGB[2];
3101 if(sum != 0.f) for(int c = 0; c < 3; c++) RGB[c] /= sum;
3102 }
3103
3104 // Get the current values bound of the slider, taking into account the possible soft rescaling
3105 const float RR_min = DT_BAUHAUS_WIDGET(g->scale_red_R)->data.slider.min;
3106 const float RR_max = DT_BAUHAUS_WIDGET(g->scale_red_R)->data.slider.max;
3107 const float RR_range = RR_max - RR_min;
3108
3109 for(int i = 0; i < DT_BAUHAUS_SLIDER_MAX_STOPS; i++)
3110 {
3111 const float stop = ((float)i / (float)(DT_BAUHAUS_SLIDER_MAX_STOPS - 1));
3112 const float RR = RR_min + stop * RR_range;
3113 const float stop_R = RR + RGB[1] + RGB[2];
3114 const dt_aligned_pixel_t LMS = { 0.5f * stop_R, 0.5f, 0.5f };
3115 dt_aligned_pixel_t RGB_t = { 0.5f };
3116 _convert_GUI_colors(p, work_profile, LMS, RGB_t);
3117 dt_bauhaus_slider_set_stop(g->scale_red_R, stop, RGB_t[0], RGB_t[1], RGB_t[2]);
3118 }
3119
3120 const float RG_min = DT_BAUHAUS_WIDGET(g->scale_red_G)->data.slider.min;
3121 const float RG_max = DT_BAUHAUS_WIDGET(g->scale_red_G)->data.slider.max;
3122 const float RG_range = RG_max - RG_min;
3123
3124 for(int i = 0; i < DT_BAUHAUS_SLIDER_MAX_STOPS; i++)
3125 {
3126 const float stop = ((float)i / (float)(DT_BAUHAUS_SLIDER_MAX_STOPS - 1));
3127 const float RG = RG_min + stop * RG_range;
3128 const float stop_R = RGB[0] + RG + RGB[2];
3129 const dt_aligned_pixel_t LMS = { 0.5f * stop_R, 0.5f, 0.5f };
3130 dt_aligned_pixel_t RGB_t = { 0.5f };
3131 _convert_GUI_colors(p, work_profile, LMS, RGB_t);
3132 dt_bauhaus_slider_set_stop(g->scale_red_G, stop, RGB_t[0], RGB_t[1], RGB_t[2]);
3133 }
3134
3135 const float RB_min = DT_BAUHAUS_WIDGET(g->scale_red_B)->data.slider.min;
3136 const float RB_max = DT_BAUHAUS_WIDGET(g->scale_red_B)->data.slider.max;
3137 const float RB_range = RB_max - RB_min;
3138
3139 for(int i = 0; i < DT_BAUHAUS_SLIDER_MAX_STOPS; i++)
3140 {
3141 const float stop = ((float)i / (float)(DT_BAUHAUS_SLIDER_MAX_STOPS - 1));
3142 const float RB = RB_min + stop * RB_range;
3143 const float stop_R = RGB[0] + RGB[1] + RB;
3144 const dt_aligned_pixel_t LMS = { 0.5f * stop_R, 0.5f, 0.5f };
3145 dt_aligned_pixel_t RGB_t = { 0.5f };
3146 _convert_GUI_colors(p, work_profile, LMS, RGB_t);
3147 dt_bauhaus_slider_set_stop(g->scale_red_B, stop, RGB_t[0], RGB_t[1], RGB_t[2]);
3148 }
3149
3150 gtk_widget_queue_draw(g->scale_red_R);
3151 gtk_widget_queue_draw(g->scale_red_G);
3152 gtk_widget_queue_draw(g->scale_red_B);
3153 }
3154
3155
update_B_colors(dt_iop_module_t * self)3156 static void update_B_colors(dt_iop_module_t *self)
3157 {
3158 // update the fill background color of x, y sliders
3159 dt_iop_channelmixer_rgb_gui_data_t *g = (dt_iop_channelmixer_rgb_gui_data_t *)self->gui_data;
3160 dt_iop_channelmixer_rgb_params_t *p = (dt_iop_channelmixer_rgb_params_t *)self->params;
3161 const struct dt_iop_order_iccprofile_info_t *const work_profile = dt_ioppr_get_pipe_current_profile_info(self, self->dev->pipe);
3162
3163 // scale params if needed
3164 dt_aligned_pixel_t RGB = { p->blue[0], p->blue[1], p->blue[2] };
3165
3166 if(p->normalize_B)
3167 {
3168 const float sum = RGB[0] + RGB[1] + RGB[2];
3169 if(sum != 0.f) for(int c = 0; c < 3; c++) RGB[c] /= sum;
3170 }
3171
3172 // Get the current values bound of the slider, taking into account the possible soft rescaling
3173 const float BR_min = DT_BAUHAUS_WIDGET(g->scale_blue_R)->data.slider.min;
3174 const float BR_max = DT_BAUHAUS_WIDGET(g->scale_blue_R)->data.slider.max;
3175 const float BR_range = BR_max - BR_min;
3176
3177 for(int i = 0; i < DT_BAUHAUS_SLIDER_MAX_STOPS; i++)
3178 {
3179 const float stop = ((float)i / (float)(DT_BAUHAUS_SLIDER_MAX_STOPS - 1));
3180 const float BR = BR_min + stop * BR_range;
3181 const float stop_B = BR + RGB[1] + RGB[2];
3182 const dt_aligned_pixel_t LMS = { 0.5f, 0.5f, 0.5f * stop_B };
3183 dt_aligned_pixel_t RGB_t = { 0.5f };
3184 _convert_GUI_colors(p, work_profile, LMS, RGB_t);
3185 dt_bauhaus_slider_set_stop(g->scale_blue_R, stop, RGB_t[0], RGB_t[1], RGB_t[2]);
3186 }
3187
3188 const float BG_min = DT_BAUHAUS_WIDGET(g->scale_blue_G)->data.slider.min;
3189 const float BG_max = DT_BAUHAUS_WIDGET(g->scale_blue_G)->data.slider.max;
3190 const float BG_range = BG_max - BG_min;
3191
3192 for(int i = 0; i < DT_BAUHAUS_SLIDER_MAX_STOPS; i++)
3193 {
3194 const float stop = ((float)i / (float)(DT_BAUHAUS_SLIDER_MAX_STOPS - 1));
3195 const float BG = BG_min + stop * BG_range;
3196 const float stop_B = RGB[0] + BG + RGB[2];
3197 const dt_aligned_pixel_t LMS = { 0.5f , 0.5f, 0.5f * stop_B };
3198 dt_aligned_pixel_t RGB_t = { 0.5f };
3199 _convert_GUI_colors(p, work_profile, LMS, RGB_t);
3200 dt_bauhaus_slider_set_stop(g->scale_blue_G, stop, RGB_t[0], RGB_t[1], RGB_t[2]);
3201 }
3202
3203 const float BB_min = DT_BAUHAUS_WIDGET(g->scale_blue_B)->data.slider.min;
3204 const float BB_max = DT_BAUHAUS_WIDGET(g->scale_blue_B)->data.slider.max;
3205 const float BB_range = BB_max - BB_min;
3206
3207 for(int i = 0; i < DT_BAUHAUS_SLIDER_MAX_STOPS; i++)
3208 {
3209 const float stop = ((float)i / (float)(DT_BAUHAUS_SLIDER_MAX_STOPS - 1));
3210 const float BB = BB_min + stop * BB_range;
3211 const float stop_B = RGB[0] + RGB[1] + BB;
3212 const dt_aligned_pixel_t LMS = { 0.5f, 0.5f, 0.5f * stop_B , 0.f};
3213 dt_aligned_pixel_t RGB_t = { 0.5f };
3214 _convert_GUI_colors(p, work_profile, LMS, RGB_t);
3215 dt_bauhaus_slider_set_stop(g->scale_blue_B, stop, RGB_t[0], RGB_t[1], RGB_t[2]);
3216 }
3217
3218 gtk_widget_queue_draw(g->scale_blue_R);
3219 gtk_widget_queue_draw(g->scale_blue_G);
3220 gtk_widget_queue_draw(g->scale_blue_B);
3221 }
3222
update_G_colors(dt_iop_module_t * self)3223 static void update_G_colors(dt_iop_module_t *self)
3224 {
3225 // update the fill background color of x, y sliders
3226 dt_iop_channelmixer_rgb_gui_data_t *g = (dt_iop_channelmixer_rgb_gui_data_t *)self->gui_data;
3227 dt_iop_channelmixer_rgb_params_t *p = (dt_iop_channelmixer_rgb_params_t *)self->params;
3228 const struct dt_iop_order_iccprofile_info_t *const work_profile = dt_ioppr_get_pipe_current_profile_info(self, self->dev->pipe);
3229
3230 // scale params if needed
3231 dt_aligned_pixel_t RGB = { p->green[0], p->green[1], p->green[2] };
3232
3233 if(p->normalize_G)
3234 {
3235 float sum = RGB[0] + RGB[1] + RGB[2];
3236 if(sum != 0.f) for(int c = 0; c < 3; c++) RGB[c] /= sum;
3237 }
3238
3239 // Get the current values bound of the slider, taking into account the possible soft rescaling
3240 const float GR_min = DT_BAUHAUS_WIDGET(g->scale_green_R)->data.slider.min;
3241 const float GR_max = DT_BAUHAUS_WIDGET(g->scale_green_R)->data.slider.max;
3242 const float GR_range = GR_max - GR_min;
3243
3244 for(int i = 0; i < DT_BAUHAUS_SLIDER_MAX_STOPS; i++)
3245 {
3246 const float stop = ((float)i / (float)(DT_BAUHAUS_SLIDER_MAX_STOPS - 1));
3247 const float GR = GR_min + stop * GR_range;
3248 const float stop_G = GR + RGB[1] + RGB[2];
3249 const dt_aligned_pixel_t LMS = { 0.5f , 0.5f * stop_G, 0.5f };
3250 dt_aligned_pixel_t RGB_t = { 0.5f };
3251 _convert_GUI_colors(p, work_profile, LMS, RGB_t);
3252 dt_bauhaus_slider_set_stop(g->scale_green_R, stop, RGB_t[0], RGB_t[1], RGB_t[2]);
3253 }
3254
3255 const float GG_min = DT_BAUHAUS_WIDGET(g->scale_green_G)->data.slider.min;
3256 const float GG_max = DT_BAUHAUS_WIDGET(g->scale_green_G)->data.slider.max;
3257 const float GG_range = GG_max - GG_min;
3258
3259 for(int i = 0; i < DT_BAUHAUS_SLIDER_MAX_STOPS; i++)
3260 {
3261 const float stop = ((float)i / (float)(DT_BAUHAUS_SLIDER_MAX_STOPS - 1));
3262 const float GG = GG_min + stop * GG_range;
3263 const float stop_G = RGB[0] + GG + RGB[2];
3264 const dt_aligned_pixel_t LMS = { 0.5f, 0.5f * stop_G, 0.5f };
3265 dt_aligned_pixel_t RGB_t = { 0.5f };
3266 _convert_GUI_colors(p, work_profile, LMS, RGB_t);
3267 dt_bauhaus_slider_set_stop(g->scale_green_G, stop, RGB_t[0], RGB_t[1], RGB_t[2]);
3268 }
3269
3270 const float GB_min = DT_BAUHAUS_WIDGET(g->scale_green_B)->data.slider.min;
3271 const float GB_max = DT_BAUHAUS_WIDGET(g->scale_green_B)->data.slider.max;
3272 const float GB_range = GB_max - GB_min;
3273
3274 for(int i = 0; i < DT_BAUHAUS_SLIDER_MAX_STOPS; i++)
3275 {
3276 const float stop = ((float)i / (float)(DT_BAUHAUS_SLIDER_MAX_STOPS - 1));
3277 const float GB = GB_min + stop * GB_range;
3278 const float stop_G = RGB[0] + RGB[1] + GB;
3279 const dt_aligned_pixel_t LMS = { 0.5f, 0.5f * stop_G , 0.5f};
3280 dt_aligned_pixel_t RGB_t = { 0.5f };
3281 _convert_GUI_colors(p, work_profile, LMS, RGB_t);
3282 dt_bauhaus_slider_set_stop(g->scale_green_B, stop, RGB_t[0], RGB_t[1], RGB_t[2]);
3283 }
3284
3285 gtk_widget_queue_draw(g->scale_green_R);
3286 gtk_widget_queue_draw(g->scale_green_G);
3287 gtk_widget_queue_draw(g->scale_green_B);
3288 }
3289
paint_temperature_background(struct dt_iop_module_t * self)3290 static void paint_temperature_background(struct dt_iop_module_t *self)
3291 {
3292 dt_iop_channelmixer_rgb_gui_data_t *g = (dt_iop_channelmixer_rgb_gui_data_t *)self->gui_data;
3293
3294 const float max_temp = DT_BAUHAUS_WIDGET(g->temperature)->data.slider.max;
3295 const float min_temp = DT_BAUHAUS_WIDGET(g->temperature)->data.slider.min;
3296 const float temp_range = max_temp - min_temp;
3297
3298 for(int i = 0; i < DT_BAUHAUS_SLIDER_MAX_STOPS; i++)
3299 {
3300 const float stop = ((float)i / (float)(DT_BAUHAUS_SLIDER_MAX_STOPS - 1));
3301 const float t = min_temp + stop * temp_range;
3302 dt_aligned_pixel_t RGB = { 0 };
3303 illuminant_CCT_to_RGB(t, RGB);
3304 dt_bauhaus_slider_set_stop(g->temperature, stop, RGB[0], RGB[1], RGB[2]);
3305 }
3306
3307 gtk_widget_queue_draw(g->temperature);
3308 }
3309
3310
update_illuminant_color(dt_iop_module_t * self)3311 static void update_illuminant_color(dt_iop_module_t *self)
3312 {
3313 dt_iop_channelmixer_rgb_gui_data_t *g = (dt_iop_channelmixer_rgb_gui_data_t *)self->gui_data;
3314 gtk_widget_queue_draw(g->illum_color);
3315 update_xy_color(self);
3316 }
3317
illuminant_color_draw(GtkWidget * widget,cairo_t * crf,gpointer user_data)3318 static gboolean illuminant_color_draw(GtkWidget *widget, cairo_t *crf, gpointer user_data)
3319 {
3320 dt_iop_module_t *self = (dt_iop_module_t *)user_data;
3321 dt_iop_channelmixer_rgb_params_t *p = (dt_iop_channelmixer_rgb_params_t *)self->params;
3322
3323 // Init
3324 GtkAllocation allocation;
3325 gtk_widget_get_allocation(widget, &allocation);
3326 int width = allocation.width, height = allocation.height;
3327 cairo_surface_t *cst = dt_cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height);
3328 cairo_t *cr = cairo_create(cst);
3329
3330 // Margins
3331 const double INNER_PADDING = 4.0;
3332 const float margin = 2. * DT_PIXEL_APPLY_DPI(darktable.bauhaus->line_space);
3333 width -= 2* INNER_PADDING;
3334 height -= 2 * margin;
3335
3336 // Paint illuminant color - we need to recompute it in full in case camera RAW is chosen
3337 float x = p->x;
3338 float y = p->y;
3339 dt_aligned_pixel_t RGB = { 0 };
3340 dt_aligned_pixel_t custom_wb;
3341 get_white_balance_coeff(self, custom_wb);
3342 illuminant_to_xy(p->illuminant, &(self->dev->image_storage), custom_wb,
3343 &x, &y, p->temperature, p->illum_fluo, p->illum_led);
3344 illuminant_xy_to_RGB(x, y, RGB);
3345 cairo_set_source_rgb(cr, RGB[0], RGB[1], RGB[2]);
3346 cairo_rectangle(cr, INNER_PADDING, margin, width, height);
3347 cairo_fill(cr);
3348
3349 // Clean
3350 cairo_stroke(cr);
3351 cairo_destroy(cr);
3352 cairo_set_source_surface(crf, cst, 0, 0);
3353 cairo_paint(crf);
3354 cairo_surface_destroy(cst);
3355 return TRUE;
3356 }
3357
update_approx_cct(dt_iop_module_t * self)3358 static void update_approx_cct(dt_iop_module_t *self)
3359 {
3360 dt_iop_channelmixer_rgb_gui_data_t *g = (dt_iop_channelmixer_rgb_gui_data_t *)self->gui_data;
3361 dt_iop_channelmixer_rgb_params_t *p = (dt_iop_channelmixer_rgb_params_t *)self->params;
3362
3363 float x = p->x;
3364 float y = p->y;
3365 dt_aligned_pixel_t custom_wb;
3366 get_white_balance_coeff(self, custom_wb);
3367 illuminant_to_xy(p->illuminant, &(self->dev->image_storage), custom_wb, &x, &y, p->temperature, p->illum_fluo, p->illum_led);
3368
3369 dt_illuminant_t test_illuminant;
3370 float t = 5000.f;
3371 check_if_close_to_daylight(x, y, &t, &test_illuminant, NULL);
3372
3373 gchar *str;
3374 if(t > 1667.f && t < 25000.f)
3375 {
3376 if(test_illuminant == DT_ILLUMINANT_D)
3377 {
3378 str = g_strdup_printf(_("CCT: %.0f K (daylight)"), t);
3379 gtk_widget_set_tooltip_text(GTK_WIDGET(g->approx_cct),
3380 _("approximated correlated color temperature.\n"
3381 "this illuminant can be accurately modeled by a daylight spectrum,\n"
3382 "so its temperature is relevant and meaningful with a D illuminant."));
3383 }
3384 else if(test_illuminant == DT_ILLUMINANT_BB)
3385 {
3386 str = g_strdup_printf(_("CCT: %.0f K (black body)"), t);
3387 gtk_widget_set_tooltip_text(GTK_WIDGET(g->approx_cct),
3388 _("approximated correlated color temperature.\n"
3389 "this illuminant can be accurately modeled by a black body spectrum,\n"
3390 "so its temperature is relevant and meaningful with a Planckian illuminant."));
3391 }
3392 else
3393 {
3394 str = g_strdup_printf(_("CCT: %.0f K (invalid)"), t);
3395 gtk_widget_set_tooltip_text(GTK_WIDGET(g->approx_cct),
3396 _("approximated correlated color temperature.\n"
3397 "this illuminant cannot be accurately modeled by a daylight or black body spectrum,\n"
3398 "so its temperature is not relevant and meaningful and you need to use a custom illuminant."));
3399 }
3400 }
3401 else
3402 {
3403 str = g_strdup_printf(_("CCT: undefined"));
3404 gtk_widget_set_tooltip_text(GTK_WIDGET(g->approx_cct),
3405 _("the approximated correlated color temperature\n"
3406 "cannot be computed at all so you need to use a custom illuminant."));
3407 }
3408 gtk_label_set_text(GTK_LABEL(g->approx_cct), str);
3409 g_free(str);
3410 }
3411
3412
illum_xy_callback(GtkWidget * slider,gpointer user_data)3413 static void illum_xy_callback(GtkWidget *slider, gpointer user_data)
3414 {
3415 dt_iop_module_t *self = (dt_iop_module_t *)user_data;
3416 if(darktable.gui->reset) return;
3417 dt_iop_channelmixer_rgb_params_t *p = (dt_iop_channelmixer_rgb_params_t *)self->params;
3418 dt_iop_channelmixer_rgb_gui_data_t *g = (dt_iop_channelmixer_rgb_gui_data_t *)self->gui_data;
3419
3420 dt_aligned_pixel_t Lch = { 0 };
3421 Lch[0] = 100.f;
3422 Lch[2] = dt_bauhaus_slider_get(g->illum_x) / 180. * M_PI;
3423 Lch[1] = dt_bauhaus_slider_get(g->illum_y);
3424
3425 dt_aligned_pixel_t xyY = { 0 };
3426 dt_Lch_to_xyY(Lch, xyY);
3427 p->x = xyY[0];
3428 p->y = xyY[1];
3429
3430 float t = xy_to_CCT(p->x, p->y);
3431 // xy_to_CCT is valid only above 3000 K
3432 if(t < 3000.f) t = CCT_reverse_lookup(p->x, p->y);
3433 p->temperature = t;
3434
3435 ++darktable.gui->reset;
3436 dt_bauhaus_slider_set_soft(g->temperature, p->temperature);
3437 update_approx_cct(self);
3438 update_illuminant_color(self);
3439 paint_temperature_background(self);
3440 --darktable.gui->reset;
3441
3442 dt_dev_add_history_item(darktable.develop, self, TRUE);
3443 }
3444
init_pipe(struct dt_iop_module_t * self,dt_dev_pixelpipe_t * pipe,dt_dev_pixelpipe_iop_t * piece)3445 void init_pipe(struct dt_iop_module_t *self, dt_dev_pixelpipe_t *pipe, dt_dev_pixelpipe_iop_t *piece)
3446 {
3447 piece->data = dt_calloc_align(64, sizeof(dt_iop_channelmixer_rbg_data_t));
3448 }
3449
cleanup_pipe(struct dt_iop_module_t * self,dt_dev_pixelpipe_t * pipe,dt_dev_pixelpipe_iop_t * piece)3450 void cleanup_pipe(struct dt_iop_module_t *self, dt_dev_pixelpipe_t *pipe, dt_dev_pixelpipe_iop_t *piece)
3451 {
3452 self->dev->proxy.chroma_adaptation = NULL;
3453 dt_free_align(piece->data);
3454 piece->data = NULL;
3455 }
3456
gui_reset(dt_iop_module_t * self)3457 void gui_reset(dt_iop_module_t *self)
3458 {
3459 dt_iop_channelmixer_rgb_gui_data_t *g = (dt_iop_channelmixer_rgb_gui_data_t *)self->gui_data;
3460 g->is_profiling_started = FALSE;
3461 dt_iop_color_picker_reset(self, TRUE);
3462 gui_changed(self, NULL, NULL);
3463 }
3464
gui_update(struct dt_iop_module_t * self)3465 void gui_update(struct dt_iop_module_t *self)
3466 {
3467 dt_iop_module_t *module = (dt_iop_module_t *)self;
3468 dt_iop_channelmixer_rgb_gui_data_t *g = (dt_iop_channelmixer_rgb_gui_data_t *)self->gui_data;
3469 dt_iop_channelmixer_rgb_params_t *p = (dt_iop_channelmixer_rgb_params_t *)module->params;
3470
3471 dt_iop_color_picker_reset(self, TRUE);
3472
3473 dt_bauhaus_combobox_set(g->illuminant, p->illuminant);
3474 dt_bauhaus_combobox_set(g->illum_fluo, p->illum_fluo);
3475 dt_bauhaus_combobox_set(g->illum_led, p->illum_led);
3476 dt_bauhaus_slider_set_soft(g->temperature, p->temperature);
3477 dt_bauhaus_slider_set_soft(g->gamut, p->gamut);
3478 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->clip), p->clip);
3479
3480 dt_bauhaus_combobox_set(g->adaptation, p->adaptation);
3481
3482 dt_bauhaus_slider_set_soft(g->scale_red_R, p->red[0]);
3483 dt_bauhaus_slider_set_soft(g->scale_red_G, p->red[1]);
3484 dt_bauhaus_slider_set_soft(g->scale_red_B, p->red[2]);
3485
3486 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->normalize_R), p->normalize_R);
3487
3488 dt_bauhaus_slider_set_soft(g->scale_green_R, p->green[0]);
3489 dt_bauhaus_slider_set_soft(g->scale_green_G, p->green[1]);
3490 dt_bauhaus_slider_set_soft(g->scale_green_B, p->green[2]);
3491
3492 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->normalize_G), p->normalize_G);
3493
3494 dt_bauhaus_slider_set_soft(g->scale_blue_R, p->blue[0]);
3495 dt_bauhaus_slider_set_soft(g->scale_blue_G, p->blue[1]);
3496 dt_bauhaus_slider_set_soft(g->scale_blue_B, p->blue[2]);
3497
3498 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->normalize_B), p->normalize_B);
3499
3500 dt_bauhaus_slider_set_soft(g->scale_saturation_R, p->saturation[0]);
3501 dt_bauhaus_slider_set_soft(g->scale_saturation_G, p->saturation[1]);
3502 dt_bauhaus_slider_set_soft(g->scale_saturation_B, p->saturation[2]);
3503
3504 if(p->version != CHANNELMIXERRGB_V_3)
3505 dt_bauhaus_combobox_set(g->saturation_version, p->version);
3506 else
3507 gtk_widget_hide(GTK_WIDGET(g->saturation_version));
3508
3509 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->normalize_sat), p->normalize_sat);
3510
3511 dt_bauhaus_slider_set_soft(g->scale_lightness_R, p->lightness[0]);
3512 dt_bauhaus_slider_set_soft(g->scale_lightness_G, p->lightness[1]);
3513 dt_bauhaus_slider_set_soft(g->scale_lightness_B, p->lightness[2]);
3514
3515 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->normalize_light), p->normalize_light);
3516
3517 dt_bauhaus_slider_set_soft(g->scale_grey_R, p->grey[0]);
3518 dt_bauhaus_slider_set_soft(g->scale_grey_G, p->grey[1]);
3519 dt_bauhaus_slider_set_soft(g->scale_grey_B, p->grey[2]);
3520
3521 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->normalize_grey), p->normalize_grey);
3522
3523 dt_iop_gui_enter_critical_section(self);
3524
3525 const int i = dt_conf_get_int("darkroom/modules/channelmixerrgb/colorchecker");
3526 dt_bauhaus_combobox_set(g->checkers_list, i);
3527 g->checker = dt_get_color_checker(i);
3528
3529 const int j = dt_conf_get_int("darkroom/modules/channelmixerrgb/optimization");
3530 dt_bauhaus_combobox_set(g->optimize, j);
3531 g->optimization = j;
3532
3533 g->safety_margin = 0.5f;
3534 if(dt_conf_key_exists("darkroom/modules/channelmixerrgb/safety"))
3535 g->safety_margin = dt_conf_get_float("darkroom/modules/channelmixerrgb/safety");
3536 dt_bauhaus_slider_set_soft(g->safety, g->safety_margin);
3537
3538 dt_iop_gui_leave_critical_section(self);
3539
3540 g->is_profiling_started = FALSE;
3541 gtk_widget_hide(g->collapsible);
3542 dt_bauhaus_widget_set_quad_paint(g->start_profiling, dtgtk_cairo_paint_solid_arrow, CPF_STYLE_BOX | CPF_DIRECTION_LEFT, NULL);
3543 gui_changed(self, NULL, NULL);
3544 }
3545
init(dt_iop_module_t * module)3546 void init(dt_iop_module_t *module)
3547 {
3548 dt_iop_default_init(module);
3549
3550 dt_iop_channelmixer_rgb_params_t *d = (dt_iop_channelmixer_rgb_params_t *)module->default_params;
3551 d->red[0] = d->green[1] = d->blue[2] = 1.0;
3552 }
3553
reload_defaults(dt_iop_module_t * module)3554 void reload_defaults(dt_iop_module_t *module)
3555 {
3556 dt_iop_channelmixer_rgb_params_t *d = (dt_iop_channelmixer_rgb_params_t *)module->default_params;
3557
3558 d->x = module->get_f("x")->Float.Default;
3559 d->y = module->get_f("y")->Float.Default;
3560 d->temperature = module->get_f("temperature")->Float.Default;
3561 d->illuminant = module->get_f("illuminant")->Enum.Default;
3562 d->adaptation = module->get_f("adaptation")->Enum.Default;
3563
3564 const gboolean is_modern =
3565 dt_conf_is_equal("plugins/darkroom/chromatic-adaptation", "modern");
3566
3567 // note that if there is already an instance of this module with an
3568 // adaptation set we default to RGB (none) in this instance.
3569 // try to register the CAT here
3570 declare_cat_on_pipe(module, is_modern);
3571 // check if we could register
3572 gboolean CAT_already_applied =
3573 (module->dev->proxy.chroma_adaptation != NULL) // CAT exists
3574 && (module->dev->proxy.chroma_adaptation != module); // and it is not us
3575
3576 module->default_enabled = FALSE;
3577
3578 const dt_image_t *img = &module->dev->image_storage;
3579
3580 dt_aligned_pixel_t custom_wb;
3581 if(!CAT_already_applied
3582 && is_modern
3583 && !get_white_balance_coeff(module, custom_wb))
3584 {
3585 // if workflow = modern and we find WB coeffs, take care of white balance here
3586 if(find_temperature_from_raw_coeffs(img, custom_wb, &(d->x), &(d->y)))
3587 d->illuminant = DT_ILLUMINANT_CAMERA;
3588
3589 check_if_close_to_daylight(d->x, d->y, &(d->temperature), &(d->illuminant), &(d->adaptation));
3590 }
3591 else
3592 {
3593 // otherwise, simple channel mixer
3594 d->illuminant = DT_ILLUMINANT_PIPE;
3595 d->adaptation = DT_ADAPTATION_RGB;
3596 }
3597
3598 dt_iop_channelmixer_rgb_gui_data_t *g = (dt_iop_channelmixer_rgb_gui_data_t *)module->gui_data;
3599 if(g)
3600 {
3601 const dt_aligned_pixel_t xyY = { d->x, d->y, 1.f };
3602 dt_aligned_pixel_t Lch = { 0 };
3603 dt_xyY_to_Lch(xyY, Lch);
3604
3605 dt_bauhaus_slider_set_default(g->illum_x, Lch[2] / M_PI * 180.f);
3606 dt_bauhaus_slider_set_default(g->illum_y, Lch[1]);
3607 dt_bauhaus_slider_set_default(g->temperature, d->temperature);
3608 dt_bauhaus_combobox_set_default(g->illuminant, d->illuminant);
3609 dt_bauhaus_combobox_set_default(g->adaptation, d->adaptation);
3610 if(g->delta_E_label_text)
3611 {
3612 g_free(g->delta_E_label_text);
3613 g->delta_E_label_text = NULL;
3614 }
3615
3616 if(dt_image_is_matrix_correction_supported(img))
3617 {
3618 if(dt_bauhaus_combobox_length(g->illuminant) < DT_ILLUMINANT_CAMERA + 1)
3619 dt_bauhaus_combobox_add_full(g->illuminant, _("as shot in camera"), DT_BAUHAUS_COMBOBOX_ALIGN_RIGHT,
3620 GINT_TO_POINTER(DT_ILLUMINANT_CAMERA), NULL, TRUE);
3621 }
3622 else
3623 dt_bauhaus_combobox_remove_at(g->illuminant, DT_ILLUMINANT_CAMERA);
3624
3625 gui_changed(module, NULL, NULL);
3626 }
3627 }
3628
gui_changed(dt_iop_module_t * self,GtkWidget * w,void * previous)3629 void gui_changed(dt_iop_module_t *self, GtkWidget *w, void *previous)
3630 {
3631 dt_iop_channelmixer_rgb_params_t *p = (dt_iop_channelmixer_rgb_params_t *)self->params;
3632 dt_iop_channelmixer_rgb_gui_data_t *g = (dt_iop_channelmixer_rgb_gui_data_t *)self->gui_data;
3633
3634 if(w == g->illuminant)
3635 {
3636 if(previous)
3637 {
3638 dt_illuminant_t *prev_illuminant = (dt_illuminant_t *)previous;
3639 if(*prev_illuminant == DT_ILLUMINANT_CAMERA)
3640 {
3641 // If illuminant was previously set with "as set in camera",
3642 // when changing it, we need to ensure the temperature and chromaticity
3643 // are inited with the correct values taken from camera EXIF.
3644 // Otherwise, if using a preset defining illuminant = "as set in camera",
3645 // temperature and chromaticity are inited with the preset content when illuminant is changed.
3646 dt_aligned_pixel_t custom_wb;
3647 get_white_balance_coeff(self, custom_wb);
3648 find_temperature_from_raw_coeffs(&(self->dev->image_storage), custom_wb, &(p->x), &(p->y));
3649 check_if_close_to_daylight(p->x, p->y, &(p->temperature), NULL, &(p->adaptation));
3650 }
3651 }
3652 if(p->illuminant == DT_ILLUMINANT_CAMERA)
3653 {
3654 // Get camera WB and update illuminant
3655 dt_aligned_pixel_t custom_wb;
3656 get_white_balance_coeff(self, custom_wb);
3657 const int found = find_temperature_from_raw_coeffs(&(self->dev->image_storage), custom_wb, &(p->x), &(p->y));
3658 check_if_close_to_daylight(p->x, p->y, &(p->temperature), NULL, &(p->adaptation));
3659
3660 if(found)
3661 dt_control_log(_("white balance successfully extracted from raw image"));
3662 }
3663 else if(p->illuminant == DT_ILLUMINANT_DETECT_EDGES
3664 || p->illuminant == DT_ILLUMINANT_DETECT_SURFACES)
3665 {
3666 // We need to recompute only the full preview
3667 dt_control_log(_("auto-detection of white balance started…"));
3668 }
3669 }
3670
3671 if(w == g->illuminant || w == g->illum_fluo || w == g->illum_led || w == g->temperature)
3672 {
3673 // Convert and synchronize all the possible ways to define an illuminant to allow swapping modes
3674
3675 if(p->illuminant != DT_ILLUMINANT_CUSTOM && p->illuminant != DT_ILLUMINANT_CAMERA)
3676 {
3677 // We are in any mode defining (x, y) indirectly from an interface, so commit (x, y) explicitly
3678 illuminant_to_xy(p->illuminant, NULL, NULL, &(p->x), &(p->y), p->temperature, p->illum_fluo, p->illum_led);
3679 }
3680
3681 if(p->illuminant != DT_ILLUMINANT_D && p->illuminant != DT_ILLUMINANT_BB && p->illuminant != DT_ILLUMINANT_CAMERA)
3682 {
3683 // We are in any mode not defining explicitly a temperature, so find the the closest CCT and commit it
3684 check_if_close_to_daylight(p->x, p->y, &(p->temperature), NULL, NULL);
3685 }
3686 }
3687
3688 ++darktable.gui->reset;
3689
3690 if(!w || w == g->illuminant || w == g->illum_fluo || w == g->illum_led || w == g->temperature)
3691 {
3692 update_illuminants(self);
3693 update_approx_cct(self);
3694 update_illuminant_color(self);
3695
3696 // force-update all the illuminant sliders in case something above changed them
3697 // notice the hue/chroma of the illuminant has to be computed on-the-fly anyway
3698 dt_aligned_pixel_t xyY = { p->x, p->y, 1.f };
3699 dt_aligned_pixel_t Lch;
3700 dt_xyY_to_Lch(xyY, Lch);
3701
3702 // If the chroma is zero then there is not a meaningful hue angle. In this case
3703 // leave the hue slider where it was, so that if chroma is set to zero and then
3704 // set to a nonzero value, the hue setting will remain unchanged.
3705 if(Lch[1] > 0)
3706 dt_bauhaus_slider_set(g->illum_x, Lch[2] / M_PI * 180.f);
3707 dt_bauhaus_slider_set_soft(g->illum_y, Lch[1]);
3708
3709 // Redraw the temperature background color taking new soft bounds into account
3710 dt_bauhaus_slider_set_soft(g->temperature, p->temperature);
3711 paint_temperature_background(self);
3712 }
3713
3714 if(!w || w == g->scale_red_R || w == g->scale_red_G || w == g->scale_red_B || w == g->normalize_R)
3715 update_R_colors(self);
3716 if(!w || w == g->scale_green_R || w == g->scale_green_G || w == g->scale_green_B || w == g->normalize_G)
3717 update_G_colors(self);
3718 if(!w || w == g->scale_blue_R || w == g->scale_blue_G || w == g->scale_blue_B || w == g->normalize_B)
3719 update_B_colors(self);
3720
3721 // if grey channel is used and norm = 0 and normalization = ON, we are going to have a division by zero
3722 // in commit_param, we avoid dividing by zero automatically, but user needs a notification
3723 if((p->grey[0] != 0.f) || (p->grey[1] != 0.f) || (p->grey[2] != 0.f))
3724 if((p->grey[0] + p->grey[1] + p->grey[2] == 0.f) && p->normalize_grey)
3725 dt_control_log(_("color calibration: the sum of the gray channel parameters is zero, normalization will be disabled."));
3726
3727 if(w == g->adaptation)
3728 {
3729 update_illuminants(self);
3730 update_R_colors(self);
3731 update_G_colors(self);
3732 update_B_colors(self);
3733 }
3734
3735 // If "as shot in camera" illuminant is used, CAT space is forced automatically
3736 // therefore, make the control insensitive
3737 gtk_widget_set_sensitive(g->adaptation, p->illuminant != DT_ILLUMINANT_CAMERA);
3738
3739 declare_cat_on_pipe(self, FALSE);
3740
3741 _check_for_wb_issue_and_set_trouble_message(self);
3742
3743 --darktable.gui->reset;
3744 }
3745
3746
gui_focus(struct dt_iop_module_t * self,gboolean in)3747 void gui_focus(struct dt_iop_module_t *self, gboolean in)
3748 {
3749 gui_changed(self, NULL, NULL);
3750 }
3751
3752
color_picker_apply(dt_iop_module_t * self,GtkWidget * picker,dt_dev_pixelpipe_iop_t * piece)3753 void color_picker_apply(dt_iop_module_t *self, GtkWidget *picker, dt_dev_pixelpipe_iop_t *piece)
3754 {
3755 if(darktable.gui->reset) return;
3756
3757 dt_iop_channelmixer_rgb_gui_data_t *g = (dt_iop_channelmixer_rgb_gui_data_t *)self->gui_data;
3758 dt_iop_channelmixer_rgb_params_t *p = (dt_iop_channelmixer_rgb_params_t *)self->params;
3759
3760 // capture gui color picked event.
3761 if(self->picked_color_max[0] < self->picked_color_min[0]) return;
3762 const float *RGB = self->picked_color;
3763
3764 // Get work profile
3765 const dt_iop_order_iccprofile_info_t *const work_profile = dt_ioppr_get_pipe_work_profile_info(piece->pipe);
3766 if(work_profile == NULL) return;
3767
3768 // Convert to XYZ
3769 dt_aligned_pixel_t XYZ;
3770 dot_product(RGB, work_profile->matrix_in, XYZ);
3771
3772 // Convert to xyY
3773 const float sum = fmaxf(XYZ[0] + XYZ[1] + XYZ[2], NORM_MIN);
3774 XYZ[0] /= sum; // x
3775 XYZ[2] = XYZ[1]; // Y
3776 XYZ[1] /= sum; // y
3777
3778 p->x = XYZ[0];
3779 p->y = XYZ[1];
3780
3781 ++darktable.gui->reset;
3782
3783 check_if_close_to_daylight(p->x, p->y, &p->temperature, &p->illuminant, NULL);
3784
3785 dt_bauhaus_slider_set_soft(g->temperature, p->temperature);
3786 dt_bauhaus_combobox_set(g->illuminant, p->illuminant);
3787 dt_bauhaus_combobox_set(g->adaptation, p->adaptation);
3788
3789 const dt_aligned_pixel_t xyY = { p->x, p->y, 1.f };
3790 dt_aligned_pixel_t Lch = { 0 };
3791 dt_xyY_to_Lch(xyY, Lch);
3792 dt_bauhaus_slider_set(g->illum_x, Lch[2] / M_PI * 180.f);
3793 dt_bauhaus_slider_set_soft(g->illum_y, Lch[1]);
3794
3795 update_illuminants(self);
3796 update_approx_cct(self);
3797 update_illuminant_color(self);
3798 paint_temperature_background(self);
3799
3800 --darktable.gui->reset;
3801
3802 dt_dev_add_history_item(darktable.develop, self, TRUE);
3803 }
3804
3805
gui_init(struct dt_iop_module_t * self)3806 void gui_init(struct dt_iop_module_t *self)
3807 {
3808 dt_iop_channelmixer_rgb_gui_data_t *g = IOP_GUI_ALLOC(channelmixer_rgb);
3809
3810 // Init the color checker UI
3811 for(size_t k = 0; k < 4; k++)
3812 {
3813 g->box[k].x = g->box[k].y = -1.;
3814 g->active_node[k] = FALSE;
3815 }
3816 g->is_cursor_close = FALSE;
3817 g->drag_drop = FALSE;
3818 g->is_profiling_started = FALSE;
3819 g->run_profile = FALSE;
3820 g->run_validation = FALSE;
3821 g->profile_ready = FALSE;
3822 g->checker_ready = FALSE;
3823 g->delta_E_in = NULL;
3824 g->delta_E_label_text = NULL;
3825
3826 g->XYZ[0] = NAN;
3827
3828 DT_DEBUG_CONTROL_SIGNAL_CONNECT(darktable.signals, DT_SIGNAL_DEVELOP_UI_PIPE_FINISHED,
3829 G_CALLBACK(_develop_ui_pipe_finished_callback), self);
3830 DT_DEBUG_CONTROL_SIGNAL_CONNECT(darktable.signals, DT_SIGNAL_DEVELOP_PREVIEW_PIPE_FINISHED,
3831 G_CALLBACK(_preview_pipe_finished_callback), self);
3832
3833 // Init GTK notebook
3834 static dt_action_def_t notebook_def = { };
3835 g->notebook = dt_ui_notebook_new(¬ebook_def);
3836 dt_action_define_iop(self, NULL, N_("page"), GTK_WIDGET(g->notebook), ¬ebook_def);
3837
3838 // Page CAT
3839 self->widget = dt_ui_notebook_page(g->notebook, N_("CAT"), _("chromatic adaptation transform"));
3840
3841 g->adaptation = dt_bauhaus_combobox_from_params(self, N_("adaptation"));
3842 gtk_widget_set_tooltip_text(GTK_WIDGET(g->adaptation),
3843 _("choose the method to adapt the illuminant\n"
3844 "and the colorspace in which the module works: \n"
3845 "• Linear Bradford (1985) is consistent with ICC v4 toolchain.\n"
3846 "• CAT16 (2016) is more robust and accurate.\n"
3847 "• Non-linear Bradford (1985) is the original Bradford,\n"
3848 "it can produce better results than the linear version, but is unreliable.\n"
3849 "• XYZ is a simple scaling in XYZ space. It is not recommended in general.\n"
3850 "• none disables any adaptation and uses pipeline working RGB."));
3851
3852 GtkWidget *hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
3853
3854 g->approx_cct = dt_ui_label_new("CCT:");
3855 gtk_box_pack_start(GTK_BOX(hbox), g->approx_cct, FALSE, FALSE, 0);
3856
3857 g->illum_color = GTK_WIDGET(gtk_drawing_area_new());
3858 gtk_widget_set_size_request(g->illum_color, 2 * DT_PIXEL_APPLY_DPI(darktable.bauhaus->quad_width), -1);
3859 gtk_widget_set_tooltip_text(GTK_WIDGET(g->illum_color),
3860 _("this is the color of the scene illuminant before chromatic adaptation\n"
3861 "this color will be turned into pure white by the adaptation."));
3862
3863 g_signal_connect(G_OBJECT(g->illum_color), "draw", G_CALLBACK(illuminant_color_draw), self);
3864 gtk_box_pack_start(GTK_BOX(hbox), g->illum_color, TRUE, TRUE, 0);
3865
3866 g->color_picker = dt_color_picker_new(self, DT_COLOR_PICKER_AREA, hbox);
3867 gtk_widget_set_tooltip_text(g->color_picker, _("set white balance to detected from area"));
3868
3869 gtk_box_pack_start(GTK_BOX(self->widget), GTK_WIDGET(hbox), FALSE, FALSE, 0);
3870
3871 g->illuminant = dt_bauhaus_combobox_from_params(self, N_("illuminant"));
3872
3873 g->illum_fluo = dt_bauhaus_combobox_from_params(self, "illum_fluo");
3874
3875 g->illum_led = dt_bauhaus_combobox_from_params(self, "illum_led");
3876
3877 g->temperature = dt_bauhaus_slider_from_params(self, N_("temperature"));
3878 dt_bauhaus_slider_set_soft_range(g->temperature, 3000., 7000.);
3879 dt_bauhaus_slider_set_step(g->temperature, 50.);
3880 dt_bauhaus_slider_set_digits(g->temperature, 0);
3881 dt_bauhaus_slider_set_format(g->temperature, "%.0f K");
3882
3883 g->illum_x = dt_bauhaus_slider_new_with_range_and_feedback(self, 0., 360., 0.5, 0, 1, 0);
3884 dt_bauhaus_widget_set_label(g->illum_x, NULL, _("hue"));
3885 dt_bauhaus_slider_set_format(g->illum_x, "%.1f °");
3886 g_signal_connect(G_OBJECT(g->illum_x), "value-changed", G_CALLBACK(illum_xy_callback), self);
3887 gtk_box_pack_start(GTK_BOX(self->widget), GTK_WIDGET(g->illum_x), FALSE, FALSE, 0);
3888
3889 g->illum_y = dt_bauhaus_slider_new_with_range(self, 0., 100., 0.5, 0, 1);
3890 dt_bauhaus_widget_set_label(g->illum_y, NULL, _("chroma"));
3891 dt_bauhaus_slider_set_format(g->illum_y, "%.1f %%");
3892 dt_bauhaus_slider_set_hard_max(g->illum_y, 300.f);
3893 g_signal_connect(G_OBJECT(g->illum_y), "value-changed", G_CALLBACK(illum_xy_callback), self);
3894 gtk_box_pack_start(GTK_BOX(self->widget), GTK_WIDGET(g->illum_y), FALSE, FALSE, 0);
3895
3896 g->gamut = dt_bauhaus_slider_from_params(self, "gamut");
3897 dt_bauhaus_slider_set_hard_max(g->gamut, 12.f);
3898
3899 g->clip = dt_bauhaus_toggle_from_params(self, "clip");
3900
3901 GtkWidget *first, *second, *third;
3902 #define NOTEBOOK_PAGE(var, short, label, tooltip, section, swap) \
3903 self->widget = dt_ui_notebook_page(g->notebook, label, _(tooltip)); \
3904 \
3905 first = dt_bauhaus_slider_from_params(self, swap ? #var "[2]" : #var "[0]");\
3906 dt_bauhaus_slider_set_step(first, 0.005); \
3907 dt_bauhaus_slider_set_digits(first, 3); \
3908 dt_bauhaus_slider_set_hard_min(first, -2.f); \
3909 dt_bauhaus_slider_set_hard_max(first, 2.f); \
3910 dt_bauhaus_widget_set_label(first, section, N_("input R")); \
3911 \
3912 second = dt_bauhaus_slider_from_params(self, #var "[1]"); \
3913 dt_bauhaus_slider_set_step(second, 0.005); \
3914 dt_bauhaus_slider_set_digits(second, 3); \
3915 dt_bauhaus_slider_set_hard_min(second, -2.f); \
3916 dt_bauhaus_slider_set_hard_max(second, 2.f); \
3917 dt_bauhaus_widget_set_label(second, section, N_("input G")); \
3918 \
3919 third = dt_bauhaus_slider_from_params(self, swap ? #var "[0]" : #var "[2]");\
3920 dt_bauhaus_slider_set_step(third, 0.005); \
3921 dt_bauhaus_slider_set_digits(third, 3); \
3922 dt_bauhaus_slider_set_hard_min(third, -2.f); \
3923 dt_bauhaus_slider_set_hard_max(third, 2.f); \
3924 dt_bauhaus_widget_set_label(third, section, N_("input B")); \
3925 \
3926 g->scale_##var##_R = swap ? third : first; \
3927 g->scale_##var##_G = second; \
3928 g->scale_##var##_B = swap ? first : third; \
3929 \
3930 g->normalize_##short = dt_bauhaus_toggle_from_params(self, "normalize_" #short);
3931
3932 NOTEBOOK_PAGE(red, R, N_("R"), N_("output R"), N_("red"), FALSE)
3933 NOTEBOOK_PAGE(green, G, N_("G"), N_("output G"), N_("green"), FALSE)
3934 NOTEBOOK_PAGE(blue, B, N_("B"), N_("output B"), N_("blue"), FALSE)
3935 NOTEBOOK_PAGE(saturation, sat, N_("colorfulness"), N_("output colorfulness"), N_("colorfulness"), FALSE)
3936 g->saturation_version = dt_bauhaus_combobox_from_params(self, "version");
3937 NOTEBOOK_PAGE(lightness, light, N_("brightness"), N_("output brightness"), N_("brightness"), FALSE)
3938 NOTEBOOK_PAGE(grey, grey, N_("gray"), N_("output gray"), N_("gray"), FALSE)
3939
3940 // start building top level widget
3941 self->widget = gtk_box_new(GTK_ORIENTATION_VERTICAL, DT_BAUHAUS_SPACE);
3942
3943 gtk_box_pack_start(GTK_BOX(self->widget), GTK_WIDGET(g->notebook), FALSE, FALSE, 0);
3944 const int active_page = dt_conf_get_int("plugins/darkroom/channelmixerrgb/gui_page");
3945 gtk_widget_show(gtk_notebook_get_nth_page(g->notebook, active_page));
3946 gtk_notebook_set_current_page(g->notebook, active_page);
3947
3948 // Add the color checker collapsible panel
3949 g->start_profiling = dt_bauhaus_combobox_new(self);
3950 dt_bauhaus_widget_set_label(g->start_profiling, NULL, N_("calibrate with a color checker"));
3951 dt_bauhaus_widget_set_quad_paint(g->start_profiling, dtgtk_cairo_paint_solid_arrow, CPF_STYLE_BOX | CPF_DIRECTION_LEFT, NULL);
3952 dt_bauhaus_widget_set_quad_toggle(g->start_profiling, TRUE);
3953 gtk_widget_set_tooltip_text(g->start_profiling, _("use a color checker target to autoset CAT and channels"));
3954 g_signal_connect(G_OBJECT(g->start_profiling), "quad-pressed", G_CALLBACK(start_profiling_callback), self);
3955 gtk_box_pack_start(GTK_BOX(self->widget), GTK_WIDGET(g->start_profiling), TRUE, TRUE, 0);
3956
3957 g->collapsible = gtk_box_new(GTK_ORIENTATION_VERTICAL, DT_BAUHAUS_SPACE);
3958
3959 DT_BAUHAUS_COMBOBOX_NEW_FULL(g->checkers_list, self, NULL, N_("chart"),
3960 _("choose the vendor and the type of your chart"),
3961 0, checker_changed_callback, self,
3962 N_("Xrite ColorChecker 24 pre-2014"),
3963 N_("Xrite ColorChecker 24 post-2014"),
3964 N_("Datacolor SpyderCheckr 24 pre-2018"),
3965 N_("Datacolor SpyderCheckr 24 post-2018"),
3966 N_("Datacolor SpyderCheckr 48 pre-2018"),
3967 N_("Datacolor SpyderCheckr 48 post-2018"));
3968 gtk_box_pack_start(GTK_BOX(g->collapsible), GTK_WIDGET(g->checkers_list), TRUE, TRUE, 0);
3969
3970 DT_BAUHAUS_COMBOBOX_NEW_FULL(g->optimize, self, NULL, N_("optimize for"),
3971 _("choose the colors that will be optimized with higher priority.\n"
3972 "neutral colors gives the lowest average delta E but a high maximum delta E\n"
3973 "saturated colors gives the lowest maximum delta E but a high average delta E\n"
3974 "none is a trade-off between both\n"
3975 "the others are special behaviours to protect some hues"),
3976 0, optimize_changed_callback, self,
3977 N_("none"),
3978 N_("neutral colors"),
3979 N_("saturated colors"),
3980 N_("skin and soil colors"),
3981 N_("foliage colors"),
3982 N_("sky and water colors"),
3983 N_("average delta E"),
3984 N_("maximum delta E"));
3985 gtk_box_pack_start(GTK_BOX(g->collapsible), GTK_WIDGET(g->optimize), TRUE, TRUE, 0);
3986
3987 g->safety = dt_bauhaus_slider_new_with_range_and_feedback(self, 0., 1., 0.1, 0.5, 3, TRUE);
3988 dt_bauhaus_widget_set_label(g->safety, NULL, _("patch scale"));
3989 gtk_widget_set_tooltip_text(g->safety, _("reduce the radius of the patches to select the more or less central part.\n"
3990 "useful when the perspective correction is sloppy or\n"
3991 "the patches frame cast a shadows on the edges of the patch." ));
3992 g_signal_connect(G_OBJECT(g->safety), "value-changed", G_CALLBACK(safety_changed_callback), self);
3993 gtk_box_pack_start(GTK_BOX(g->collapsible), GTK_WIDGET(g->safety), TRUE, TRUE, 0);
3994
3995 g->label_delta_E = dt_ui_label_new("");
3996 gtk_box_pack_start(GTK_BOX(g->collapsible), GTK_WIDGET(g->label_delta_E), TRUE, TRUE, 0);
3997 gtk_widget_set_tooltip_text(g->label_delta_E, _("the delta E is using the CIE 2000 formula."));
3998
3999 GtkWidget *toolbar = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, DT_BAUHAUS_SPACE);
4000
4001 g->button_commit = dtgtk_button_new(dtgtk_cairo_paint_check_mark, CPF_STYLE_BOX, NULL);
4002 gtk_box_pack_end(GTK_BOX(toolbar), GTK_WIDGET(g->button_commit), FALSE, FALSE, 0);
4003 gtk_widget_set_tooltip_text(g->button_commit, _("accept the computed profile and set it in the module"));
4004 g_signal_connect(G_OBJECT(g->button_commit), "button-press-event", G_CALLBACK(commit_profile_callback), (gpointer)self);
4005
4006 g->button_profile = dtgtk_button_new(dtgtk_cairo_paint_refresh, CPF_STYLE_BOX, NULL);
4007 g_signal_connect(G_OBJECT(g->button_profile), "button-press-event", G_CALLBACK(run_profile_callback), (gpointer)self);
4008 gtk_widget_set_tooltip_text(g->button_profile, _("recompute the profile"));
4009 gtk_box_pack_end(GTK_BOX(toolbar), GTK_WIDGET(g->button_profile), FALSE, FALSE, 0);
4010
4011 g->button_validate = dtgtk_button_new(dtgtk_cairo_paint_softproof, CPF_STYLE_BOX, NULL);
4012 g_signal_connect(G_OBJECT(g->button_validate), "button-press-event", G_CALLBACK(run_validation_callback), (gpointer)self);
4013 gtk_widget_set_tooltip_text(g->button_validate, _("check the output delta E"));
4014 gtk_box_pack_end(GTK_BOX(toolbar), GTK_WIDGET(g->button_validate), FALSE, FALSE, 0);
4015
4016 gtk_box_pack_start(GTK_BOX(g->collapsible), GTK_WIDGET(toolbar), FALSE, FALSE, 0);
4017
4018 gtk_box_pack_start(GTK_BOX(self->widget), GTK_WIDGET(g->collapsible), FALSE, FALSE, 0);
4019 }
4020
gui_cleanup(struct dt_iop_module_t * self)4021 void gui_cleanup(struct dt_iop_module_t *self)
4022 {
4023 self->request_color_pick = DT_REQUEST_COLORPICK_OFF;
4024 DT_DEBUG_CONTROL_SIGNAL_DISCONNECT(darktable.signals,
4025 G_CALLBACK(_develop_ui_pipe_finished_callback), self);
4026 DT_DEBUG_CONTROL_SIGNAL_DISCONNECT(darktable.signals, G_CALLBACK(_preview_pipe_finished_callback), self);
4027
4028 dt_iop_channelmixer_rgb_gui_data_t *g = (dt_iop_channelmixer_rgb_gui_data_t *)self->gui_data;
4029 dt_conf_set_int("plugins/darkroom/channelmixerrgb/gui_page", gtk_notebook_get_current_page (g->notebook));
4030
4031 if(g->delta_E_in)
4032 {
4033 dt_free_align(g->delta_E_in);
4034 g->delta_E_in = NULL;
4035 }
4036
4037 g_free(g->delta_E_label_text);
4038
4039 IOP_GUI_FREE;
4040 }
4041
4042 // modelines: These editor modelines have been set for all relevant files by tools/update_modelines.sh
4043 // vim: shiftwidth=2 expandtab tabstop=2 cindent
4044 // kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified;
4045