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(&notebook_def);
3836   dt_action_define_iop(self, NULL, N_("page"), GTK_WIDGET(g->notebook), &notebook_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