Ansel 0.0
A darktable fork - bloat + design vision
Loading...
Searching...
No Matches
channelmixerrgb.c
Go to the documentation of this file.
1/*
2 This file is part of the Ansel project.
3 Copyright (C) 2020-2023, 2025-2026 Aurélien PIERRE.
4 Copyright (C) 2020-2022 Chris Elston.
5 Copyright (C) 2020-2022 Diederik Ter Rahe.
6 Copyright (C) 2020 EdgarLux.
7 Copyright (C) 2020 Harold le Clément de Saint-Marcq.
8 Copyright (C) 2020 Heiko Bauke.
9 Copyright (C) 2020-2021 Hubert Kowalski.
10 Copyright (C) 2020-2021 Nicolas Auffray.
11 Copyright (C) 2020-2022 Pascal Obry.
12 Copyright (C) 2020-2021 Ralf Brown.
13 Copyright (C) 2021 luzpaz.
14 Copyright (C) 2021 Marco Carrarini.
15 Copyright (C) 2021 Mark-64.
16 Copyright (C) 2021 Olivier Samyn 🎻.
17 Copyright (C) 2021 Paolo DePetrillo.
18 Copyright (C) 2021 paolodepetrillo.
19 Copyright (C) 2021-2022 Sakari Kapanen.
20 Copyright (C) 2021-2022 Victor Forsiuk.
21 Copyright (C) 2022 Aldric Renaudin.
22 Copyright (C) 2022 Hanno Schwalm.
23 Copyright (C) 2022 Martin Bařinka.
24 Copyright (C) 2022 Philipp Lutz.
25 Copyright (C) 2023-2024 Alynx Zhou.
26 Copyright (C) 2023 Luca Zulberti.
27 Copyright (C) 2025-2026 Guillaume Stutin.
28
29 Ansel is free software: you can redistribute it and/or modify
30 it under the terms of the GNU General Public License as published by
31 the Free Software Foundation, either version 3 of the License, or
32 (at your option) any later version.
33
34 Ansel is distributed in the hope that it will be useful,
35 but WITHOUT ANY WARRANTY; without even the implied warranty of
36 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
37 GNU General Public License for more details.
38
39 You should have received a copy of the GNU General Public License
40 along with Ansel. If not, see <http://www.gnu.org/licenses/>.
41*/
42
43#ifdef HAVE_CONFIG_H
45#include "widgets/button.h"
46#include "common/conf.h"
47#include "config.h"
48#endif
49#include "widgets/bauhaus.h"
50#include "common/paths.h" // DT_PATH_MAX
52#include "system/macros.h"
53#include "system/openmp.h"
55#include "system/mem_alloc.h"
56#include "system/simd.h"
57#include "common/logging.h"
59#include "math/homography.h"
62#include "common/colorchecker.h"
63#include "math/matrices.h"
65#include "pixel/illuminants.h"
66#include "common/imagebuf.h"
68#include "develop/iop_profile.h"
69#include "common/opencl.h"
70#include "control/control.h"
71#include "develop/imageop_gui.h"
72#include "math/openmp_maths.h"
75#include "gui/presets.h"
77#include "iop/iop_api.h"
78
79// The shared implementation is its own translation unit, compiled once and linked
80// into the single IOP object set. It used to be textually included here, once per
81// consuming module, because two shared objects could not each carry a copy of its
82// globals; with the modules linked statically there is exactly one link and one copy.
83
84#include <assert.h>
85#include <float.h>
86#include <gtk/gtk.h>
87#include <glib.h>
88#include <inttypes.h>
89#include <math.h>
90#include <stdlib.h>
91#include <string.h>
92#include <time.h>
93#include "widgets/label.h"
94#include "widgets/notebook.h"
95#include "gui/screen_metrics.h"
96#include "control/signal.h"
97
99
100#define CHANNEL_SIZE 4
101#define INVERSE_SQRT_3 0.5773502691896258f
102#define COLOR_MIN -2.0
103#define COLOR_MAX 2.0
104#define ILLUM_X_MAX 360.0
105#define ILLUM_Y_MAX 300.0
106#define LIGHTNESS_MAX 100.0
107#define HUE_MAX 360.0
108#define CHROMA_MAX 128.0
109#define TEMP_MIN 1667.
110#define TEMP_MAX 25000.
111#define DT_CHANNELMIXERRGB_SIMPLE_MODE_CONF "plugins/darkroom/channelmixerrgb/mixer_mode"
112#define DT_CHANNELMIXERRGB_SIMPLE_TAN_SCALE DT_IOP_CHANNELMIXER_SHARED_SIMPLE_TAN_SCALE
113#define DT_CHANNELMIXERRGB_SIMPLE_EPS DT_IOP_CHANNELMIXER_SHARED_SIMPLE_EPS
114#define DT_CHANNELMIXERRGB_SIMPLE_CHROMA_PROBE DT_IOP_CHANNELMIXER_SHARED_SIMPLE_CHROMA_PROBE
115
117{
118 CHANNELMIXERRGB_V_1 = 0, // $DESCRIPTION: "version 1 (2020)"
119 CHANNELMIXERRGB_V_2 = 1, // $DESCRIPTION: "version 2 (2021)"
120 CHANNELMIXERRGB_V_3 = 2, // $DESCRIPTION: "version 3 (Apr 2021)"
122
124{
125 /* params of v1 and v2 */
126 float red[CHANNEL_SIZE]; // $MIN: COLOR_MIN $MAX: COLOR_MAX
127 float green[CHANNEL_SIZE]; // $MIN: COLOR_MIN $MAX: COLOR_MAX
128 float blue[CHANNEL_SIZE]; // $MIN: COLOR_MIN $MAX: COLOR_MAX
129 float saturation[CHANNEL_SIZE]; // $MIN: -1.0 $MAX: 1.0
130 float lightness[CHANNEL_SIZE]; // $MIN: -1.0 $MAX: 1.0
131 float grey[CHANNEL_SIZE]; // $MIN: -2.0 $MAX: 2.0
132 gboolean normalize_R, normalize_G, normalize_B, normalize_sat, normalize_light, normalize_grey; // $DESCRIPTION: "normalize channels"
133 dt_illuminant_t illuminant; // $DEFAULT: DT_ILLUMINANT_D
134 dt_illuminant_fluo_t illum_fluo; // $DEFAULT: DT_ILLUMINANT_FLUO_F3 $DESCRIPTION: "F source"
135 dt_illuminant_led_t illum_led; // $DEFAULT: DT_ILLUMINANT_LED_B5 $DESCRIPTION: "LED source"
136 dt_adaptation_t adaptation; // $DEFAULT: DT_ADAPTATION_CAT16
137 float x, y; // $DEFAULT: 0.333
138 float temperature; // $MIN: TEMP_MIN $MAX: TEMP_MAX $DEFAULT: 5003.
139 float gamut; // $MIN: 0.0 $MAX: 12.0 $DEFAULT: 1.0 $DESCRIPTION: "gamut compression"
140 gboolean clip; // $DEFAULT: TRUE $DESCRIPTION: "clip negative RGB from gamut"
141
142 /* params of v3 */
143 dt_iop_channelmixer_rgb_version_t version; // $DEFAULT: CHANNELMIXERRGB_V_3 $DESCRIPTION: "saturation algorithm"
144
145 /* always add new params after this so we can import legacy params with memcpy on the common part of the struct */
146
148
149
161
162
169
177
183
184#define DT_CHANNELMIXERRGB_SIMPLE_PROBE_ROTATION DT_IOP_CHANNELMIXER_SHARED_SIMPLE_PROBE_ROTATION
185#define DT_CHANNELMIXERRGB_SIMPLE_PROBE_AXIS_1 DT_IOP_CHANNELMIXER_SHARED_SIMPLE_PROBE_AXIS_1
186#define DT_CHANNELMIXERRGB_SIMPLE_PROBE_AXIS_2 DT_IOP_CHANNELMIXER_SHARED_SIMPLE_PROBE_AXIS_2
187#define DT_CHANNELMIXERRGB_PRIMARIES_BASIS_RGB DT_IOP_CHANNELMIXER_SHARED_PRIMARIES_BASIS_RGB
188#define DT_CHANNELMIXERRGB_PRIMARIES_BASIS_XYZ DT_IOP_CHANNELMIXER_SHARED_PRIMARIES_BASIS_XYZ
189#define DT_CHANNELMIXERRGB_PRIMARIES_BASIS_BRADFORD DT_IOP_CHANNELMIXER_SHARED_PRIMARIES_BASIS_BRADFORD
190#define DT_CHANNELMIXERRGB_PRIMARIES_BASIS_CAT16 DT_IOP_CHANNELMIXER_SHARED_PRIMARIES_BASIS_CAT16
191
193{
194 GtkNotebook *notebook;
212 float xy[2];
213 float XYZ[4];
214
215 point_t box[4]; // the current coordinates, possibly non rectangle, of the bounding box for the color checker
216 point_t ideal_box[4]; // the desired coordinates of the perfect rectangle bounding box for the color checker
217 point_t center_box; // the barycenter of both boxes
218 gboolean active_node[4]; // true if the cursor is close to a node (node = corner of the bounding box)
219 gboolean is_cursor_close; // do we have the cursor close to a node ?
220 gboolean drag_drop; // are we currently dragging and dropping a node ?
221 point_t click_start; // the coordinates where the drag and drop started
222 point_t click_end; // the coordinates where the drag and drop started
226
227 float homography[9]; // the perspective correction matrix
228 float inverse_homography[9]; // The inverse perspective correction matrix
229 gboolean run_profile; // order a profiling at next pipeline recompute
230 gboolean run_validation; // order a profile validation at next pipeline recompute
231 gboolean profile_ready; // notify that a profile is ready to be applied
232 gboolean checker_ready; // notify that a checker bounding box is ready to be used
234
236
237 GList *colorcheckers_all_color; // list of CGATS files
238 GList *colorcheckers_color; // list of CGATS files with the same number of patches as the current checker
240 GtkWidget *checker_msg; // message label
241
244
246
248
251
259
274
283
284
288
289const char *name()
290{
291 return _("color _calibration");
292}
293
294const char *aliases()
295{
296 return _("channel mixer|white balance|monochrome");
297}
298
299const char **description(struct dt_iop_module_t *self)
300{
301 return dt_iop_set_description(self, _("perform color space corrections\n"
302 "such as white balance, channels mixing\n"
303 "and conversions to monochrome emulating film"),
304 _("corrective or creative"),
305 _("linear, RGB, scene-referred"),
306 _("linear, RGB or XYZ"),
307 _("linear, RGB, scene-referred"));
308}
309
314
316{
317 return IOP_GROUP_COLOR;
318}
319
321{
322 return IOP_CS_RGB;
323}
324
327{
328 default_input_format(self, pipe, piece, dsc);
329 dsc->channels = 4;
330 dsc->datatype = TYPE_FLOAT;
331}
332
333int legacy_params(dt_iop_module_t *self, const void *const old_params, const int old_version, void *new_params,
334 const int new_version)
335{
336 if(old_version == 1 && new_version == 3)
337 {
338 // V1 and V2 use the same param structure but the normalize_grey param had no effect since commit_params
339 // forced normalization no matter what. So we re-import the params and force the param to TRUE to keep edits.
340 memcpy(new_params, old_params, sizeof(dt_iop_channelmixer_rgb_params_t));
343
344 // V2 and V3 use the same param structure but these :
345
346 // swap the saturation parameters for R and B to put them in natural order
347 const float R = n->saturation[0];
348 const float B = n->saturation[2];
349 n->saturation[0] = B;
350 n->saturation[2] = R;
351
352 // say that these params were created with legacy code
353 n->version = CHANNELMIXERRGB_V_1;
354
355 return 0;
356 }
357 if(old_version == 2 && new_version == 3)
358 {
359 typedef struct dt_iop_channelmixer_rgb_params_v2_t
360 {
361 float red[CHANNEL_SIZE]; // $MIN: -2.0 $MAX: 2.0
362 float green[CHANNEL_SIZE]; // $MIN: -2.0 $MAX: 2.0
363 float blue[CHANNEL_SIZE]; // $MIN: -2.0 $MAX: 2.0
364 float saturation[CHANNEL_SIZE]; // $MIN: -1.0 $MAX: 1.0
365 float lightness[CHANNEL_SIZE]; // $MIN: -1.0 $MAX: 1.0
366 float grey[CHANNEL_SIZE]; // $MIN: 0.0 $MAX: 1.0
367 gboolean normalize_R, normalize_G, normalize_B, normalize_sat, normalize_light, normalize_grey; // $DESCRIPTION: "normalize channels"
368 dt_illuminant_t illuminant; // $DEFAULT: DT_ILLUMINANT_D
369 dt_illuminant_fluo_t illum_fluo; // $DEFAULT: DT_ILLUMINANT_FLUO_F3 $DESCRIPTION: "F source"
370 dt_illuminant_led_t illum_led; // $DEFAULT: DT_ILLUMINANT_LED_B5 $DESCRIPTION: "LED source"
371 dt_adaptation_t adaptation; // $DEFAULT: DT_ADAPTATION_LINEAR_BRADFORD
372 float x, y; // $DEFAULT: 0.333
373 float temperature; // $MIN: 1667. $MAX: 25000. $DEFAULT: 5003.
374 float gamut; // $MIN: 0.0 $MAX: 4.0 $DEFAULT: 1.0 $DESCRIPTION: "gamut compression"
375 gboolean clip; // $DEFAULT: TRUE $DESCRIPTION: "clip negative RGB from gamut"
376 } dt_iop_channelmixer_rgb_params_v2_t;
377
378 memcpy(new_params, old_params, sizeof(dt_iop_channelmixer_rgb_params_v2_t));
380
381 // swap the saturation parameters for R and B to put them in natural order
382 const float R = n->saturation[0];
383 const float B = n->saturation[2];
384 n->saturation[0] = B;
385 n->saturation[2] = R;
386
387 // say that these params were created with legacy code
388 n->version = CHANNELMIXERRGB_V_1;
389
390 return 0;
391 }
392 return 1;
393}
394
396{
398 memset(&p, 0, sizeof(p));
399
400 p.version = CHANNELMIXERRGB_V_3;
401
402 // bypass adaptation
403 p.illuminant = DT_ILLUMINANT_PIPE;
404 p.adaptation = DT_ADAPTATION_XYZ;
405
406 // set everything to no-op
407 p.gamut = 0.f;
408 p.clip = FALSE;
409 p.illum_fluo = DT_ILLUMINANT_FLUO_F3;
410 p.illum_led = DT_ILLUMINANT_LED_B5;
411 p.temperature = 5003.f;
413
414 p.red[0] = 1.f;
415 p.red[1] = 0.f;
416 p.red[2] = 0.f;
417 p.green[0] = 0.f;
418 p.green[1] = 1.f;
419 p.green[2] = 0.f;
420 p.blue[0] = 0.f;
421 p.blue[1] = 0.f;
422 p.blue[2] = 1.f;
423
424 p.saturation[0] = 0.f;
425 p.saturation[1] = 0.f;
426 p.saturation[2] = 0.f;
427 p.lightness[0] = 0.f;
428 p.lightness[1] = 0.f;
429 p.lightness[2] = 0.f;
430 p.grey[0] = 0.f;
431 p.grey[1] = 0.f;
432 p.grey[2] = 0.f;
433
434 p.normalize_R = TRUE;
435 p.normalize_G = TRUE;
436 p.normalize_B = TRUE;
437 p.normalize_sat = FALSE;
438 p.normalize_light = FALSE;
439 p.normalize_grey = TRUE;
440
441 // Create B&W presets
442 p.clip = TRUE;
443 p.grey[0] = 0.f;
444 p.grey[1] = 1.f;
445 p.grey[2] = 0.f;
446
447 dt_gui_presets_add_generic(_("B&W: luminance-based"), self->op,
448 self->version(), &p, sizeof(p), 1);
449
450 // film emulations
451
452 /* These emulations are built using spectral sensitivies provided by film manufacturers for tungsten light,
453 * corrected in spectral domain for D50 illuminant, and integrated in spectral space against CIE 2° 1931 XYZ
454 * color matching functions in the Python lib Colour, with the following code :
455 *
456 import colour
457 import numpy as np
458
459 XYZ = np.zeros((3))
460
461 for l in range(360, 830):
462 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]
463
464 XYZ / np.sum(XYZ)
465 *
466 * The film CMF is visually approximated from the graph. It is still more accurate than bullshit factors
467 * in legacy channel mixer that don't even say in which RGB space they are supposed to be applied.
468 */
469
470 // ILFORD HP5 +
471 // https://www.ilfordphoto.com/amfile/file/download/file/1903/product/695/
472 p.grey[0] = 0.25304098f;
473 p.grey[1] = 0.25958747f;
474 p.grey[2] = 0.48737156f;
475
476 dt_gui_presets_add_generic(_("B&W: ILFORD HP5+"), self->op,
477 self->version(), &p, sizeof(p), 1);
478
479 // ILFORD Delta 100
480 // https://www.ilfordphoto.com/amfile/file/download/file/3/product/681/
481 p.grey[0] = 0.24552374f;
482 p.grey[1] = 0.25366007f;
483 p.grey[2] = 0.50081619f;
484
485 dt_gui_presets_add_generic(_("B&W: ILFORD DELTA 100"), self->op,
486 self->version(), &p, sizeof(p), 1);
487
488 // ILFORD Delta 400 and 3200 - they have the same curve
489 // https://www.ilfordphoto.com/amfile/file/download/file/1915/product/685/
490 // https://www.ilfordphoto.com/amfile/file/download/file/1913/product/683/
491 p.grey[0] = 0.24376712f;
492 p.grey[1] = 0.23613559f;
493 p.grey[2] = 0.52009729f;
494
495 dt_gui_presets_add_generic(_("B&W: ILFORD DELTA 400 - 3200"), self->op,
496 self->version(), &p, sizeof(p), 1);
497
498 // ILFORD FP4+
499 // https://www.ilfordphoto.com/amfile/file/download/file/1919/product/690/
500 p.grey[0] = 0.24149085f;
501 p.grey[1] = 0.22149272f;
502 p.grey[2] = 0.53701643f;
503
504 dt_gui_presets_add_generic(_("B&W: ILFORD FP4+"), self->op,
505 self->version(), &p, sizeof(p), 1);
506
507 // Fuji Acros 100
508 // https://dacnard.wordpress.com/2013/02/15/the-real-shades-of-gray-bw-film-is-a-matter-of-heart-pt-1/
509 p.grey[0] = 0.333f;
510 p.grey[1] = 0.313f;
511 p.grey[2] = 0.353f;
512
513 dt_gui_presets_add_generic(_("B&W: Fuji Acros 100"), self->op,
514 self->version(), &p, sizeof(p), 1);
515
516 // Kodak ?
517 // can't find spectral sensitivity curves and the illuminant under which they are produced,
518 // so ¯\_(ツ)_/¯
519
520 // basic channel-mixer
521 p.adaptation = DT_ADAPTATION_RGB; // bypass adaptation
522 p.grey[0] = 0.f;
523 p.grey[1] = 0.f;
524 p.grey[2] = 0.f;
525 p.normalize_R = TRUE;
526 p.normalize_G = TRUE;
527 p.normalize_B = TRUE;
528 p.normalize_grey = FALSE;
529 p.clip = FALSE;
530 dt_gui_presets_add_generic(_("basic channel mixer"), self->op,
531 self->version(), &p, sizeof(p), 1);
532
533 // swap G-B
534 p.red[0] = 1.f;
535 p.red[1] = 0.f;
536 p.red[2] = 0.f;
537 p.green[0] = 0.f;
538 p.green[1] = 0.f;
539 p.green[2] = 1.f;
540 p.blue[0] = 0.f;
541 p.blue[1] = 1.f;
542 p.blue[2] = 0.f;
543 dt_gui_presets_add_generic(_("swap G and B"), self->op,
544 self->version(), &p, sizeof(p), 1);
545
546 // swap G-R
547 p.red[0] = 0.f;
548 p.red[1] = 1.f;
549 p.red[2] = 0.f;
550 p.green[0] = 1.f;
551 p.green[1] = 0.f;
552 p.green[2] = 0.f;
553 p.blue[0] = 0.f;
554 p.blue[1] = 0.f;
555 p.blue[2] = 1.f;
556 dt_gui_presets_add_generic(_("swap G and R"), self->op,
557 self->version(), &p, sizeof(p), 1);
558
559 // swap R-B
560 p.red[0] = 0.f;
561 p.red[1] = 0.f;
562 p.red[2] = 1.f;
563 p.green[0] = 0.f;
564 p.green[1] = 1.f;
565 p.green[2] = 0.f;
566 p.blue[0] = 1.f;
567 p.blue[1] = 0.f;
568 p.blue[2] = 0.f;
569 dt_gui_presets_add_generic(_("swap R and B"), self->op,
570 self->version(), &p, sizeof(p), 1);
571}
572
573
574// Core: compute the CAT adaptation ratio between the camera matrix's bogus-D65 and the given white
575// balance multipliers `wb_coeffs`. The coeff SOURCE differs by caller thread, and that is the whole
576// point of splitting this out: the pipeline must read temperature's coeffs from the per-piece buffer
577// descriptor (piece->dsc_in.temperature.coeffs, propagated in pipe order), NEVER from dev->proxy
578// (a GUI/main-thread inter-module channel). The GUI wrapper get_white_balance_coeff() sources proxy.
580static int _custom_wb_from_coeffs(struct dt_iop_module_t *self, const dt_aligned_pixel_t wb_coeffs,
581 dt_aligned_pixel_t custom_wb)
582{
583 // Init output with a no-op
584 for(size_t k = 0; k < 4; k++) custom_wb[k] = 1.f;
585
587 {
588 dt_iop_fmt_log(self, "get_white_balance_coeff: class=%s matrix_supported=0 -> custom_wb=no-op (CAT uses identity)",
590 return 1;
591 }
592
593 // First, get the D65-ish coeffs from the input matrix
594 // keep this in synch with calculate_bogus_daylight_wb from temperature.c !
595 // predicts the bogus D65 that temperature.c will compute for the camera input matrix
596 double bwb[4];
597
599 NULL, NULL,
601 {
602 // normalize green:
603 const double green = bwb[1];
604 bwb[0] /= green;
605 bwb[2] /= green;
606 bwb[1] = 1.0;
607 // bwb[3] is the SECOND green. For non-4-colour sensors the matrix's 4th coefficient is bogus
608 // (often huge or inf, see the temperature.c "usually NAN for RGB" note): both green sites
609 // share the first green, so force it to the green reference instead of propagating garbage.
610 bwb[3] = (self->dev->image_storage.flags & DT_IMAGE_4BAYER) ? bwb[3] / green : 1.0;
611 }
612 else
613 {
614 return 1;
615 }
616
617 // Second, if the temperature module is not using these, for example because they are wrong
618 // and user made a correct preset, find the WB adaptation ratio
619 if(wb_coeffs[0] != 0.f)
620 {
621 for(size_t k = 0; k < 4; k++)
622 {
623 // Guard the second-green ratio: wb_coeffs[3] may be 0 (non-RGBG sensor), which would
624 // yield inf. Mirror the first-green ratio in that case so custom_wb stays finite.
625 const float denom = wb_coeffs[k];
626 custom_wb[k] = (k == 3 && !isnormal(denom)) ? custom_wb[1] : bwb[k] / denom;
627 }
628 }
629
630 return 0;
631}
632
633// GUI-thread wrapper: source temperature's WB from the dev proxy (the GUI inter-module channel).
634// Pipeline code must NOT call this -- use _custom_wb_from_coeffs(self, piece->dsc_in.temperature.coeffs, ...).
637{
638 return _custom_wb_from_coeffs(self, self->dev->proxy.wb_coeffs, custom_wb);
639}
640
641
642static inline __attribute__((always_inline)) dt_aligned_pixel_simd_t
643gamut_mapping(const dt_aligned_pixel_simd_t input, const float compression, const int clip)
644{
645 // Get the sum XYZ
646 const float sum = input[0] + input[1] + input[2];
647 const float Y = input[1];
648
649 if(sum > 0.f && Y > 0.f)
650 {
651 // Convert to xyY
652 float x = input[0] / sum;
653 float y = input[1] / sum;
654
655 // Convert to uvY
656 const float uv_denominator = -2.f * x + 12.f * y + 3.f;
657 float u = 4.f * x / uv_denominator;
658 float v = 9.f * y / uv_denominator;
659
660 // Get the chromaticity difference with white point uv
661 const float D50[2] DT_ALIGNED_PIXEL = { 0.20915914598542354f, 0.488075320769787f };
662 const float delta[2] DT_ALIGNED_PIXEL = { D50[0] - u, D50[1] - v };
663 const float Delta = Y * (sqf(delta[0]) + sqf(delta[1]));
664
665 // Compress chromaticity (move toward white point)
666 const float correction = (compression == 0.0f) ? 0.f : powf(Delta, compression);
667 // Ensure the correction does not bring our uyY vector the other side of D50
668 // that would switch to the opposite color, so we clip at D50
669 const float tmp_u = DT_FMA(correction, delta[0], u);
670 const float tmp_v = DT_FMA(correction, delta[1], v);
671 u = (u > D50[0]) ? fmaxf(tmp_u, D50[0]) : fminf(tmp_u, D50[0]);
672 v = (v > D50[1]) ? fmaxf(tmp_v, D50[1]) : fminf(tmp_v, D50[1]);
673
674 // Convert back to xyY
675 const float xy_denominator = 6.f * u - 16.f * v + 12.f;
676 x = 9.f * u / xy_denominator;
677 y = 4.f * v / xy_denominator;
678
679 // Clip upon request
680 if(clip)
681 {
682 x = fmaxf(x, 0.0f);
683 y = fmaxf(y, 0.0f);
684 }
685
686 // Check sanity of y
687 // since we later divide by y, it can't be zero
688 y = fmaxf(y, NORM_MIN);
689
690 // Check sanity of x and y :
691 // since Z = Y (1 - x - y) / y, if x + y >= 1, Z will be negative
692 const float scale = x + y;
693 const int sanitize = (scale >= 1.f);
694 if(sanitize)
695 {
696 x /= scale;
697 y /= scale;
698 }
699
700 return (dt_aligned_pixel_simd_t){ Y * x / y, Y, Y * (1.f - x - y) / y, 0.f };
701 }
702 else
703 {
704 // sum of channels == 0, and/or Y == 0 so we have black
705 return (dt_aligned_pixel_simd_t){ 0.f };
706 }
707}
708__OMP_DECLARE_SIMD__(aligned(input, saturation, lightness, output:16) uniform(version))
709static inline __attribute__((always_inline)) void
710luma_chroma(const dt_aligned_pixel_t input, const dt_aligned_pixel_t saturation,
711 const dt_aligned_pixel_t lightness, dt_aligned_pixel_t output,
713{
714 // Compute euclidean norm
715 float norm = euclidean_norm(input);
716 const float avg = fmaxf((input[0] + input[1] + input[2]) / 3.0f, NORM_MIN);
717
718 if(norm > 0.f && avg > 0.f)
719 {
720 // Compute flat lightness adjustment
721 const float mix = scalar_product(input, lightness);
722
723 // Compensate the norm to get color ratios (R, G, B) = (1, 1, 1) for grey (colorless) pixels.
724 if(version == CHANNELMIXERRGB_V_3) norm *= INVERSE_SQRT_3;
725
726 // Ratios
727 for(size_t c = 0; c < 3; c++) output[c] = input[c] / norm;
728
729 // Compute ratios and a flat colorfulness adjustment for the whole pixel
730 float coeff_ratio = 0.f;
731
732 if(version == CHANNELMIXERRGB_V_1)
733 {
734 for(size_t c = 0; c < 3; c++)
735 coeff_ratio += sqf(1.0f - output[c]) * saturation[c];
736 }
737 else
738 coeff_ratio = scalar_product(output, saturation) / 3.f;
739
740 // Adjust the RGB ratios with the pixel correction
741 for(size_t c = 0; c < 3; c++)
742 {
743 // if the ratio was already invalid (negative), we accept the result to be invalid too
744 // otherwise bright saturated blues end up solid black
745 const float min_ratio = (output[c] < 0.0f) ? output[c] : 0.0f;
746 const float output_inverse = 1.0f - output[c];
747 output[c] = fmaxf(DT_FMA(output_inverse, coeff_ratio, output[c]),
748 min_ratio); // output_inverse * coeff_ratio + output
749 }
750
751 // The above interpolation between original pixel ratios and (1, 1, 1) might change the norm of the
752 // ratios. Compensate for that.
753 if(version == CHANNELMIXERRGB_V_3) norm /= euclidean_norm(output) * INVERSE_SQRT_3;
754
755 // Apply colorfulness adjustment channel-wise and repack with lightness to get LMS back
756 norm *= fmaxf(1.f + mix / avg, 0.f);
757 for(size_t c = 0; c < 3; c++) output[c] *= norm;
758 }
759 else
760 {
761 // we have black, 0 stays 0, no luminance = no color
762 for(size_t c = 0; c < 3; c++) output[c] = input[c];
763 }
764}
765
767static inline void loop_switch(const float *const restrict in, float *const restrict out,
768 const size_t width, const size_t height, const size_t ch,
769 const dt_colormatrix_t XYZ_to_RGB, const dt_colormatrix_t RGB_to_XYZ,
771 const dt_aligned_pixel_t saturation, const dt_aligned_pixel_t lightness,
772 const dt_aligned_pixel_t grey, const float p, const float gamut,
773 const int clip, const int apply_grey, const dt_adaptation_t kind,
775{
776 dt_colormatrix_t RGB_to_XYZ_t;
777 dt_colormatrix_t XYZ_to_RGB_t;
778 dt_colormatrix_t MIX_t;
779 transpose_3xSSE(RGB_to_XYZ, RGB_to_XYZ_t);
780 transpose_3xSSE(XYZ_to_RGB, XYZ_to_RGB_t);
781 transpose_3xSSE(MIX, MIX_t);
782 const dt_aligned_pixel_simd_t rgb_to_xyz0 = dt_colormatrix_row_to_simd(RGB_to_XYZ_t, 0);
783 const dt_aligned_pixel_simd_t rgb_to_xyz1 = dt_colormatrix_row_to_simd(RGB_to_XYZ_t, 1);
784 const dt_aligned_pixel_simd_t rgb_to_xyz2 = dt_colormatrix_row_to_simd(RGB_to_XYZ_t, 2);
785 const dt_aligned_pixel_simd_t xyz_to_rgb0 = dt_colormatrix_row_to_simd(XYZ_to_RGB_t, 0);
786 const dt_aligned_pixel_simd_t xyz_to_rgb1 = dt_colormatrix_row_to_simd(XYZ_to_RGB_t, 1);
787 const dt_aligned_pixel_simd_t xyz_to_rgb2 = dt_colormatrix_row_to_simd(XYZ_to_RGB_t, 2);
788 const dt_aligned_pixel_simd_t mix0 = dt_colormatrix_row_to_simd(MIX_t, 0);
789 const dt_aligned_pixel_simd_t mix1 = dt_colormatrix_row_to_simd(MIX_t, 1);
790 const dt_aligned_pixel_simd_t mix2 = dt_colormatrix_row_to_simd(MIX_t, 2);
791 const dt_aligned_pixel_simd_t illuminant_v = dt_load_simd_aligned(illuminant);
793 for(size_t k = 0; k < height * width * 4; k += 4)
794 {
795 const dt_aligned_pixel_simd_t in_v = dt_load_simd_aligned(in + k);
796 dt_aligned_pixel_simd_t temp_one_v = { 0.f };
797 dt_aligned_pixel_simd_t temp_two_v = clip ? dt_simd_max_zero(in_v) : in_v;
798
799 /* WE START IN PIPELINE RGB */
800
801 switch(kind)
802 {
804 {
805 // Convert from RGB to XYZ
806 temp_one_v = dt_mat3x4_mul_vec4(temp_two_v, rgb_to_xyz0, rgb_to_xyz1, rgb_to_xyz2);
807 const float Y = temp_one_v[1];
808
809 // Convert to LMS
810 temp_two_v = _downscale_vector_simd(convert_XYZ_to_bradford_LMS(temp_one_v), Y);
811 // Do white balance
812 temp_one_v = _upscale_vector_simd(bradford_adapt_D50(temp_two_v, illuminant_v, p, TRUE), Y);
813 // Compute the 3D mix - this is a rotation + homothety of the vector base
814 temp_two_v = dt_mat3x4_mul_vec4(temp_one_v, mix0, mix1, mix2);
815 temp_one_v = convert_bradford_LMS_to_XYZ(temp_two_v);
816
817 break;
818 }
820 {
821 // Convert from RGB to XYZ
822 temp_one_v = dt_mat3x4_mul_vec4(temp_two_v, rgb_to_xyz0, rgb_to_xyz1, rgb_to_xyz2);
823 const float Y = temp_one_v[1];
824
825 // Convert to LMS
826 temp_two_v = _downscale_vector_simd(convert_XYZ_to_bradford_LMS(temp_one_v), Y);
827 // Do white balance
828 temp_one_v = _upscale_vector_simd(bradford_adapt_D50(temp_two_v, illuminant_v, p, FALSE), Y);
829 // Compute the 3D mix - this is a rotation + homothety of the vector base
830 temp_two_v = dt_mat3x4_mul_vec4(temp_one_v, mix0, mix1, mix2);
831 temp_one_v = convert_bradford_LMS_to_XYZ(temp_two_v);
832
833 break;
834 }
836 {
837 // Convert from RGB to XYZ
838 temp_one_v = dt_mat3x4_mul_vec4(temp_two_v, rgb_to_xyz0, rgb_to_xyz1, rgb_to_xyz2);
839 const float Y = temp_one_v[1];
840
841 // Convert to LMS
842 temp_two_v = _downscale_vector_simd(convert_XYZ_to_CAT16_LMS(temp_one_v), Y);
843 // Do white balance
844 temp_one_v = _upscale_vector_simd(CAT16_adapt_D50(temp_two_v, illuminant_v, 1.0f, TRUE), Y);
845 // Compute the 3D mix - this is a rotation + homothety of the vector base
846 temp_two_v = dt_mat3x4_mul_vec4(temp_one_v, mix0, mix1, mix2);
847 temp_one_v = convert_CAT16_LMS_to_XYZ(temp_two_v);
848
849 break;
850 }
852 {
853 // Convert from RGB to XYZ
854 temp_one_v = dt_mat3x4_mul_vec4(temp_two_v, rgb_to_xyz0, rgb_to_xyz1, rgb_to_xyz2);
855 const float Y = temp_one_v[1];
856
857 // Do white balance in XYZ
858 temp_two_v = _upscale_vector_simd(XYZ_adapt_D50(_downscale_vector_simd(temp_one_v, Y), illuminant_v), Y);
859 temp_one_v = dt_mat3x4_mul_vec4(temp_two_v, mix0, mix1, mix2);
860
861 break;
862 }
865 default:
866 {
867 // No white balance.
868
869 // Compute the 3D mix in RGB - this is a rotation + homothety of the vector base
870 temp_one_v = dt_mat3x4_mul_vec4(temp_two_v, mix0, mix1, mix2);
871 temp_one_v = dt_mat3x4_mul_vec4(temp_one_v, rgb_to_xyz0, rgb_to_xyz1, rgb_to_xyz2);
872 break;
873 }
874 }
875
876 /* FROM HERE WE ARE MANDATORILY IN XYZ - DATA IS IN temp_one */
877
878 // Gamut mapping happens in XYZ space no matter what
879 temp_two_v = gamut_mapping(temp_one_v, gamut, clip);
880
881 // convert to LMS, XYZ or pipeline RGB
882 switch(kind)
883 {
888 {
889 temp_one_v = convert_any_XYZ_to_LMS(temp_two_v, kind);
890 break;
891 }
894 default:
895 {
896 // Convert from XYZ to RGB
897 temp_one_v = dt_mat3x4_mul_vec4(temp_two_v, xyz_to_rgb0, xyz_to_rgb1, xyz_to_rgb2);
898 break;
899 }
900 }
901
902 /* FROM HERE WE ARE IN LMS, XYZ OR PIPELINE RGB depending on user param - DATA IS IN temp_one */
903
904 // Clip in LMS
905 if(clip) temp_one_v = dt_simd_max_zero(temp_one_v);
906
907 // Apply lightness / saturation adjustment
908 dt_aligned_pixel_t luma_input;
909 dt_aligned_pixel_t luma_output;
910 dt_store_simd_aligned(luma_input, temp_one_v);
911 luma_chroma(luma_input, saturation, lightness, luma_output, version);
912 temp_two_v = dt_load_simd_aligned(luma_output);
913
914 // Clip in LMS
915 if(clip) temp_two_v = dt_simd_max_zero(temp_two_v);
916
917 // Save
918 if(apply_grey)
919 {
920 // Turn LMS, XYZ or pipeline RGB into monochrome
921 const float grey_mix = fmaxf(temp_two_v[0] * grey[0] + temp_two_v[1] * grey[1] + temp_two_v[2] * grey[2], 0.0f);
922 dt_store_simd_nontemporal(out + k, (dt_aligned_pixel_simd_t){ grey_mix, grey_mix, grey_mix, in_v[3] });
923 }
924 else
925 {
926 // Convert back to XYZ
927 switch(kind)
928 {
933 {
934 temp_one_v = convert_any_LMS_to_XYZ(temp_two_v, kind);
935 break;
936 }
939 default:
940 {
941 // Convert from RBG to XYZ
942 temp_one_v = dt_mat3x4_mul_vec4(temp_two_v, rgb_to_xyz0, rgb_to_xyz1, rgb_to_xyz2);
943 break;
944 }
945 }
946
947 /* FROM HERE WE ARE MANDATORILY IN XYZ - DATA IS IN temp_one */
948
949 // Clip in XYZ
950 if(clip) temp_one_v = dt_simd_max_zero(temp_one_v);
951
952 // Convert back to RGB
953 temp_two_v = dt_mat3x4_mul_vec4(temp_one_v, xyz_to_rgb0, xyz_to_rgb1, xyz_to_rgb2);
954 if(clip) temp_two_v = dt_simd_max_zero(temp_two_v);
955
956 dt_store_simd_nontemporal(out + k, (dt_aligned_pixel_simd_t){ temp_two_v[0], temp_two_v[1], temp_two_v[2], in_v[3] });
957 }
958 }
959 dt_omploop_sfence(); // ensure that nontemporal writes complete before we attempt to read output
960}
961
962// util to shift pixel index without headache
963#define SHF(ii, jj, c) ((i + ii) * width + j + jj) * ch + c
964#define OFF 4
965
967static inline int auto_detect_WB(const float *const restrict in, dt_illuminant_t illuminant,
968 const size_t width, const size_t height, const size_t ch,
969 const dt_colormatrix_t RGB_to_XYZ, dt_aligned_pixel_t xyz)
970{
984 float *const restrict temp = dt_pixelpipe_cache_alloc_align_float_cache(width * height * ch, 0);
985 if(IS_NULL_PTR(temp)) return 1;
986
987 // Convert RGB to xy
988 __OMP_PARALLEL_FOR__(collapse(2) )
989 for(size_t i = 0; i < height; i++)
990 for(size_t j = 0; j < width; j++)
991 {
992 const size_t index = (i * width + j) * ch;
995
996 // Clip negatives
997 for_each_channel(c,aligned(in))
998 RGB[c] = fmaxf(in[index + c], 0.0f);
999
1000 // Convert to XYZ
1001 dot_product(RGB, RGB_to_XYZ, XYZ);
1002
1003 // Convert to xyY
1004 const float sum = fmaxf(XYZ[0] + XYZ[1] + XYZ[2], NORM_MIN);
1005 XYZ[0] /= sum; // x
1006 XYZ[2] = XYZ[1]; // Y
1007 XYZ[1] /= sum; // y
1008
1009 // Shift the chromaticity plane so the D50 point (target) becomes the origin
1010 const float D50[2] = { 0.34567f, 0.35850f };
1011 const float norm = dt_fast_hypotf(D50[0], D50[1]);
1012
1013 temp[index ] = (XYZ[0] - D50[0]) / norm;
1014 temp[index + 1] = (XYZ[1] - D50[1]) / norm;
1015 temp[index + 2] = XYZ[2];
1016 }
1017
1018 float elements = 0.f;
1019 dt_aligned_pixel_t xyY = { 0.f };
1020
1022 {
1023 __OMP_PARALLEL_FOR__(reduction(+:xyY, elements))
1024 for(size_t i = 2 * OFF; i < height - 4 * OFF; i += OFF)
1025 for(size_t j = 2 * OFF; j < width - 4 * OFF; j += OFF)
1026 {
1027 float DT_ALIGNED_PIXEL central_average[2];
1028
1029 for(size_t c = 0; c < 2; c++)
1030 {
1031 // B-spline local average / blur
1032 central_average[c] = ( temp[SHF(-OFF, -OFF, c)] + 2.f * temp[SHF(-OFF, 0, c)] + temp[SHF(-OFF, +OFF, c)] +
1033 2.f * temp[SHF( 0, -OFF, c)] + 4.f * temp[SHF( 0, 0, c)] + 2.f * temp[SHF( 0, +OFF, c)] +
1034 temp[SHF(+OFF, -OFF, c)] + 2.f * temp[SHF(+OFF, 0, c)] + temp[SHF(+OFF, +OFF, c)]) / 16.0f;
1035 central_average[c] = fmaxf(central_average[c], 0.0f);
1036 }
1037
1038 dt_aligned_pixel_t var = { 0.f };
1039
1040 // compute patch-wise variance
1041 // If variance = 0, we are on a flat surface and want to discard that patch.
1042 for(size_t c = 0; c < 2; c++)
1043 {
1044 var[c] = (
1045 sqf(temp[SHF(-OFF, -OFF, c)] - central_average[c]) +
1046 sqf(temp[SHF(-OFF, 0, c)] - central_average[c]) +
1047 sqf(temp[SHF(-OFF, +OFF, c)] - central_average[c]) +
1048 sqf(temp[SHF(0, -OFF, c)] - central_average[c]) +
1049 sqf(temp[SHF(0, 0, c)] - central_average[c]) +
1050 sqf(temp[SHF(0, +OFF, c)] - central_average[c]) +
1051 sqf(temp[SHF(+OFF, -OFF, c)] - central_average[c]) +
1052 sqf(temp[SHF(+OFF, 0, c)] - central_average[c]) +
1053 sqf(temp[SHF(+OFF, +OFF, c)] - central_average[c])
1054 ) / 9.0f;
1055 }
1056
1057 // Compute the patch-wise chroma covariance.
1058 // If covariance = 0, chroma channels are not correlated and we either have noise or chromatic aberrations.
1059 // Both ways, we want to discard that patch from the chroma average.
1060 var[2] = (
1061 (temp[SHF(-OFF, -OFF, 0)] - central_average[0]) * (temp[SHF(-OFF, -OFF, 1)] - central_average[1]) +
1062 (temp[SHF(-OFF, 0, 0)] - central_average[0]) * (temp[SHF(-OFF, 0, 1)] - central_average[1]) +
1063 (temp[SHF(-OFF, +OFF, 0)] - central_average[0]) * (temp[SHF(-OFF, +OFF, 1)] - central_average[1]) +
1064 (temp[SHF( 0, -OFF, 0)] - central_average[0]) * (temp[SHF( 0, -OFF, 1)] - central_average[1]) +
1065 (temp[SHF( 0, 0, 0)] - central_average[0]) * (temp[SHF( 0, 0, 1)] - central_average[1]) +
1066 (temp[SHF( 0, +OFF, 0)] - central_average[0]) * (temp[SHF( 0, +OFF, 1)] - central_average[1]) +
1067 (temp[SHF(+OFF, -OFF, 0)] - central_average[0]) * (temp[SHF(+OFF, -OFF, 1)] - central_average[1]) +
1068 (temp[SHF(+OFF, 0, 0)] - central_average[0]) * (temp[SHF(+OFF, 0, 1)] - central_average[1]) +
1069 (temp[SHF(+OFF, +OFF, 0)] - central_average[0]) * (temp[SHF(+OFF, +OFF, 1)] - central_average[1])
1070 ) / 9.0f;
1071
1072 // Compute the Minkowski p-norm for regularization
1073 const float p = 8.f;
1074 const float p_norm
1075 = powf(powf(fabsf(central_average[0]), p) + powf(fabsf(central_average[1]), p), 1.f / p) + NORM_MIN;
1076 const float weight = var[0] * var[1] * var[2];
1077
1078 for(size_t c = 0; c < 2; c++) xyY[c] += central_average[c] * weight / p_norm;
1079 elements += weight / p_norm;
1080 }
1081 }
1083 {
1084 __OMP_PARALLEL_FOR__(reduction(+:xyY, elements))
1085 for(size_t i = 2 * OFF; i < height - 4 * OFF; i += OFF)
1086 for(size_t j = 2 * OFF; j < width - 4 * OFF; j += OFF)
1087 {
1088 float DT_ALIGNED_PIXEL dd[2];
1089 float DT_ALIGNED_PIXEL central_average[2];
1090
1091 for(size_t c = 0; c < 2; c++)
1092 {
1093 // B-spline local average / blur
1094 central_average[c] = ( temp[SHF(-OFF, -OFF, c)] + 2.f * temp[SHF(-OFF, 0, c)] + temp[SHF(-OFF, +OFF, c)] +
1095 2.f * temp[SHF( 0, -OFF, c)] + 4.f * temp[SHF( 0, 0, c)] + 2.f * temp[SHF( 0, +OFF, c)] +
1096 temp[SHF(+OFF, -OFF, c)] + 2.f * temp[SHF(+OFF, 0, c)] + temp[SHF(+OFF, +OFF, c)]) / 16.0f;
1097
1098 // image - blur = laplacian = edges
1099 dd[c] = temp[SHF(0, 0, c)] - central_average[c];
1100 }
1101
1102 // Compute the Minkowski p-norm for regularization
1103 const float p = 8.f;
1104 const float p_norm = powf(powf(fabsf(dd[0]), p) + powf(fabsf(dd[1]), p), 1.f / p) + NORM_MIN;
1105
1106 for(size_t c = 0; c < 2; c++) xyY[c] -= dd[c] / p_norm;
1107 elements += 1.f;
1108 }
1109 }
1110
1111 const float D50[2] = { 0.34567f, 0.35850 };
1112 const float norm_D50 = dt_fast_hypotf(D50[0], D50[1]);
1113
1114 for(size_t c = 0; c < 2; c++)
1115 xyz[c] = norm_D50 * (xyY[c] / elements) + D50[c];
1116
1118 return 0;
1119}
1120
1122static void declare_cat_on_pipe(struct dt_iop_module_t *self, gboolean preset)
1123{
1124 // Advertise to the pipeline that we are doing chromatic adaptation here
1125 // preset = TRUE allows to capture the CAT a priori at init time
1127
1128 if((self->enabled && !(p->adaptation == DT_ADAPTATION_RGB || p->illuminant == DT_ILLUMINANT_PIPE)) || preset)
1129 {
1130 // We do CAT here so we need to register this instance as CAT-handler.
1132 {
1133 // We are the first to try to register, let's go !
1134 self->dev->proxy.chroma_adaptation = self;
1135 }
1136 else if(self->dev->proxy.chroma_adaptation == self)
1137 {
1138 }
1139 else
1140 {
1141 // Another instance already registered.
1142 // If we are lower in the pipe than it, register in its place.
1143 if(dt_iop_is_first_instance(self->dev->iop, self))
1144 self->dev->proxy.chroma_adaptation = self;
1145 }
1146 }
1147 else
1148 {
1150 {
1151 // We do NOT do CAT here.
1152 // Deregister this instance as CAT-handler if it previously registered
1153 if(self->dev->proxy.chroma_adaptation == self)
1154 self->dev->proxy.chroma_adaptation = NULL;
1155 }
1156 }
1157}
1158
1159static inline gboolean _is_another_module_cat_on_pipe(struct dt_iop_module_t *self)
1160{
1162 if(IS_NULL_PTR(g)) return FALSE;
1163 return self->dev->proxy.chroma_adaptation && self->dev->proxy.chroma_adaptation != self;
1164}
1165
1166
1167static void update_illuminants(struct dt_iop_module_t *self);
1168static void update_approx_cct(struct dt_iop_module_t *self);
1169static void update_illuminant_color(struct dt_iop_module_t *self);
1170
1171
1172static inline __attribute__((always_inline)) void check_if_close_to_daylight(const float x, const float y, float *temperature,
1174{
1175 /* Check if a chromaticity x, y is close to daylight within 2.5 % error margin.
1176 * If so, we enable the daylight GUI for better ergonomics
1177 * Otherwise, we default to direct x, y control for better accuracy
1178 *
1179 * Note : The use of CCT is discouraged if dE > 5 % in CIE 1960 Yuv space
1180 * reference : https://onlinelibrary.wiley.com/doi/abs/10.1002/9780470175637.ch3
1181 */
1182
1183 // Get the correlated color temperature (CCT)
1184 float t = xy_to_CCT(x, y);
1185
1186 // xy_to_CCT is valid only in 3000 - 25000 K. We need another model below
1187 if(t < 3000.f && t > 1667.f)
1188 t = CCT_reverse_lookup(x, y);
1189
1190 if(!IS_NULL_PTR(temperature))
1191 *temperature = t;
1192
1193 // Convert to CIE 1960 Yuv space
1194 float xy_ref[2] = { x, y };
1195 float uv_ref[2];
1196 xy_to_uv(xy_ref, uv_ref);
1197
1198 float xy_test[2] = { 0.f };
1199 float uv_test[2];
1200
1201 // Compute the test chromaticity from the daylight model
1202 illuminant_to_xy(DT_ILLUMINANT_D, NULL, NULL, &xy_test[0], &xy_test[1], t, DT_ILLUMINANT_FLUO_LAST, DT_ILLUMINANT_LED_LAST);
1203 xy_to_uv(xy_test, uv_test);
1204
1205 // Compute the error between the reference illuminant and the test illuminant derivated from the CCT with daylight model
1206 const float delta_daylight = dt_fast_hypotf((uv_test[0] - uv_ref[0]), (uv_test[1] - uv_ref[1]));
1207
1208 // Compute the test chromaticity from the blackbody model
1209 illuminant_to_xy(DT_ILLUMINANT_BB, NULL, NULL, &xy_test[0], &xy_test[1], t, DT_ILLUMINANT_FLUO_LAST, DT_ILLUMINANT_LED_LAST);
1210 xy_to_uv(xy_test, uv_test);
1211
1212 // Compute the error between the reference illuminant and the test illuminant derivated from the CCT with black body model
1213 const float delta_bb = dt_fast_hypotf((uv_test[0] - uv_ref[0]), (uv_test[1] - uv_ref[1]));
1214
1215 // Check the error between original and test chromaticity
1216 if(delta_bb < 0.005f || delta_daylight < 0.005f)
1217 {
1218 if(illuminant)
1219 {
1220 if(delta_bb < delta_daylight)
1222 else
1224 }
1225 }
1226 else
1227 {
1228 // error is too big to use a CCT-based model, we fall back to a custom/freestyle chroma selection for the illuminant
1230 }
1231
1232 // CAT16 is more accurate no matter the illuminant
1234}
1235
1236#define DEG_TO_RAD(x) (x * M_PI / 180.f)
1237#define RAD_TO_DEG(x) (x * 180.f / M_PI)
1238
1239static inline void compute_patches_delta_E(const float *const restrict patches,
1240 const dt_color_checker_t *const checker,
1241 float *const restrict delta_E, float *const restrict avg_delta_E, float *const restrict max_delta_E)
1242{
1243 // Compute the delta E
1244
1245 float dE = 0.f;
1246 float max_dE = 0.f;
1247
1248 for(size_t k = 0; k < checker->patches; k++)
1249 {
1250 // Convert to Lab
1251 dt_aligned_pixel_t Lab_test;
1252 dt_aligned_pixel_t XYZ_test;
1253
1254 // If exposure was normalized, denormalized it before
1255 for(size_t c = 0; c < 4; c++) XYZ_test[c] = patches[k * 4 + c];
1256 dt_XYZ_to_Lab(XYZ_test, Lab_test);
1257
1258 const float *const restrict Lab_ref = checker->values[k].Lab;
1259
1260 // Compute delta E 2000 to make your computer heat
1261 // ref: https://en.wikipedia.org/wiki/Color_difference#CIEDE2000
1262 // note : it will only be luck if I didn't mess-up the computation somewhere
1263 const float DL = Lab_ref[0] - Lab_test[0];
1264 const float L_avg = (Lab_ref[0] + Lab_test[0]) / 2.f;
1265 const float C_ref = dt_fast_hypotf(Lab_ref[1], Lab_ref[2]);
1266 const float C_test = dt_fast_hypotf(Lab_test[1], Lab_test[2]);
1267 const float C_avg = (C_ref + C_test) / 2.f;
1268 float C_avg_7 = C_avg * C_avg; // C_avg²
1269 C_avg_7 *= C_avg_7; // C_avg⁴
1270 C_avg_7 *= C_avg_7; // C_avg⁸
1271 C_avg_7 /= C_avg; // C_avg⁷
1272 const float C_avg_7_ratio_sqrt = sqrtf(C_avg_7 / (C_avg_7 + 6103515625.f)); // 25⁷ = 6103515625
1273 const float a_ref_prime = Lab_ref[1] * (1.f + 0.5f * (1.f - C_avg_7_ratio_sqrt));
1274 const float a_test_prime = Lab_test[1] * (1.f + 0.5f * (1.f - C_avg_7_ratio_sqrt));
1275 const float C_ref_prime = dt_fast_hypotf(a_ref_prime, Lab_ref[2]);
1276 const float C_test_prime = dt_fast_hypotf(a_test_prime, Lab_test[2]);
1277 const float DC_prime = C_ref_prime - C_test_prime;
1278 const float C_avg_prime = (C_ref_prime + C_test_prime) / 2.f;
1279 float h_ref_prime = atan2f(Lab_ref[2], a_ref_prime);
1280 float h_test_prime = atan2f(Lab_test[2], a_test_prime);
1281
1282 // Comply with recommendations, h = 0° where C = 0 by convention
1283 if(C_ref_prime == 0.f) h_ref_prime = 0.f;
1284 if(C_test_prime == 0.f) h_test_prime = 0.f;
1285
1286 // Get the hue angles from [-pi ; pi] back to [0 ; 2 pi],
1287 // again, to comply with specifications
1288 if(h_ref_prime < 0.f) h_ref_prime = 2.f * M_PI - h_ref_prime;
1289 if(h_test_prime < 0.f) h_test_prime = 2.f * M_PI - h_test_prime;
1290
1291 // Convert to degrees, again to comply with specs
1292 h_ref_prime = RAD_TO_DEG(h_ref_prime);
1293 h_test_prime = RAD_TO_DEG(h_test_prime);
1294
1295 float Dh_prime = h_test_prime - h_ref_prime;
1296 float Dh_prime_abs = fabsf(Dh_prime);
1297 if(C_test_prime == 0.f || C_ref_prime == 0.f)
1298 Dh_prime = 0.f;
1299 else if(Dh_prime_abs <= 180.f)
1300 ;
1301 else if(Dh_prime_abs > 180.f && (h_test_prime <= h_ref_prime))
1302 Dh_prime += 360.f;
1303 else if(Dh_prime_abs > 180.f && (h_test_prime > h_ref_prime))
1304 Dh_prime -= 360.f;
1305
1306 // update abs(Dh_prime) for later
1307 Dh_prime_abs = fabsf(Dh_prime);
1308
1309 const float DH_prime = 2.f * sqrtf(C_test_prime * C_ref_prime) * sinf(DEG_TO_RAD(Dh_prime) / 2.f);
1310 float H_avg_prime = h_ref_prime + h_test_prime;
1311 if(C_test_prime == 0.f || C_ref_prime == 0.f)
1312 ;
1313 else if(Dh_prime_abs <= 180.f)
1314 H_avg_prime /= 2.f;
1315 else if(Dh_prime_abs > 180.f && (H_avg_prime < 360.f))
1316 H_avg_prime = (H_avg_prime + 360.f) / 2.f;
1317 else if(Dh_prime_abs > 180.f && (H_avg_prime >= 360.f))
1318 H_avg_prime = (H_avg_prime - 360.f) / 2.f;
1319
1320 const float T = 1.f
1321 - 0.17f * cosf(DEG_TO_RAD(H_avg_prime) - DEG_TO_RAD(30.f))
1322 + 0.24f * cosf(2.f * DEG_TO_RAD(H_avg_prime))
1323 + 0.32f * cosf(3.f * DEG_TO_RAD(H_avg_prime) + DEG_TO_RAD(6.f))
1324 - 0.20f * cosf(4.f * DEG_TO_RAD(H_avg_prime) - DEG_TO_RAD(63.f));
1325
1326 const float S_L = 1.f + (0.015f * sqf(L_avg - 50.f)) / sqrtf(20.f + sqf(L_avg - 50.f));
1327 const float S_C = 1.f + 0.045f * C_avg_prime;
1328 const float S_H = 1.f + 0.015f * C_avg_prime * T;
1329 const float R_T = -2.f * C_avg_7_ratio_sqrt
1330 * sinf(DEG_TO_RAD(60.f) * expf(-sqf((H_avg_prime - 275.f) / 25.f)));
1331
1332 // roll the drum, here goes the Delta E, finally...
1333 const float DE = sqrtf(sqf(DL / S_L) + sqf(DC_prime / S_C) + sqf(DH_prime / S_H)
1334 + R_T * (DC_prime / S_C) * (DH_prime / S_H));
1335
1336 // Delta E 1976 for reference :
1337 //float DE = sqrtf(sqf(Lab_test[0] - Lab_ref[0]) + sqf(Lab_test[1] - Lab_ref[1]) + sqf(Lab_test[2] - Lab_ref[2]));
1338
1339 //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]);
1340 //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]);
1341 //fprintf(stdout, "patch %s : dE mes \t= \t%.3f \n", checker->values[k].name, DE);
1342
1343 delta_E[k] = DE;
1344 dE += DE / (float)checker->patches;
1345 if(DE > max_dE) max_dE = DE;
1346 }
1347
1348 *avg_delta_E = dE;
1349 *max_delta_E = max_dE;
1350}
1351
1352#define GET_WEIGHT \
1353 float hue = atan2f(reference[2], reference[1]); \
1354 const float chroma = hypotf(reference[2], reference[1]); \
1355 float delta_hue = hue - ref_hue; \
1356 if(chroma == 0.f) \
1357 delta_hue = 0.f; \
1358 else if(fabsf(delta_hue) <= M_PI) \
1359 ; \
1360 else if(fabsf(delta_hue) > M_PI && (hue <= ref_hue)) \
1361 delta_hue += 2.f * M_PI; \
1362 else if(fabsf(delta_hue) > M_PI && (hue > ref_hue)) \
1363 delta_hue -= 2.f * M_PI; \
1364 w = sqrtf(expf(-sqf(delta_hue) / 2.f));
1365
1366
1367typedef struct {
1368 float black;
1371
1386{
1387 float scale_x, scale_y; // preview-request pixels -> buffer pixels
1388 float offset_x, offset_y; // this buffer's origin, in buffer pixels
1390
1391
1399 const dt_iop_roi_t *const roi)
1400{
1401 dt_iop_channelmixer_rgb_box_map_t map = { 1.f, 1.f, (float)roi->x, (float)roi->y };
1403
1404 // Fall back to the identity -- i.e. the behaviour that is correct whenever the pipe does run
1405 // at natural_scale -- rather than scaling by a number we have no reason to trust.
1406 if(request.valid && request.preview_width > 0 && request.preview_height > 0)
1407 {
1408 map.scale_x = (float)roi->width / (float)request.preview_width;
1409 map.scale_y = (float)roi->height / (float)request.preview_height;
1410 }
1411
1412 return map;
1413}
1414
1415
1423 const dt_iop_channelmixer_rgb_box_map_t *const map,
1424 float forward[9], float backward[9])
1425{
1426 // apply_homography() divides rows 0 and 1 by row 2, so the translation belongs in the
1427 // numerators, weighted by that same row -- it is not a plain matrix addition.
1428 for(size_t c = 0; c < 3; c++)
1429 {
1430 forward[0 * 3 + c] = map->scale_x * g->homography[0 * 3 + c] - map->offset_x * g->homography[2 * 3 + c];
1431 forward[1 * 3 + c] = map->scale_y * g->homography[1 * 3 + c] - map->offset_y * g->homography[2 * 3 + c];
1432 forward[2 * 3 + c] = g->homography[2 * 3 + c];
1433 }
1434
1435 // The backward map consumes buffer pixels, so it takes the INVERSE affine on its input.
1436 for(size_t r = 0; r < 3; r++)
1437 {
1438 const float column_x = g->inverse_homography[r * 3 + 0] / map->scale_x;
1439 const float column_y = g->inverse_homography[r * 3 + 1] / map->scale_y;
1440 backward[r * 3 + 0] = column_x;
1441 backward[r * 3 + 1] = column_y;
1442 backward[r * 3 + 2] = g->inverse_homography[r * 3 + 2] + column_x * map->offset_x
1443 + column_y * map->offset_y;
1444 }
1445}
1446
1447
1449static int _extract_patches(const float *const restrict in, const dt_iop_roi_t *const roi_in,
1451 const dt_iop_channelmixer_rgb_box_map_t *const box_map,
1452 const dt_colormatrix_t RGB_to_XYZ, const dt_colormatrix_t XYZ_to_CAM,
1453 float *const restrict patches,
1454 const gboolean normalize_exposure,
1455 extraction_result_t *result)
1456{
1457 const size_t width = roi_in->width;
1458 const size_t height = roi_in->height;
1459 const float radius_x = g->checker->radius * hypotf(1.f, g->checker->ratio) * g->safety_margin;
1460 const float radius_y = radius_x / g->checker->ratio;
1461
1462 // The chart box is authored in preview-request pixels; this buffer may be a different grid.
1463 // Both radii stay in ideal chart units, so only the two homographies need the mapping.
1464 float homography[9];
1465 float inverse_homography[9];
1466 _map_homographies(g, box_map, homography, inverse_homography);
1467
1469 "[channelmixerrgb] sampling %" G_GSIZE_FORMAT " patches over a %" G_GSIZE_FORMAT
1470 "x%" G_GSIZE_FORMAT " buffer, box scaled by (%.5f, %.5f) offset (%.1f, %.1f)\n",
1471 g->checker->patches, width, height, box_map->scale_x, box_map->scale_y,
1472 box_map->offset_x, box_map->offset_y);
1473
1474 if(IS_NULL_PTR(g->delta_E_in))
1475 {
1476 g->delta_E_in = dt_alloc_align_float(g->checker->patches);
1477 if(IS_NULL_PTR(g->delta_E_in)) return 1;
1478 }
1479
1480 /* Get the average color over each patch */
1481 for(size_t k = 0; k < g->checker->patches; k++)
1482 {
1483 // center of the patch in the ideal reference
1484 const point_t center = { g->checker->values[k].x, g->checker->values[k].y };
1485
1486 // corners of the patch in the ideal reference
1487 const point_t corners[4] = { {center.x - radius_x, center.y - radius_y},
1488 {center.x + radius_x, center.y - radius_y},
1489 {center.x + radius_x, center.y + radius_y},
1490 {center.x - radius_x, center.y + radius_y} };
1491
1492 // apply patch coordinates transform depending on perspective
1493 point_t new_corners[4];
1494 // find the bounding box of the patch at the same time
1495 size_t x_min = width - 1;
1496 size_t x_max = 0;
1497 size_t y_min = height - 1;
1498 size_t y_max = 0;
1499 for(size_t c = 0; c < 4; c++) {
1500 new_corners[c] = apply_homography(corners[c], homography);
1501 x_min = fminf(new_corners[c].x, x_min);
1502 x_max = fmaxf(new_corners[c].x, x_max);
1503 y_min = fminf(new_corners[c].y, y_min);
1504 y_max = fmaxf(new_corners[c].y, y_max);
1505 }
1506
1507 x_min = CLAMP((size_t)floorf(x_min), 0, width - 1);
1508 x_max = CLAMP((size_t)ceilf(x_max), 0, width - 1);
1509 y_min = CLAMP((size_t)floorf(y_min), 0, height - 1);
1510 y_max = CLAMP((size_t)ceilf(y_max), 0, height - 1);
1511
1512 // Get the average color on the patch
1513 patches[k * 4] = patches[k * 4 + 1] = patches[k * 4 + 2] = patches[k * 4 + 3] = 0.f;
1514 size_t num_elem = 0;
1515
1516 // Loop through the rectangular bounding box
1517 for(size_t j = y_min; j < y_max; j++)
1518 for(size_t i = x_min; i < x_max; i++)
1519 {
1520 // Check if this pixel lies inside the sampling area and sample if it does
1521 point_t current_point = { i + 0.5f, j + 0.5f };
1522 current_point = apply_homography(current_point, inverse_homography);
1523 current_point.x -= center.x;
1524 current_point.y -= center.y;
1525
1526 if(current_point.x < radius_x && current_point.x > -radius_x &&
1527 current_point.y < radius_y && current_point.y > -radius_y)
1528 {
1529 for(size_t c = 0; c < 3; c++)
1530 {
1531 patches[k * 4 + c] += in[(j * width + i) * 4 + c];
1532
1533 // Debug : inpaint a black square in the preview to ensure the coordanites of
1534 // overlay drawings and actual pixel processing match
1535 // out[(j * width + i) * 4 + c] = 0.f;
1536 }
1537 num_elem++;
1538 }
1539 }
1540
1541 if(num_elem == 0)
1542 {
1543 // Dividing by zero here hands the solver a NaN row, and it returns a matrix fitted on
1544 // nothing at all -- silently, with a plausible-looking delta E report next to it.
1546 "[channelmixerrgb] patch %" G_GSIZE_FORMAT " (%s) sampled no pixel: the chart box"
1547 " does not lie on the %" G_GSIZE_FORMAT "x%" G_GSIZE_FORMAT " buffer."
1548 " Refusing to profile.\n",
1549 k, g->checker->values[k].name ? g->checker->values[k].name : "?", width, height);
1550 dt_control_log(_("color calibration: the chart area does not lie on the image"));
1551 return 1;
1552 }
1553
1554 for(size_t c = 0; c < 3; c++) patches[k * 4 + c] /= (float)num_elem;
1555
1556 // Convert to XYZ
1557 dt_aligned_pixel_t XYZ = { 0 };
1558 dot_product(patches + k * 4, RGB_to_XYZ, XYZ);
1559 for(size_t c = 0; c < 3; c++) patches[k * 4 + c] = XYZ[c];
1560 }
1561
1562 // find reference white patch
1563 dt_aligned_pixel_t XYZ_white_ref;
1564 dt_Lab_to_XYZ(g->checker->values[g->checker->white].Lab, XYZ_white_ref);
1565 const float white_ref_norm = euclidean_norm(XYZ_white_ref);
1566
1567 // find test white patch
1568 dt_aligned_pixel_t XYZ_white_test;
1569 for(size_t c = 0; c < 3; c++) XYZ_white_test[c] = patches[g->checker->white * 4 + c];
1570 const float white_test_norm = euclidean_norm(XYZ_white_test);
1571
1572 /* match global exposure */
1573 // white exposure depends on camera settings and raw white point,
1574 // we want our profile to be independent from that
1575 float exposure = white_ref_norm / white_test_norm;
1576
1577 /* Exposure compensation */
1578 // Ensure the relative luminance of the test patch (compared to white patch)
1579 // is the same as the relative luminance of the reference patch.
1580 // This compensate for lighting fall-off and unevenness
1581 if(normalize_exposure)
1582 {
1583 for(size_t k = 0; k < g->checker->patches; k++)
1584 {
1585 float *const sample = patches + k * 4;
1586
1587 dt_aligned_pixel_t XYZ_ref;
1588 dt_Lab_to_XYZ(g->checker->values[k].Lab, XYZ_ref);
1589
1590 const float sample_norm = euclidean_norm(sample);
1591 const float ref_norm = euclidean_norm(XYZ_ref);
1592
1593 const float relative_luminance_test = sample_norm / white_test_norm;
1594 const float relative_luminance_ref = ref_norm / white_ref_norm;
1595
1596 const float luma_correction = relative_luminance_ref / relative_luminance_test;
1597 for(size_t c = 0; c < 3; ++c) sample[c] *= luma_correction * exposure;
1598 }
1599 }
1600
1601 // black point is evaluated by rawspeed on each picture using the dark pixels
1602 // we want our profile to be also independent from its discrepancies
1603 // so we convert back the patches to camera RGB space and search the best fit of
1604 // RGB_ref = exposure * (RGB_test - offset) for offset.
1605 float black = 0.f;
1606
1607 if(XYZ_to_CAM)
1608 {
1609 float mean_ref = 0.f;
1610 float mean_test = 0.f;
1611
1612 for(size_t k = 0; k < g->checker->patches; k++)
1613 {
1614 dt_aligned_pixel_t XYZ_ref, RGB_ref;
1615 dt_aligned_pixel_t XYZ_test, RGB_test;
1616
1617 for(size_t c = 0; c < 3; c++) XYZ_test[c] = patches[k * 4 + c];
1618 dt_Lab_to_XYZ(g->checker->values[k].Lab, XYZ_ref);
1619
1620 dot_product(XYZ_test, XYZ_to_CAM, RGB_test);
1621 dot_product(XYZ_ref, XYZ_to_CAM, RGB_ref);
1622
1623 // From now on, we have all the reference and test data in camera RGB space
1624 // where exposure and black level are applied
1625
1626 for(int c = 0; c < 3; c++)
1627 {
1628 mean_test += RGB_test[c];
1629 mean_ref += RGB_ref[c];
1630 }
1631 }
1632 mean_test /= 3.f * g->checker->patches;
1633 mean_ref /= 3.f * g->checker->patches;
1634
1635 float variance = 0.f;
1636 float covariance = 0.f;
1637
1638 for(size_t k = 0; k < g->checker->patches; k++)
1639 {
1640 dt_aligned_pixel_t XYZ_ref, RGB_ref;
1641 dt_aligned_pixel_t XYZ_test, RGB_test;
1642
1643 for(size_t c = 0; c < 3; c++) XYZ_test[c] = patches[k * 4 + c];
1644 dt_Lab_to_XYZ(g->checker->values[k].Lab, XYZ_ref);
1645
1646 dot_product(XYZ_test, XYZ_to_CAM, RGB_test);
1647 dot_product(XYZ_ref, XYZ_to_CAM, RGB_ref);
1648
1649 for(int c = 0; c < 3; c++)
1650 {
1651 variance += sqf(RGB_test[c] - mean_test);
1652 // Both factors are centred on their OWN mean. Using mean_ref for the test term too is
1653 // algebraically the same covariance -- the surplus is (mean_test - mean_ref) times
1654 // sum(RGB_ref - mean_ref), and that sum is zero over the very set mean_ref averages --
1655 // but it computes it as a difference of two large numbers instead of a sum of small
1656 // ones, and it reads as a typo every time anyone audits this.
1657 covariance += (RGB_ref[c] - mean_ref) * (RGB_test[c] - mean_test);
1658 }
1659 }
1660 variance /= 3.f * g->checker->patches;
1661 covariance /= 3.f * g->checker->patches;
1662
1663 // Here, we solve the least-squares problem RGB_ref = exposure * RGB_test + offset
1664 // using :
1665 // exposure = covariance(RGB_test, RGB_ref) / variance(RGB_test)
1666 // offset = mean(RGB_ref) - exposure * mean(RGB_test)
1667 exposure = covariance / variance;
1668 black = mean_ref - exposure * mean_test;
1669 }
1670
1671 // the exposure module applies output = (input - offset) * exposure
1672 // but we compute output = input * exposure + offset
1673 // so, rescale offset to adapt our offset to exposure module GUI
1674 black /= -exposure;
1675
1676 result->black = black;
1677 result->exposure = exposure;
1678 return 0;
1679}
1680
1682int extract_color_checker(const float *const restrict in, float *const restrict out,
1684 const dt_iop_channelmixer_rgb_box_map_t *const box_map,
1685 const dt_colormatrix_t RGB_to_XYZ, const dt_colormatrix_t XYZ_to_RGB,
1686 const dt_colormatrix_t XYZ_to_CAM,
1687 const dt_adaptation_t kind)
1688{
1689 float *const restrict patches = dt_alloc_align_float(g->checker->patches * 4);
1690 if(IS_NULL_PTR(patches)) return 1;
1691
1692 dt_simd_memcpy(in, out, (size_t)roi_in->width * roi_in->height * 4);
1693
1694 extraction_result_t extraction_result = { 0 };
1695 if(_extract_patches(out, roi_in, g, box_map, RGB_to_XYZ, XYZ_to_CAM, patches, TRUE, &extraction_result))
1696 {
1697 dt_free_align(patches);
1698 return 1;
1699 }
1700
1701 // Compute the delta E
1702 float pre_wb_delta_E = 0.f;
1703 float pre_wb_max_delta_E = 0.f;
1704 compute_patches_delta_E(patches, g->checker, g->delta_E_in, &pre_wb_delta_E, &pre_wb_max_delta_E);
1705
1706 /* find the scene illuminant */
1707
1708 // find reference grey patch
1709 dt_aligned_pixel_t XYZ_grey_ref;
1710 dt_Lab_to_XYZ(g->checker->values[g->checker->middle_grey].Lab, XYZ_grey_ref);
1711
1712 // find test grey patch
1713 dt_aligned_pixel_t XYZ_grey_test;
1714 for(size_t c = 0; c < 3; c++) XYZ_grey_test[c] = patches[g->checker->middle_grey * 4 + c];
1715
1716 // compute reference illuminant
1717 dt_aligned_pixel_t D50_XYZ;
1718 illuminant_xy_to_XYZ(0.34567f, 0.35850f, D50_XYZ);
1719
1720 // normalize luminances - note : illuminant is normalized by definition
1721 const float Y_test = XYZ_grey_test[1];
1722 const float Y_ref = XYZ_grey_ref[1];
1723 for(size_t c = 0; c < 3; c++)
1724 {
1725 XYZ_grey_ref[c] /= Y_ref;
1726 XYZ_grey_test[c] /= Y_test;
1727 }
1728
1729 // convert XYZ to LMS
1730 dt_aligned_pixel_t LMS_grey_ref, LMS_grey_test, D50_LMS;
1731 dt_store_simd_aligned(LMS_grey_ref, convert_any_XYZ_to_LMS(dt_load_simd_aligned(XYZ_grey_ref), kind));
1732 dt_store_simd_aligned(LMS_grey_test, convert_any_XYZ_to_LMS(dt_load_simd_aligned(XYZ_grey_test), kind));
1733 dt_store_simd_aligned(D50_LMS, convert_any_XYZ_to_LMS(dt_load_simd_aligned(D50_XYZ), kind));
1734
1735 // solve the equation to find the scene illuminant
1736 dt_aligned_pixel_t illuminant = { 0.0f };
1737 for(size_t c = 0; c < 3; c++) illuminant[c] = D50_LMS[c] * LMS_grey_test[c] / LMS_grey_ref[c];
1738
1739 // convert back the illuminant to XYZ then xyY
1740 dt_aligned_pixel_t illuminant_XYZ, illuminant_xyY = { .0f };
1741 dt_store_simd_aligned(illuminant_XYZ, convert_any_LMS_to_XYZ(dt_load_simd_aligned(illuminant), kind));
1742 const float Y_illu = illuminant_XYZ[1];
1743 for(size_t c = 0; c < 3; c++) illuminant_XYZ[c] /= Y_illu;
1744 dt_XYZ_to_xyY(illuminant_XYZ, illuminant_xyY);
1745
1746 // save the illuminant in GUI struct for commit
1747 g->xy[0] = illuminant_xyY[0];
1748 g->xy[1] = illuminant_xyY[1];
1749
1750 // and recompute back the LMS to be sure we use the parameters that will be computed later
1751 illuminant_xy_to_XYZ(illuminant_xyY[0], illuminant_xyY[1], illuminant_XYZ);
1752 dt_store_simd_aligned(illuminant, convert_any_XYZ_to_LMS(dt_load_simd_aligned(illuminant_XYZ), kind));
1753 const float p = powf(0.818155f / illuminant[2], 0.0834f);
1754
1755 /* White-balance the patches */
1756 for(size_t k = 0; k < g->checker->patches; k++)
1757 {
1758 // keep in synch with loop_switch() from process()
1759 float *const sample = patches + k * 4;
1760 const float Y = sample[1];
1761 downscale_vector(sample, Y);
1762
1764 dt_store_simd_aligned(LMS, convert_any_XYZ_to_LMS(dt_load_simd_aligned(sample), kind));
1765
1766 dt_aligned_pixel_t temp;
1767 const dt_aligned_pixel_simd_t LMS_v = dt_load_simd_aligned(LMS);
1768 const dt_aligned_pixel_simd_t illuminant_v = dt_load_simd_aligned(illuminant);
1769
1770 switch(kind)
1771 {
1773 {
1774 dt_store_simd_aligned(temp, bradford_adapt_D50(LMS_v, illuminant_v, p, TRUE));
1775 break;
1776 }
1778 {
1779 dt_store_simd_aligned(temp, bradford_adapt_D50(LMS_v, illuminant_v, 1.f, FALSE));
1780 break;
1781 }
1783 {
1784 dt_store_simd_aligned(temp, CAT16_adapt_D50(LMS_v, illuminant_v, 1.f, TRUE)); // force full-adaptation
1785 break;
1786 }
1787 case DT_ADAPTATION_XYZ:
1788 {
1789 dt_store_simd_aligned(temp, XYZ_adapt_D50(LMS_v, illuminant_v));
1790 break;
1791 }
1792 case DT_ADAPTATION_RGB:
1793 case DT_ADAPTATION_LAST:
1794 default:
1795 {
1796 // No white balance.
1797 for(size_t c = 0; c < 3; ++c) temp[c] = LMS[c];
1798 break;
1799 }
1800 }
1801
1802 dt_store_simd_aligned(sample, convert_any_LMS_to_XYZ(dt_load_simd_aligned(temp), kind));
1803 upscale_vector(sample, Y);
1804 }
1805
1806 // Compute the delta E
1807 float post_wb_delta_E = 0.f;
1808 float post_wb_max_delta_E = 0.f;
1809 compute_patches_delta_E(patches, g->checker, g->delta_E_in, &post_wb_delta_E, &post_wb_max_delta_E);
1810
1811 /* Compute the matrix of mix */
1812 double *const restrict Y = dt_alloc_align(g->checker->patches * 3 * sizeof(double));
1813 double *const restrict A = dt_alloc_align(g->checker->patches * 3 * 9 * sizeof(double));
1814 if(IS_NULL_PTR(Y) || IS_NULL_PTR(A))
1815 {
1816 dt_free_align(Y);
1818 dt_free_align(patches);
1819 return 1;
1820 }
1821
1822 for(size_t k = 0; k < g->checker->patches; k++)
1823 {
1824 float *const sample = patches + k * 4;
1825 dt_aligned_pixel_t LMS_test;
1826 dt_store_simd_aligned(LMS_test, convert_any_XYZ_to_LMS(dt_load_simd_aligned(sample), kind));
1827
1828 const float *const reference = g->checker->values[k].Lab;
1829 dt_aligned_pixel_t XYZ_ref, LMS_ref;
1830 dt_Lab_to_XYZ(reference, XYZ_ref);
1831 dt_store_simd_aligned(LMS_ref, convert_any_XYZ_to_LMS(dt_load_simd_aligned(XYZ_ref), kind));
1832
1833 // get the optimization weights
1834 float w = 1.f;
1835 if(g->optimization == DT_SOLVE_OPTIMIZE_NONE)
1836 w = sqrtf(1.f / (float)g->checker->patches);
1837 else if(g->optimization == DT_SOLVE_OPTIMIZE_HIGH_SAT)
1838 w = sqrtf(hypotf(reference[1] / 128.f, reference[2] / 128.f));
1839 else if(g->optimization == DT_SOLVE_OPTIMIZE_LOW_SAT)
1840 w = sqrtf(1.f - hypotf(reference[1] / 128.f, reference[2] / 128.f));
1841 else if(g->optimization == DT_SOLVE_OPTIMIZE_SKIN)
1842 {
1843 // average skin hue angle is 1.0 rad, hue range is [0.75 ; 1.25]
1844 const float ref_hue = 1.f;
1845 GET_WEIGHT;
1846 }
1847 else if(g->optimization == DT_SOLVE_OPTIMIZE_FOLIAGE)
1848 {
1849 // average foliage hue angle is 2.23 rad, hue range is [1.94 ; 2.44]
1850 const float ref_hue = 2.23f;
1851 GET_WEIGHT;
1852 }
1853 else if(g->optimization == DT_SOLVE_OPTIMIZE_SKY)
1854 {
1855 // average sky/water hue angle is -1.93 rad, hue range is [-1.64 ; -2.41]
1856 const float ref_hue = -1.93f;
1857 GET_WEIGHT;
1858 }
1859 else if(g->optimization == DT_SOLVE_OPTIMIZE_AVG_DELTA_E)
1860 w = sqrtf(sqrtf(1.f / g->delta_E_in[k]));
1861 else if(g->optimization == DT_SOLVE_OPTIMIZE_MAX_DELTA_E)
1862 w = sqrtf(sqrtf(g->delta_E_in[k]));
1863
1864 // fill 3 rows of the y column vector
1865 for(size_t c = 0; c < 3; c++) Y[k * 3 + c] = w * LMS_ref[c];
1866
1867 // fill line one of the A matrix
1868 A[k * 3 * 9 + 0] = w * LMS_test[0];
1869 A[k * 3 * 9 + 1] = w * LMS_test[1];
1870 A[k * 3 * 9 + 2] = w * LMS_test[2];
1871 A[k * 3 * 9 + 3] = 0.f;
1872 A[k * 3 * 9 + 4] = 0.f;
1873 A[k * 3 * 9 + 5] = 0.f;
1874 A[k * 3 * 9 + 6] = 0.f;
1875 A[k * 3 * 9 + 7] = 0.f;
1876 A[k * 3 * 9 + 8] = 0.f;
1877
1878 // fill line two of the A matrix
1879 A[k * 3 * 9 + 9 + 0] = 0.f;
1880 A[k * 3 * 9 + 9 + 1] = 0.f;
1881 A[k * 3 * 9 + 9 + 2] = 0.f;
1882 A[k * 3 * 9 + 9 + 3] = w * LMS_test[0];
1883 A[k * 3 * 9 + 9 + 4] = w * LMS_test[1];
1884 A[k * 3 * 9 + 9 + 5] = w * LMS_test[2];
1885 A[k * 3 * 9 + 9 + 6] = 0.f;
1886 A[k * 3 * 9 + 9 + 7] = 0.f;
1887 A[k * 3 * 9 + 9 + 8] = 0.f;
1888
1889 // fill line three of the A matrix
1890 A[k * 3 * 9 + 18 + 0] = 0.f;
1891 A[k * 3 * 9 + 18 + 1] = 0.f;
1892 A[k * 3 * 9 + 18 + 2] = 0.f;
1893 A[k * 3 * 9 + 18 + 3] = 0.f;
1894 A[k * 3 * 9 + 18 + 4] = 0.f;
1895 A[k * 3 * 9 + 18 + 5] = 0.f;
1896 A[k * 3 * 9 + 18 + 6] = w * LMS_test[0];
1897 A[k * 3 * 9 + 18 + 7] = w * LMS_test[1];
1898 A[k * 3 * 9 + 18 + 8] = w * LMS_test[2];
1899 }
1900
1901 if(pseudo_solve_gaussian(A, Y, g->checker->patches * 3, 9, TRUE) != 0)
1902 {
1903 dt_free_align(Y);
1905 dt_free_align(patches);
1906 return 1;
1907 }
1908
1909 // repack the matrix
1911
1912 dt_free_align(Y);
1914
1915 // apply the matrix mix
1916 for(size_t k = 0; k < g->checker->patches; k++)
1917 {
1918 float *const sample = patches + k * 4;
1919 dt_aligned_pixel_t LMS_test;
1920 dt_aligned_pixel_t temp = { 0.f };
1921
1922 // Restore the original exposure of the patch
1923 for(size_t c = 0; c < 3; c++) temp[c] = sample[c];
1924
1925 dt_store_simd_aligned(LMS_test, convert_any_XYZ_to_LMS(dt_load_simd_aligned(temp), kind));
1926 dot_product(LMS_test, g->mix, temp);
1927 dt_store_simd_aligned(sample, convert_any_LMS_to_XYZ(dt_load_simd_aligned(temp), kind));
1928 }
1929
1930 // Compute the delta E
1931 float post_mix_delta_E = 0.f;
1932 float post_mix_max_delta_E = 0.f;
1933 compute_patches_delta_E(patches, g->checker, g->delta_E_in, &post_mix_delta_E, &post_mix_max_delta_E);
1934
1935 // get the temperature
1936 float temperature;
1937 dt_illuminant_t test_illuminant;
1938 float x = illuminant_xyY[0];
1939 float y = illuminant_xyY[1];
1940 check_if_close_to_daylight(x, y, &temperature, &test_illuminant, NULL);
1941 gchar *string;
1942 if(test_illuminant == DT_ILLUMINANT_D)
1943 string = _("(daylight)");
1944 else if(test_illuminant == DT_ILLUMINANT_BB)
1945 string = _("(black body)");
1946 else
1947 string = _("(invalid)");
1948
1949 gchar *diagnostic;
1950 if(post_mix_delta_E <= 1.2f)
1951 diagnostic = _("very good");
1952 else if(post_mix_delta_E <= 2.3f)
1953 diagnostic = _("good");
1954 else if(post_mix_delta_E <= 3.4f)
1955 diagnostic = _("passable");
1956 else
1957 diagnostic = _("bad");
1958
1959 g->profile_ready = TRUE;
1960
1961 // Update GUI label
1962 dt_free(g->delta_E_label_text);
1963 g->delta_E_label_text
1964 = g_strdup_printf(_("\n<b>Profile quality report: %s</b>\n"
1965 "input \316\224E: \tavg. %.2f ; \tmax. %.2f\n"
1966 "WB \316\224E: \tavg. %.2f; \tmax. %.2f\n"
1967 "output \316\224E: \tavg. %.2f; \tmax. %.2f\n\n"
1968 "<b>Profile data</b>\n"
1969 "illuminant: \t%.0f K \t%s\n"
1970 "matrix in adaptation space:\n"
1971 "<tt>%+.4f \t%+.4f \t%+.4f\n"
1972 "%+.4f \t%+.4f \t%+.4f\n"
1973 "%+.4f \t%+.4f \t%+.4f</tt>\n\n"
1974 "<b>Normalization values</b>\n"
1975 "exposure compensation: \t%+.2f EV\n"
1976 "black offset: \t%+.4f"
1977 ),
1978 diagnostic, pre_wb_delta_E, pre_wb_max_delta_E, post_wb_delta_E, post_wb_max_delta_E,
1979 post_mix_delta_E, post_mix_max_delta_E, temperature, string, g->mix[0][0], g->mix[0][1],
1980 g->mix[0][2], g->mix[1][0], g->mix[1][1], g->mix[1][2], g->mix[2][0], g->mix[2][1],
1981 g->mix[2][2], log2f(extraction_result.exposure), extraction_result.black);
1982
1983 dt_free_align(patches);
1984 return 0;
1985}
1986
1987int validate_color_checker(const float *const restrict in,
1989 const dt_iop_channelmixer_rgb_box_map_t *const box_map,
1990 const dt_colormatrix_t RGB_to_XYZ, const dt_colormatrix_t XYZ_to_RGB, const dt_colormatrix_t XYZ_to_CAM)
1991{
1992 float *const restrict patches = dt_alloc_align_float(4 * g->checker->patches);
1993 if(IS_NULL_PTR(patches)) return 1;
1994 extraction_result_t extraction_result = { 0 };
1995 if(_extract_patches(in, roi_in, g, box_map, RGB_to_XYZ, XYZ_to_CAM, patches, FALSE, &extraction_result))
1996 {
1997 dt_free_align(patches);
1998 return 1;
1999 }
2000
2001 // Compute the delta E
2002 float pre_wb_delta_E = 0.f;
2003 float pre_wb_max_delta_E = 0.f;
2004 compute_patches_delta_E(patches, g->checker, g->delta_E_in, &pre_wb_delta_E, &pre_wb_max_delta_E);
2005
2006 gchar *diagnostic;
2007 if(pre_wb_delta_E <= 1.2f)
2008 diagnostic = _("very good");
2009 else if(pre_wb_delta_E <= 2.3f)
2010 diagnostic = _("good");
2011 else if(pre_wb_delta_E <= 3.4f)
2012 diagnostic = _("passable");
2013 else
2014 diagnostic = _("bad");
2015
2016 // Update GUI label
2017 dt_free(g->delta_E_label_text);
2018 g->delta_E_label_text = g_strdup_printf(_("\n<b>Profile quality report: %s</b>\n"
2019 "output \316\224E: \tavg. %.2f; \tmax. %.2f\n\n"
2020 "<b>Normalization values</b>\n"
2021 "exposure compensation: \t%+.2f EV\n"
2022 "black offset: \t%+.4f"),
2023 diagnostic, pre_wb_delta_E, pre_wb_max_delta_E, log2f(extraction_result.exposure),
2024 extraction_result.black);
2025
2026 dt_free_align(patches);
2027 return 0;
2028}
2029
2030int process(struct dt_iop_module_t *self, const dt_dev_pixelpipe_t *pipe, const dt_dev_pixelpipe_iop_t *piece,
2031 const void *const restrict ivoid, void *const restrict ovoid)
2032{
2033 const dt_iop_roi_t *const roi_in = &piece->roi_in;
2034 const dt_iop_roi_t *const roi_out = &piece->roi_out;
2036 const struct dt_iop_order_iccprofile_info_t *const work_profile = dt_ioppr_get_pipe_current_profile_info(self, pipe);
2037 const struct dt_iop_order_iccprofile_info_t *const input_profile = dt_ioppr_get_pipe_input_profile_info(pipe);
2039
2041
2042 dt_colormatrix_t RGB_to_XYZ;
2043 dt_colormatrix_t XYZ_to_RGB;
2044 dt_colormatrix_t XYZ_to_CAM;
2045
2046 // repack the matrices as flat AVX2-compliant matrice
2047 if(!IS_NULL_PTR(work_profile))
2048 {
2049 // work profile can't be fetched in commit_params since it is not yet initialised
2050 memcpy(RGB_to_XYZ, work_profile->matrix_in, sizeof(RGB_to_XYZ));
2051 memcpy(XYZ_to_RGB, work_profile->matrix_out, sizeof(XYZ_to_RGB));
2052 memcpy(XYZ_to_CAM, input_profile->matrix_out, sizeof(XYZ_to_CAM));
2053 }
2054
2055 const size_t ch = 4;
2056
2057 const float *const restrict in = (const float *const restrict)ivoid;
2058 float *const restrict out = (float *const restrict)ovoid;
2059
2060 // auto-detect WB upon request
2061 if(self->dev->gui_attached && g)
2062 {
2063 gboolean exit = FALSE;
2064
2065 if(g->run_profile && pipe->type == DT_DEV_PIXELPIPE_PREVIEW)
2066 {
2068 const dt_iop_channelmixer_rgb_box_map_t box_map = _box_map(pipe, roi_in);
2069 const int err = extract_color_checker(in, out, roi_in, g, &box_map,
2070 RGB_to_XYZ, XYZ_to_RGB, XYZ_to_CAM, data->adaptation);
2071 g->run_profile = FALSE;
2074 if(err) return 1;
2075 }
2076
2078 {
2079 if(pipe->type == DT_DEV_PIXELPIPE_FULL)
2080 {
2081 // detection on full image only
2083 const int err = auto_detect_WB(in, data->illuminant_type, roi_in->width, roi_in->height, ch, RGB_to_XYZ, g->XYZ);
2085 if(err) return 1;
2086 }
2087
2088 // passthrough pixels
2089 dt_iop_image_copy_by_size(out, in, roi_in->width, roi_in->height, ch);
2090
2091 dt_control_log(_("auto-detection of white balance completed"));
2092
2093 exit = TRUE;
2094 }
2095
2096 if(exit) return 0;
2097 }
2098
2100 {
2101 // The camera illuminant is a behaviour rather than a preset of values:
2102 // it uses whatever is in the RAW EXIF. But it depends on what temperature.c is doing
2103 // and needs to be updated accordingly, to give a consistent result.
2104 // We initialise the CAT defaults using the temperature coeffs at startup, but if temperature
2105 // is changed later, we get no notification of the change here, so we can't update the defaults.
2106 // So we need to re-run the detection at runtime...
2107 float x, y;
2108 dt_aligned_pixel_t custom_wb;
2109 // Pipeline thread: source temperature's WB from the per-piece descriptor propagated in pipe
2110 // order (temperature writes piece->dsc_out.temperature.coeffs), NOT from dev->proxy.
2111 _custom_wb_from_coeffs(self, piece->dsc_in.temperature.coeffs, custom_wb);
2112
2113 if(find_temperature_from_raw_coeffs(&(self->dev->image_storage), custom_wb, &(x), &(y)))
2114 {
2115 // Convert illuminant from xyY to XYZ
2118
2119 // Convert illuminant from XYZ to Bradford modified LMS
2120 dt_store_simd_aligned(data->illuminant, convert_any_XYZ_to_LMS(dt_load_simd_aligned(XYZ), data->adaptation));
2121 data->illuminant[3] = 0.f;
2122 }
2123 else
2124 {
2125 // just use whatever was defined in commit_params hoping the defaults work...
2126 }
2127 }
2128
2129 // force loop unswitching in a controlled way
2130 switch(data->adaptation)
2131 {
2133 {
2134 loop_switch(in, out, roi_out->width, roi_out->height, ch,
2135 XYZ_to_RGB, RGB_to_XYZ, data->MIX,
2136 data->illuminant, data->saturation, data->lightness, data->grey,
2137 data->p, data->gamut, data->clip, data->apply_grey, DT_ADAPTATION_FULL_BRADFORD, data->version);
2138 break;
2139 }
2141 {
2142 loop_switch(in, out, roi_out->width, roi_out->height, ch,
2143 XYZ_to_RGB, RGB_to_XYZ, data->MIX,
2144 data->illuminant, data->saturation, data->lightness, data->grey,
2145 data->p, data->gamut, data->clip, data->apply_grey, DT_ADAPTATION_LINEAR_BRADFORD, data->version);
2146 break;
2147 }
2149 {
2150 loop_switch(in, out, roi_out->width, roi_out->height, ch,
2151 XYZ_to_RGB, RGB_to_XYZ, data->MIX,
2152 data->illuminant, data->saturation, data->lightness, data->grey,
2153 data->p, data->gamut, data->clip, data->apply_grey, DT_ADAPTATION_CAT16, data->version);
2154 break;
2155 }
2156 case DT_ADAPTATION_XYZ:
2157 {
2158 loop_switch(in, out, roi_out->width, roi_out->height, ch,
2159 XYZ_to_RGB, RGB_to_XYZ, data->MIX,
2160 data->illuminant, data->saturation, data->lightness, data->grey,
2161 data->p, data->gamut, data->clip, data->apply_grey, DT_ADAPTATION_XYZ, data->version);
2162 break;
2163 }
2164 case DT_ADAPTATION_RGB:
2165 {
2166 loop_switch(in, out, roi_out->width, roi_out->height, ch,
2167 XYZ_to_RGB, RGB_to_XYZ, data->MIX,
2168 data->illuminant, data->saturation, data->lightness, data->grey,
2169 data->p, data->gamut, data->clip, data->apply_grey, DT_ADAPTATION_RGB, data->version);
2170 break;
2171 }
2172 case DT_ADAPTATION_LAST:
2173 default:
2174 {
2175 break;
2176 }
2177 }
2178
2179 // run dE validation at output
2180 if(self->dev->gui_attached && g)
2181 if(g->run_validation && pipe->type == DT_DEV_PIXELPIPE_PREVIEW)
2182 {
2183 const dt_iop_channelmixer_rgb_box_map_t box_map = _box_map(pipe, roi_out);
2184 const int err = validate_color_checker(out, roi_out, g, &box_map, RGB_to_XYZ, XYZ_to_RGB, XYZ_to_CAM);
2185 g->run_validation = FALSE;
2186 if(err) return 1;
2187 }
2188
2189 return 0;
2190}
2191
2192#if HAVE_OPENCL
2193int process_cl(struct dt_iop_module_t *self, const dt_dev_pixelpipe_t *pipe, const dt_dev_pixelpipe_iop_t *piece, cl_mem dev_in, cl_mem dev_out)
2194{
2195 const dt_iop_roi_t *const roi_in = &piece->roi_in;
2198 const struct dt_iop_order_iccprofile_info_t *const work_profile = dt_ioppr_get_pipe_current_profile_info(self, pipe);
2199 //dt_iop_channelmixer_rgb_gui_data_t *g = (dt_iop_channelmixer_rgb_gui_data_t *)self->gui_data;
2200
2202
2203 if(d->illuminant_type == DT_ILLUMINANT_CAMERA)
2204 {
2205 // The camera illuminant is a behaviour rather than a preset of values:
2206 // it uses whatever is in the RAW EXIF. But it depends on what temperature.c is doing
2207 // and needs to be updated accordingly, to give a consistent result.
2208 // We initialise the CAT defaults using the temperature coeffs at startup, but if temperature
2209 // is changed later, we get no notification of the change here, so we can't update the defaults.
2210 // So we need to re-run the detection at runtime...
2211 float x, y;
2212 dt_aligned_pixel_t custom_wb;
2213 // Pipeline thread: source temperature's WB from the per-piece descriptor propagated in pipe
2214 // order (temperature writes piece->dsc_out.temperature.coeffs), NOT from dev->proxy.
2215 _custom_wb_from_coeffs(self, piece->dsc_in.temperature.coeffs, custom_wb);
2216
2217 if(find_temperature_from_raw_coeffs(&(self->dev->image_storage), custom_wb, &(x), &(y)))
2218 {
2219 // Convert illuminant from xyY to XYZ
2222
2223 // Convert illuminant from XYZ to Bradford modified LMS
2224 dt_store_simd_aligned(d->illuminant, convert_any_XYZ_to_LMS(dt_load_simd_aligned(XYZ), d->adaptation));
2225 d->illuminant[3] = 0.f;
2226 }
2227 }
2228
2229 cl_int err = -999;
2230
2231 const int devid = pipe->devid;
2232 const int width = roi_in->width;
2233 const int height = roi_in->height;
2234
2235 size_t sizes[] = { ROUNDUPDWD(width, devid), ROUNDUPDHT(height, devid), 1 };
2236
2237 cl_mem input_matrix_cl = NULL;
2238 cl_mem output_matrix_cl = NULL;
2239
2240 float input_matrix_3x4[12];
2241 float output_matrix_3x4[12];
2242 float mix_matrix_3x4[12];
2243 pack_3xSSE_to_3x4(work_profile->matrix_in, input_matrix_3x4);
2244 pack_3xSSE_to_3x4(work_profile->matrix_out, output_matrix_3x4);
2245 pack_3xSSE_to_3x4(d->MIX, mix_matrix_3x4);
2246
2247 input_matrix_cl = dt_opencl_copy_host_to_device_constant(devid, sizeof(input_matrix_3x4), input_matrix_3x4);
2248 output_matrix_cl = dt_opencl_copy_host_to_device_constant(devid, sizeof(output_matrix_3x4), output_matrix_3x4);
2249 cl_mem MIX_cl = dt_opencl_copy_host_to_device_constant(devid, sizeof(mix_matrix_3x4), mix_matrix_3x4);
2250
2251 // select the right kernel for the current LMS space
2253
2254 switch(d->adaptation)
2255 {
2257 {
2259 break;
2260 }
2262 {
2264 break;
2265 }
2267 {
2269 break;
2270 }
2271 case DT_ADAPTATION_XYZ:
2272 {
2274 break;
2275 }
2276 case DT_ADAPTATION_RGB:
2277 case DT_ADAPTATION_LAST:
2278 default:
2279 {
2281 break;
2282 }
2283 }
2284
2285 dt_opencl_set_kernel_arg(devid, kernel, 0, sizeof(cl_mem), (void *)&dev_in);
2286 dt_opencl_set_kernel_arg(devid, kernel, 1, sizeof(cl_mem), (void *)&dev_out);
2287 dt_opencl_set_kernel_arg(devid, kernel, 2, sizeof(int), (void *)&width);
2288 dt_opencl_set_kernel_arg(devid, kernel, 3, sizeof(int), (void *)&height);
2289 dt_opencl_set_kernel_arg(devid, kernel, 4, sizeof(cl_mem), (void *)&input_matrix_cl);
2290 dt_opencl_set_kernel_arg(devid, kernel, 5, sizeof(cl_mem), (void *)&output_matrix_cl);
2291 dt_opencl_set_kernel_arg(devid, kernel, 6, sizeof(cl_mem), (void *)&MIX_cl);
2292 dt_opencl_set_kernel_arg(devid, kernel, 7, 4 * sizeof(float), (void *)&d->illuminant);
2293 dt_opencl_set_kernel_arg(devid, kernel, 8, 4 * sizeof(float), (void *)&d->saturation);
2294 dt_opencl_set_kernel_arg(devid, kernel, 9, 4 * sizeof(float), (void *)&d->lightness);
2295 dt_opencl_set_kernel_arg(devid, kernel, 10, 4 * sizeof(float), (void *)&d->grey);
2296 dt_opencl_set_kernel_arg(devid, kernel, 11, sizeof(float), (void *)&d->p);
2297 dt_opencl_set_kernel_arg(devid, kernel, 12, sizeof(float), (void *)&d->gamut);
2298 dt_opencl_set_kernel_arg(devid, kernel, 13, sizeof(int), (void *)&d->clip);
2299 dt_opencl_set_kernel_arg(devid, kernel, 14, sizeof(int), (void *)&d->apply_grey);
2300 dt_opencl_set_kernel_arg(devid, kernel, 15, sizeof(int), (void *)&d->version);
2301 err = dt_opencl_enqueue_kernel_2d(devid, kernel, sizes);
2302 if(err != CL_SUCCESS) goto error;
2303
2304 dt_opencl_release_mem_object(input_matrix_cl);
2305 dt_opencl_release_mem_object(output_matrix_cl);
2307 return TRUE;
2308
2309error:
2310 dt_opencl_release_mem_object(input_matrix_cl);
2311 dt_opencl_release_mem_object(output_matrix_cl);
2313 dt_print(DT_DEBUG_OPENCL, "[opencl_channelmixerrgb] couldn't enqueue kernel! %d\n", err);
2314 return FALSE;
2315}
2316
2318{
2319 const int program = 32; // extended.cl in programs.conf
2322
2323 module->data = gd;
2324 gd->kernel_channelmixer_rgb_cat16 = dt_opencl_create_kernel(program, "channelmixerrgb_CAT16");
2325 gd->kernel_channelmixer_rgb_bradford_full = dt_opencl_create_kernel(program, "channelmixerrgb_bradford_full");
2326 gd->kernel_channelmixer_rgb_bradford_linear = dt_opencl_create_kernel(program, "channelmixerrgb_bradford_linear");
2327 gd->kernel_channelmixer_rgb_xyz = dt_opencl_create_kernel(program, "channelmixerrgb_XYZ");
2328 gd->kernel_channelmixer_rgb_rgb = dt_opencl_create_kernel(program, "channelmixerrgb_RGB");
2329}
2330
2331
2342#endif
2343
2344
2346 const float x_increment, const float y_increment)
2347{
2348 // update box nodes
2349 for(size_t k = 0; k < 4; k++)
2350 {
2351 if(g->active_node[k])
2352 {
2353 g->box[k].x += x_increment;
2354 g->box[k].y += y_increment;
2355 }
2356 }
2357
2358 // update the homography
2359 get_homography(g->ideal_box, g->box, g->homography);
2360 get_homography(g->box, g->ideal_box, g->inverse_homography);
2361}
2362
2363static inline void init_bounding_box(dt_iop_channelmixer_rgb_gui_data_t *g, const float width, const float height)
2364{
2365 if(g->checker && !g->checker_ready)
2366 {
2367 // top left
2368 g->box[0].x = g->box[0].y = 10.;
2369
2370 // top right
2371 g->box[1].x = (width - 10.);
2372 g->box[1].y = g->box[0].y;
2373
2374 // bottom right
2375 g->box[2].x = g->box[1].x;
2376 g->box[2].y = (width - 10.) * g->checker->ratio;
2377
2378 // bottom left
2379 g->box[3].x = g->box[0].x;
2380 g->box[3].y = g->box[2].y;
2381
2382 g->checker_ready = TRUE;
2383 }
2384
2385 g->center_box.x = 0.5f;
2386 g->center_box.y = 0.5f;
2387
2388 g->ideal_box[0].x = 0.f;
2389 g->ideal_box[0].y = 0.f;
2390 g->ideal_box[1].x = 1.f;
2391 g->ideal_box[1].y = 0.f;
2392 g->ideal_box[2].x = 1.f;
2393 g->ideal_box[2].y = 1.f;
2394 g->ideal_box[3].x = 0.f;
2395 g->ideal_box[3].y = 1.f;
2396
2397 update_bounding_box(g, 0.f, 0.f);
2398}
2399
2400
2401
2402int mouse_moved(struct dt_iop_module_t *self, double x, double y, double pressure, int which)
2403{
2404 if(!self->enabled) return 0;
2405
2407 if(IS_NULL_PTR(g) || !g->is_profiling_started) return 0;
2408 if(g->box[0].x == -1.0f || g->box[1].y == -1.0f) return 0;
2409
2410 dt_develop_t *dev = self->dev;
2411 const float wd = dt_dev_roi_request_preview_width(dev);
2412 const float ht = dt_dev_roi_request_preview_height(dev);
2413 if(wd == 0.f || ht == 0.f) return 0;
2414
2415 float pzxpy[2] = { (float)x, (float)y };
2418 const float pzx = pzxpy[0];
2419 const float pzy = pzxpy[1];
2420
2421 // if dragging and dropping, don't update active nodes,
2422 // just update cursor coordinates then redraw
2423 // this ensure smooth updates
2424 if(g->drag_drop)
2425 {
2427 g->click_end.x = pzx;
2428 g->click_end.y = pzy;
2429
2430 update_bounding_box(g, g->click_end.x - g->click_start.x, g->click_end.y - g->click_start.y);
2431
2432 g->click_start.x = pzx;
2433 g->click_start.y = pzy;
2435
2437 return 1;
2438 }
2439
2440 // Find out if we are close to a node
2442 g->is_cursor_close = FALSE;
2443
2444 for(size_t k = 0; k < 4; k++)
2445 {
2446 if(hypotf(pzx - g->box[k].x, pzy - g->box[k].y) < 15.f)
2447 {
2448 g->active_node[k] = TRUE;
2449 g->is_cursor_close = TRUE;
2450 }
2451 else
2452 g->active_node[k] = FALSE;
2453 }
2455
2456 // if cursor is close from a node, remove the system pointer arrow to prevent hiding the spot behind it
2457 if(g->is_cursor_close)
2458 {
2460 }
2461 else
2462 {
2463 // Un-hide the cursor (it may have been hidden by the branch above) and leave it at
2464 // darkroom's own default (dot/crosshair/left_ptr, already queued per position by
2465 // _darkroom_set_default_cursor before this handler runs) instead of forcing "default".
2467 }
2468
2470
2471 return 1;
2472}
2473
2474int button_pressed(struct dt_iop_module_t *self, double x, double y, double pressure, int which, int type,
2475 uint32_t state)
2476{
2477 if(!self->enabled) return 0;
2478
2480 if(IS_NULL_PTR(g) || !g->is_profiling_started) return 0;
2481
2482 dt_develop_t *dev = self->dev;
2483 const float wd = dt_dev_roi_request_preview_width(dev);
2484 const float ht = dt_dev_roi_request_preview_height(dev);
2485 if(wd == 0.f || ht == 0.f) return 0;
2486
2487 // double click : reset the perspective correction
2488 if(type == GDK_DOUBLE_BUTTON_PRESS)
2489 {
2491 g->checker_ready = FALSE;
2492 g->profile_ready = FALSE;
2493 init_bounding_box(g, wd, ht);
2495
2497 return 1;
2498 }
2499
2500 // bounded box not inited, abort
2501 if(g->box[0].x == -1.0f || g->box[1].y == -1.0f) return 0;
2502
2503 // cursor is not on a node, abort
2504 if(!g->is_cursor_close) return 0;
2505
2506 float pzxpy[2] = { (float)x, (float)y };
2509 const float pzx = pzxpy[0];
2510 const float pzy = pzxpy[1];
2511
2513 g->drag_drop = TRUE;
2514 g->click_start.x = pzx;
2515 g->click_start.y = pzy;
2517
2519
2520 return 1;
2521}
2522
2523int button_released(struct dt_iop_module_t *self, double x, double y, int which, uint32_t state)
2524{
2525 if(!self->enabled) return 0;
2526
2528 if(IS_NULL_PTR(g) || !g->is_profiling_started) return 0;
2529 if(g->box[0].x == -1.0f || g->box[1].y == -1.0f) return 0;
2530 if(!g->is_cursor_close || !g->drag_drop) return 0;
2531
2532 dt_develop_t *dev = self->dev;
2533 const float wd = dt_dev_roi_request_preview_width(dev);
2534 const float ht = dt_dev_roi_request_preview_height(dev);
2535 if(wd == 0.f || ht == 0.f) return 0;
2536
2537 float pzxpy[2] = { (float)x, (float)y };
2540 const float pzx = pzxpy[0];
2541 const float pzy = pzxpy[1];
2542
2544 g->drag_drop = FALSE;
2545 g->click_end.x = pzx;
2546 g->click_end.y = pzy;
2547 update_bounding_box(g, g->click_end.x - g->click_start.x, g->click_end.y - g->click_start.y);
2549
2551
2552 return 1;
2553}
2554
2555void gui_post_expose(struct dt_iop_module_t *self, cairo_t *cr, int32_t width, int32_t height,
2556 int32_t pointerx, int32_t pointery)
2557{
2559 if(IS_NULL_PTR(work_profile)) return;
2560
2562 if(IS_NULL_PTR(g) || !g->is_profiling_started) return;
2563
2564 // Rescale and shift Cairo drawing coordinates
2565 dt_develop_t *dev = self->dev;
2566 if(dt_dev_rescale_roi(dev, cr, width, height)) return;
2567 const float zoom_scale = dt_dev_get_overlay_scale(dev);
2568
2569 cairo_set_line_width(cr, 2.0 / zoom_scale);
2570 const double origin = 9. / zoom_scale;
2571 const double destination = 18. / zoom_scale;
2572
2573 for(size_t k = 0; k < 4; k++)
2574 {
2575 if(g->active_node[k])
2576 {
2577 // draw cross hair
2578 cairo_set_source_rgba(cr, 1., 1., 1., 1.);
2579
2580 cairo_move_to(cr, g->box[k].x - origin, g->box[k].y);
2581 cairo_line_to(cr, g->box[k].x - destination, g->box[k].y);
2582
2583 cairo_move_to(cr, g->box[k].x + origin, g->box[k].y);
2584 cairo_line_to(cr, g->box[k].x + destination, g->box[k].y);
2585
2586 cairo_move_to(cr, g->box[k].x, g->box[k].y - origin);
2587 cairo_line_to(cr, g->box[k].x, g->box[k].y - destination);
2588
2589 cairo_move_to(cr, g->box[k].x, g->box[k].y + origin);
2590 cairo_line_to(cr, g->box[k].x, g->box[k].y + destination);
2591
2592 cairo_stroke(cr);
2593 }
2594
2595 // draw outline circle
2596 cairo_set_source_rgba(cr, 1., 1., 1., 1.);
2597 cairo_arc(cr, g->box[k].x, g->box[k].y, 8. / zoom_scale, 0, 2. * M_PI);
2598 cairo_stroke(cr);
2599
2600 // draw black dot
2601 cairo_set_source_rgba(cr, 0., 0., 0., 1.);
2602 cairo_arc(cr, g->box[k].x, g->box[k].y, 1.5 / zoom_scale, 0, 2. * M_PI);
2603 cairo_fill(cr);
2604 }
2605
2606 // draw symmetry axes
2607 cairo_set_line_width(cr, 1.5 / zoom_scale);
2608 cairo_set_source_rgba(cr, 1., 1., 1., 1.);
2609 const point_t top_ideal = { 0.5f, 1.f };
2610 const point_t top = apply_homography(top_ideal, g->homography);
2611 const point_t bottom_ideal = { 0.5f, 0.f };
2612 const point_t bottom = apply_homography(bottom_ideal, g->homography);
2613 cairo_move_to(cr, top.x, top.y);
2614 cairo_line_to(cr, bottom.x, bottom.y);
2615 cairo_stroke(cr);
2616
2617 const point_t left_ideal = { 0.f, 0.5f };
2618 const point_t left = apply_homography(left_ideal, g->homography);
2619 const point_t right_ideal = { 1.f, 0.5f };
2620 const point_t right = apply_homography(right_ideal, g->homography);
2621 cairo_move_to(cr, left.x, left.y);
2622 cairo_line_to(cr, right.x, right.y);
2623 cairo_stroke(cr);
2624
2625 /* For debug : display center of the image and center of the ideal target
2626 point_t new_target_center = apply_homography(target_center, g->homography);
2627 cairo_set_source_rgba(cr, 1., 1., 1., 1.);
2628 cairo_arc(cr, new_target_center.x, new_target_center.y, 7., 0, 2. * M_PI);
2629 cairo_stroke(cr);
2630
2631 cairo_set_source_rgba(cr, 0., 1., 1., 1.);
2632 cairo_arc(cr, 0.5 * wd, 0.5 * ht, 7., 0, 2. * M_PI);
2633 cairo_stroke(cr);
2634 */
2635 if(!g->checker || !g->checker->finished || !g->checker->values)
2636 {
2637 // no color data available, display a message
2638
2639 const point_t target_center = { 0.5f, 0.5f };
2640 point_t new_target_center = apply_homography(target_center, g->homography);
2641
2642 const char *msg = _("Error: No color data available for the selected chart.");
2643 cairo_set_font_size(cr, 20.0 / zoom_scale);
2644 cairo_text_extents_t extents;
2645 cairo_text_extents(cr, msg, &extents);
2646
2647 // Draw a white rectangle behind the text
2648 cairo_set_source_rgba(cr, 1., 1., 1., 1.);
2649 cairo_rectangle(cr,
2650 new_target_center.x - extents.width / 2.0 - 5.0 / zoom_scale,
2651 new_target_center.y - extents.height / 2.0 - 5.0 / zoom_scale,
2652 extents.width + 10.0 / zoom_scale,
2653 extents.height + 10.0 / zoom_scale);
2654 cairo_fill(cr);
2655
2656 // Draw the text in red
2657 cairo_set_source_rgba(cr, 1., 0., 0., 1.);
2658 cairo_select_font_face(cr, "roboto", CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_NORMAL);
2659 cairo_move_to(cr, new_target_center.x - extents.width / 2.0, new_target_center.y + extents.height / 2.0);
2660 cairo_show_text(cr, msg);
2661 return;
2662 }
2663
2664 const float radius_x = g->checker->radius * hypotf(1.f, g->checker->ratio) * g->safety_margin;
2665 const float radius_y = radius_x / g->checker->ratio;
2666
2667 for(size_t k = 0; k < g->checker->patches; k++)
2668 {
2669 // center of the patch in the ideal reference
2670 const point_t center = { g->checker->values[k].x, g->checker->values[k].y };
2671
2672 // corners of the patch in the ideal reference
2673 const point_t corners[4] = { {center.x - radius_x, center.y - radius_y},
2674 {center.x + radius_x, center.y - radius_y},
2675 {center.x + radius_x, center.y + radius_y},
2676 {center.x - radius_x, center.y + radius_y} };
2677
2678 // apply patch coordinates transform depending on perspective
2679 const point_t new_center = apply_homography(center, g->homography);
2680 // apply_homography_scaling gives a scaling of areas. we need to scale the
2681 // radius of the center circle so take a square root.
2682 const float scaling = sqrtf(apply_homography_scaling(center, g->homography));
2683 point_t new_corners[4];
2684 for(size_t c = 0; c < 4; c++) new_corners[c] = apply_homography(corners[c], g->homography);
2685
2686 cairo_set_line_cap(cr, CAIRO_LINE_CAP_SQUARE);
2687 cairo_set_source_rgba(cr, 0., 0., 0., 1.);
2688 cairo_move_to(cr, new_corners[0].x, new_corners[0].y);
2689 cairo_line_to(cr, new_corners[1].x, new_corners[1].y);
2690 cairo_line_to(cr, new_corners[2].x, new_corners[2].y);
2691 cairo_line_to(cr, new_corners[3].x, new_corners[3].y);
2692 cairo_line_to(cr, new_corners[0].x, new_corners[0].y);
2693
2694 if(g->delta_E_in)
2695 {
2696 // draw delta E feedback
2697 if(g->delta_E_in[k] > 2.3f)
2698 {
2699 // one diagonal if delta E > 3
2700 cairo_move_to(cr, new_corners[0].x, new_corners[0].y);
2701 cairo_line_to(cr, new_corners[2].x, new_corners[2].y);
2702 }
2703 if(g->delta_E_in[k] > 4.6f)
2704 {
2705 // the other diagonal if delta E > 6
2706 cairo_move_to(cr, new_corners[1].x, new_corners[1].y);
2707 cairo_line_to(cr, new_corners[3].x, new_corners[3].y);
2708 }
2709 }
2710
2711 cairo_set_line_width(cr, 5.0 / zoom_scale);
2712 cairo_stroke_preserve(cr);
2713 cairo_set_line_width(cr, 2.0 / zoom_scale);
2714 cairo_set_source_rgba(cr, 1., 1., 1., 1.);
2715 cairo_stroke(cr);
2716
2717 cairo_set_line_cap(cr, CAIRO_LINE_CAP_BUTT);
2718
2720 dt_ioppr_lab_to_rgb_matrix(g->checker->values[k].Lab, RGB, work_profile->matrix_out_transposed, work_profile->lut_out,
2721 work_profile->unbounded_coeffs_out, work_profile->lutsize,
2722 work_profile->nonlinearlut);
2723
2724 cairo_set_source_rgba(cr, RGB[0], RGB[1], RGB[2], 1.);
2725 cairo_arc(cr, new_center.x, new_center.y, 0.25 * (radius_x + radius_y) * scaling, 0, 2. * M_PI);
2726 cairo_fill(cr);
2727 }
2728}
2729
2743{
2744 const char *name = dt_bauhaus_combobox_get_entry(combobox, dt_bauhaus_combobox_get(combobox));
2745 if(IS_NULL_PTR(name)) return NULL;
2746
2747 for(GList *l = g_list_first(labels); l; l = g_list_next(l))
2748 {
2749 const dt_colorchecker_label_t *label = (const dt_colorchecker_label_t *)l->data;
2750 if(!g_strcmp0(label->name, name)) return label;
2751 }
2752 return NULL;
2753}
2754
2763 const dt_colorchecker_label_t *checker_label,
2764 const dt_colorchecker_label_t *color_label)
2765{
2766 dt_colorchecker_cleanup(g->checker);
2767 g->checker = dt_get_color_checker(!IS_NULL_PTR(checker_label) ? checker_label->type : COLOR_CHECKER_XRITE_24_2000,
2768 !IS_NULL_PTR(checker_label) ? checker_label->path : NULL,
2769 !IS_NULL_PTR(color_label) ? color_label->path : NULL);
2770}
2771
2773{
2775
2776 if(!IS_NULL_PTR(checker_label) && checker_label->type == COLOR_CHECKER_USER_REF)
2777 if(dt_bauhaus_combobox_get_entry(GTK_WIDGET(g->checkers_color_list), 0) != NULL)
2778 {
2779 gtk_widget_show(GTK_WIDGET(g->checkers_color_list));
2780 gtk_widget_hide(GTK_WIDGET(g->checker_msg));
2781 }
2782 else
2783 {
2784 gtk_widget_hide(GTK_WIDGET(g->checkers_color_list));
2785 gtk_widget_show_now(GTK_WIDGET(g->checker_msg));
2786 }
2787
2788 else
2789 {
2790 gtk_widget_hide(GTK_WIDGET(g->checkers_color_list));
2791 gtk_widget_hide(GTK_WIDGET(g->checker_msg));
2792 }
2793}
2794
2801{
2803
2804 if(!g) return;
2805 if(!g->colorcheckers) return;
2806
2807 // the CGATS reference values only apply to a user chart described by a .cht file
2808 const dt_colorchecker_label_t *checker_label = _checker_label_from_combobox(g->checkers_list, g->colorcheckers);
2809 if(IS_NULL_PTR(checker_label) || checker_label->type != COLOR_CHECKER_USER_REF) return;
2810
2811 // find all CGATS files if not already done
2812 if(!g->n_color)
2813 g->n_color = dt_colorchecker_find_color(&(g->colorcheckers_all_color));
2814 // no CGATS files found, early return
2815 if(g->n_color == 0) return;
2816
2817 // update the gui
2818 dt_bauhaus_combobox_clear(g->checkers_color_list);
2819 g_list_free(g->colorcheckers_color); // don't free_full because data pointers belong to g->colorcheckers_all_color
2820 g->colorcheckers_color = NULL;
2821
2822 const int checker_patch_nb = checker_label->patch_nb;
2823
2824 if(checker_patch_nb > 0)
2825 {
2826 for(GList *l = g_list_first(g->colorcheckers_all_color); l; l = g_list_next(l))
2827 {
2828 dt_colorchecker_label_t *cht_label = (dt_colorchecker_label_t *)l->data;
2829 // Filter out colorcheckers with different patch number.
2830 // A separate GList is required to associate the color checkers with their corresponding combobox item IDs.
2831 if(cht_label->patch_nb == checker_patch_nb)
2832 {
2833 dt_bauhaus_combobox_add(g->checkers_color_list, cht_label->name);
2834 g->colorcheckers_color = g_list_append(g->colorcheckers_color, cht_label);
2835 }
2836 }
2837 }
2838 dt_control_queue_redraw_widget(g->checkers_color_list);
2839}
2840
2842{
2844 if(IS_NULL_PTR(g)) return;
2845
2846 dt_colorchecker_label_list_cleanup(&(g->colorcheckers));
2847
2848 dt_colorchecker_find(&(g->colorcheckers));
2849
2850 // update the gui
2851 dt_bauhaus_combobox_clear(g->checkers_list);
2852 gboolean has_builtin_checker = FALSE;
2853 gboolean user_checker_separator_added = FALSE;
2854 for(GList *l = g_list_first(g->colorcheckers); l; l = g_list_next(l))
2855 {
2856 const dt_colorchecker_label_t *checker_data = (const dt_colorchecker_label_t*)l->data;
2857
2858 if(checker_data->type < COLOR_CHECKER_USER_REF)
2859 {
2860 has_builtin_checker = TRUE;
2861 }
2862 else if(has_builtin_checker && !user_checker_separator_added)
2863 {
2864 dt_bauhaus_combobox_add_separator(g->checkers_list);
2865 user_checker_separator_added = TRUE;
2866 }
2867
2868 dt_bauhaus_combobox_add(g->checkers_list, checker_data->name);
2869 }
2870}
2871
2872static void optimize_changed_callback(GtkWidget *widget, gpointer user_data)
2873{
2874 if(dt_gui_widgets_suppressed()) return;
2875 dt_iop_module_t *self = (dt_iop_module_t *)user_data;
2877
2878 const int i = dt_bauhaus_combobox_get(widget);
2879 dt_conf_set_int("darkroom/modules/channelmixerrgb/optimization", i);
2880
2882 g->optimization = i;
2884}
2885
2886static void checker_color_changed_callback(GtkWidget *widget, gpointer user_data)
2887{
2888 if(dt_gui_widgets_suppressed()) return;
2889 dt_iop_module_t *self = (dt_iop_module_t *)user_data;
2891
2892 const dt_colorchecker_label_t *color_label = _checker_label_from_combobox(widget, g->colorcheckers_color);
2893 dt_conf_set_string("darkroom/modules/channelmixerrgb/colorchecker_color",
2894 !IS_NULL_PTR(color_label) ? color_label->name : "");
2895
2896 _set_checker_from_labels(g, _checker_label_from_combobox(g->checkers_list, g->colorcheckers), color_label);
2897
2898 dt_develop_t *dev = self->dev;
2899 const float wd = dt_dev_roi_request_preview_width(dev);
2900 const float ht = dt_dev_roi_request_preview_height(dev);
2901 if(wd == 0.f || ht == 0.f) return;
2902
2904 g->profile_ready = FALSE;
2905 init_bounding_box(g, wd, ht);
2907
2909}
2910
2911void checker_changed_callback(GtkWidget *widget, gpointer user_data)
2912{
2913 if(dt_gui_widgets_suppressed()) return;
2914 dt_iop_module_t *self = (dt_iop_module_t *)user_data;
2916
2917 const dt_colorchecker_label_t *checker_label = _checker_label_from_combobox(widget, g->colorcheckers);
2918 dt_conf_set_string("darkroom/modules/channelmixerrgb/colorchecker",
2919 !IS_NULL_PTR(checker_label) ? checker_label->name : "");
2922 color_list_visibility(self, checker_label);
2924
2925 _set_checker_from_labels(g, checker_label, _checker_label_from_combobox(g->checkers_color_list, g->colorcheckers_color));
2926
2927 dt_develop_t *dev = self->dev;
2928 const float wd = dt_dev_roi_request_preview_width(dev);
2929 const float ht = dt_dev_roi_request_preview_height(dev);
2930 if(wd == 0.f || ht == 0.f) return;
2931
2933 g->profile_ready = FALSE;
2934 init_bounding_box(g, wd, ht);
2936
2938}
2939
2940static void safety_changed_callback(GtkWidget *widget, gpointer user_data)
2941{
2942 if(dt_gui_widgets_suppressed()) return;
2943 dt_iop_module_t *self = (dt_iop_module_t *)user_data;
2945
2947 g->safety_margin = dt_bauhaus_slider_get(widget);
2949
2950 dt_conf_set_float("darkroom/modules/channelmixerrgb/safety", g->safety_margin);
2952}
2953
2954
2955static void start_profiling_callback(GtkWidget *togglebutton, dt_iop_module_t *self)
2956{
2957 if(dt_gui_widgets_suppressed()) return;
2959 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(self->gui->off), TRUE);
2960
2961 dt_develop_t *dev = self->dev;
2962 const float wd = dt_dev_roi_request_preview_width(dev);
2963 const float ht = dt_dev_roi_request_preview_height(dev);
2964 if(wd == 0.f || ht == 0.f) return;
2965
2967 g->is_profiling_started = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(g->cs.toggle));
2968
2969 // init bounding box
2971 init_bounding_box(g, wd, ht);
2973
2975}
2976
2977static void run_profile_callback(GtkWidget *widget, GdkEventButton *event, gpointer user_data)
2978{
2979 if(dt_gui_widgets_suppressed()) return;
2980 dt_iop_module_t *self = (dt_iop_module_t *)user_data;
2982
2984 g->run_profile = TRUE;
2988}
2989
2990static void run_validation_callback(GtkWidget *widget, GdkEventButton *event, gpointer user_data)
2991{
2992 if(dt_gui_widgets_suppressed()) return;
2993 dt_iop_module_t *self = (dt_iop_module_t *)user_data;
2995
2997 g->run_validation = TRUE;
3001}
3002
3003static void commit_profile_callback(GtkWidget *widget, GdkEventButton *event, gpointer user_data)
3004{
3005 if(dt_gui_widgets_suppressed()) return;
3006 dt_iop_module_t *self = (dt_iop_module_t *)user_data;
3009
3010 if(!g->profile_ready) return;
3011
3013
3014 p->x = g->xy[0];
3015 p->y = g->xy[1];
3016 p->illuminant = DT_ILLUMINANT_CUSTOM;
3017 check_if_close_to_daylight(p->x, p->y, &p->temperature, NULL, NULL);
3018
3019 p->red[0] = g->mix[0][0];
3020 p->red[1] = g->mix[0][1];
3021 p->red[2] = g->mix[0][2];
3022
3023 p->green[0] = g->mix[1][0];
3024 p->green[1] = g->mix[1][1];
3025 p->green[2] = g->mix[1][2];
3026
3027 p->blue[0] = g->mix[2][0];
3028 p->blue[1] = g->mix[2][1];
3029 p->blue[2] = g->mix[2][2];
3030
3032
3034 dt_bauhaus_combobox_set(g->illuminant, p->illuminant);
3035 dt_bauhaus_slider_set(g->temperature, p->temperature);
3036
3037 dt_aligned_pixel_t xyY = { p->x, p->y, 1.f };
3038 dt_aligned_pixel_t Lch = { 0 };
3039 dt_xyY_to_Lch(xyY, Lch);
3040 dt_bauhaus_slider_set(g->illum_x, Lch[2] / M_PI * 180.f);
3041 dt_bauhaus_slider_set(g->illum_y, Lch[1]);
3042
3043 dt_bauhaus_slider_set(g->scale_red_R, p->red[0]);
3044 dt_bauhaus_slider_set(g->scale_red_G, p->red[1]);
3045 dt_bauhaus_slider_set(g->scale_red_B, p->red[2]);
3046
3047 dt_bauhaus_slider_set(g->scale_green_R, p->green[0]);
3048 dt_bauhaus_slider_set(g->scale_green_G, p->green[1]);
3049 dt_bauhaus_slider_set(g->scale_green_B, p->green[2]);
3050
3051 dt_bauhaus_slider_set(g->scale_blue_R, p->blue[0]);
3052 dt_bauhaus_slider_set(g->scale_blue_G, p->blue[1]);
3053 dt_bauhaus_slider_set(g->scale_blue_B, p->blue[2]);
3054
3056
3057 gui_changed(self, NULL, NULL);
3058
3059 dt_print(DT_DEBUG_DEV, "[picker/channelmixerrgb] history commit source=commit_profile\n");
3060 dt_dev_add_history_item(self->dev, self, TRUE, TRUE);
3061}
3062
3063static void _develop_ui_pipe_finished_callback(gpointer instance, gpointer user_data)
3064{
3065 dt_iop_module_t *self = (dt_iop_module_t *)user_data;
3068
3069 if(IS_NULL_PTR(g)) return;
3070 if(p->illuminant != DT_ILLUMINANT_DETECT_EDGES && p->illuminant != DT_ILLUMINANT_DETECT_SURFACES)
3071 return;
3072
3074 p->x = g->XYZ[0];
3075 p->y = g->XYZ[1];
3077
3078 check_if_close_to_daylight(p->x, p->y, &p->temperature, &p->illuminant, &p->adaptation);
3079
3081
3082 dt_bauhaus_slider_set(g->temperature, p->temperature);
3083 dt_bauhaus_combobox_set(g->illuminant, p->illuminant);
3084 dt_bauhaus_combobox_set(g->adaptation, p->adaptation);
3085
3086 const dt_aligned_pixel_t xyY = { p->x, p->y, 1.f };
3088 dt_xyY_to_Lch(xyY, Lch);
3089 dt_bauhaus_slider_set(g->illum_x, Lch[2] / M_PI * 180.f);
3090 dt_bauhaus_slider_set(g->illum_y, Lch[1]);
3091
3092 update_illuminants(self);
3093 update_approx_cct(self);
3096
3098
3099 gui_changed(self, NULL, NULL);
3100
3101 dt_print(DT_DEBUG_DEV, "[picker/channelmixerrgb] history commit source=ui_pipe_finished\n");
3102 dt_dev_add_history_item(self->dev, self, TRUE, TRUE);
3103}
3104
3105static void _preview_pipe_finished_callback(gpointer instance, gpointer user_data)
3106{
3107 dt_iop_module_t *self = (dt_iop_module_t *)user_data;
3109
3111 gtk_label_set_markup(GTK_LABEL(g->label_delta_E), g->delta_E_label_text);
3113}
3114
3117{
3121
3122 d->version = p->version;
3123
3124 float norm_R = 1.0f;
3125 if(p->normalize_R) norm_R = p->red[0] + p->red[1] + p->red[2];
3126
3127 float norm_G = 1.0f;
3128 if(p->normalize_G) norm_G = p->green[0] + p->green[1] + p->green[2];
3129
3130 float norm_B = 1.0f;
3131 if(p->normalize_B) norm_B = p->blue[0] + p->blue[1] + p->blue[2];
3132
3133 float norm_sat = 0.0f;
3134 if(p->normalize_sat) norm_sat = (p->saturation[0] + p->saturation[1] + p->saturation[2]) / 3.f;
3135
3136 float norm_light = 0.0f;
3137 if(p->normalize_light) norm_light = (p->lightness[0] + p->lightness[1] + p->lightness[2]) / 3.f;
3138
3139 float norm_grey = p->grey[0] + p->grey[1] + p->grey[2];
3140 d->apply_grey = (p->grey[0] != 0.f) || (p->grey[1] != 0.f) || (p->grey[2] != 0.f);
3141 if(!p->normalize_grey || norm_grey == 0.f) norm_grey = 1.f;
3142
3143 for(int i = 0; i < 3; i++)
3144 {
3145 d->MIX[0][i] = p->red[i] / norm_R;
3146 d->MIX[1][i] = p->green[i] / norm_G;
3147 d->MIX[2][i] = p->blue[i] / norm_B;
3148 d->saturation[i] = -p->saturation[i] + norm_sat;
3149 d->lightness[i] = p->lightness[i] - norm_light;
3150 d->grey[i] = p->grey[i] / norm_grey; // = NaN if (norm_grey == 0.f) but we don't care since (d->apply_grey == FALSE)
3151 }
3152
3153 if(p->version == CHANNELMIXERRGB_V_1)
3154 {
3155 // for the v1 saturation algo, the effect of R and B coeffs is reversed
3156 d->saturation[0] = -p->saturation[2] + norm_sat;
3157 d->saturation[2] = -p->saturation[0] + norm_sat;
3158 }
3159
3160 // just in case compiler feels clever and uses SSE 4x1 dot product
3161 d->saturation[CHANNEL_SIZE - 1] = 0.0f;
3162 d->lightness[CHANNEL_SIZE - 1] = 0.0f;
3163 d->grey[CHANNEL_SIZE - 1] = 0.0f;
3164
3165 d->adaptation = p->adaptation;
3166 d->clip = p->clip;
3167 d->gamut = (p->gamut == 0.f) ? p->gamut : 1.f / p->gamut;
3168
3169 // find x y coordinates of illuminant for CIE 1931 2° observer
3170 float x = p->x;
3171 float y = p->y;
3172 dt_aligned_pixel_t custom_wb;
3173 // Pipeline thread: source temperature's WB from the per-piece descriptor propagated in pipe order
3174 // (temperature writes piece->dsc_out.temperature.coeffs upstream), NOT from dev->proxy.
3175 _custom_wb_from_coeffs(self, piece->dsc_in.temperature.coeffs, custom_wb);
3176 illuminant_to_xy(p->illuminant, &(self->dev->image_storage), custom_wb, &x, &y, p->temperature, p->illum_fluo, p->illum_led);
3177
3178 // if illuminant is set as camera, x and y are set on-the-fly at commit time, so we need to set adaptation too
3179 if(p->illuminant == DT_ILLUMINANT_CAMERA)
3180 check_if_close_to_daylight(x, y, NULL, NULL, &(d->adaptation));
3181
3182 d->illuminant_type = p->illuminant;
3183
3184 // Convert illuminant from xyY to XYZ
3187
3188 // Convert illuminant from XYZ to Bradford modified LMS
3189 dt_store_simd_aligned(d->illuminant, convert_any_XYZ_to_LMS(dt_load_simd_aligned(XYZ), d->adaptation));
3190 d->illuminant[3] = 0.f;
3191
3192 dt_iop_fmt_log(self, "commit: class=%s matrix_supported=%d illuminant=%d adaptation=%d custom_wb=[%.4f %.4f %.4f %.4f] xy=[%.4f %.4f]",
3195 p->illuminant, d->adaptation, custom_wb[0], custom_wb[1], custom_wb[2], custom_wb[3], x, y);
3196
3197 //fprintf(stdout, "illuminant: %i\n", p->illuminant);
3198 //fprintf(stdout, "x: %f, y: %f\n", x, y);
3199 //fprintf(stdout, "X: %f - Y: %f - Z: %f\n", XYZ[0], XYZ[1], XYZ[2]);
3200 //fprintf(stdout, "L: %f - M: %f - S: %f\n", d->illuminant[0], d->illuminant[1], d->illuminant[2]);
3201
3202 // blue compensation for Bradford transform = (test illuminant blue / reference illuminant blue)^0.0834
3203 // reference illuminant is hard-set D50 for darktable's pipeline
3204 // test illuminant is user params
3205 d->p = powf(0.818155f / d->illuminant[2], 0.0834f);
3206
3207 // Disable OpenCL path if we are in any kind of diagnose mode (only C path has diagnostics)
3208 if(self->dev->gui_attached && g)
3209 {
3210 if( (g->run_profile && pipe->type == DT_DEV_PIXELPIPE_PREVIEW) || // color checker extraction mode
3211 (g->run_validation && pipe->type == DT_DEV_PIXELPIPE_PREVIEW) || // delta E validation
3212 ( (d->illuminant_type == DT_ILLUMINANT_DETECT_EDGES ||
3213 d->illuminant_type == DT_ILLUMINANT_DETECT_SURFACES ) && // WB extraction mode
3214 pipe->type == DT_DEV_PIXELPIPE_FULL ) )
3215 {
3216 piece->process_cl_ready = 0;
3217
3218 // Same reason, and the one nobody wrote down: all three modes consume the WHOLE frame in a
3219 // single call -- the two colour-checker paths address it with chart-box coordinates, and
3220 // auto_detect_WB() averages it. Tiling calls process() once per tile with a tile-local
3221 // roi_in, so the chart box would be applied to a fragment and whichever tile ran last would
3222 // win. Whether that happens is a runtime memory decision
3223 // (dt_tiling_piece_fits_host_memory(), which reads dt_get_available_mem() and the pixelpipe
3224 // cache's largest free run), so it shows up on one machine and not another.
3225 piece->process_tiling_ready = 0;
3226 }
3227 }
3228}
3229
3230
3232{
3235
3236 if(p->adaptation == DT_ADAPTATION_RGB || p->adaptation == DT_ADAPTATION_LAST)
3237 {
3238 // user disabled CAT at all, hide everything and exit
3239 gtk_widget_set_visible(g->illuminant, FALSE);
3240 gtk_widget_set_visible(g->illum_color, FALSE);
3241 gtk_widget_set_visible(g->approx_cct, FALSE);
3242 gtk_widget_set_visible(g->color_picker, FALSE);
3243 gtk_widget_set_visible(g->temperature, FALSE);
3244 gtk_widget_set_visible(g->illum_fluo, FALSE);
3245 gtk_widget_set_visible(g->illum_led, FALSE);
3246 gtk_widget_set_visible(g->illum_x, FALSE);
3247 gtk_widget_set_visible(g->illum_y, FALSE);
3248 return;
3249 }
3250 else
3251 {
3252 // set everything visible again and carry on
3253 gtk_widget_set_visible(g->illuminant, TRUE);
3254 gtk_widget_set_visible(g->illum_color, TRUE);
3255 gtk_widget_set_visible(g->approx_cct, TRUE);
3256 gtk_widget_set_visible(g->color_picker, TRUE);
3257 gtk_widget_set_visible(g->temperature, TRUE);
3258 gtk_widget_set_visible(g->illum_fluo, TRUE);
3259 gtk_widget_set_visible(g->illum_led, TRUE);
3260 gtk_widget_set_visible(g->illum_x, TRUE);
3261 }
3262
3263 // Display only the relevant sliders
3264 switch(p->illuminant)
3265 {
3266 case DT_ILLUMINANT_PIPE:
3267 case DT_ILLUMINANT_A:
3268 case DT_ILLUMINANT_E:
3269 {
3270 gtk_widget_set_visible(g->adaptation, TRUE);
3271 gtk_widget_set_visible(g->temperature, FALSE);
3272 gtk_widget_set_visible(g->illum_fluo, FALSE);
3273 gtk_widget_set_visible(g->illum_led, FALSE);
3274 gtk_widget_set_visible(g->illum_x, FALSE);
3275 gtk_widget_set_visible(g->illum_y, FALSE);
3276 break;
3277 }
3278 case DT_ILLUMINANT_D:
3279 case DT_ILLUMINANT_BB:
3280 {
3281 gtk_widget_set_visible(g->adaptation, TRUE);
3282 gtk_widget_set_visible(g->temperature, TRUE);
3283 gtk_widget_set_visible(g->illum_fluo, FALSE);
3284 gtk_widget_set_visible(g->illum_led, FALSE);
3285 gtk_widget_set_visible(g->illum_x, FALSE);
3286 gtk_widget_set_visible(g->illum_y, FALSE);
3287 break;
3288 }
3289 case DT_ILLUMINANT_F:
3290 {
3291 gtk_widget_set_visible(g->adaptation, TRUE);
3292 gtk_widget_set_visible(g->temperature, FALSE);
3293 gtk_widget_set_visible(g->illum_fluo, TRUE);
3294 gtk_widget_set_visible(g->illum_led, FALSE);
3295 gtk_widget_set_visible(g->illum_x, FALSE);
3296 gtk_widget_set_visible(g->illum_y, FALSE);
3297 break;
3298 }
3299 case DT_ILLUMINANT_LED:
3300 {
3301 gtk_widget_set_visible(g->adaptation, TRUE);
3302 gtk_widget_set_visible(g->temperature, FALSE);
3303 gtk_widget_set_visible(g->illum_fluo, FALSE);
3304 gtk_widget_set_visible(g->illum_led, TRUE);
3305 gtk_widget_set_visible(g->illum_x, FALSE);
3306 gtk_widget_set_visible(g->illum_y, FALSE);
3307 break;
3308 }
3310 {
3311 gtk_widget_set_visible(g->adaptation, TRUE);
3312 gtk_widget_set_visible(g->temperature, FALSE);
3313 gtk_widget_set_visible(g->illum_fluo, FALSE);
3314 gtk_widget_set_visible(g->illum_led, FALSE);
3315 gtk_widget_set_visible(g->illum_x, TRUE);
3316 gtk_widget_set_visible(g->illum_y, TRUE);
3317 break;
3318 }
3320 {
3321 gtk_widget_set_visible(g->adaptation, TRUE);
3322 gtk_widget_set_visible(g->temperature, FALSE);
3323 gtk_widget_set_visible(g->illum_fluo, FALSE);
3324 gtk_widget_set_visible(g->illum_led, FALSE);
3325 gtk_widget_set_visible(g->illum_x, FALSE);
3326 gtk_widget_set_visible(g->illum_y, FALSE);
3327 break;
3328 }
3331 {
3332 gtk_widget_set_visible(g->adaptation, FALSE);
3333 gtk_widget_set_visible(g->temperature, FALSE);
3334 gtk_widget_set_visible(g->illum_fluo, FALSE);
3335 gtk_widget_set_visible(g->illum_led, FALSE);
3336 gtk_widget_set_visible(g->illum_x, FALSE);
3337 gtk_widget_set_visible(g->illum_y, FALSE);
3338 break;
3339 }
3340 case DT_ILLUMINANT_LAST:
3341 default:
3342 {
3343 break;
3344 }
3345 }
3346}
3347
3376{
3377 // update the fill background color of x, y sliders
3380
3381 if(IS_NULL_PTR(g) || IS_NULL_PTR(p) || IS_NULL_PTR(g->illum_x) || IS_NULL_PTR(g->illum_y)) return;
3382
3383 // Varies x in range around current y param
3384 for(int i = 0; i < DT_BAUHAUS_SLIDER_MAX_STOPS; i++)
3385 {
3386 const float stop = ((float)i / (float)(DT_BAUHAUS_SLIDER_MAX_STOPS - 1));
3387 const float x = stop * ILLUM_X_MAX;
3388 dt_aligned_pixel_t RGB = { 0 };
3389
3390 dt_aligned_pixel_t Lch = { 100.f, 50.f, x / 180.f * M_PI };
3391 dt_aligned_pixel_t xyY = { 0 };
3392 dt_Lch_to_xyY(Lch, xyY);
3394 dt_bauhaus_slider_set_stop(g->illum_x, stop, RGB[0], RGB[1], RGB[2]);
3395
3396 const float y = stop * ILLUM_Y_MAX / 2.0f;
3397
3398 // Find current hue
3399 const dt_aligned_pixel_t xyY2 = { p->x, p->y, 1.f };
3400 dt_xyY_to_Lch(xyY2, Lch);
3401
3402 // Replace chroma by current step
3403 Lch[0] = 75.f;
3404 Lch[1] = y;
3405
3406 // Go back to xyY
3407 dt_Lch_to_xyY(Lch, xyY);
3409 dt_bauhaus_slider_set_stop(g->illum_y, stop, RGB[0], RGB[1], RGB[2]);
3410 }
3411
3412 gtk_widget_queue_draw(g->illum_x);
3413 gtk_widget_queue_draw(g->illum_y);
3414}
3415
3416static void paint_hue(dt_iop_module_t *self)
3417{
3418 // update the fill background color of LCh sliders
3420
3421 const float hue = dt_bauhaus_slider_get(g->hue_spot);
3422
3423 for(int i = 0; i < DT_BAUHAUS_SLIDER_MAX_STOPS; i++)
3424 {
3425 const float stop = ((float)i / (float)(DT_BAUHAUS_SLIDER_MAX_STOPS - 1));
3426 dt_aligned_pixel_t RGB = { 0 }, Lab = { 0 }, XYZ = { 0 };
3427
3428 const dt_aligned_pixel_t Lch_hue = { 67.f, 96.f, (stop * HUE_MAX) / 360.f};
3429
3430 dt_LCH_2_Lab(Lch_hue, Lab);
3433
3434 dt_bauhaus_slider_set_stop(g->hue_spot, stop, RGB[0], RGB[1], RGB[2]);
3435
3436 const dt_aligned_pixel_t Lch_lightness = { stop * LIGHTNESS_MAX, 0.f, 0. };
3437
3438 dt_LCH_2_Lab(Lch_lightness, Lab);
3441
3442 dt_bauhaus_slider_set_stop(g->lightness_spot, stop, RGB[0], RGB[1], RGB[2]);
3443
3444 const dt_aligned_pixel_t Lch_chroma = { 50., stop * CHROMA_MAX, hue / 360.f };
3445
3446 dt_LCH_2_Lab(Lch_chroma, Lab);
3449
3450 dt_bauhaus_slider_set_stop(g->chroma_spot, stop, RGB[0], RGB[1], RGB[2]);
3451 }
3452
3453 gtk_widget_queue_draw(g->hue_spot);
3454 gtk_widget_queue_draw(g->lightness_spot);
3455 gtk_widget_queue_draw(g->chroma_spot);
3456 gtk_widget_queue_draw(g->target_spot);
3457}
3458
3459
3468{
3471 GtkWidget *const widgets[9]
3472 = { g->primaries_achromatic_hue, g->primaries_achromatic_purity, g->primaries_red_hue,
3473 g->primaries_red_purity, g->primaries_green_hue, g->primaries_green_purity,
3474 g->primaries_blue_hue, g->primaries_blue_purity, g->primaries_gain };
3478 const float rows[3][3] = { { p->red[0], p->red[1], p->red[2] },
3479 { p->green[0], p->green[1], p->green[2] },
3480 { p->blue[0], p->blue[1], p->blue[2] } };
3481 const gboolean normalize[3] = { p->normalize_R, p->normalize_G, p->normalize_B };
3482 float M[3][3] = { { 0.f } };
3483 float roundtrip[3][3] = { { 0.f } };
3484
3486 if(!dt_iop_channelmixer_shared_primaries_from_matrix(basis, M, &primaries)) return FALSE;
3487 if(!dt_iop_channelmixer_shared_primaries_to_matrix(basis, &primaries, roundtrip)) return FALSE;
3488
3489 const float roundtrip_error = dt_iop_channelmixer_shared_roundtrip_error(M, roundtrip);
3490 if(!IS_NULL_PTR(error)) *error = roundtrip_error;
3491
3495 return isfinite(roundtrip_error) && roundtrip_error <= DT_CHANNELMIXERRGB_SIMPLE_EPS;
3496}
3497
3499{
3503 const dt_iop_order_iccprofile_info_t *const display_profile
3505 GtkWidget *const widgets[9]
3506 = { g->primaries_achromatic_hue, g->primaries_achromatic_purity, g->primaries_red_hue,
3507 g->primaries_red_purity, g->primaries_green_hue, g->primaries_green_purity,
3508 g->primaries_blue_hue, g->primaries_blue_purity, g->primaries_gain };
3512
3514 dt_iop_channelmixer_shared_paint_primaries_sliders(p->adaptation, work_profile, display_profile, basis, &primaries,
3515 widgets);
3516}
3517
3526 GtkWidget *widgets[6])
3527{
3528 widgets[0] = g->white_preserving_red_rotation;
3529 widgets[1] = g->white_preserving_red_saturation;
3530 widgets[2] = g->white_preserving_green_rotation;
3531 widgets[3] = g->white_preserving_green_saturation;
3532 widgets[4] = g->white_preserving_blue_rotation;
3533 widgets[5] = g->white_preserving_blue_saturation;
3534
3535 for(int widget = 0; widget < 6; widget++)
3536 if(IS_NULL_PTR(widgets[widget])) return FALSE;
3537
3538 return TRUE;
3539}
3540
3550{
3553 GtkWidget *widgets[6] = { NULL };
3557 const float rows[3][3] = { { p->red[0], p->red[1], p->red[2] },
3558 { p->green[0], p->green[1], p->green[2] },
3559 { p->blue[0], p->blue[1], p->blue[2] } };
3560 const gboolean normalize[3] = { p->normalize_R, p->normalize_G, p->normalize_B };
3561 float M[3][3] = { { 0.f } };
3562 float roundtrip[3][3] = { { 0.f } };
3563
3566 if(!dt_iop_channelmixer_shared_white_preserving_from_matrix(basis, M, &white_preserving)) return FALSE;
3567 if(!dt_iop_channelmixer_shared_white_preserving_to_matrix(basis, &white_preserving, roundtrip)) return FALSE;
3568
3569 const float roundtrip_error = dt_iop_channelmixer_shared_roundtrip_error_relative(M, roundtrip);
3570 if(!IS_NULL_PTR(error)) *error = roundtrip_error;
3571
3575 return isfinite(roundtrip_error) && roundtrip_error <= DT_CHANNELMIXERRGB_SIMPLE_EPS;
3576}
3577
3596
3598 const struct dt_iop_order_iccprofile_info_t *const work_profile,
3599 const struct dt_iop_order_iccprofile_info_t *const display_profile,
3601{
3602 dt_aligned_pixel_t work_rgb = { LMS[0], LMS[1], LMS[2], 0.f };
3603
3604 if(p->adaptation != DT_ADAPTATION_RGB)
3605 {
3606 convert_any_LMS_to_RGB(LMS, work_rgb, p->adaptation);
3607 }
3608 else
3609 {
3610 for(size_t c = 0; c < 3; c++) work_rgb[c] = LMS[c];
3611 }
3612
3613 dt_iop_channelmixer_shared_work_rgb_to_display(work_rgb, work_profile, display_profile, RGB);
3614}
3615
3617 const struct dt_iop_order_iccprofile_info_t *const work_profile,
3618 const struct dt_iop_order_iccprofile_info_t *const display_profile,
3619 GtkWidget *w, float stop, float c, float r, float g, float b)
3620{
3621 const dt_aligned_pixel_t LMS = { 0.5f * (c * r + 1 - r),
3622 0.5f * (c * g + 1 - g),
3623 0.5f * (c * b + 1 - b)};
3624 dt_aligned_pixel_t RGB_t = { 0.5f };
3625 _convert_GUI_colors(p, work_profile, display_profile, LMS, RGB_t);
3626 dt_bauhaus_slider_set_stop(w, stop, RGB_t[0], RGB_t[1], RGB_t[2]);
3627}
3628
3629static void _update_RGB_colors(dt_iop_module_t *self, float r, float g, float b, gboolean normalize, float *a,
3630 GtkWidget *w_r, GtkWidget *w_g, GtkWidget *w_b)
3631{
3632 // update the fill background color of x, y sliders
3634 const struct dt_iop_order_iccprofile_info_t *const work_profile = dt_ioppr_get_pipe_work_profile_info(self->dev->pipe);
3635 const struct dt_iop_order_iccprofile_info_t *const display_profile = dt_ioppr_get_pipe_output_profile_info(self->dev->pipe);
3636
3637 // scale params if needed
3638 dt_aligned_pixel_t RGB = { a[0], a[1], a[2] };
3639
3640 if(normalize)
3641 {
3642 const float sum = RGB[0] + RGB[1] + RGB[2];
3643 if(sum != 0.f) for(int c = 0; c < 3; c++) RGB[c] /= sum;
3644 }
3645
3646 for(int i = 0; i < DT_BAUHAUS_SLIDER_MAX_STOPS; i++)
3647 {
3648 const float stop = ((float)i / (float)(DT_BAUHAUS_SLIDER_MAX_STOPS - 1));
3649 // Use the hard bounds of the sliders; drawing will take into account the possible soft rescaling
3650 const float range_x = COLOR_MAX - COLOR_MIN;
3651
3652 const float x = COLOR_MIN + stop * range_x;
3653
3654 _update_RGB_slider_stop(p, work_profile, display_profile, w_r, stop, x + RGB[1] + RGB[2], r, g, b);
3655 _update_RGB_slider_stop(p, work_profile, display_profile, w_g, stop, RGB[0] + x + RGB[2], r, g, b);
3656 _update_RGB_slider_stop(p, work_profile, display_profile, w_b, stop, RGB[0] + RGB[1] + x , r, g, b);
3657 }
3658
3659 gtk_widget_queue_draw(w_r);
3660 gtk_widget_queue_draw(w_b);
3661 gtk_widget_queue_draw(w_g);
3662}
3663
3672{
3675 GtkWidget *const widgets[6]
3676 = { g->simple_theta, g->simple_psi, g->simple_stretch_1, g->simple_stretch_2, g->simple_coupling_1,
3677 g->simple_coupling_2 };
3678 const float rows[3][3] = { { p->red[0], p->red[1], p->red[2] },
3679 { p->green[0], p->green[1], p->green[2] },
3680 { p->blue[0], p->blue[1], p->blue[2] } };
3681 const gboolean normalize[3] = { p->normalize_R, p->normalize_G, p->normalize_B };
3682 float M[3][3] = { { 0.f } };
3683 float roundtrip[3][3] = { { 0.f } };
3685
3686 if(!IS_NULL_PTR(error)) *error = INFINITY;
3689
3692
3693 const float roundtrip_error = dt_iop_channelmixer_shared_roundtrip_error(M, roundtrip);
3694 if(!IS_NULL_PTR(error)) *error = roundtrip_error;
3695
3699
3700 return isfinite(roundtrip_error) && roundtrip_error <= DT_CHANNELMIXERRGB_SIMPLE_EPS;
3701}
3702
3714{
3718 const dt_iop_order_iccprofile_info_t *const display_profile
3720 GtkWidget *const widgets[6]
3721 = { g->simple_theta, g->simple_psi, g->simple_stretch_1, g->simple_stretch_2, g->simple_coupling_1,
3722 g->simple_coupling_2 };
3724
3726 dt_iop_channelmixer_shared_paint_simple_sliders(p->adaptation, work_profile, display_profile, &simple, widgets);
3727}
3728
3729
3731{
3733 if(IS_NULL_PTR(g)) return;
3734
3735 if(!IS_NULL_PTR(g->illum_color))
3736 gtk_widget_queue_draw(g->illum_color);
3737
3738 update_xy_color(self);
3739}
3740
3741static gboolean illuminant_color_draw(GtkWidget *widget, cairo_t *crf, gpointer user_data)
3742{
3743 dt_iop_module_t *self = (dt_iop_module_t *)user_data;
3745
3746 // Init
3747 GtkAllocation allocation;
3748 gtk_widget_get_allocation(widget, &allocation);
3749 int width = allocation.width, height = allocation.height;
3750 cairo_surface_t *cst = dt_cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height);
3751 cairo_t *cr = cairo_create(cst);
3752
3753 // Margins
3754 const float margin = 2. * DT_PIXEL_APPLY_DPI(1.5);
3756 height -= 2 * margin;
3757
3758 // Paint illuminant color - we need to recompute it in full in case camera RAW is chosen
3759 float x = p->x;
3760 float y = p->y;
3761 dt_aligned_pixel_t RGB = { 0 };
3762 dt_aligned_pixel_t custom_wb;
3763 get_white_balance_coeff(self, custom_wb);
3764 illuminant_to_xy(p->illuminant, &(self->dev->image_storage), custom_wb,
3765 &x, &y, p->temperature, p->illum_fluo, p->illum_led);
3767 cairo_set_source_rgb(cr, RGB[0], RGB[1], RGB[2]);
3768 cairo_rectangle(cr, INNER_PADDING, margin, width, height);
3769 cairo_fill(cr);
3770
3771 // Clean
3772 cairo_stroke(cr);
3773 cairo_destroy(cr);
3774 cairo_set_source_surface(crf, cst, 0, 0);
3775 cairo_paint(crf);
3776 cairo_surface_destroy(cst);
3777 return TRUE;
3778}
3779
3780static gboolean target_color_draw(GtkWidget *widget, cairo_t *crf, gpointer user_data)
3781{
3782 dt_iop_module_t *self = (dt_iop_module_t *)user_data;
3784
3785 // Init
3786 GtkAllocation allocation;
3787 gtk_widget_get_allocation(widget, &allocation);
3788 int width = allocation.width, height = allocation.height;
3789 cairo_surface_t *cst = dt_cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height);
3790 cairo_t *cr = cairo_create(cst);
3791
3792 // Margins
3793 const float margin = 2. * DT_PIXEL_APPLY_DPI(1.5);
3795 height -= 2 * margin;
3796
3797 // Paint target color
3798 dt_aligned_pixel_t RGB = { 0 };
3799 dt_aligned_pixel_t Lch = { 0 };
3800 dt_aligned_pixel_t Lab = { 0 };
3801 dt_aligned_pixel_t XYZ = { 0 };
3802 Lch[0] = dt_bauhaus_slider_get(g->lightness_spot);
3803 Lch[1] = dt_bauhaus_slider_get(g->chroma_spot);
3804 Lch[2] = dt_bauhaus_slider_get(g->hue_spot) / 360.f;
3805 dt_LCH_2_Lab(Lch, Lab);
3808
3809 cairo_set_source_rgb(cr, RGB[0], RGB[1], RGB[2]);
3810 cairo_rectangle(cr, INNER_PADDING, margin, width, height);
3811 cairo_fill(cr);
3812
3813 // Clean
3814 cairo_stroke(cr);
3815 cairo_destroy(cr);
3816 cairo_set_source_surface(crf, cst, 0, 0);
3817 cairo_paint(crf);
3818 cairo_surface_destroy(cst);
3819 return TRUE;
3820}
3821
3822static gboolean origin_color_draw(GtkWidget *widget, cairo_t *crf, gpointer user_data)
3823{
3824 dt_iop_module_t *self = (dt_iop_module_t *)user_data;
3826
3827 // Init
3828 GtkAllocation allocation;
3829 gtk_widget_get_allocation(widget, &allocation);
3830 int width = allocation.width, height = allocation.height;
3831 cairo_surface_t *cst = dt_cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height);
3832 cairo_t *cr = cairo_create(cst);
3833
3834 // Margins
3835 const float margin = 2. * DT_PIXEL_APPLY_DPI(1.5);
3837 height -= 2 * margin;
3838
3839 cairo_set_source_rgb(cr, g->spot_RGB[0], g->spot_RGB[1], g->spot_RGB[2]);
3840 cairo_rectangle(cr, INNER_PADDING, margin, width, height);
3841 cairo_fill(cr);
3842
3843 // Clean
3844 cairo_stroke(cr);
3845 cairo_destroy(cr);
3846 cairo_set_source_surface(crf, cst, 0, 0);
3847 cairo_paint(crf);
3848 cairo_surface_destroy(cst);
3849 return TRUE;
3850}
3851
3853{
3856
3857 float x = p->x;
3858 float y = p->y;
3859 dt_aligned_pixel_t custom_wb;
3860 get_white_balance_coeff(self, custom_wb);
3861 illuminant_to_xy(p->illuminant, &(self->dev->image_storage), custom_wb, &x, &y, p->temperature, p->illum_fluo, p->illum_led);
3862
3863 dt_illuminant_t test_illuminant;
3864 float t = 5000.f;
3865 check_if_close_to_daylight(x, y, &t, &test_illuminant, NULL);
3866
3867 gchar *str;
3868 if(t > 1667.f && t < 25000.f)
3869 {
3870 if(test_illuminant == DT_ILLUMINANT_D)
3871 {
3872 str = g_strdup_printf(_("CCT: %.0f K (daylight)"), t);
3873 gtk_widget_set_tooltip_text(GTK_WIDGET(g->approx_cct),
3874 _("approximated correlated color temperature.\n"
3875 "this illuminant can be accurately modeled by a daylight spectrum,\n"
3876 "so its temperature is relevant and meaningful with a D illuminant."));
3877 }
3878 else if(test_illuminant == DT_ILLUMINANT_BB)
3879 {
3880 str = g_strdup_printf(_("CCT: %.0f K (black body)"), t);
3881 gtk_widget_set_tooltip_text(GTK_WIDGET(g->approx_cct),
3882 _("approximated correlated color temperature.\n"
3883 "this illuminant can be accurately modeled by a black body spectrum,\n"
3884 "so its temperature is relevant and meaningful with a Planckian illuminant."));
3885 }
3886 else
3887 {
3888 str = g_strdup_printf(_("CCT: %.0f K (invalid)"), t);
3889 gtk_widget_set_tooltip_text(GTK_WIDGET(g->approx_cct),
3890 _("approximated correlated color temperature.\n"
3891 "this illuminant cannot be accurately modeled by a daylight or black body spectrum,\n"
3892 "so its temperature is not relevant and meaningful and you need to use a custom illuminant."));
3893 }
3894 }
3895 else
3896 {
3897 str = g_strdup_printf(_("CCT: undefined"));
3898 gtk_widget_set_tooltip_text(GTK_WIDGET(g->approx_cct),
3899 _("the approximated correlated color temperature\n"
3900 "cannot be computed at all so you need to use a custom illuminant."));
3901 }
3902 gtk_label_set_text(GTK_LABEL(g->approx_cct), str);
3903 dt_free(str);
3904}
3905
3906
3907static void illum_xy_callback(GtkWidget *slider, gpointer user_data)
3908{
3909 dt_iop_module_t *self = (dt_iop_module_t *)user_data;
3910 if(dt_gui_widgets_suppressed()) return;
3913
3914 dt_aligned_pixel_t Lch = { 0 };
3915 Lch[0] = 100.f;
3916 Lch[2] = dt_bauhaus_slider_get(g->illum_x) / 180. * M_PI;
3917 Lch[1] = dt_bauhaus_slider_get(g->illum_y);
3918
3919 dt_aligned_pixel_t xyY = { 0 };
3920 dt_Lch_to_xyY(Lch, xyY);
3921 p->x = xyY[0];
3922 p->y = xyY[1];
3923
3924 float t = xy_to_CCT(p->x, p->y);
3925 // xy_to_CCT is valid only above 3000 K
3926 if(t < 3000.f) t = CCT_reverse_lookup(p->x, p->y);
3927 p->temperature = t;
3928
3930 dt_bauhaus_slider_set(g->temperature, p->temperature);
3931 update_approx_cct(self);
3935
3936 dt_print(DT_DEBUG_DEV, "[picker/channelmixerrgb] history commit source=illum_xy_callback slider=%p\n",
3937 (void *)slider);
3938 dt_dev_add_history_item(self->dev, self, TRUE, TRUE);
3939}
3940
3946
3948{
3949 self->dev->proxy.chroma_adaptation = NULL;
3950 dt_free_align(piece->data);
3951 piece->data = NULL;
3952}
3953
3955{
3957 g->is_profiling_started = FALSE;
3959 gui_changed(self, NULL, NULL);
3960}
3961
3962void gui_update(struct dt_iop_module_t *self)
3963{
3964 dt_iop_module_t *module = (dt_iop_module_t *)self;
3967 float simple_error = INFINITY;
3968 float primaries_error = INFINITY;
3969 float white_preserving_error = INFINITY;
3970
3972
3973 /* Restoring GUI state from params must not re-enter widget callbacks.
3974 Channelmixer's spot/color widgets can otherwise rerun auto-picking while
3975 a history refresh is already in flight, which feeds TOP_CHANGED/full-pipe
3976 loops when the picker is active. */
3978
3979 // always reset the mode the correct
3981
3982 // get the saved params
3984
3985 gboolean use_mixing = TRUE;
3986 if(dt_conf_key_exists("darkroom/modules/channelmixerrgb/use_mixing"))
3987 use_mixing = dt_conf_get_bool("darkroom/modules/channelmixerrgb/use_mixing");
3988 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->use_mixing), use_mixing);
3989
3990 float lightness = 50.f;
3991 if(dt_conf_key_exists("darkroom/modules/channelmixerrgb/lightness"))
3992 lightness = dt_conf_get_float("darkroom/modules/channelmixerrgb/lightness");
3993 dt_bauhaus_slider_set(g->lightness_spot, lightness);
3994
3995 float hue = 0.f;
3996 if(dt_conf_key_exists("darkroom/modules/channelmixerrgb/hue"))
3997 hue = dt_conf_get_float("darkroom/modules/channelmixerrgb/hue");
3998 dt_bauhaus_slider_set(g->hue_spot, hue);
3999
4000 float chroma = 0.f;
4001 if(dt_conf_key_exists("darkroom/modules/channelmixerrgb/chroma"))
4002 chroma = dt_conf_get_float("darkroom/modules/channelmixerrgb/chroma");
4003 dt_bauhaus_slider_set(g->chroma_spot, chroma);
4004
4006
4007 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->clip), p->clip);
4008 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->normalize_R), p->normalize_R);
4009 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->normalize_G), p->normalize_G);
4010 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->normalize_B), p->normalize_B);
4011
4012 const gboolean simple_ok = _channelmixerrgb_sync_simple_from_params(self, &simple_error);
4013 const gboolean primaries_ok = _channelmixerrgb_sync_primaries_from_params(self, &primaries_error);
4014 const gboolean white_preserving_ok
4015 = _channelmixerrgb_sync_white_preserving_from_params(self, &white_preserving_error);
4016 const dt_iop_channelmixer_rgb_mixer_mode_t requested_mode
4021 = requested_mode == DT_CHANNELMIXERRGB_MIXER_SIMPLE && simple_ok
4023 : requested_mode == DT_CHANNELMIXERRGB_MIXER_PRIMARIES && primaries_ok
4025 : requested_mode == DT_CHANNELMIXERRGB_MIXER_WHITE_PRESERVING && white_preserving_ok
4028 dt_bauhaus_combobox_set(g->mixer_mode, mixer_mode);
4030
4031 if(p->version != CHANNELMIXERRGB_V_3)
4032 dt_bauhaus_combobox_set(g->saturation_version, p->version);
4033 else
4034 gtk_widget_hide(GTK_WIDGET(g->saturation_version));
4035
4036 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->normalize_sat), p->normalize_sat);
4037 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->normalize_light), p->normalize_light);
4038 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->normalize_grey), p->normalize_grey);
4039
4041
4042 // Restore both selections by name: the chart list is rebuilt from scratch here, and the
4043 // stored name has to survive a built-in chart being added or a .cht file appearing.
4044 // The color list is filtered by the selected chart, so the chart combo box comes first.
4046
4047 gchar *selected_checker = dt_conf_get_string("darkroom/modules/channelmixerrgb/colorchecker");
4048 if(!dt_bauhaus_combobox_set_from_text(g->checkers_list, selected_checker))
4049 dt_bauhaus_combobox_set(g->checkers_list, 0);
4050 dt_free(selected_checker);
4051
4052 const dt_colorchecker_label_t *checker_label = _checker_label_from_combobox(g->checkers_list, g->colorcheckers);
4053
4055
4056 gchar *selected_color = dt_conf_get_string("darkroom/modules/channelmixerrgb/colorchecker_color");
4057 if(!dt_bauhaus_combobox_set_from_text(g->checkers_color_list, selected_color))
4058 dt_bauhaus_combobox_set(g->checkers_color_list, 0);
4059 dt_free(selected_color);
4060
4061 _set_checker_from_labels(g, checker_label,
4062 _checker_label_from_combobox(g->checkers_color_list, g->colorcheckers_color));
4063
4064 color_list_visibility(self, checker_label);
4065
4066 const int j = dt_conf_get_int("darkroom/modules/channelmixerrgb/optimization");
4067 dt_bauhaus_combobox_set(g->optimize, j);
4068 g->optimization = j;
4069
4070 g->safety_margin = dt_conf_get_float("darkroom/modules/channelmixerrgb/safety");
4071 dt_bauhaus_slider_set(g->safety, g->safety_margin);
4072
4074
4075 // always disable profiling mode by default
4076 g->is_profiling_started = FALSE;
4077
4080
4081 g->spot_RGB[0] = 0.f;
4082 g->spot_RGB[1] = 0.f;
4083 g->spot_RGB[2] = 0.f;
4084 g->spot_RGB[3] = 0.f;
4085
4086 // Widget defaults (reset values) and the image-dependent "as shot in camera" illuminant entry,
4087 // derived from the per-image defaults that reload_defaults() already computed into default_params.
4088 // This lives here, not in reload_defaults(), so it only touches widgets when they exist and on the
4089 // GUI thread (reload_defaults also runs on export/thumbnail devs that have no widgets).
4090 {
4092 const dt_image_t *img = &self->dev->image_storage;
4093 const dt_aligned_pixel_t xyY = { d->x, d->y, 1.f };
4094 dt_aligned_pixel_t Lch = { 0 };
4095 dt_xyY_to_Lch(xyY, Lch);
4096
4097 dt_bauhaus_slider_set_default(g->illum_x, Lch[2] / M_PI * 180.f);
4098 dt_bauhaus_slider_set_default(g->illum_y, Lch[1]);
4099 dt_bauhaus_slider_set_default(g->temperature, d->temperature);
4100 dt_bauhaus_combobox_set_default(g->illuminant, d->illuminant);
4101 dt_bauhaus_combobox_set_default(g->adaptation, d->adaptation);
4102 if(g->delta_E_label_text)
4103 {
4104 dt_free(g->delta_E_label_text);
4105 g->delta_E_label_text = NULL;
4106 }
4107
4109 {
4111 dt_bauhaus_combobox_add_full(g->illuminant, _("as shot in camera"), DT_BAUHAUS_COMBOBOX_ALIGN_RIGHT,
4112 GINT_TO_POINTER(DT_ILLUMINANT_CAMERA), NULL, TRUE);
4113 }
4114 else
4116 }
4117
4119
4120 gui_changed(self, NULL, NULL);
4121}
4122
4124{
4125 dt_iop_default_init(module);
4126
4128 d->red[0] = d->green[1] = d->blue[2] = 1.0;
4129 d->normalize_R = TRUE;
4130 d->normalize_G = TRUE;
4131 d->normalize_B = TRUE;
4132}
4133
4135{
4137
4138 // Our per-image defaults (illuminant chromaticity x/y, temperature) are derived from the white
4139 // balance temperature publishes into dev->proxy.wb_coeffs (see get_white_balance_coeff()).
4140 // temperature publishes the per-image DEFAULT WB from its own reload_defaults() (main thread, no
4141 // pipeline involved). For that seed to be visible here, temperature must run before us:
4142 // dt_dev_init_default_history() iterates modules in iop order, and temperature's iop_order is far
4143 // below ours, so this holds -- but it is a silent data dependency, so assert it. A future
4144 // iop-order change that put us first would otherwise regress our defaults on a brand-new history.
4145 // (The EFFECTIVE/history WB is published later via temperature's commit_proxy(); our defaults are
4146 // intentionally keyed on the metadata-default WB so a fresh init always matches a manual reset.)
4147 const dt_iop_module_t *const temp = dt_iop_get_module_from_list(module->dev->iop, "temperature");
4148 if(temp && temp->iop_order >= module->iop_order)
4149 fprintf(stderr, "[channelmixerrgb] BUG: temperature (iop_order %d) must run before channelmixerrgb "
4150 "(iop_order %d); white-balance-derived defaults will be wrong on a fresh history\n",
4151 temp->iop_order, module->iop_order);
4152 assert(!temp || temp->iop_order < module->iop_order);
4153
4154 d->normalize_R = TRUE;
4155 d->normalize_G = TRUE;
4156 d->normalize_B = TRUE;
4157
4158 d->x = module->get_f("x")->Float.Default;
4159 d->y = module->get_f("y")->Float.Default;
4160 d->temperature = module->get_f("temperature")->Float.Default;
4161 d->illuminant = module->get_f("illuminant")->Enum.Default;
4162 d->adaptation = module->get_f("adaptation")->Enum.Default;
4163
4164 // Note : this is not an user param anymore, and is set to modern by default
4165 // except for old histories using temperature without having an history entry for it.
4166 // see develop/develop.c/_dev_auto_apply_presets()
4167 const gboolean is_modern =
4168 dt_conf_is_equal("plugins/darkroom/chromatic-adaptation", "modern");
4169
4170 // note that if there is already an instance of this module with an
4171 // adaptation set we default to RGB (none) in this instance.
4172 // try to register the CAT here
4173 declare_cat_on_pipe(module, is_modern);
4174 const dt_image_t *img = &module->dev->image_storage;
4175
4176 // check if we could register
4177 gboolean CAT_already_applied =
4178 (!IS_NULL_PTR(module->dev->proxy.chroma_adaptation)) // CAT exists
4179 && (module->dev->proxy.chroma_adaptation != module) // and it is not us
4180 && (!dt_image_is_monochrome(img));
4181
4182 module->default_enabled = FALSE;
4183
4184 dt_aligned_pixel_t custom_wb = { 1.f, 1.f, 1.f, 1.f };
4185 if(!CAT_already_applied
4186 && is_modern
4187 && !get_white_balance_coeff(module, custom_wb)
4188 && !dt_image_is_monochrome(img))
4189 {
4190 // if workflow = modern and we find WB coeffs, take care of white balance here
4191 if(find_temperature_from_raw_coeffs(img, custom_wb, &(d->x), &(d->y)))
4192 d->illuminant = DT_ILLUMINANT_CAMERA;
4193
4194 check_if_close_to_daylight(d->x, d->y, &(d->temperature), &(d->illuminant), &(d->adaptation));
4195 module->workflow_enabled = dt_image_needs_rawprepare(img);
4196 }
4197 else
4198 {
4199 // otherwise, simple channel mixer
4200 d->illuminant = DT_ILLUMINANT_PIPE;
4201 d->adaptation = DT_ADAPTATION_RGB;
4202 }
4203
4204 // NOTE: widget state (slider/combo defaults, the image-dependent "as shot in camera" illuminant
4205 // entry) is intentionally NOT set here. reload_defaults() runs on export/thumbnail devs with no
4206 // widgets and off the GUI thread; touching bauhaus here re-entered callbacks against half-built
4207 // widgets and crashed. That work now lives in gui_update(), reading these same default_params.
4208}
4209
4210
4212{
4213 if(dt_gui_widgets_suppressed()) return;
4214
4216
4217 dt_aligned_pixel_t Lch_target = { 0.f };
4218
4220 Lch_target[0] = dt_bauhaus_slider_get(g->lightness_spot);
4221 Lch_target[1] = dt_bauhaus_slider_get(g->chroma_spot);
4222 Lch_target[2] = dt_bauhaus_slider_get(g->hue_spot) / 360.f;
4223 const gboolean use_mixing = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(g->use_mixing));
4225
4226 // Save the color on change
4227 dt_conf_set_float("darkroom/modules/channelmixerrgb/lightness", Lch_target[0]);
4228 dt_conf_set_float("darkroom/modules/channelmixerrgb/chroma", Lch_target[1]);
4229 dt_conf_set_float("darkroom/modules/channelmixerrgb/hue", Lch_target[2] * 360.f);
4230 dt_conf_set_bool("darkroom/modules/channelmixerrgb/use_mixing", use_mixing);
4231
4233 paint_hue(self);
4235
4236 // Re-run auto illuminant only for the module that currently owns the active picker.
4237 const dt_spot_mode_t mode = dt_bauhaus_combobox_get(g->spot_mode);
4238 const gboolean picker_active = dt_iop_color_picker_is_active_module(self);
4239 if(mode == DT_SPOT_MODE_CORRECT && picker_active)
4240 _auto_set_illuminant(self, self->dev->pipe);
4241 // else : just record new values and do nothing
4242}
4243
4246{
4247 gtk_stack_set_visible_child_name(GTK_STACK(g->mixer_stack),
4248 mode == DT_CHANNELMIXERRGB_MIXER_SIMPLE ? "simple"
4249 : mode == DT_CHANNELMIXERRGB_MIXER_PRIMARIES ? "primaries"
4250 : mode == DT_CHANNELMIXERRGB_MIXER_WHITE_PRESERVING ? "white-preserving"
4251 : "complete");
4252}
4253
4264static gboolean _channelmixerrgb_push_matrix_to_params(dt_iop_module_t *self, const float M[3][3])
4265{
4268 gboolean changed = p->normalize_R || p->normalize_G || p->normalize_B;
4269
4270 for(int col = 0; col < 3; col++)
4271 {
4272 changed = changed || p->red[col] != M[0][col] || p->green[col] != M[1][col] || p->blue[col] != M[2][col];
4273 p->red[col] = M[0][col];
4274 p->green[col] = M[1][col];
4275 p->blue[col] = M[2][col];
4276 }
4277
4278 p->normalize_R = FALSE;
4279 p->normalize_G = FALSE;
4280 p->normalize_B = FALSE;
4281
4283 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->normalize_R), FALSE);
4284 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->normalize_G), FALSE);
4285 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->normalize_B), FALSE);
4286 dt_bauhaus_slider_set(g->scale_red_R, p->red[0]);
4287 dt_bauhaus_slider_set(g->scale_red_G, p->red[1]);
4288 dt_bauhaus_slider_set(g->scale_red_B, p->red[2]);
4289 dt_bauhaus_slider_set(g->scale_green_R, p->green[0]);
4290 dt_bauhaus_slider_set(g->scale_green_G, p->green[1]);
4291 dt_bauhaus_slider_set(g->scale_green_B, p->green[2]);
4292 dt_bauhaus_slider_set(g->scale_blue_R, p->blue[0]);
4293 dt_bauhaus_slider_set(g->scale_blue_G, p->blue[1]);
4294 dt_bauhaus_slider_set(g->scale_blue_B, p->blue[2]);
4296
4297 return changed;
4298}
4299
4317
4318static void _channelmixerrgb_mixer_mode_callback(GtkWidget *combo, gpointer user_data)
4319{
4320 if(dt_gui_widgets_suppressed()) return;
4321
4322 dt_iop_module_t *self = (dt_iop_module_t *)user_data;
4326
4328
4330 {
4332 return;
4333 }
4334
4336 {
4337 float error = INFINITY;
4339 {
4340 dt_control_log(_("simple mixer mode requires all three output rows to be normalized with non-zero sums."));
4346 return;
4347 }
4348
4350 gui_changed(self, NULL, NULL);
4351 return;
4352 }
4353
4355 {
4356 const float rows[3][3] = { { p->red[0], p->red[1], p->red[2] },
4357 { p->green[0], p->green[1], p->green[2] },
4358 { p->blue[0], p->blue[1], p->blue[2] } };
4359 const gboolean normalize[3] = { p->normalize_R, p->normalize_G, p->normalize_B };
4360 float M[3][3] = { { 0.f } };
4361 float error = INFINITY;
4364 {
4365 dt_control_log(_("primaries mixer mode requires a non-singular 3x3 matrix with non-zero affine sums."));
4371 return;
4372 }
4373
4374 gboolean changed = p->normalize_R || p->normalize_G || p->normalize_B;
4375 for(int col = 0; col < 3; col++)
4376 {
4377 changed = changed || p->red[col] != M[0][col] || p->green[col] != M[1][col] || p->blue[col] != M[2][col];
4378 p->red[col] = M[0][col];
4379 p->green[col] = M[1][col];
4380 p->blue[col] = M[2][col];
4381 }
4382
4383 p->normalize_R = FALSE;
4384 p->normalize_G = FALSE;
4385 p->normalize_B = FALSE;
4386
4388 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->normalize_R), FALSE);
4389 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->normalize_G), FALSE);
4390 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->normalize_B), FALSE);
4391 dt_bauhaus_slider_set(g->scale_red_R, p->red[0]);
4392 dt_bauhaus_slider_set(g->scale_red_G, p->red[1]);
4393 dt_bauhaus_slider_set(g->scale_red_B, p->red[2]);
4394 dt_bauhaus_slider_set(g->scale_green_R, p->green[0]);
4395 dt_bauhaus_slider_set(g->scale_green_G, p->green[1]);
4396 dt_bauhaus_slider_set(g->scale_green_B, p->green[2]);
4397 dt_bauhaus_slider_set(g->scale_blue_R, p->blue[0]);
4398 dt_bauhaus_slider_set(g->scale_blue_G, p->blue[1]);
4399 dt_bauhaus_slider_set(g->scale_blue_B, p->blue[2]);
4401
4403 gui_changed(self, NULL, NULL);
4404
4405 if(changed)
4406 {
4407 dt_print(DT_DEBUG_DEV, "[channelmixerrgb] history commit source=mixer_mode_primaries\n");
4408 dt_dev_add_history_item(self->dev, self, TRUE, TRUE);
4409 }
4410 }
4411
4413 {
4414 const float rows[3][3] = { { p->red[0], p->red[1], p->red[2] },
4415 { p->green[0], p->green[1], p->green[2] },
4416 { p->blue[0], p->blue[1], p->blue[2] } };
4417 const gboolean normalize[3] = { p->normalize_R, p->normalize_G, p->normalize_B };
4418 float M[3][3] = { { 0.f } };
4419 float error = INFINITY;
4422 {
4424 self, _("white-preserving mixer mode requires a matrix that leaves the basis white unchanged."));
4425 return;
4426 }
4427
4428 const gboolean changed = _channelmixerrgb_push_matrix_to_params(self, M);
4429
4431 gui_changed(self, NULL, NULL);
4432
4433 if(changed)
4434 {
4435 dt_print(DT_DEBUG_DEV, "[channelmixerrgb] history commit source=mixer_mode_white_preserving\n");
4436 dt_dev_add_history_item(self->dev, self, TRUE, TRUE);
4437 }
4438 }
4439}
4440
4441static void _channelmixerrgb_simple_slider_callback(GtkWidget *slider, gpointer user_data)
4442{
4443 if(dt_gui_widgets_suppressed()) return;
4444
4445 dt_iop_module_t *self = (dt_iop_module_t *)user_data;
4448 GtkWidget *const widgets[6]
4449 = { g->simple_theta, g->simple_psi, g->simple_stretch_1, g->simple_stretch_2, g->simple_coupling_1,
4450 g->simple_coupling_2 };
4452 float M[3][3] = { { 0.f } };
4453
4456
4457 for(int col = 0; col < 3; col++)
4458 {
4459 p->red[col] = M[0][col];
4460 p->green[col] = M[1][col];
4461 p->blue[col] = M[2][col];
4462 }
4463
4465 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->normalize_R), p->normalize_R);
4466 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->normalize_G), p->normalize_G);
4467 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->normalize_B), p->normalize_B);
4468
4469 dt_bauhaus_slider_set(g->scale_red_R, p->red[0]);
4470 dt_bauhaus_slider_set(g->scale_red_G, p->red[1]);
4471 dt_bauhaus_slider_set(g->scale_red_B, p->red[2]);
4472 dt_bauhaus_slider_set(g->scale_green_R, p->green[0]);
4473 dt_bauhaus_slider_set(g->scale_green_G, p->green[1]);
4474 dt_bauhaus_slider_set(g->scale_green_B, p->green[2]);
4475 dt_bauhaus_slider_set(g->scale_blue_R, p->blue[0]);
4476 dt_bauhaus_slider_set(g->scale_blue_G, p->blue[1]);
4477 dt_bauhaus_slider_set(g->scale_blue_B, p->blue[2]);
4479
4480 gui_changed(self, slider, NULL);
4481
4482 dt_print(DT_DEBUG_DEV, "[channelmixerrgb] history commit source=simple_slider slider=%p\n", (void *)slider);
4483 dt_dev_add_history_item(self->dev, self, TRUE, TRUE);
4484}
4485
4486static void _channelmixerrgb_primaries_slider_callback(GtkWidget *slider, gpointer user_data)
4487{
4488 if(dt_gui_widgets_suppressed()) return;
4489
4490 dt_iop_module_t *self = (dt_iop_module_t *)user_data;
4493 GtkWidget *const widgets[9]
4494 = { g->primaries_achromatic_hue, g->primaries_achromatic_purity, g->primaries_red_hue,
4495 g->primaries_red_purity, g->primaries_green_hue, g->primaries_green_purity,
4496 g->primaries_blue_hue, g->primaries_blue_purity, g->primaries_gain };
4500 float M[3][3] = { { 0.f } };
4501
4503 if(!dt_iop_channelmixer_shared_primaries_to_matrix(basis, &primaries, M))
4504 {
4505 dt_control_log(_("primaries mixer mode requires a non-singular 3x3 matrix with non-zero affine sums."));
4506 return;
4507 }
4508
4509 for(int col = 0; col < 3; col++)
4510 {
4511 p->red[col] = M[0][col];
4512 p->green[col] = M[1][col];
4513 p->blue[col] = M[2][col];
4514 }
4515 p->normalize_R = FALSE;
4516 p->normalize_G = FALSE;
4517 p->normalize_B = FALSE;
4518
4520 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->normalize_R), FALSE);
4521 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->normalize_G), FALSE);
4522 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->normalize_B), FALSE);
4523
4524 dt_bauhaus_slider_set(g->scale_red_R, p->red[0]);
4525 dt_bauhaus_slider_set(g->scale_red_G, p->red[1]);
4526 dt_bauhaus_slider_set(g->scale_red_B, p->red[2]);
4527 dt_bauhaus_slider_set(g->scale_green_R, p->green[0]);
4528 dt_bauhaus_slider_set(g->scale_green_G, p->green[1]);
4529 dt_bauhaus_slider_set(g->scale_green_B, p->green[2]);
4530 dt_bauhaus_slider_set(g->scale_blue_R, p->blue[0]);
4531 dt_bauhaus_slider_set(g->scale_blue_G, p->blue[1]);
4532 dt_bauhaus_slider_set(g->scale_blue_B, p->blue[2]);
4534
4535 gui_changed(self, slider, NULL);
4536
4537 dt_print(DT_DEBUG_DEV, "[channelmixerrgb] history commit source=primaries_slider slider=%p\n", (void *)slider);
4538 dt_dev_add_history_item(self->dev, self, TRUE, TRUE);
4539}
4540
4541
4542static void _channelmixerrgb_white_preserving_slider_callback(GtkWidget *slider, gpointer user_data)
4543{
4544 if(dt_gui_widgets_suppressed()) return;
4545
4546 dt_iop_module_t *self = (dt_iop_module_t *)user_data;
4549 GtkWidget *widgets[6] = { NULL };
4553 float M[3][3] = { { 0.f } };
4554
4555 if(!_channelmixerrgb_white_preserving_widgets(g, widgets)) return;
4556
4558 if(!dt_iop_channelmixer_shared_white_preserving_to_matrix(basis, &white_preserving, M))
4559 {
4560 // Three primaries collapsed onto the white have no transform to describe. Snap the sliders
4561 // back to the last representable state rather than leaving the page showing a setting the
4562 // params do not hold -- the render would otherwise stop following the controls.
4563 dt_control_log(_("white-preserving mixer mode requires non-degenerate primaries."));
4565 return;
4566 }
4567
4569
4570 gui_changed(self, slider, NULL);
4571
4572 dt_print(DT_DEBUG_DEV, "[channelmixerrgb] history commit source=white_preserving_slider slider=%p\n",
4573 (void *)slider);
4574 dt_dev_add_history_item(self->dev, self, TRUE, TRUE);
4575}
4576
4577
4578void gui_changed(dt_iop_module_t *self, GtkWidget *w, void *previous)
4579{
4582 const gboolean simple_widget
4583 = w == g->simple_theta || w == g->simple_psi || w == g->simple_stretch_1 || w == g->simple_stretch_2
4584 || w == g->simple_coupling_1 || w == g->simple_coupling_2;
4585 const gboolean primaries_widget
4586 = w == g->primaries_achromatic_hue || w == g->primaries_achromatic_purity || w == g->primaries_red_hue
4587 || w == g->primaries_red_purity || w == g->primaries_green_hue || w == g->primaries_green_purity
4588 || w == g->primaries_blue_hue || w == g->primaries_blue_purity || w == g->primaries_gain;
4589 const gboolean white_preserving_widget
4590 = w == g->white_preserving_red_rotation || w == g->white_preserving_red_saturation
4591 || w == g->white_preserving_green_rotation || w == g->white_preserving_green_saturation
4592 || w == g->white_preserving_blue_rotation || w == g->white_preserving_blue_saturation;
4593 GtkWidget *white_preserving_widgets[6] = { NULL };
4594 // Bauhaus setters emit gui_changed() from gui_init(), before this page has been built.
4595 const gboolean white_preserving_ready = _channelmixerrgb_white_preserving_widgets(g, white_preserving_widgets);
4596 const gboolean normalize[3] = { p->normalize_R, p->normalize_G, p->normalize_B };
4597 const gboolean rows_are_normalized = dt_iop_channelmixer_shared_rows_are_normalized(normalize);
4598 const gboolean complete_widget
4599 = w == g->scale_red_R || w == g->scale_red_G || w == g->scale_red_B || w == g->scale_green_R
4600 || w == g->scale_green_G || w == g->scale_green_B || w == g->scale_blue_R || w == g->scale_blue_G
4601 || w == g->scale_blue_B || w == g->normalize_R || w == g->normalize_G || w == g->normalize_B;
4602 // Some Bauhaus setters emit during gui_init(), so synchronize the illuminant group only after
4603 // every widget sharing the same xyY/Lch state has been created.
4604 const gboolean illuminant_widgets_ready
4605 = !IS_NULL_PTR(g->illuminant) && !IS_NULL_PTR(g->illum_fluo) && !IS_NULL_PTR(g->illum_led)
4606 && !IS_NULL_PTR(g->temperature) && !IS_NULL_PTR(g->illum_color) && !IS_NULL_PTR(g->illum_x)
4607 && !IS_NULL_PTR(g->illum_y);
4608
4609 if(!IS_NULL_PTR(w) && w == g->illuminant)
4610 {
4611 if(!IS_NULL_PTR(previous))
4612 {
4613 dt_illuminant_t *prev_illuminant = (dt_illuminant_t *)previous;
4614 if(*prev_illuminant == DT_ILLUMINANT_CAMERA)
4615 {
4616 // If illuminant was previously set with "as set in camera",
4617 // when changing it, we need to ensure the temperature and chromaticity
4618 // are inited with the correct values taken from camera EXIF.
4619 // Otherwise, if using a preset defining illuminant = "as set in camera",
4620 // temperature and chromaticity are inited with the preset content when illuminant is changed.
4621 dt_aligned_pixel_t custom_wb;
4622 get_white_balance_coeff(self, custom_wb);
4623 find_temperature_from_raw_coeffs(&(self->dev->image_storage), custom_wb, &(p->x), &(p->y));
4624 check_if_close_to_daylight(p->x, p->y, &(p->temperature), NULL, &(p->adaptation));
4625 }
4626 }
4627 if(p->illuminant == DT_ILLUMINANT_CAMERA)
4628 {
4629 // Get camera WB and update illuminant
4630 dt_aligned_pixel_t custom_wb;
4631 get_white_balance_coeff(self, custom_wb);
4632 const int found = find_temperature_from_raw_coeffs(&(self->dev->image_storage), custom_wb, &(p->x), &(p->y));
4633 check_if_close_to_daylight(p->x, p->y, &(p->temperature), NULL, &(p->adaptation));
4634
4635 if(found)
4636 dt_control_log(_("white balance successfully extracted from raw image"));
4637 }
4638 else if(p->illuminant == DT_ILLUMINANT_DETECT_EDGES
4639 || p->illuminant == DT_ILLUMINANT_DETECT_SURFACES)
4640 {
4641 // We need to recompute only the full preview
4642 dt_control_log(_("auto-detection of white balance started..."));
4643 }
4644 }
4645
4646 if(!IS_NULL_PTR(w) && (w == g->illuminant || w == g->illum_fluo || w == g->illum_led || w == g->temperature))
4647 {
4648 // Convert and synchronize all the possible ways to define an illuminant to allow swapping modes
4649
4650 if(p->illuminant != DT_ILLUMINANT_CUSTOM && p->illuminant != DT_ILLUMINANT_CAMERA)
4651 {
4652 // We are in any mode defining (x, y) indirectly from an interface, so commit (x, y) explicitly
4653 illuminant_to_xy(p->illuminant, NULL, NULL, &(p->x), &(p->y), p->temperature, p->illum_fluo, p->illum_led);
4654 }
4655
4656 if(p->illuminant != DT_ILLUMINANT_D && p->illuminant != DT_ILLUMINANT_BB && p->illuminant != DT_ILLUMINANT_CAMERA)
4657 {
4658 // We are in any mode not defining explicitly a temperature, so find the the closest CCT and commit it
4659 check_if_close_to_daylight(p->x, p->y, &(p->temperature), NULL, NULL);
4660 }
4661 }
4662
4664
4665 if(IS_NULL_PTR(w) || w == g->hue_spot || w == g->chroma_spot || w == g->lightness_spot || w == g->spot_settings)
4666 {
4667 paint_hue(self);
4668 }
4669
4670 if((IS_NULL_PTR(w) || w == g->illuminant || w == g->illum_fluo || w == g->illum_led || w == g->temperature)
4671 && illuminant_widgets_ready)
4672 {
4673 update_illuminants(self);
4674 update_approx_cct(self);
4676
4677 // force-update all the illuminant sliders in case something above changed them
4678 // notice the hue/chroma of the illuminant has to be computed on-the-fly anyway
4679 dt_aligned_pixel_t xyY = { p->x, p->y, 1.f };
4681 dt_xyY_to_Lch(xyY, Lch);
4682
4683 // If the chroma is zero then there is not a meaningful hue angle. In this case
4684 // leave the hue slider where it was, so that if chroma is set to zero and then
4685 // set to a nonzero value, the hue setting will remain unchanged.
4686 if(Lch[1] > 0)
4687 dt_bauhaus_slider_set(g->illum_x, Lch[2] / M_PI * 180.f);
4688 dt_bauhaus_slider_set(g->illum_y, Lch[1]);
4689
4690 // Redraw the temperature background color taking new soft bounds into account
4691 dt_bauhaus_slider_set(g->temperature, p->temperature);
4693 }
4694
4695 if(w == g->adaptation)
4696 update_illuminants(self);
4697
4698 if(IS_NULL_PTR(w) || w == g->adaptation || primaries_widget || white_preserving_widget || w == g->scale_red_R || w == g->scale_red_G || w == g->scale_red_B || w == g->normalize_R)
4699 _update_RGB_colors(self, 1, 0, 0, p->normalize_R, p->red, g->scale_red_R, g->scale_red_G, g->scale_red_B);
4700 if(IS_NULL_PTR(w) || w == g->adaptation || primaries_widget || white_preserving_widget || w == g->scale_green_R || w == g->scale_green_G || w == g->scale_green_B || w == g->normalize_G)
4701 _update_RGB_colors(self, 0, 1, 0, p->normalize_G, p->green, g->scale_green_R, g->scale_green_G, g->scale_green_B);
4702 if(IS_NULL_PTR(w) || w == g->adaptation || primaries_widget || white_preserving_widget || w == g->scale_blue_R || w == g->scale_blue_G || w == g->scale_blue_B || w == g->normalize_B)
4703 _update_RGB_colors(self, 0, 0, 1, p->normalize_B, p->blue, g->scale_blue_R, g->scale_blue_G, g->scale_blue_B);
4704
4705 if(rows_are_normalized && !simple_widget && (IS_NULL_PTR(w) || complete_widget))
4706 {
4707 float error = INFINITY;
4709 {
4711 {
4712 dt_print(DT_DEBUG_DEV, "[channelmixerrgb] simple mixer rejected error=%g normalized=%d%d%d\n",
4713 error, p->normalize_R, p->normalize_G, p->normalize_B);
4714 dt_control_log(_("simple mixer mode requires all three output rows to be normalized with non-zero sums."));
4718 }
4719 else
4720 dt_print(DT_DEBUG_DEV, "[channelmixerrgb] simple mixer roundtrip error=%g\n", error);
4721 }
4722 }
4723
4724 if(!primaries_widget && (IS_NULL_PTR(w) || complete_widget || simple_widget || w == g->adaptation))
4725 {
4726 float error = INFINITY;
4728 {
4730 {
4731 dt_print(DT_DEBUG_DEV, "[channelmixerrgb] primaries mixer roundtrip error=%g\n", error);
4735 }
4736 }
4737 }
4738
4739 if(white_preserving_ready && !white_preserving_widget
4740 && (IS_NULL_PTR(w) || complete_widget || simple_widget || primaries_widget || w == g->adaptation))
4741 {
4742 float error = INFINITY;
4744 {
4746 {
4747 dt_print(DT_DEBUG_DEV, "[channelmixerrgb] white-preserving mixer roundtrip error=%g\n", error);
4751 }
4752 }
4753 }
4754
4755 if(IS_NULL_PTR(w) || w == g->adaptation || complete_widget || simple_widget || primaries_widget
4756 || white_preserving_widget)
4758
4759 if(IS_NULL_PTR(w) || w == g->adaptation || complete_widget || simple_widget || primaries_widget
4760 || white_preserving_widget)
4762
4763 if(IS_NULL_PTR(w) || w == g->adaptation || complete_widget || simple_widget || primaries_widget
4764 || white_preserving_widget)
4766
4767 // if grey channel is used and norm = 0 and normalization = ON, we are going to have a division by zero
4768 // in commit_param, we avoid dividing by zero automatically, but user needs a notification
4769 if((p->grey[0] != 0.f) || (p->grey[1] != 0.f) || (p->grey[2] != 0.f))
4770 if((p->grey[0] + p->grey[1] + p->grey[2] == 0.f) && p->normalize_grey)
4771 dt_control_log(_("color calibration: the sum of the gray channel parameters is zero, normalization will be disabled."));
4772
4773 // If "as shot in camera" illuminant is used, CAT space is forced automatically
4774 // therefore, make the control insensitive
4775 gtk_widget_set_sensitive(g->adaptation, p->illuminant != DT_ILLUMINANT_CAMERA);
4776 gtk_widget_queue_draw(g->adaptation);
4777
4779
4781}
4782
4784{
4787 const dt_iop_channelmixer_rgb_params_t previous = *p;
4788
4789 // capture gui color picked event.
4790 if(self->picked_color_max[0] < self->picked_color_min[0])
4791 {
4792 dt_print(DT_DEBUG_DEV, "[picker/channelmixerrgb] rejected invalid min/max min=%g max=%g\n",
4793 self->picked_color_min[0], self->picked_color_max[0]);
4794 return;
4795 }
4796 const float *RGB = self->picked_color;
4797 if(!isfinite(RGB[0]) || !isfinite(RGB[1]) || !isfinite(RGB[2]))
4798 {
4799 dt_print(DT_DEBUG_DEV, "[picker/channelmixerrgb] rejected nonfinite RGB=(%g,%g,%g)\n",
4800 RGB[0], RGB[1], RGB[2]);
4801 return;
4802 }
4803 dt_print(DT_DEBUG_DEV, "[picker/channelmixerrgb] RGB=(%g,%g,%g) pipe=%p\n",
4804 RGB[0], RGB[1], RGB[2], (void *)pipe);
4805
4806 // Get the module-stage profile matching the sampled buffer.
4807 const dt_iop_order_iccprofile_info_t *const current_profile
4809 if(IS_NULL_PTR(current_profile))
4810 {
4811 dt_print(DT_DEBUG_DEV, "[picker/channelmixerrgb] missing current profile\n");
4812 return;
4813 }
4814
4815 // Convert the sampled linear RGB code values to XYZ in the module profile.
4816 // The picker already sampled the live module buffer, so keep the conversion explicit here.
4817 dt_aligned_pixel_t XYZ = { 0.f };
4818 dot_product(RGB, current_profile->matrix_in, XYZ);
4819 if(!isfinite(XYZ[0]) || !isfinite(XYZ[1]) || !isfinite(XYZ[2])) return;
4820 dt_XYZ_to_sRGB(XYZ, g->spot_RGB);
4821
4822 // Convert to Lch for GUI feedback (input)
4826 dt_Lab_2_LCH(Lab, Lch);
4827
4828 // Write report in GUI
4830 gtk_label_set_text(GTK_LABEL(g->Lch_origin),
4831 g_strdup_printf(_("L: \t%.1f %%\nh: \t%.1f \302\260\nc: \t%.1f"),
4832 Lch[0], Lch[2] * 360.f, Lch[1] ));
4833 gtk_widget_queue_draw(g->origin_spot);
4835
4836 const dt_spot_mode_t mode = dt_bauhaus_combobox_get(g->spot_mode);
4837 const gboolean use_mixing = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(g->use_mixing));
4838
4839 // build the channel mixing matrix - keep in synch with commit_params()
4840 dt_colormatrix_t MIX = { { 0.f } };
4841
4842 float norm_R = 1.0f;
4843 if(p->normalize_R) norm_R = p->red[0] + p->red[1] + p->red[2];
4844
4845 float norm_G = 1.0f;
4846 if(p->normalize_G) norm_G = p->green[0] + p->green[1] + p->green[2];
4847
4848 float norm_B = 1.0f;
4849 if(p->normalize_B) norm_B = p->blue[0] + p->blue[1] + p->blue[2];
4850
4851 for(int i = 0; i < 3; i++)
4852 {
4853 MIX[0][i] = p->red[i] / norm_R;
4854 MIX[1][i] = p->green[i] / norm_G;
4855 MIX[2][i] = p->blue[i] / norm_B;
4856 }
4857
4858 if(mode == DT_SPOT_MODE_MEASURE)
4859 {
4860 // Keep the following in sync with commit_params()
4861
4862 // find x y coordinates of illuminant for CIE 1931 2° observer
4863 float x = p->x;
4864 float y = p->y;
4865 dt_adaptation_t adaptation = p->adaptation;
4866 dt_aligned_pixel_t custom_wb;
4867 get_white_balance_coeff(self, custom_wb);
4868 illuminant_to_xy(p->illuminant, &(self->dev->image_storage), custom_wb, &x, &y, p->temperature, p->illum_fluo, p->illum_led);
4869
4870 // if illuminant is set as camera, x and y are set on-the-fly at commit time, so we need to set adaptation too
4871 if(p->illuminant == DT_ILLUMINANT_CAMERA) check_if_close_to_daylight(x, y, NULL, NULL, &adaptation);
4872
4873 // Convert illuminant from xyY to XYZ
4874 dt_aligned_pixel_t XYZ_illuminant = { 0.f };
4875 illuminant_xy_to_XYZ(x, y, XYZ_illuminant);
4876
4877 // Convert illuminant from XYZ to Bradford modified LMS
4878 dt_aligned_pixel_t LMS_illuminant = { 0.f };
4879 dt_store_simd_aligned(LMS_illuminant, convert_any_XYZ_to_LMS(dt_load_simd_aligned(XYZ_illuminant), adaptation));
4880
4881 // For the non-linear Bradford
4882 const float pp = powf(0.818155f / LMS_illuminant[2], 0.0834f);
4883
4884 //fprintf(stdout, "illuminant: %i\n", p->illuminant);
4885 //fprintf(stdout, "x: %f, y: %f\n", x, y);
4886 //fprintf(stdout, "X: %f - Y: %f - Z: %f\n", XYZ_illuminant[0], XYZ_illuminant[1], XYZ_illuminant[2]);
4887 //fprintf(stdout, "L: %f - M: %f - S: %f\n", LMS_illuminant[0], LMS_illuminant[1], LMS_illuminant[2]);
4888
4889 // Finally, chroma-adapt the pixel
4890 dt_aligned_pixel_t XYZ_output = { 0.f };
4891 dt_store_simd_aligned(XYZ_output, chroma_adapt_pixel(dt_load_simd_aligned(XYZ),
4892 dt_load_simd_aligned(LMS_illuminant),
4893 adaptation, pp));
4894
4895 // Optionaly, apply the channel mixing
4896 if(use_mixing)
4897 {
4898 dt_aligned_pixel_t LMS_output = { 0.f };
4899 dt_store_simd_aligned(LMS_output, convert_any_XYZ_to_LMS(dt_load_simd_aligned(XYZ_output), adaptation));
4900 dt_aligned_pixel_t temp = { 0.f };
4901 dot_product(LMS_output, MIX, temp);
4902 dt_store_simd_aligned(XYZ_output, convert_any_LMS_to_XYZ(dt_load_simd_aligned(temp), adaptation));
4903 }
4904
4905 // Convert to Lab and Lch for GUI feedback
4906 dt_aligned_pixel_t Lab_output = { 0.f };
4907 dt_aligned_pixel_t Lch_output = { 0.f };
4908 dt_XYZ_to_Lab(XYZ_output, Lab_output);
4909 dt_Lab_2_LCH(Lab_output, Lch_output);
4910
4911 // Return the values in sliders
4913 dt_bauhaus_slider_set(g->lightness_spot, Lch_output[0]);
4914 dt_bauhaus_slider_set(g->chroma_spot, Lch_output[1]);
4915 dt_bauhaus_slider_set(g->hue_spot, Lch_output[2] * 360.f);
4916 paint_hue(self);
4918
4919 dt_conf_set_float("darkroom/modules/channelmixerrgb/lightness", Lch_output[0]);
4920 dt_conf_set_float("darkroom/modules/channelmixerrgb/chroma", Lch_output[1]);
4921 dt_conf_set_float("darkroom/modules/channelmixerrgb/hue", Lch_output[2] * 360.f);
4922 dt_conf_set_bool("darkroom/modules/channelmixerrgb/use_mixing", use_mixing);
4923 }
4924 else if(mode == DT_SPOT_MODE_CORRECT)
4925 {
4926 // Get the target color in LMS space
4927 dt_aligned_pixel_t Lch_target = { 0.f };
4928 dt_aligned_pixel_t Lab_target = { 0.f };
4929 dt_aligned_pixel_t XYZ_target = { 0.f };
4930 dt_aligned_pixel_t LMS_target = { 0.f };
4931
4933 Lch_target[0] = dt_bauhaus_slider_get(g->lightness_spot);
4934 Lch_target[1] = dt_bauhaus_slider_get(g->chroma_spot);
4935 Lch_target[2] = dt_bauhaus_slider_get(g->hue_spot) / 360.f;
4937
4938 dt_LCH_2_Lab(Lch_target, Lab_target);
4939 dt_Lab_to_XYZ(Lab_target, XYZ_target);
4940 const float Y_target = XYZ_target[1];
4941 for(int c = 0; c < 3; c++) XYZ_target[c] /= Y_target;
4942 dt_store_simd_aligned(LMS_target, convert_any_XYZ_to_LMS(dt_load_simd_aligned(XYZ_target), p->adaptation));
4943
4944 // optionaly, apply the inverse mixing on the target
4945 if(use_mixing)
4946 {
4947 // Repack the MIX matrix to 3x3 to support the pseudoinverse function
4948 // I'm just too lazy to rewrite the pseudo-inverse for 3x4 padded input
4949 float MIX_3x3[9];
4950 pack_3xSSE_to_3x3(MIX, MIX_3x3);
4951
4952 /* DEBUG
4953 fprintf(stdout, "Repacked channel mixer matrix :\n");
4954 fprintf(stdout, "%f \t%f \t%f\n", MIX_3x3[0][0], MIX_3x3[0][1], MIX_3x3[0][2]);
4955 fprintf(stdout, "%f \t%f \t%f\n", MIX_3x3[1][0], MIX_3x3[1][1], MIX_3x3[1][2]);
4956 fprintf(stdout, "%f \t%f \t%f\n", MIX_3x3[2][0], MIX_3x3[2][1], MIX_3x3[2][2]);
4957 */
4958
4959 // Invert the matrix
4960 float MIX_INV_3x3[9];
4961 matrice_pseudoinverse((float (*)[3])MIX_3x3, (float (*)[3])MIX_INV_3x3, 3);
4962
4963 // Transpose and repack the inverse to SSE matrix because the inversion transposes too
4964 dt_colormatrix_t MIX_INV;
4965 transpose_3x3_to_3xSSE(MIX_INV_3x3, MIX_INV);
4966
4967 /* DEBUG
4968 fprintf(stdout, "Repacked inverted channel mixer matrix :\n");
4969 fprintf(stdout, "%f \t%f \t%f\n", MIX_INV[0][0], MIX_INV[0][1], MIX_INV[0][2]);
4970 fprintf(stdout, "%f \t%f \t%f\n", MIX_INV[1][0], MIX_INV[1][1], MIX_INV[1][2]);
4971 fprintf(stdout, "%f \t%f \t%f\n", MIX_INV[2][0], MIX_INV[2][1], MIX_INV[2][2]);
4972 */
4973
4974 // Undo the channel mixing on the reference color
4975 // So we get the expected target color after the CAT
4976 dt_aligned_pixel_t temp;
4977 dot_product(LMS_target, MIX_INV, temp);
4978
4979 //fprintf(stdout, "LMS before channel mixer inversion : \t%f \t%f \t%f\n", LMS_target[0], LMS_target[1], LMS_target[2]);
4980 //fprintf(stdout, "LMS after channel mixer inversion : \t%f \t%f \t%f\n", temp[0], temp[1], temp[2]);
4981
4982 // convert back to XYZ to normalize luminance again
4983 // in case the matrix is not normalized
4984 dt_store_simd_aligned(XYZ_target, convert_any_LMS_to_XYZ(dt_load_simd_aligned(temp), p->adaptation));
4985 const float Y_mix = XYZ_target[1];
4986 for(int c = 0; c < 3; c++) XYZ_target[c] /= Y_mix;
4987 dt_store_simd_aligned(LMS_target, convert_any_XYZ_to_LMS(dt_load_simd_aligned(XYZ_target), p->adaptation));
4988
4989 //fprintf(stdout, "LMS target after everything : %f \t%f \t%f\n", LMS_target[0], LMS_target[1], LMS_target[2]);
4990
4991 // So now we got the target color after CAT and before mixing
4992 // in LMS space
4993 }
4994
4995 // Get the input color in LMS space
4996 dt_aligned_pixel_t LMS = { 0.f };
4997 const float Y = XYZ[1];
4998 for(int c = 0; c < 3; c++) XYZ[c] /= Y;
4999 dt_store_simd_aligned(LMS, convert_any_XYZ_to_LMS(dt_load_simd_aligned(XYZ), p->adaptation));
5000
5001 // Find the illuminant
5003 convert_D50_to_LMS(p->adaptation, D50);
5004
5005 dt_aligned_pixel_t illuminant_LMS = { 0.f };
5006 dt_aligned_pixel_t illuminant_XYZ = { 0.f };
5007
5008 // We solve the equation : target color / input color = D50 / illuminant, for illuminant
5009 for(int c = 0; c < 3; c++) illuminant_LMS[c] = D50[c] * LMS[c] / LMS_target[c];
5010 dt_store_simd_aligned(illuminant_XYZ, convert_any_LMS_to_XYZ(dt_load_simd_aligned(illuminant_LMS), p->adaptation));
5011
5012 // Convert to xyY
5013 const float sum = fmaxf(illuminant_XYZ[0] + illuminant_XYZ[1] + illuminant_XYZ[2], NORM_MIN);
5014 illuminant_XYZ[0] /= sum; // x
5015 illuminant_XYZ[2] = illuminant_XYZ[1]; // Y
5016 illuminant_XYZ[1] /= sum; // y
5017
5018 p->x = illuminant_XYZ[0];
5019 p->y = illuminant_XYZ[1];
5020
5021 // Force illuminant to custom, the daylight/black body approximations are
5022 // not accurate enough for color matching
5023 p->illuminant = DT_ILLUMINANT_CUSTOM;
5024
5026
5027 check_if_close_to_daylight(p->x, p->y, &p->temperature, NULL, NULL);
5028
5029 dt_bauhaus_slider_set(g->temperature, p->temperature);
5030 dt_bauhaus_combobox_set(g->illuminant, p->illuminant);
5031 dt_bauhaus_combobox_set(g->adaptation, p->adaptation);
5032
5033 const dt_aligned_pixel_t xyY = { p->x, p->y, 1.f };
5034 dt_aligned_pixel_t Lch_illuminant = { 0 };
5035 dt_xyY_to_Lch(xyY, Lch_illuminant);
5036 dt_bauhaus_slider_set(g->illum_x, Lch_illuminant[2] / M_PI * 180.f);
5037 dt_bauhaus_slider_set(g->illum_y, Lch_illuminant[1]);
5038
5039 update_illuminants(self);
5040 update_approx_cct(self);
5042 paint_hue(self);
5044 gtk_widget_queue_draw(g->origin_spot);
5045
5047
5048 if(memcmp(&previous, p, sizeof(previous)) != 0)
5049 {
5050 /* Auto-illuminant is driven by live picker motion, so this commits once per sampled
5051 move. That used to outpace the preview worker and keep it permanently in
5052 TOP_CHANGED, which is why this call was routed through a throttle of its own; the
5053 history commit now batches the resync it triggers, so the picker stays live without
5054 any special handling here. */
5055 dt_print(DT_DEBUG_DEV, "[picker/channelmixerrgb] history commit source=auto_set_illuminant\n");
5056 dt_dev_add_history_item(self->dev, self, TRUE, TRUE);
5057 }
5058 }
5059}
5060
5061
5063{
5064 if(dt_gui_widgets_suppressed()) return;
5065 dt_print(DT_DEBUG_DEV, "[picker/channelmixerrgb] apply picker=%p pipe=%p\n", (void *)picker, (void *)pipe);
5066 _auto_set_illuminant(self, pipe);
5067}
5068
5069
5070void autoset(struct dt_iop_module_t *self, const struct dt_dev_pixelpipe_t *pipe,
5071 const struct dt_dev_pixelpipe_iop_t *piece, const void *i)
5072{
5073 const dt_iop_roi_t *const roi_out = &piece->roi_out;
5075 if(piece->dsc_in.channels != 4) return;
5076
5077 const dt_iop_order_iccprofile_info_t *const current_profile
5079 if(IS_NULL_PTR(current_profile)) return;
5080
5081 dt_colormatrix_t MIX = { { 0.f } };
5082
5083 float norm_R = 1.0f;
5084 if(p->normalize_R) norm_R = p->red[0] + p->red[1] + p->red[2];
5085
5086 float norm_G = 1.0f;
5087 if(p->normalize_G) norm_G = p->green[0] + p->green[1] + p->green[2];
5088
5089 float norm_B = 1.0f;
5090 if(p->normalize_B) norm_B = p->blue[0] + p->blue[1] + p->blue[2];
5091
5092 for(int c = 0; c < 3; c++)
5093 {
5094 MIX[0][c] = p->red[c] / norm_R;
5095 MIX[1][c] = p->green[c] / norm_G;
5096 MIX[2][c] = p->blue[c] / norm_B;
5097 }
5098
5099 float lightness = 50.f;
5100 if(dt_conf_key_exists("darkroom/modules/channelmixerrgb/lightness"))
5101 lightness = dt_conf_get_float("darkroom/modules/channelmixerrgb/lightness");
5102
5103 float hue = 0.f;
5104 if(dt_conf_key_exists("darkroom/modules/channelmixerrgb/hue"))
5105 hue = dt_conf_get_float("darkroom/modules/channelmixerrgb/hue");
5106
5107 float chroma = 0.f;
5108 if(dt_conf_key_exists("darkroom/modules/channelmixerrgb/chroma"))
5109 chroma = dt_conf_get_float("darkroom/modules/channelmixerrgb/chroma");
5110
5111 dt_aligned_pixel_t Lch_target = { lightness, chroma, hue / 360.f, 0.f };
5112 dt_aligned_pixel_t Lab_target = { 0.f };
5113 dt_aligned_pixel_t XYZ_target = { 0.f };
5114 dt_aligned_pixel_t LMS_target = { 0.f };
5115 dt_LCH_2_Lab(Lch_target, Lab_target);
5116 dt_Lab_to_XYZ(Lab_target, XYZ_target);
5117
5118 // Normalize for unit luminance (illuminant)
5119 const float Y_target = XYZ_target[1];
5120 for(int c = 0; c < 3; c++) XYZ_target[c] /= Y_target;
5121 dt_store_simd_aligned(LMS_target, convert_any_XYZ_to_LMS(dt_load_simd_aligned(XYZ_target), p->adaptation));
5122
5123 float MIX_3x3[9];
5124 pack_3xSSE_to_3x3(MIX, MIX_3x3);
5125
5126 float MIX_INV_3x3[9];
5127 matrice_pseudoinverse((float (*)[3])MIX_3x3, (float (*)[3])MIX_INV_3x3, 3);
5128
5129 dt_colormatrix_t MIX_INV;
5130 transpose_3x3_to_3xSSE(MIX_INV_3x3, MIX_INV);
5131
5132 dt_aligned_pixel_t temp = { 0.f };
5133 dot_product(LMS_target, MIX_INV, temp);
5134
5135 dt_store_simd_aligned(XYZ_target, convert_any_LMS_to_XYZ(dt_load_simd_aligned(temp), p->adaptation));
5136 const float Y_mix = XYZ_target[1];
5137 if(Y_mix <= NORM_MIN) return;
5138 for(int c = 0; c < 3; c++) XYZ_target[c] /= Y_mix;
5139 dt_store_simd_aligned(LMS_target, convert_any_XYZ_to_LMS(dt_load_simd_aligned(XYZ_target), p->adaptation));
5140
5141 const float *const restrict in = (float *)i;
5142 float average_L = 0.f;
5143 float average_M = 0.f;
5144 float average_S = 0.f;
5145 size_t valid_pixels = 0;
5146
5147 // Compute average LMS in the image
5148 __OMP_PARALLEL_FOR__(reduction(+:average_L, average_M, average_S, valid_pixels))
5149 for(size_t k = 0; k < roi_out->width * roi_out->height * 4; k += 4)
5150 {
5151 // Convert each input pixel to the module CAT space first, then average the chromaticity there.
5152 dt_aligned_pixel_t XYZ = { 0.f };
5153 dt_aligned_pixel_t LMS = { 0.f };
5154 dt_ioppr_rgb_matrix_to_xyz(in + k, XYZ, current_profile->matrix_in_transposed, current_profile->lut_in,
5155 current_profile->unbounded_coeffs_in, current_profile->lutsize,
5156 current_profile->nonlinearlut);
5157
5158 dt_store_simd_aligned(LMS, convert_any_XYZ_to_LMS(dt_load_simd_aligned(XYZ), p->adaptation));
5159 if(!isfinite(LMS[0]) || !isfinite(LMS[1]) || !isfinite(LMS[2])) continue;
5160
5161 average_L += LMS[0];
5162 average_M += LMS[1];
5163 average_S += LMS[2];
5164 valid_pixels++;
5165 }
5166
5167 if(valid_pixels == 0) return;
5168
5169 const float norm = 1.f / (float)valid_pixels;
5170 dt_aligned_pixel_t average_LMS = { average_L * norm, average_M * norm, average_S * norm, 0.f };
5171
5172 // Normalize for unit luminance
5173 dt_aligned_pixel_t average_XYZ = { 0.f };
5174 dt_store_simd_aligned(average_XYZ, convert_any_LMS_to_XYZ(dt_load_simd_aligned(average_LMS), p->adaptation));
5175 const float Y_average_lms = average_XYZ[1];
5176 for(int c = 0; c < 3; c++) average_XYZ[c] /= Y_average_lms;
5177 dt_store_simd_aligned(average_LMS, convert_any_XYZ_to_LMS(dt_load_simd_aligned(average_XYZ), p->adaptation));
5178
5179 dt_aligned_pixel_t D50 = { 0.f };
5180 dt_aligned_pixel_t illuminant_LMS = { 0.f };
5181 dt_aligned_pixel_t illuminant_XYZ = { 0.f };
5182 convert_D50_to_LMS(p->adaptation, D50);
5183
5184 for(int c = 0; c < 3; c++)
5185 {
5186 const float target = copysignf(fmaxf(fabsf(LMS_target[c]), NORM_MIN), LMS_target[c]);
5187 illuminant_LMS[c] = D50[c] * average_LMS[c] / target;
5188 }
5189
5190 dt_store_simd_aligned(illuminant_XYZ, convert_any_LMS_to_XYZ(dt_load_simd_aligned(illuminant_LMS), p->adaptation));
5191
5192 const float sum = fmaxf(illuminant_XYZ[0] + illuminant_XYZ[1] + illuminant_XYZ[2], NORM_MIN);
5193 p->x = illuminant_XYZ[0] / sum;
5194 p->y = illuminant_XYZ[1] / sum;
5195 p->illuminant = DT_ILLUMINANT_CUSTOM;
5196 check_if_close_to_daylight(p->x, p->y, &p->temperature, NULL, NULL);
5197}
5198
5199
5200void gui_init(struct dt_iop_module_t *self)
5201{
5203
5204 // Init the color checker UI
5205 for(size_t k = 0; k < 4; k++)
5206 {
5207 g->box[k].x = g->box[k].y = -1.;
5208 g->active_node[k] = FALSE;
5209 }
5210 g->is_cursor_close = FALSE;
5211 g->drag_drop = FALSE;
5212 g->is_profiling_started = FALSE;
5213 g->run_profile = FALSE;
5214 g->run_validation = FALSE;
5215 g->profile_ready = FALSE;
5216 g->checker_ready = FALSE;
5217 g->delta_E_in = NULL;
5218 g->delta_E_label_text = NULL;
5219 g->colorcheckers = NULL;
5220
5221 g->XYZ[0] = NAN;
5222
5224 G_CALLBACK(_develop_ui_pipe_finished_callback), self);
5226 G_CALLBACK(_preview_pipe_finished_callback), self);
5227
5228 // Init GTK notebook
5229 g->notebook = dt_ui_notebook_new();
5230 // The CAT page's illuminant picker is read at apply time; reset it if it is
5231 // still active once the user switches away from that page.
5232 dt_ui_notebook_set_picker_owner(g->notebook, self);
5233
5234 // Page CAT
5235 self->gui->widget = dt_ui_notebook_page(g->notebook, N_("CAT"), _("chromatic adaptation transform"));
5236
5237 g->adaptation = dt_bauhaus_combobox_from_params(self, N_("adaptation"));
5238 gtk_widget_set_tooltip_text(GTK_WIDGET(g->adaptation),
5239 _("choose the method to adapt the illuminant\n"
5240 "and the colorspace in which the module works: \n"
5241 "- Linear Bradford (1985) is consistent with ICC v4 toolchain.\n"
5242 "- CAT16 (2016) is more robust and accurate.\n"
5243 "- Non-linear Bradford (1985) is the original Bradford,\n"
5244 " it can produce better results than the linear version, but is unreliable.\n"
5245 "- XYZ is a simple scaling in XYZ space. It is not recommended in general.\n"
5246 "- none disables any adaptation and uses pipeline working RGB."));
5247
5248 GtkWidget *hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, DT_GUI_BOX_SPACING);
5249
5250 g->approx_cct = dt_ui_label_new("CCT:");
5251 gtk_box_pack_start(GTK_BOX(hbox), g->approx_cct, FALSE, FALSE, 0);
5252
5253 g->illum_color = GTK_WIDGET(gtk_drawing_area_new());
5254 gtk_widget_set_size_request(g->illum_color, 2 * DT_PIXEL_APPLY_DPI(dt_bauhaus_get_global()->quad_width),
5256 gtk_widget_set_tooltip_text(GTK_WIDGET(g->illum_color),
5257 _("this is the color of the scene illuminant before chromatic adaptation\n"
5258 "this color will be turned into pure white by the adaptation."));
5259
5260 g_signal_connect(G_OBJECT(g->illum_color), "draw", G_CALLBACK(illuminant_color_draw), self);
5261 gtk_box_pack_start(GTK_BOX(hbox), g->illum_color, TRUE, TRUE, 0);
5262
5263 g->color_picker = dt_color_picker_new(self, DT_COLOR_PICKER_AREA, hbox);
5264 gtk_widget_set_tooltip_text(g->color_picker, _("set white balance to detected from area"));
5265
5266 gtk_box_pack_start(GTK_BOX(self->gui->widget), GTK_WIDGET(hbox), FALSE, FALSE, 0);
5267
5268 g->illuminant = dt_bauhaus_combobox_from_params(self, N_("illuminant"));
5269
5270 g->illum_fluo = dt_bauhaus_combobox_from_params(self, "illum_fluo");
5271
5272 g->illum_led = dt_bauhaus_combobox_from_params(self, "illum_led");
5273
5274 g->temperature = dt_bauhaus_slider_from_params(self, N_("temperature"));
5275
5277 dt_bauhaus_widget_set_label(g->illum_x, N_("hue"));
5278 dt_bauhaus_slider_set_format(g->illum_x, "\302\260");
5279 g_signal_connect(G_OBJECT(g->illum_x), "value-changed", G_CALLBACK(illum_xy_callback), self);
5280 gtk_box_pack_start(GTK_BOX(self->gui->widget), GTK_WIDGET(g->illum_x), FALSE, FALSE, 0);
5281
5282 g->illum_y = dt_bauhaus_slider_new_with_range(dt_bauhaus_get_global(), DT_GUI_MODULE(self), 0., 100., 0, 0, 1);
5283 dt_bauhaus_widget_set_label(g->illum_y, N_("chroma"));
5284 dt_bauhaus_slider_set_format(g->illum_y, "%");
5286 g_signal_connect(G_OBJECT(g->illum_y), "value-changed", G_CALLBACK(illum_xy_callback), self);
5287 gtk_box_pack_start(GTK_BOX(self->gui->widget), GTK_WIDGET(g->illum_y), FALSE, FALSE, 0);
5288
5289 dt_bauhaus_slider_set_soft_range(g->temperature, 3000., 7000.);
5290 dt_bauhaus_slider_set_digits(g->temperature, 0);
5291 dt_bauhaus_slider_set_format(g->temperature, " K");
5292
5293 g->gamut = dt_bauhaus_slider_from_params(self, "gamut");
5294 dt_bauhaus_slider_set_soft_max(g->gamut, 4.f);
5295
5296 g->clip = dt_bauhaus_toggle_from_params(self, "clip");
5297
5298 // Add the color mapping collapsible panel
5299
5301 (&g->csspot,
5302 "plugins/darkroom/channelmixerrgb/expand_picker_mapping",
5303 _("spot color mapping"),
5304 GTK_BOX(self->gui->widget), GTK_PACK_END);
5305
5306 gtk_widget_set_tooltip_text(g->csspot.expander, _("use a color checker target to autoset CAT and channels"));
5307
5308 DT_BAUHAUS_COMBOBOX_NEW_FULL(dt_bauhaus_get_global(), g->spot_mode, DT_GUI_MODULE(self), N_("spot mode"),
5309 _("\"correction\" automatically adjust the illuminant\n"
5310 "such that the input color is mapped to the target.\n"
5311 "\"measure\" simply shows how an input color is mapped by the CAT\n"
5312 "and can be used to sample a target."),
5313 0, NULL, self,
5314 N_("correction"),
5315 N_("measure"));
5316 gtk_box_pack_start(GTK_BOX(g->csspot.container), GTK_WIDGET(g->spot_mode), TRUE, TRUE, 0);
5317 g_signal_connect(G_OBJECT(g->spot_mode), "value-changed", G_CALLBACK(_spot_settings_changed_callback), self);
5318
5319 gchar *label = N_("take channel mixing into account");
5320 g->use_mixing = gtk_check_button_new_with_label(_(label));
5321 gtk_label_set_ellipsize(GTK_LABEL(gtk_bin_get_child(GTK_BIN(g->use_mixing))), PANGO_ELLIPSIZE_END);
5322 gtk_widget_set_tooltip_text(g->use_mixing,
5323 _("compute the target by taking the channel mixing into account.\n"
5324 "if disabled, only the CAT is considered."));
5325 gtk_box_pack_start(GTK_BOX(g->csspot.container), GTK_WIDGET(g->use_mixing), TRUE, TRUE, 0);
5326 g_signal_connect(G_OBJECT(g->use_mixing), "toggled", G_CALLBACK(_spot_settings_changed_callback), self);
5327
5328 GtkWidget *hhbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, DT_GUI_BOX_SPACING);
5329 GtkWidget *vvbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, DT_GUI_BOX_SPACING);
5330
5331 gtk_box_pack_start(GTK_BOX(vvbox), dt_ui_section_label_new(_("input")), FALSE, FALSE, 0);
5332
5333 g->origin_spot = GTK_WIDGET(gtk_drawing_area_new());
5334 gtk_widget_set_size_request(g->origin_spot, 2 * DT_PIXEL_APPLY_DPI(dt_bauhaus_get_global()->quad_width),
5336 gtk_widget_set_tooltip_text(GTK_WIDGET(g->origin_spot),
5337 _("the input color that should be mapped to the target"));
5338
5339 g_signal_connect(G_OBJECT(g->origin_spot), "draw", G_CALLBACK(origin_color_draw), self);
5340 gtk_box_pack_start(GTK_BOX(vvbox), g->origin_spot, TRUE, TRUE, 0);
5341
5342 g->Lch_origin = gtk_label_new(_("L: \tN/A\nh: \tN/A\nc: \tN/A"));
5343 gtk_widget_set_tooltip_text(GTK_WIDGET(g->Lch_origin),
5344 _("these LCh coordinates are computed from CIE Lab 1976 coordinates"));
5345 gtk_box_pack_start(GTK_BOX(vvbox), GTK_WIDGET(g->Lch_origin), FALSE, FALSE, 0);
5346
5347 gtk_box_pack_start(GTK_BOX(hhbox), GTK_WIDGET(vvbox), FALSE, FALSE, DT_BAUHAUS_SPACE);
5348
5349 vvbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, DT_GUI_BOX_SPACING);
5350
5351 gtk_box_pack_start(GTK_BOX(vvbox), dt_ui_section_label_new(_("target")), TRUE, TRUE, 0);
5352
5353 g->target_spot = GTK_WIDGET(gtk_drawing_area_new());
5354 gtk_widget_set_size_request(g->target_spot, 2 * DT_PIXEL_APPLY_DPI(dt_bauhaus_get_global()->quad_width),
5356 gtk_widget_set_tooltip_text(GTK_WIDGET(g->target_spot),
5357 _("the desired target color after mapping"));
5358
5359 g_signal_connect(G_OBJECT(g->target_spot), "draw", G_CALLBACK(target_color_draw), self);
5360 gtk_box_pack_start(GTK_BOX(vvbox), g->target_spot, TRUE, TRUE, 0);
5361
5363 dt_bauhaus_widget_set_label(g->lightness_spot, N_("lightness"));
5364 dt_bauhaus_slider_set_format(g->lightness_spot, "%");
5365 dt_bauhaus_slider_set_default(g->lightness_spot, 50.f);
5366 gtk_box_pack_start(GTK_BOX(vvbox), GTK_WIDGET(g->lightness_spot), TRUE, TRUE, 0);
5367 g_signal_connect(G_OBJECT(g->lightness_spot), "value-changed", G_CALLBACK(_spot_settings_changed_callback), self);
5368
5370 dt_bauhaus_widget_set_label(g->hue_spot, N_("hue"));
5371 dt_bauhaus_slider_set_format(g->hue_spot, "\302\260");
5372 dt_bauhaus_slider_set_default(g->hue_spot, 0.f);
5373 gtk_box_pack_start(GTK_BOX(vvbox), GTK_WIDGET(g->hue_spot), TRUE, TRUE, 0);
5374 g_signal_connect(G_OBJECT(g->hue_spot), "value-changed", G_CALLBACK(_spot_settings_changed_callback), self);
5375
5377 dt_bauhaus_widget_set_label(g->chroma_spot, N_("chroma"));
5378 dt_bauhaus_slider_set_default(g->chroma_spot, 0.f);
5379 gtk_box_pack_start(GTK_BOX(vvbox), GTK_WIDGET(g->chroma_spot), TRUE, TRUE, 0);
5380 g_signal_connect(G_OBJECT(g->chroma_spot), "value-changed", G_CALLBACK(_spot_settings_changed_callback), self);
5381
5382 gtk_box_pack_start(GTK_BOX(hhbox), GTK_WIDGET(vvbox), TRUE, TRUE, DT_BAUHAUS_SPACE);
5383
5384 gtk_box_pack_start(GTK_BOX(g->csspot.container), GTK_WIDGET(hhbox), FALSE, FALSE, 0);
5385
5386 GtkWidget *mixer_page = dt_ui_notebook_page(g->notebook, N_("Mixer"), _("channel mixing"));
5388 dt_bauhaus_widget_set_label(g->mixer_mode, N_("mode"));
5389 dt_bauhaus_combobox_add(g->mixer_mode, _("Complete"));
5390 dt_bauhaus_combobox_add(g->mixer_mode, _("Simple"));
5391 dt_bauhaus_combobox_add(g->mixer_mode, _("Primaries"));
5392 dt_bauhaus_combobox_add(g->mixer_mode, _("White-preserving"));
5393 gtk_widget_set_tooltip_text(g->mixer_mode,
5394 _("complete exposes the original nine mixer coefficients.\n"
5395 "simple rebuilds the normalized mixer as an exact chroma-plane rotation,\n"
5396 "two signed stretches and two neutral couplings.\n"
5397 "primaries rebuilds the mixer as a generalized primaries, white tint and gain model.\n"
5398 "white-preserving rotates and saturates each primary around the white,\n"
5399 "which stays exactly where it is."));
5400 gtk_box_pack_start(GTK_BOX(mixer_page), GTK_WIDGET(g->mixer_mode), FALSE, FALSE, 0);
5401 g_signal_connect(G_OBJECT(g->mixer_mode), "value-changed", G_CALLBACK(_channelmixerrgb_mixer_mode_callback), self);
5402
5403 g->mixer_stack = gtk_stack_new();
5404 gtk_stack_set_transition_type(GTK_STACK(g->mixer_stack), GTK_STACK_TRANSITION_TYPE_NONE);
5405 gtk_box_pack_start(GTK_BOX(mixer_page), GTK_WIDGET(g->mixer_stack), FALSE, FALSE, 0);
5406
5407 GtkWidget *mixer_complete = gtk_box_new(GTK_ORIENTATION_VERTICAL, DT_GUI_BOX_SPACING);
5408 gtk_stack_add_named(GTK_STACK(g->mixer_stack), mixer_complete, "complete");
5409
5410 GtkWidget *first, *second, *third;
5411#define MIXER_ROW(var, short, section, swap) \
5412 gtk_box_pack_start(GTK_BOX(mixer_complete), dt_ui_section_label_new(section), FALSE, FALSE, 0); \
5413 self->gui->widget = mixer_complete; \
5414 first = dt_bauhaus_slider_from_params(self, swap ? #var "[2]" : #var "[0]");\
5415 dt_bauhaus_slider_set_digits(first, 3); \
5416 dt_bauhaus_widget_set_label(first, N_("input R")); \
5417 second = dt_bauhaus_slider_from_params(self, #var "[1]"); \
5418 dt_bauhaus_slider_set_digits(second, 3); \
5419 dt_bauhaus_widget_set_label(second, N_("input G")); \
5420 third = dt_bauhaus_slider_from_params(self, swap ? #var "[0]" : #var "[2]");\
5421 dt_bauhaus_slider_set_digits(third, 3); \
5422 dt_bauhaus_widget_set_label(third, N_("input B")); \
5423 g->scale_##var##_R = swap ? third : first; \
5424 g->scale_##var##_G = second; \
5425 g->scale_##var##_B = swap ? first : third; \
5426 g->normalize_##short = dt_bauhaus_toggle_from_params(self, "normalize_" #short);
5427
5428 MIXER_ROW(red, R, _("output red"), FALSE)
5429 MIXER_ROW(green, G, _("output green"), FALSE)
5430 MIXER_ROW(blue, B, _("output blue"), FALSE)
5431
5432 GtkWidget *mixer_simple = gtk_box_new(GTK_ORIENTATION_VERTICAL, DT_GUI_BOX_SPACING);
5433 gtk_stack_add_named(GTK_STACK(g->mixer_stack), mixer_simple, "simple");
5434
5435 g->simple_theta = dt_bauhaus_slider_new_with_range(dt_bauhaus_get_global(), DT_GUI_MODULE(self), -1.f, 1.f, 0, 0, 3);
5436 dt_bauhaus_widget_set_label(g->simple_theta, N_("global hue rotation"));
5437 dt_bauhaus_slider_set_factor(g->simple_theta, 180.f);
5438 dt_bauhaus_slider_set_format(g->simple_theta, "\302\260");
5439 gtk_widget_set_tooltip_text(g->simple_theta, _("global rotation of the normalized chroma plane."));
5440 gtk_box_pack_start(GTK_BOX(mixer_simple), GTK_WIDGET(g->simple_theta), FALSE, FALSE, 0);
5441 g_signal_connect(G_OBJECT(g->simple_theta), "value-changed", G_CALLBACK(_channelmixerrgb_simple_slider_callback), self);
5442
5443 gtk_box_pack_start(GTK_BOX(mixer_simple), dt_ui_section_label_new(_("chroma")), FALSE, FALSE, 0);
5444
5445 g->simple_psi = dt_bauhaus_slider_new_with_range(dt_bauhaus_get_global(), DT_GUI_MODULE(self), -1.f, 1.f, 0, 0, 3);
5446 dt_bauhaus_widget_set_label(g->simple_psi, N_("chroma (u,v) axes orientation"));
5447 dt_bauhaus_slider_set_factor(g->simple_psi, 90.f);
5448 dt_bauhaus_slider_set_format(g->simple_psi, "\302\260");
5449 gtk_widget_set_tooltip_text(g->simple_psi, _("orientation of the principal stretch axes in the chroma plane."));
5450 gtk_box_pack_start(GTK_BOX(mixer_simple), GTK_WIDGET(g->simple_psi), FALSE, FALSE, 0);
5451 g_signal_connect(G_OBJECT(g->simple_psi), "value-changed", G_CALLBACK(_channelmixerrgb_simple_slider_callback), self);
5452
5453 g->simple_stretch_1 = dt_bauhaus_slider_new_with_range(dt_bauhaus_get_global(), DT_GUI_MODULE(self), -1.5f, 1.5f, 0, 1.f, 3);
5454 dt_bauhaus_widget_set_label(g->simple_stretch_1, N_("u stretch"));
5455 gtk_widget_set_tooltip_text(g->simple_stretch_1, _("stretch along the first principal chroma axis. 0 neutralizes chroma, 1 keeps identity, -1 reverses chroma and +/-1.5 add contrast."));
5456 gtk_box_pack_start(GTK_BOX(mixer_simple), GTK_WIDGET(g->simple_stretch_1), FALSE, FALSE, 0);
5457 g_signal_connect(G_OBJECT(g->simple_stretch_1), "value-changed", G_CALLBACK(_channelmixerrgb_simple_slider_callback), self);
5458
5459 g->simple_stretch_2 = dt_bauhaus_slider_new_with_range(dt_bauhaus_get_global(), DT_GUI_MODULE(self), -1.5f, 1.5f, 0, 1.f, 3);
5460 dt_bauhaus_widget_set_label(g->simple_stretch_2, N_("v stretch"));
5461 gtk_widget_set_tooltip_text(g->simple_stretch_2, _("stretch along the second principal chroma axis. 0 neutralizes chroma, 1 keeps identity, -1 reverses chroma and +/-1.5 add contrast."));
5462 gtk_box_pack_start(GTK_BOX(mixer_simple), GTK_WIDGET(g->simple_stretch_2), FALSE, FALSE, 0);
5463 g_signal_connect(G_OBJECT(g->simple_stretch_2), "value-changed", G_CALLBACK(_channelmixerrgb_simple_slider_callback), self);
5464
5465 gtk_box_pack_start(GTK_BOX(mixer_simple), dt_ui_section_label_new(_("achromatic coupling")), FALSE, FALSE, 0);
5466
5467 g->simple_coupling_2 = dt_bauhaus_slider_new_with_range(dt_bauhaus_get_global(), DT_GUI_MODULE(self), -1.f, 1.f, 0, 0, 3);
5468 dt_bauhaus_widget_set_label(g->simple_coupling_2, N_("achromatic coupling hue"));
5469 dt_bauhaus_slider_set_factor(g->simple_coupling_2, 180.f);
5470 dt_bauhaus_slider_set_format(g->simple_coupling_2, "\302\260");
5471 gtk_widget_set_tooltip_text(g->simple_coupling_2, _("chroma direction, in the fixed chroma basis, that is coupled into the achromatic axis."));
5472 gtk_box_pack_start(GTK_BOX(mixer_simple), GTK_WIDGET(g->simple_coupling_2), FALSE, FALSE, 0);
5473 g_signal_connect(G_OBJECT(g->simple_coupling_2), "value-changed", G_CALLBACK(_channelmixerrgb_simple_slider_callback), self);
5474
5475 g->simple_coupling_1 = dt_bauhaus_slider_new_with_range(dt_bauhaus_get_global(), DT_GUI_MODULE(self), 0.f, 1.f, 0, 0, 3);
5476 dt_bauhaus_widget_set_label(g->simple_coupling_1, N_("achromatic coupling amount"));
5477 gtk_widget_set_tooltip_text(g->simple_coupling_1, _("strength of the chroma-to-achromatic coupling in the fixed chroma basis."));
5478 gtk_box_pack_start(GTK_BOX(mixer_simple), GTK_WIDGET(g->simple_coupling_1), FALSE, FALSE, 0);
5479 g_signal_connect(G_OBJECT(g->simple_coupling_1), "value-changed", G_CALLBACK(_channelmixerrgb_simple_slider_callback), self);
5480
5481 GtkWidget *mixer_primaries = gtk_box_new(GTK_ORIENTATION_VERTICAL, DT_GUI_BOX_SPACING);
5482 gtk_stack_add_named(GTK_STACK(g->mixer_stack), mixer_primaries, "primaries");
5483
5484 gtk_box_pack_start(GTK_BOX(mixer_primaries), dt_ui_section_label_new(_("achromatic axis")), FALSE, FALSE, 0);
5485
5486 g->primaries_achromatic_hue = dt_bauhaus_slider_new_with_range(dt_bauhaus_get_global(), DT_GUI_MODULE(self), -2.f, 2.f, 0, 0, 3);
5487 dt_bauhaus_widget_set_label(g->primaries_achromatic_hue, N_("white hue"));
5488 dt_bauhaus_slider_set_factor(g->primaries_achromatic_hue, 90.f);
5489 dt_bauhaus_slider_set_format(g->primaries_achromatic_hue, "\302\260");
5490 gtk_widget_set_tooltip_text(g->primaries_achromatic_hue,
5491 _("rotate the custom white vector around the D50 white of the current mixer basis."));
5492 gtk_box_pack_start(GTK_BOX(mixer_primaries), GTK_WIDGET(g->primaries_achromatic_hue), FALSE, FALSE, 0);
5493 g_signal_connect(G_OBJECT(g->primaries_achromatic_hue), "value-changed",
5495
5496 g->primaries_achromatic_purity = dt_bauhaus_slider_new_with_range(dt_bauhaus_get_global(), DT_GUI_MODULE(self), 0.f, 2.f, 0, 0, 3);
5497 dt_bauhaus_widget_set_label(g->primaries_achromatic_purity, N_("white purity"));
5498 gtk_widget_set_tooltip_text(g->primaries_achromatic_purity,
5499 _("distance of the custom white vector from the D50 white within the current mixer basis."));
5500 gtk_box_pack_start(GTK_BOX(mixer_primaries), GTK_WIDGET(g->primaries_achromatic_purity), FALSE, FALSE, 0);
5501 g_signal_connect(G_OBJECT(g->primaries_achromatic_purity), "value-changed",
5503
5504 gtk_box_pack_start(GTK_BOX(mixer_primaries), dt_ui_section_label_new(_("red primary")), FALSE, FALSE, 0);
5505
5506 g->primaries_red_hue = dt_bauhaus_slider_new_with_range(dt_bauhaus_get_global(), DT_GUI_MODULE(self), -1.f, 1.f, 0, 0, 3);
5507 dt_bauhaus_widget_set_label(g->primaries_red_hue, N_("red hue"));
5508 dt_bauhaus_slider_set_factor(g->primaries_red_hue, 90.f);
5509 dt_bauhaus_slider_set_format(g->primaries_red_hue, "\302\260");
5510 gtk_widget_set_tooltip_text(g->primaries_red_hue,
5511 _("rotate the first basis vector around the D50 white of the current mixer basis."));
5512 gtk_box_pack_start(GTK_BOX(mixer_primaries), GTK_WIDGET(g->primaries_red_hue), FALSE, FALSE, 0);
5513 g_signal_connect(G_OBJECT(g->primaries_red_hue), "value-changed",
5515
5516 g->primaries_red_purity = dt_bauhaus_slider_new_with_range(dt_bauhaus_get_global(), DT_GUI_MODULE(self), 0.f, 2.f, 0, 1.f, 3);
5517 dt_bauhaus_widget_set_label(g->primaries_red_purity, N_("red purity"));
5518 gtk_widget_set_tooltip_text(g->primaries_red_purity,
5519 _("radial scaling of the first basis vector inside the affine primaries footprint."));
5520 gtk_box_pack_start(GTK_BOX(mixer_primaries), GTK_WIDGET(g->primaries_red_purity), FALSE, FALSE, 0);
5521 g_signal_connect(G_OBJECT(g->primaries_red_purity), "value-changed",
5523
5524 gtk_box_pack_start(GTK_BOX(mixer_primaries), dt_ui_section_label_new(_("green primary")), FALSE, FALSE, 0);
5525
5526 g->primaries_green_hue = dt_bauhaus_slider_new_with_range(dt_bauhaus_get_global(), DT_GUI_MODULE(self), -1.f, 1.f, 0, 0, 3);
5527 dt_bauhaus_widget_set_label(g->primaries_green_hue, N_("green hue"));
5528 dt_bauhaus_slider_set_factor(g->primaries_green_hue, 90.f);
5529 dt_bauhaus_slider_set_format(g->primaries_green_hue, "\302\260");
5530 gtk_widget_set_tooltip_text(g->primaries_green_hue,
5531 _("rotate the second basis vector around the D50 white of the current mixer basis."));
5532 gtk_box_pack_start(GTK_BOX(mixer_primaries), GTK_WIDGET(g->primaries_green_hue), FALSE, FALSE, 0);
5533 g_signal_connect(G_OBJECT(g->primaries_green_hue), "value-changed",
5535
5536 g->primaries_green_purity = dt_bauhaus_slider_new_with_range(dt_bauhaus_get_global(), DT_GUI_MODULE(self), 0.f, 2.f, 0, 1.f, 3);
5537 dt_bauhaus_widget_set_label(g->primaries_green_purity, N_("green purity"));
5538 gtk_widget_set_tooltip_text(g->primaries_green_purity,
5539 _("radial scaling of the second basis vector inside the affine primaries footprint."));
5540 gtk_box_pack_start(GTK_BOX(mixer_primaries), GTK_WIDGET(g->primaries_green_purity), FALSE, FALSE, 0);
5541 g_signal_connect(G_OBJECT(g->primaries_green_purity), "value-changed",
5543
5544 gtk_box_pack_start(GTK_BOX(mixer_primaries), dt_ui_section_label_new(_("blue primary")), FALSE, FALSE, 0);
5545
5546 g->primaries_blue_hue = dt_bauhaus_slider_new_with_range(dt_bauhaus_get_global(), DT_GUI_MODULE(self), -1.f, 1.f, 0, 0, 3);
5547 dt_bauhaus_widget_set_label(g->primaries_blue_hue, N_("blue hue"));
5548 dt_bauhaus_slider_set_factor(g->primaries_blue_hue, 90.f);
5549 dt_bauhaus_slider_set_format(g->primaries_blue_hue, "\302\260");
5550 gtk_widget_set_tooltip_text(g->primaries_blue_hue,
5551 _("rotate the third basis vector around the D50 white of the current mixer basis."));
5552 gtk_box_pack_start(GTK_BOX(mixer_primaries), GTK_WIDGET(g->primaries_blue_hue), FALSE, FALSE, 0);
5553 g_signal_connect(G_OBJECT(g->primaries_blue_hue), "value-changed",
5555
5556 g->primaries_blue_purity = dt_bauhaus_slider_new_with_range(dt_bauhaus_get_global(), DT_GUI_MODULE(self), 0.f, 2.f, 0, 1.f, 3);
5557 dt_bauhaus_widget_set_label(g->primaries_blue_purity, N_("blue purity"));
5558 gtk_widget_set_tooltip_text(g->primaries_blue_purity,
5559 _("radial scaling of the third basis vector inside the affine primaries footprint."));
5560 gtk_box_pack_start(GTK_BOX(mixer_primaries), GTK_WIDGET(g->primaries_blue_purity), FALSE, FALSE, 0);
5561 g_signal_connect(G_OBJECT(g->primaries_blue_purity), "value-changed",
5563
5564 gtk_box_pack_start(GTK_BOX(mixer_primaries), dt_ui_section_label_new(_("gain correction")), FALSE, FALSE, 0);
5565
5566 g->primaries_gain = dt_bauhaus_slider_new_with_range(dt_bauhaus_get_global(), DT_GUI_MODULE(self), -8.f, 8.f, 0, 1.f, 3);
5567 dt_bauhaus_widget_set_label(g->primaries_gain, N_("gain"));
5568 gtk_widget_set_tooltip_text(g->primaries_gain,
5569 _("global gain multiplying the custom white vector after the affine primaries transform."));
5570 gtk_box_pack_start(GTK_BOX(mixer_primaries), GTK_WIDGET(g->primaries_gain), FALSE, FALSE, 0);
5571 g_signal_connect(G_OBJECT(g->primaries_gain), "value-changed",
5573
5574 GtkWidget *mixer_white_preserving = gtk_box_new(GTK_ORIENTATION_VERTICAL, DT_GUI_BOX_SPACING);
5575 gtk_stack_add_named(GTK_STACK(g->mixer_stack), mixer_white_preserving, "white-preserving");
5576
5577#define WHITE_PRESERVING_PRIMARY(var, section, rotation_label, rotation_tooltip, saturation_label, \
5578 saturation_tooltip) \
5579 gtk_box_pack_start(GTK_BOX(mixer_white_preserving), dt_ui_section_label_new(section), FALSE, FALSE, 0); \
5580 g->white_preserving_##var##_rotation \
5581 = dt_bauhaus_slider_new_with_range(dt_bauhaus_get_global(), DT_GUI_MODULE(self), -1.f, 1.f, 0, 0, 3); \
5582 dt_bauhaus_widget_set_label(g->white_preserving_##var##_rotation, rotation_label); \
5583 dt_bauhaus_slider_set_factor(g->white_preserving_##var##_rotation, 90.f); \
5584 dt_bauhaus_slider_set_format(g->white_preserving_##var##_rotation, "\302\260"); \
5585 gtk_widget_set_tooltip_text(g->white_preserving_##var##_rotation, rotation_tooltip); \
5586 gtk_box_pack_start(GTK_BOX(mixer_white_preserving), GTK_WIDGET(g->white_preserving_##var##_rotation), FALSE,\
5587 FALSE, 0); \
5588 g_signal_connect(G_OBJECT(g->white_preserving_##var##_rotation), "value-changed", \
5589 G_CALLBACK(_channelmixerrgb_white_preserving_slider_callback), self); \
5590 /* -100%/+100% is the travel this control is meant to be used over, but a matrix hand-built */ \
5591 /* in complete mode can legitimately push a primary further out. Keep the hard range wider so */ \
5592 /* the page reports such a matrix instead of clamping and quietly lying about it. */ \
5593 g->white_preserving_##var##_saturation \
5594 = dt_bauhaus_slider_new_with_range(dt_bauhaus_get_global(), DT_GUI_MODULE(self), -4.f, 4.f, 0, 0, 3); \
5595 dt_bauhaus_slider_set_soft_range(g->white_preserving_##var##_saturation, -1.f, 1.f); \
5596 dt_bauhaus_widget_set_label(g->white_preserving_##var##_saturation, saturation_label); \
5597 /* On a range under 10, a "%" format makes bauhaus scale by 100 and take two digits off by */ \
5598 /* itself. Do not also set a factor, and leave it enough digits to take : it subtracts them */ \
5599 /* unconditionally, and a negative digit count used to hang the whole GUI in ipow(). */ \
5600 dt_bauhaus_slider_set_format(g->white_preserving_##var##_saturation, "%"); \
5601 gtk_widget_set_tooltip_text(g->white_preserving_##var##_saturation, saturation_tooltip); \
5602 gtk_box_pack_start(GTK_BOX(mixer_white_preserving), GTK_WIDGET(g->white_preserving_##var##_saturation), \
5603 FALSE, FALSE, 0); \
5604 g_signal_connect(G_OBJECT(g->white_preserving_##var##_saturation), "value-changed", \
5605 G_CALLBACK(_channelmixerrgb_white_preserving_slider_callback), self);
5606
5607 WHITE_PRESERVING_PRIMARY(red, _("red primary"), N_("red rotation"),
5608 _("rotate the red primary around the white of the current mixer basis.\n"
5609 "the white itself is left exactly where it is."),
5610 N_("red saturation"),
5611 _("scale the red primary's distance to the white of the current mixer basis.\n"
5612 "-100% collapses it onto the white, +100% doubles it."))
5613 WHITE_PRESERVING_PRIMARY(green, _("green primary"), N_("green rotation"),
5614 _("rotate the green primary around the white of the current mixer basis.\n"
5615 "the white itself is left exactly where it is."),
5616 N_("green saturation"),
5617 _("scale the green primary's distance to the white of the current mixer basis.\n"
5618 "-100% collapses it onto the white, +100% doubles it."))
5619 WHITE_PRESERVING_PRIMARY(blue, _("blue primary"), N_("blue rotation"),
5620 _("rotate the blue primary around the white of the current mixer basis.\n"
5621 "the white itself is left exactly where it is."),
5622 N_("blue saturation"),
5623 _("scale the blue primary's distance to the white of the current mixer basis.\n"
5624 "-100% collapses it onto the white, +100% doubles it."))
5625
5626#undef WHITE_PRESERVING_PRIMARY
5627
5628 GtkWidget *outputs_page = dt_ui_notebook_page(g->notebook, N_("Outputs"),
5629 _("output colorfulness, brightness and B&W mixing"));
5630 self->gui->widget = outputs_page;
5631
5632#define OUTPUT_SECTION(var, short, section, swap) \
5633 gtk_box_pack_start(GTK_BOX(outputs_page), dt_ui_section_label_new(section), FALSE, FALSE, 0); \
5634 \
5635 first = dt_bauhaus_slider_from_params(self, swap ? #var "[2]" : #var "[0]");\
5636 dt_bauhaus_slider_set_digits(first, 3); \
5637 dt_bauhaus_widget_set_label(first, N_("input R")); \
5638 \
5639 second = dt_bauhaus_slider_from_params(self, #var "[1]"); \
5640 dt_bauhaus_slider_set_digits(second, 3); \
5641 dt_bauhaus_widget_set_label(second, N_("input G")); \
5642 \
5643 third = dt_bauhaus_slider_from_params(self, swap ? #var "[0]" : #var "[2]");\
5644 dt_bauhaus_slider_set_digits(third, 3); \
5645 dt_bauhaus_widget_set_label(third, N_("input B")); \
5646 \
5647 g->scale_##var##_R = swap ? third : first; \
5648 g->scale_##var##_G = second; \
5649 g->scale_##var##_B = swap ? first : third; \
5650 \
5651 g->normalize_##short = dt_bauhaus_toggle_from_params(self, "normalize_" #short);
5652
5653 OUTPUT_SECTION(saturation, sat, _("colorfulness"), FALSE)
5654 g->saturation_version = dt_bauhaus_combobox_from_params(self, "version");
5655 OUTPUT_SECTION(lightness, light, _("brightness"), FALSE)
5656 OUTPUT_SECTION(grey, grey, _("B&W"), FALSE)
5657
5658 // start building top level widget
5659 self->gui->widget = gtk_box_new(GTK_ORIENTATION_VERTICAL, DT_GUI_BOX_SPACING);
5660
5661 gtk_box_pack_start(GTK_BOX(self->gui->widget), GTK_WIDGET(g->notebook), FALSE, FALSE, 0);
5662 const int saved_page = dt_conf_get_int("plugins/darkroom/channelmixerrgb/gui_page");
5663 const int active_page = saved_page > 2 ? 2 : CLAMP(saved_page, 0, 2);
5664 gtk_widget_show(gtk_notebook_get_nth_page(g->notebook, active_page));
5665 gtk_notebook_set_current_page(g->notebook, active_page);
5666
5667 // Add the color checker collapsible panel
5668
5670 (&g->cs,
5671 "plugins/darkroom/channelmixerrgb/expand_values",
5672 _("calibrate with a color checker"),
5673 GTK_BOX(self->gui->widget), GTK_PACK_END);
5674
5675 gtk_widget_set_tooltip_text(g->cs.toggle,
5676 _("use a color checker target to autoset CAT and channels"));
5677 g_signal_connect(G_OBJECT(g->cs.toggle), "toggled", G_CALLBACK(start_profiling_callback), self);
5678
5679 GtkWidget *collapsible = GTK_WIDGET(g->cs.container);
5680
5681 gchar *tip_files_loc = NULL;
5682 {
5683 char confdir[DT_PATH_MAX] = { 0 };
5684 dt_loc_get_user_config_dir(confdir, sizeof(confdir));
5685
5686 gchar *user_CGATS_dir = g_build_filename(confdir, "color", "checker", NULL);
5687 tip_files_loc = g_strdup_printf(_("'%s'"), user_CGATS_dir);
5688 dt_free(user_CGATS_dir);
5689 }
5690
5692 dt_bauhaus_widget_set_label(g->checkers_list, N_("Chart"));
5693 gtk_box_pack_start(GTK_BOX(collapsible), GTK_WIDGET(g->checkers_list), TRUE, TRUE, 0);
5694 dt_bauhaus_combobox_set(g->checkers_list, 0);
5695 gchar *tooltip = g_strdup_printf(_("Choose the vendor and the type of your chart.\n"
5696 ".cht files in %s."), tip_files_loc);
5697 gtk_widget_set_tooltip_text(g->checkers_list, tooltip);
5699 g_signal_connect(G_OBJECT(g->checkers_list), "value-changed", G_CALLBACK(checker_changed_callback), (gpointer)self);
5700
5701 g->checkers_color_list = dt_bauhaus_combobox_new(dt_bauhaus_get_global(), DT_GUI_MODULE(self));
5702 dt_bauhaus_widget_set_label(g->checkers_color_list, N_("Chart color"));
5703 gtk_box_pack_start(GTK_BOX(collapsible), GTK_WIDGET(g->checkers_color_list), TRUE, TRUE, 0);
5704
5705 dt_bauhaus_combobox_set(g->checkers_color_list, 0);
5706 tooltip = g_strdup_printf(_("Choose the definition of your chart.\n"
5707 "CGATS.17 files in %s."), tip_files_loc);
5708 gtk_widget_set_tooltip_text(g->checkers_color_list, tooltip);
5710 g_signal_connect(G_OBJECT(g->checkers_color_list), "value-changed", G_CALLBACK(checker_color_changed_callback), (gpointer)self);
5711
5712 tooltip = g_markup_printf_escaped(_("<i>No CGATS.17 color file matches the selected chart.\n"
5713 "Add a compatible CGATS.17 file to <b>%s</b>.</i>"), tip_files_loc);
5714 g->checker_msg = gtk_label_new(NULL);
5715 gtk_label_set_markup(GTK_LABEL(g->checker_msg), tooltip);
5717 gtk_label_set_line_wrap(GTK_LABEL(g->checker_msg), TRUE);
5718 gtk_label_set_line_wrap_mode(GTK_LABEL(g->checker_msg), PANGO_WRAP_WORD);
5719 gtk_box_pack_start(GTK_BOX(collapsible), GTK_WIDGET(g->checker_msg), FALSE, TRUE, 0);
5720 gtk_widget_set_visible(g->checker_msg, FALSE);
5721 dt_free(tip_files_loc);
5722
5723
5724
5725 DT_BAUHAUS_COMBOBOX_NEW_FULL(dt_bauhaus_get_global(), g->optimize, DT_GUI_MODULE(self), N_("optimize for"),
5726 _("choose the colors that will be optimized with higher priority.\n"
5727 "neutral colors gives the lowest average delta E but a high maximum delta E\n"
5728 "saturated colors gives the lowest maximum delta E but a high average delta E\n"
5729 "none is a trade-off between both\n"
5730 "the others are special behaviours to protect some hues"),
5732 N_("none"),
5733 N_("neutral colors"),
5734 N_("saturated colors"),
5735 N_("skin and soil colors"),
5736 N_("foliage colors"),
5737 N_("sky and water colors"),
5738 N_("average delta E"),
5739 N_("maximum delta E"));
5740 gtk_box_pack_start(GTK_BOX(collapsible), GTK_WIDGET(g->optimize), TRUE, TRUE, 0);
5741
5743 dt_bauhaus_widget_set_label(g->safety, N_("patch scale"));
5744 gtk_widget_set_tooltip_text(g->safety, _("reduce the radius of the patches to select the more or less central part.\n"
5745 "useful when the perspective correction is sloppy or\n"
5746 "the patches frame cast a shadows on the edges of the patch." ));
5747 g_signal_connect(G_OBJECT(g->safety), "value-changed", G_CALLBACK(safety_changed_callback), self);
5748 gtk_box_pack_start(GTK_BOX(collapsible), GTK_WIDGET(g->safety), TRUE, TRUE, 0);
5749
5750 g->label_delta_E = dt_ui_label_new("");
5751 gtk_box_pack_start(GTK_BOX(collapsible), GTK_WIDGET(g->label_delta_E), TRUE, TRUE, 0);
5752 gtk_widget_set_tooltip_text(g->label_delta_E, _("the delta E is using the CIE 2000 formula"));
5753
5754 GtkWidget *toolbar = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, DT_GUI_BOX_SPACING);
5755
5756 g->button_commit = dtgtk_button_new(dtgtk_cairo_paint_check_mark, 0, NULL);
5757 gtk_box_pack_end(GTK_BOX(toolbar), GTK_WIDGET(g->button_commit), FALSE, FALSE, 0);
5758 gtk_widget_set_tooltip_text(g->button_commit, _("accept the computed profile and set it in the module"));
5759 g_signal_connect(G_OBJECT(g->button_commit), "button-press-event", G_CALLBACK(commit_profile_callback), (gpointer)self);
5760
5761 g->button_profile = dtgtk_button_new(dtgtk_cairo_paint_refresh, 0, NULL);
5762 g_signal_connect(G_OBJECT(g->button_profile), "button-press-event", G_CALLBACK(run_profile_callback), (gpointer)self);
5763 gtk_widget_set_tooltip_text(g->button_profile, _("recompute the profile"));
5764 gtk_box_pack_end(GTK_BOX(toolbar), GTK_WIDGET(g->button_profile), FALSE, FALSE, 0);
5765
5766 g->button_validate = dtgtk_button_new(dtgtk_cairo_paint_softproof, 0, NULL);
5767 g_signal_connect(G_OBJECT(g->button_validate), "button-press-event", G_CALLBACK(run_validation_callback), (gpointer)self);
5768 gtk_widget_set_tooltip_text(g->button_validate, _("check the output delta E"));
5769 gtk_box_pack_end(GTK_BOX(toolbar), GTK_WIDGET(g->button_validate), FALSE, FALSE, 0);
5770
5771 gtk_box_pack_start(GTK_BOX(collapsible), GTK_WIDGET(toolbar), FALSE, FALSE, 0);
5772}
5773
5775{
5778 G_CALLBACK(_develop_ui_pipe_finished_callback), self);
5780
5782 dt_conf_set_int("plugins/darkroom/channelmixerrgb/gui_page", gtk_notebook_get_current_page (g->notebook));
5783
5784 if(g->delta_E_in)
5785 {
5786 dt_free_align(g->delta_E_in);
5787 g->delta_E_in = NULL;
5788 }
5789
5790 dt_free(g->delta_E_label_text);
5791
5792 dt_colorchecker_label_list_cleanup(&(g->colorcheckers));
5793 dt_colorchecker_label_list_cleanup(&(g->colorcheckers_all_color));
5794 g_list_free(g->colorcheckers_color); // data pointers are already freed by the cleanup function for colorcheckers_all_color
5795
5796 dt_colorchecker_cleanup(g->checker);
5797
5799}
5800
5801// clang-format off
5802// modelines: These editor modelines have been set for all relevant files by tools/update_modelines.py
5803// vim: shiftwidth=2 expandtab tabstop=2 cindent
5804// kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified;
5805// clang-format on
static __DT_CLONE_TARGETS__ void homography(float *homograph, const float angle, const float shift_v, const float shift_h, const float shear, const float f_length_kb, const float orthocorr, const float aspect, const int width, const int height, dt_iop_ashift_homodir_t dir)
Definition ashift.c:763
static void error(char *msg)
Definition ashift_lsd.c:202
#define TRUE
Definition ashift_lsd.c:162
#define FALSE
Definition ashift_lsd.c:158
void dt_bauhaus_slider_set_soft_range(GtkWidget *widget, float soft_min, float soft_max)
Definition bauhaus.c:1498
void dt_bauhaus_slider_set_digits(GtkWidget *widget, int val)
Definition bauhaus.c:3343
void dt_bauhaus_slider_set_hard_max(GtkWidget *widget, float val)
Definition bauhaus.c:1432
void dt_bauhaus_combobox_clear(GtkWidget *widget)
Definition bauhaus.c:1997
void dt_bauhaus_slider_set_default(GtkWidget *widget, float def)
Definition bauhaus.c:1491
const char * dt_bauhaus_combobox_get_entry(GtkWidget *widget, int pos)
Definition bauhaus.c:2007
void dt_bauhaus_slider_set_stop(GtkWidget *widget, float stop, float r, float g, float b)
Definition bauhaus.c:2161
float dt_bauhaus_slider_get(GtkWidget *widget)
Definition bauhaus.c:3280
GtkWidget * dt_bauhaus_slider_new_with_range_and_feedback(dt_bauhaus_t *bh, dt_gui_module_t *self, float min, float max, float step, float defval, int digits, int feedback)
Definition bauhaus.c:1638
int dt_bauhaus_combobox_get(GtkWidget *widget)
Definition bauhaus.c:2136
void dt_bauhaus_slider_set_soft_max(GtkWidget *widget, float val)
Definition bauhaus.c:1474
int dt_bauhaus_combobox_length(GtkWidget *widget)
Definition bauhaus.c:1963
void dt_bauhaus_combobox_set_default(GtkWidget *widget, int def)
Definition bauhaus.c:1400
void dt_bauhaus_combobox_add_full(GtkWidget *widget, const char *text, dt_bauhaus_combobox_alignment_t align, gpointer data, void(free_func)(void *data), gboolean sensitive)
Definition bauhaus.c:1846
void dt_bauhaus_combobox_add_separator(GtkWidget *widget)
Definition bauhaus.c:1858
void dt_bauhaus_slider_set(GtkWidget *widget, float pos)
Definition bauhaus.c:3331
void dt_bauhaus_combobox_set(GtkWidget *widget, const int pos)
Definition bauhaus.c:2090
void dt_bauhaus_widget_set_label(GtkWidget *widget, const char *label)
Definition bauhaus.c:1504
GtkWidget * dt_bauhaus_slider_new_with_range(dt_bauhaus_t *bh, dt_gui_module_t *self, float min, float max, float step, float defval, int digits)
Definition bauhaus.c:1632
void dt_bauhaus_combobox_remove_at(GtkWidget *widget, int pos)
Definition bauhaus.c:1909
GtkWidget * dt_bauhaus_combobox_new(dt_bauhaus_t *bh, dt_gui_module_t *self)
Definition bauhaus.c:1698
gboolean dt_bauhaus_combobox_set_from_text(GtkWidget *widget, const char *text)
Definition bauhaus.c:2100
void dt_bauhaus_slider_set_format(GtkWidget *widget, const char *format)
Definition bauhaus.c:3407
void dt_bauhaus_combobox_add(GtkWidget *widget, const char *text)
Definition bauhaus.c:1824
void dt_bauhaus_slider_set_factor(GtkWidget *widget, float factor)
Definition bauhaus.c:3423
@ DT_BAUHAUS_COMBOBOX_ALIGN_RIGHT
Definition bauhaus.h:127
#define DT_BAUHAUS_SPACE
Definition bauhaus.h:291
#define INTERNAL_PADDING
Definition bauhaus.h:78
#define DT_BAUHAUS_COMBOBOX_NEW_FULL(bauhaus, widget, action, label, tip, pos, callback, data,...)
Definition bauhaus.h:379
#define DT_BAUHAUS_SLIDER_MAX_STOPS
Definition bauhaus.h:73
#define INNER_PADDING
Definition bauhaus.h:81
static __DT_CLONE_TARGETS__ void normalize(float *const buffer, const size_t width, const size_t height, const float norm)
Definition blurs.c:351
GtkWidget * dtgtk_button_new(DTGTKCairoPaintIconFunc paint, gint paintflags, void *paintdata)
Definition button.c:135
typedef void((*dt_cache_allocate_t)(void *userdata, dt_cache_entry_t *entry))
static void checker_color_changed_callback(GtkWidget *widget, gpointer user_data)
#define DT_CHANNELMIXERRGB_SIMPLE_MODE_CONF
dt_iop_channelmixer_shared_white_preserving_params_t dt_iop_channelmixer_rgb_white_preserving_params_t
void commit_params(struct dt_iop_module_t *self, dt_iop_params_t *p1, dt_dev_pixelpipe_t *pipe, dt_dev_pixelpipe_iop_t *piece)
void init(dt_iop_module_t *module)
#define COLOR_MAX
#define CHANNEL_SIZE
static __DT_CLONE_TARGETS__ void 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)
const char ** description(struct dt_iop_module_t *self)
int default_group()
static void _spot_settings_changed_callback(GtkWidget *slider, dt_iop_module_t *self)
static void optimize_changed_callback(GtkWidget *widget, gpointer user_data)
__DT_CLONE_TARGETS__ int 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_iop_channelmixer_rgb_box_map_t *const box_map, 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)
void gui_reset(dt_iop_module_t *self)
static void update_xy_color(dt_iop_module_t *self)
void _auto_set_illuminant(dt_iop_module_t *self, dt_dev_pixelpipe_t *pipe)
static dt_iop_channelmixer_rgb_box_map_t _box_map(const dt_dev_pixelpipe_t *const pipe, const dt_iop_roi_t *const roi)
Derive that map from the request this pipe was planned from.
#define HUE_MAX
dt_iop_channelmixer_rgb_mixer_mode_t
@ DT_CHANNELMIXERRGB_MIXER_SIMPLE
@ DT_CHANNELMIXERRGB_MIXER_WHITE_PRESERVING
@ DT_CHANNELMIXERRGB_MIXER_PRIMARIES
@ DT_CHANNELMIXERRGB_MIXER_COMPLETE
void reload_defaults(dt_iop_module_t *module)
#define OFF
#define GET_WEIGHT
static const dt_colorchecker_label_t * _checker_label_from_combobox(GtkWidget *combobox, GList *labels)
Find the chart a combo box currently shows, in the label list backing that combo box.
static void update_bounding_box(dt_iop_channelmixer_rgb_gui_data_t *g, const float x_increment, const float y_increment)
static void update_illuminant_color(struct dt_iop_module_t *self)
static void _channelmixerrgb_reject_mixer_mode(dt_iop_module_t *self, const char *message)
Leave a reduced mixer mode that cannot represent the current matrix.
static gboolean _channelmixerrgb_sync_primaries_from_params(dt_iop_module_t *self, float *error)
Synchronize the primaries-mode GUI from the effective mixer matrix.
static gboolean _is_another_module_cat_on_pipe(struct dt_iop_module_t *self)
static void _channelmixerrgb_set_mixer_mode(dt_iop_channelmixer_rgb_gui_data_t *g, dt_iop_channelmixer_rgb_mixer_mode_t mode)
static gboolean target_color_draw(GtkWidget *widget, cairo_t *crf, gpointer user_data)
static gboolean origin_color_draw(GtkWidget *widget, cairo_t *crf, gpointer user_data)
dt_iop_channelmixer_shared_primaries_params_t dt_iop_channelmixer_rgb_primaries_params_t
static void _channelmixerrgb_mixer_mode_callback(GtkWidget *combo, gpointer user_data)
const char * aliases()
void update_colorchecker_list(dt_iop_module_t *self)
dt_iop_channelmixer_shared_primaries_basis_t dt_iop_channelmixer_rgb_primaries_basis_t
dt_spot_mode_t
@ DT_SPOT_MODE_MEASURE
@ DT_SPOT_MODE_CORRECT
@ DT_SPOT_MODE_LAST
static __DT_CLONE_TARGETS__ int 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)
void init_pipe(struct dt_iop_module_t *self, dt_dev_pixelpipe_t *pipe, dt_dev_pixelpipe_iop_t *piece)
static void start_profiling_callback(GtkWidget *togglebutton, dt_iop_module_t *self)
static __DT_CLONE_TARGETS__ int _custom_wb_from_coeffs(struct dt_iop_module_t *self, const dt_aligned_pixel_t wb_coeffs, dt_aligned_pixel_t custom_wb)
static void _update_RGB_slider_stop(dt_iop_channelmixer_rgb_params_t *p, const struct dt_iop_order_iccprofile_info_t *const work_profile, const struct dt_iop_order_iccprofile_info_t *const display_profile, GtkWidget *w, float stop, float c, float r, float g, float b)
const char * name()
static void _update_RGB_colors(dt_iop_module_t *self, float r, float g, float b, gboolean normalize, float *a, GtkWidget *w_r, GtkWidget *w_g, GtkWidget *w_b)
static void _channelmixerrgb_update_simple_colors(dt_iop_module_t *self)
Paint the simple mixer sliders from the exact chroma-basis model.
static __DT_CLONE_TARGETS__ int _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_iop_channelmixer_rgb_box_map_t *const box_map, const dt_colormatrix_t RGB_to_XYZ, const dt_colormatrix_t XYZ_to_CAM, float *const restrict patches, const gboolean normalize_exposure, extraction_result_t *result)
dt_iop_channelmixer_shared_simple_params_t dt_iop_channelmixer_rgb_simple_params_t
void gui_update(struct dt_iop_module_t *self)
#define RAD_TO_DEG(x)
#define LIGHTNESS_MAX
static void _set_checker_from_labels(dt_iop_channelmixer_rgb_gui_data_t *g, const dt_colorchecker_label_t *checker_label, const dt_colorchecker_label_t *color_label)
Rebuild g->checker from the charts currently selected in both combo boxes.
void gui_init(struct dt_iop_module_t *self)
int button_pressed(struct dt_iop_module_t *self, double x, double y, double pressure, int which, int type, uint32_t state)
void gui_changed(dt_iop_module_t *self, GtkWidget *w, void *previous)
int button_released(struct dt_iop_module_t *self, double x, double y, int which, uint32_t state)
#define MIXER_ROW(var, short, section, swap)
static __DT_CLONE_TARGETS__ int get_white_balance_coeff(struct dt_iop_module_t *self, dt_aligned_pixel_t custom_wb)
static void run_validation_callback(GtkWidget *widget, GdkEventButton *event, gpointer user_data)
static gboolean illuminant_color_draw(GtkWidget *widget, cairo_t *crf, gpointer user_data)
#define DEG_TO_RAD(x)
static void update_illuminants(struct dt_iop_module_t *self)
static void _channelmixerrgb_primaries_slider_callback(GtkWidget *slider, gpointer user_data)
static void _channelmixerrgb_simple_slider_callback(GtkWidget *slider, gpointer user_data)
void cleanup_global(dt_iop_module_so_t *module)
#define TEMP_MAX
static void run_profile_callback(GtkWidget *widget, GdkEventButton *event, gpointer user_data)
int default_colorspace(dt_iop_module_t *self, dt_dev_pixelpipe_t *pipe, const dt_dev_pixelpipe_iop_t *piece)
void input_format(dt_iop_module_t *self, dt_dev_pixelpipe_t *pipe, dt_dev_pixelpipe_iop_t *piece, dt_iop_buffer_dsc_t *dsc)
int flags()
void checker_changed_callback(GtkWidget *widget, gpointer user_data)
#define SHF(ii, jj, c)
void gui_post_expose(struct dt_iop_module_t *self, cairo_t *cr, int32_t width, int32_t height, int32_t pointerx, int32_t pointery)
static gboolean _channelmixerrgb_push_matrix_to_params(dt_iop_module_t *self, const float M[3][3])
Write a mixer matrix into the module params and mirror it on the complete-mode sliders.
void gui_cleanup(struct dt_iop_module_t *self)
void update_colorchecker_color_list(dt_iop_module_t *self)
This function filters the list of all .cht files to only those that match the currently selected colo...
static void init_bounding_box(dt_iop_channelmixer_rgb_gui_data_t *g, const float width, const float height)
void init_presets(dt_iop_module_so_t *self)
#define CHROMA_MAX
static void _preview_pipe_finished_callback(gpointer instance, gpointer user_data)
static __DT_CLONE_TARGETS__ void declare_cat_on_pipe(struct dt_iop_module_t *self, gboolean preset)
void color_list_visibility(dt_iop_module_t *self, const dt_colorchecker_label_t *checker_label)
static void _develop_ui_pipe_finished_callback(gpointer instance, gpointer user_data)
#define DT_CHANNELMIXERRGB_SIMPLE_EPS
static void illum_xy_callback(GtkWidget *slider, gpointer user_data)
static void _channelmixerrgb_update_white_preserving_colors(dt_iop_module_t *self)
static void update_approx_cct(struct dt_iop_module_t *self)
static void 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)
#define WHITE_PRESERVING_PRIMARY(var, section, rotation_label, rotation_tooltip, saturation_label, saturation_tooltip)
static void _channelmixerrgb_white_preserving_slider_callback(GtkWidget *slider, gpointer user_data)
#define COLOR_MIN
#define INVERSE_SQRT_3
void cleanup_pipe(struct dt_iop_module_t *self, dt_dev_pixelpipe_t *pipe, dt_dev_pixelpipe_iop_t *piece)
#define OUTPUT_SECTION(var, short, section, swap)
static void _map_homographies(const dt_iop_channelmixer_rgb_gui_data_t *const g, const dt_iop_channelmixer_rgb_box_map_t *const map, float forward[9], float backward[9])
Compose the box->buffer map onto the chart homographies.
static gboolean _channelmixerrgb_sync_white_preserving_from_params(dt_iop_module_t *self, float *error)
Synchronize the white-preserving mode GUI from the effective mixer matrix.
#define ILLUM_Y_MAX
#define TEMP_MIN
void init_global(dt_iop_module_so_t *module)
dt_iop_channelmixer_shared_simple_probe_t dt_iop_channelmixer_rgb_simple_probe_t
void autoset(struct dt_iop_module_t *self, const struct dt_dev_pixelpipe_t *pipe, const struct dt_dev_pixelpipe_iop_t *piece, const void *i)
static gboolean _channelmixerrgb_white_preserving_widgets(const dt_iop_channelmixer_rgb_gui_data_t *const g, GtkWidget *widgets[6])
Collect the six white-preserving mode widgets.
dt_iop_channelmixer_rgb_version_t
@ CHANNELMIXERRGB_V_1
@ CHANNELMIXERRGB_V_2
@ CHANNELMIXERRGB_V_3
dt_solving_strategy_t
@ DT_SOLVE_OPTIMIZE_SKIN
@ DT_SOLVE_OPTIMIZE_SKY
@ DT_SOLVE_OPTIMIZE_MAX_DELTA_E
@ DT_SOLVE_OPTIMIZE_LOW_SAT
@ DT_SOLVE_OPTIMIZE_AVG_DELTA_E
@ DT_SOLVE_OPTIMIZE_NONE
@ DT_SOLVE_OPTIMIZE_HIGH_SAT
@ DT_SOLVE_OPTIMIZE_FOLIAGE
int process_cl(struct dt_iop_module_t *self, const dt_dev_pixelpipe_t *pipe, const dt_dev_pixelpipe_iop_t *piece, cl_mem dev_in, cl_mem dev_out)
int mouse_moved(struct dt_iop_module_t *self, double x, double y, double pressure, int which)
static void commit_profile_callback(GtkWidget *widget, GdkEventButton *event, gpointer user_data)
int 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_iop_channelmixer_rgb_box_map_t *const box_map, const dt_colormatrix_t RGB_to_XYZ, const dt_colormatrix_t XYZ_to_RGB, const dt_colormatrix_t XYZ_to_CAM)
static void safety_changed_callback(GtkWidget *widget, gpointer user_data)
static void paint_hue(dt_iop_module_t *self)
void color_picker_apply(dt_iop_module_t *self, GtkWidget *picker, dt_dev_pixelpipe_t *pipe, dt_dev_pixelpipe_iop_t *piece)
static void _convert_GUI_colors(dt_iop_channelmixer_rgb_params_t *p, const struct dt_iop_order_iccprofile_info_t *const work_profile, const struct dt_iop_order_iccprofile_info_t *const display_profile, const dt_aligned_pixel_t LMS, dt_aligned_pixel_t RGB)
#define ILLUM_X_MAX
int legacy_params(dt_iop_module_t *self, const void *const old_params, const int old_version, void *new_params, const int new_version)
static void _channelmixerrgb_update_primaries_colors(dt_iop_module_t *self)
static gboolean _channelmixerrgb_sync_simple_from_params(dt_iop_module_t *self, float *error)
Rebuild the simple mixer sliders from the current normalized 3x3 matrix.
int process(struct dt_iop_module_t *self, const dt_dev_pixelpipe_t *pipe, const dt_dev_pixelpipe_iop_t *piece, const void *const restrict ivoid, void *const restrict ovoid)
void dt_iop_channelmixer_shared_paint_primaries_sliders(const dt_adaptation_t adaptation, const dt_iop_order_iccprofile_info_t *const work_profile, const dt_iop_order_iccprofile_info_t *const display_profile, const dt_iop_channelmixer_shared_primaries_basis_t basis, const dt_iop_channelmixer_shared_primaries_params_t *const primaries, GtkWidget *const widgets[9])
void dt_iop_channelmixer_shared_paint_white_preserving_sliders(const dt_adaptation_t adaptation, const dt_iop_order_iccprofile_info_t *const work_profile, const dt_iop_order_iccprofile_info_t *const display_profile, const dt_iop_channelmixer_shared_primaries_basis_t basis, const dt_iop_channelmixer_shared_white_preserving_params_t *const white_preserving, GtkWidget *const widgets[6])
void dt_iop_channelmixer_shared_white_preserving_from_sliders(GtkWidget *const widgets[6], dt_iop_channelmixer_shared_white_preserving_params_t *white_preserving)
void dt_iop_channelmixer_shared_simple_from_sliders(GtkWidget *const widgets[6], dt_iop_channelmixer_shared_simple_params_t *simple)
void dt_iop_channelmixer_shared_paint_temperature_slider(GtkWidget *const widget, const float temperature_min, const float temperature_max)
void dt_iop_channelmixer_shared_white_preserving_to_sliders(const dt_iop_channelmixer_shared_white_preserving_params_t *const white_preserving, GtkWidget *const widgets[6])
void dt_iop_channelmixer_shared_primaries_to_sliders(const dt_iop_channelmixer_shared_primaries_params_t *const primaries, GtkWidget *const widgets[9])
void dt_iop_channelmixer_shared_simple_to_matrix(const dt_iop_channelmixer_shared_simple_params_t *const simple, float M[3][3])
gboolean dt_iop_channelmixer_shared_white_preserving_from_matrix(const dt_iop_channelmixer_shared_primaries_basis_t basis, const float M[3][3], dt_iop_channelmixer_shared_white_preserving_params_t *white_preserving)
Read a white-preserving primaries state back from a mixer matrix.
gboolean dt_iop_channelmixer_shared_white_preserving_to_matrix(const dt_iop_channelmixer_shared_primaries_basis_t basis, const dt_iop_channelmixer_shared_white_preserving_params_t *const white_preserving, float M[3][3])
Build the mixer matrix of a white-preserving primaries state.
void dt_iop_channelmixer_shared_simple_to_sliders(const dt_iop_channelmixer_shared_simple_params_t *const simple, GtkWidget *const widgets[6])
dt_iop_channelmixer_shared_primaries_basis_t dt_iop_channelmixer_shared_primaries_basis_from_adaptation(const dt_adaptation_t adaptation)
void dt_iop_channelmixer_shared_work_rgb_to_display(const dt_aligned_pixel_t work_rgb, const dt_iop_order_iccprofile_info_t *const work_profile, const dt_iop_order_iccprofile_info_t *const display_profile, dt_aligned_pixel_t display_rgb)
void dt_iop_channelmixer_shared_paint_simple_sliders(const dt_adaptation_t adaptation, const dt_iop_order_iccprofile_info_t *const work_profile, const dt_iop_order_iccprofile_info_t *const display_profile, const dt_iop_channelmixer_shared_simple_params_t *const simple, GtkWidget *const widgets[6])
gboolean dt_iop_channelmixer_shared_primaries_from_matrix(const dt_iop_channelmixer_shared_primaries_basis_t basis, const float M[3][3], dt_iop_channelmixer_shared_primaries_params_t *primaries)
void dt_iop_channelmixer_shared_simple_from_matrix(const float M[3][3], dt_iop_channelmixer_shared_simple_params_t *simple)
gboolean dt_iop_channelmixer_shared_rows_are_normalized(const gboolean normalize[3])
float dt_iop_channelmixer_shared_roundtrip_error(const float M[3][3], const float roundtrip[3][3])
gboolean dt_iop_channelmixer_shared_get_matrix(const float rows[3][3], const gboolean normalize[3], const gboolean force_normalize, float M[3][3])
gboolean dt_iop_channelmixer_shared_primaries_to_matrix(const dt_iop_channelmixer_shared_primaries_basis_t basis, const dt_iop_channelmixer_shared_primaries_params_t *primaries, float M[3][3])
float dt_iop_channelmixer_shared_roundtrip_error_relative(const float M[3][3], const float roundtrip[3][3])
Roundtrip error, measured against the magnitude of the matrix itself.
void dt_iop_channelmixer_shared_primaries_from_sliders(GtkWidget *const widgets[9], dt_iop_channelmixer_shared_primaries_params_t *primaries)
dt_iop_channelmixer_shared_simple_probe_t
dt_iop_channelmixer_shared_primaries_basis_t
static const dt_aligned_pixel_simd_t const dt_adaptation_t adaptation
static const dt_adaptation_t kind
static void convert_D50_to_LMS(const dt_adaptation_t adaptation, dt_aligned_pixel_t D50)
@ DT_ADAPTATION_LAST
@ DT_ADAPTATION_FULL_BRADFORD
@ DT_ADAPTATION_XYZ
@ DT_ADAPTATION_CAT16
@ DT_ADAPTATION_RGB
@ DT_ADAPTATION_LINEAR_BRADFORD
static const float scaling
static const dt_aligned_pixel_simd_t illuminant
void dt_gui_hide_collapsible_section(dt_gui_collapsible_section_t *cs)
void dt_gui_new_collapsible_section(dt_gui_collapsible_section_t *cs, const char *confname, const char *label, GtkBox *parent, GtkPackType pack)
Create a collapsible section and pack it into the parent box.
void dt_gui_update_collapsible_section(dt_gui_collapsible_section_t *cs)
@ IOP_CS_RGB
void dt_iop_color_picker_reset(dt_iop_module_t *module, gboolean keep)
gboolean dt_iop_color_picker_is_active_module(const dt_iop_module_t *module)
Tell whether one module currently owns the active darkroom picker.
GtkWidget * dt_color_picker_new(dt_iop_module_t *module, dt_iop_color_picker_kind_t kind, GtkWidget *w)
@ DT_COLOR_PICKER_AREA
static int dt_colorchecker_find_color(GList **color_label)
Find all builtin and CGATS colorcheckers.
@ COLOR_CHECKER_XRITE_24_2000
@ COLOR_CHECKER_USER_REF
static void dt_colorchecker_cleanup(dt_color_checker_t *checker)
static dt_color_checker_t * dt_get_color_checker(const dt_color_checker_targets target_type, const char *cht_filename, const char *color_filename)
Build a color checker of the given type.
static void dt_colorchecker_label_list_cleanup(GList **colorcheckers)
static int dt_colorchecker_find(GList **colorcheckers_label)
Find all builtin and .cht colorcheckers.
static const float x
const int t
const float v
static void CAT16_adapt_D50(float4 *lms_in, const float4 origin_illuminant, const float D, const int full)
Definition colorspace.h:722
static float4 convert_XYZ_to_bradford_LMS(const float4 XYZ)
Definition colorspace.h:657
static float4 convert_XYZ_to_CAT16_LMS(const float4 XYZ)
Definition colorspace.h:677
static void bradford_adapt_D50(float4 *lms_in, const float4 origin_illuminant, const float p, const int full)
Definition colorspace.h:697
static void XYZ_adapt_D50(float4 *lms_in, const float4 origin_illuminant)
Definition colorspace.h:737
static float4 dt_XYZ_to_xyY(const float4 XYZ)
Definition colorspace.h:635
static float4 convert_CAT16_LMS_to_XYZ(const float4 LMS)
Definition colorspace.h:687
static float4 convert_bradford_LMS_to_XYZ(const float4 LMS)
Definition colorspace.h:667
#define B(y, x)
#define A(y, x)
__DT_CLONE_TARGETS__ int dt_colorspaces_conversion_matrices_rgb(const float *adobe_XYZ_to_CAM, double(*out_RGB_to_CAM)[3], double(*out_CAM_to_RGB)[4], const float *embedded_matrix, double *mul)
Compute the sRGB->camera and camera->sRGB matrices, and the default white balance multipliers.
struct _GtkWidget GtkWidget
GtkWidget, opaque, spelled exactly as GTK spells it.
Definition colorspaces.h:98
dt_aligned_pixel_t LMS
static dt_aligned_pixel_t xyY
dt_Lab_to_XYZ(Lab, XYZ)
dt_XYZ_to_sRGB(XYZ, result)
static dt_aligned_pixel_t XYZ
static dt_aligned_pixel_t Lab
const dt_colormatrix_t dt_aligned_pixel_t out
dt_store_simd_aligned(out, dt_mat3x4_mul_vec4(vin, dt_colormatrix_row_to_simd(matrix, 0), dt_colormatrix_row_to_simd(matrix, 1), dt_colormatrix_row_to_simd(matrix, 2)))
const float top
dt_XYZ_to_Lab(XYZ, Lab)
static dt_aligned_pixel_t RGB
static dt_aligned_pixel_t Lch
const float delta
static const dt_colormatrix_t M
void dt_conf_set_bool(const char *name, int val)
int dt_conf_get_bool(const char *name)
int dt_conf_key_exists(const char *key)
Does key have a value?
void dt_conf_set_float(const char *name, float val)
float dt_conf_get_float(const char *name)
Float for name, clamped to its declared bounds.
gchar * dt_conf_get_string(const char *name)
Read the stored string for name as a private copy.
void dt_conf_set_int(const char *name, int val)
int dt_conf_get_int(const char *name)
Integer for name, clamped to the bounds declared in the XML.
void dt_conf_set_string(const char *name, const char *val)
gboolean dt_conf_is_equal(const char *name, const char *value)
gboolean dt_image_is_matrix_correction_supported(const dt_image_t *img)
dt_image_pipe_class_t dt_image_pipe_class(const dt_image_t *img)
const char * dt_image_pipe_class_name(const dt_image_pipe_class_t klass)
gboolean dt_image_is_monochrome(const dt_image_t *img)
void dt_control_log(const char *msg,...)
Definition control.c:824
void dt_control_queue_redraw_center()
Request a redraw of the centre view.
Definition control.c:924
void dt_control_queue_redraw_widget(GtkWidget *widget)
threadsafe request of redraw of specific widget. Use this function if you need to redraw a specific w...
Definition control.c:969
#define dt_control_set_cursor_visible(visible)
Definition control.h:153
void * dt_alloc_align(size_t size)
Allocate cacheline-aligned memory.
Definition darktable.c:508
struct dt_bauhaus_t * dt_bauhaus_get_global(void)
Definition darktable.c:646
#define dt_dev_add_history_item(dev, module, enable, redraw)
void dt_iop_params_t
Definition dev_history.h:43
#define dt_dev_pixelpipe_resync_history_preview(dev)
int32_t dt_dev_roi_request_preview_height(const dt_develop_t *dev)
int32_t dt_dev_roi_request_preview_width(const dt_develop_t *dev)
dt_dev_roi_request_t dt_dev_roi_request_of_pipe(const dt_dev_pixelpipe_t *pipe)
void default_input_format(dt_iop_module_t *self, dt_dev_pixelpipe_t *pipe, dt_dev_pixelpipe_iop_t *piece, dt_iop_buffer_dsc_t *dsc)
dt_iop_order_iccprofile_info_t * dt_ioppr_get_pipe_output_profile_info(const struct dt_dev_pixelpipe_t *pipe)
dt_iop_order_iccprofile_info_t * dt_ioppr_get_pipe_work_profile_info(const struct dt_dev_pixelpipe_t *pipe)
dt_iop_order_iccprofile_info_t * dt_ioppr_get_pipe_current_profile_info(dt_iop_module_t *module, const struct dt_dev_pixelpipe_t *pipe)
dt_iop_order_iccprofile_info_t * dt_ioppr_get_pipe_input_profile_info(const struct dt_dev_pixelpipe_t *pipe)
float dt_dev_get_overlay_scale(dt_develop_t *dev)
Get the overlay scale factor in GUI logical coordinates.
Definition develop.c:1959
void dt_dev_coordinates_image_norm_to_preview_abs(dt_develop_t *dev, float *points, size_t num_points)
Definition develop.c:1291
gboolean dt_dev_rescale_roi(dt_develop_t *dev, cairo_t *cr, int32_t width, int32_t height)
Scale the ROI to fit within given width/height, centered.
Definition develop.c:2071
void dt_dev_coordinates_widget_to_image_norm(dt_develop_t *dev, float *points, size_t num_points)
Coordinate conversion helpers between widget, normalized image, and absolute image spaces.
Definition develop.c:1144
static void weight(const float *c1, const float *c2, const float sharpen, dt_aligned_pixel_t weight)
Definition eaw.c:29
void dt_loc_get_user_config_dir(char *configdir, size_t bufsize)
@ TYPE_FLOAT
Definition format.h:56
static int pseudo_solve_gaussian(double *const restrict A, double *const restrict y, const size_t m, const size_t n, const int checks)
void dt_gui_presets_add_generic(const char *name, dt_dev_operation_t op, const int32_t version, const void *params, const int32_t params_size, const int32_t enabled)
#define DT_GUI_MODULE(x)
point_t apply_homography(point_t p, const float *h)
Definition homography.c:69
float apply_homography_scaling(point_t p, const float *h)
Definition homography.c:80
int get_homography(const point_t *source, const point_t *target, float *h)
Definition homography.c:28
static int illuminant_to_xy(const dt_illuminant_t illuminant, const dt_image_t *img, const dt_aligned_pixel_t custom_wb, float *x_out, float *y_out, const float t, const dt_illuminant_fluo_t fluo, const dt_illuminant_led_t iled)
dt_illuminant_t
Definition illuminants.h:35
@ DT_ILLUMINANT_A
Definition illuminants.h:37
@ DT_ILLUMINANT_PIPE
Definition illuminants.h:36
@ DT_ILLUMINANT_CAMERA
Definition illuminants.h:46
@ DT_ILLUMINANT_BB
Definition illuminants.h:42
@ DT_ILLUMINANT_F
Definition illuminants.h:40
@ DT_ILLUMINANT_CUSTOM
Definition illuminants.h:43
@ DT_ILLUMINANT_LED
Definition illuminants.h:41
@ DT_ILLUMINANT_DETECT_EDGES
Definition illuminants.h:45
@ DT_ILLUMINANT_E
Definition illuminants.h:39
@ DT_ILLUMINANT_LAST
Definition illuminants.h:47
@ DT_ILLUMINANT_DETECT_SURFACES
Definition illuminants.h:44
@ DT_ILLUMINANT_D
Definition illuminants.h:38
static void illuminant_xy_to_RGB(const float x, const float y, dt_aligned_pixel_t RGB)
static void illuminant_xy_to_XYZ(const float x, const float y, dt_aligned_pixel_t XYZ)
static float xy_to_CCT(const float x, const float y)
dt_illuminant_led_t
Definition illuminants.h:70
@ DT_ILLUMINANT_LED_B5
Definition illuminants.h:75
@ DT_ILLUMINANT_LED_LAST
Definition illuminants.h:80
static float CCT_reverse_lookup(const float x, const float y)
static int find_temperature_from_raw_coeffs(const dt_image_t *img, const dt_aligned_pixel_t custom_wb, float *chroma_x, float *chroma_y)
static void matrice_pseudoinverse(float(*in)[3], float(*out)[3], int size)
dt_illuminant_fluo_t
Definition illuminants.h:52
@ DT_ILLUMINANT_FLUO_F3
Definition illuminants.h:55
@ DT_ILLUMINANT_FLUO_LAST
Definition illuminants.h:65
static void xy_to_uv(const float xy[2], float uv[2])
const char * tooltip
Definition image.h:310
@ DT_IMAGE_4BAYER
Definition image.h:140
static __DT_CLONE_TARGETS__ void dt_simd_memcpy(const float *const __restrict__ in, float *const __restrict__ out, const size_t num_elem)
Definition imagebuf.h:72
static void dt_iop_image_copy_by_size(float *const __restrict__ out, const float *const __restrict__ in, const size_t width, const size_t height, const size_t ch)
Definition imagebuf.h:91
dt_iop_module_t * dt_iop_get_module_from_list(GList *iop_list, const char *op)
Definition imageop.c:1697
gboolean dt_iop_is_first_instance(GList *modules, dt_iop_module_t *module)
Definition imageop.c:1875
void dt_iop_default_init(dt_iop_module_t *module)
Definition imageop.c:308
const char ** dt_iop_set_description(dt_iop_module_t *module, const char *main_text, const char *purpose, const char *input, const char *process, const char *output)
Definition imageop.c:1893
void dt_iop_set_cache_bypass(dt_iop_module_t *module, gboolean state)
Definition imageop.c:1669
@ DT_REQUEST_COLORPICK_OFF
Definition imageop.h:215
void dt_iop_gui_leave_critical_section(dt_iop_module_t *const module)
Release what dt_iop_gui_enter_critical_section() took. Also a no-op headless.
void dt_iop_request_focus(dt_iop_module_t *module)
Move darkroom focus to module, or clear it with NULL.
#define dt_iop_fmt_log(module, fmt,...)
Debug helper to trace a module's input-format-driven decisions on the -d pipe channel (DT_DEBUG_PIPE)...
Definition imageop.h:535
@ IOP_FLAGS_INCLUDE_IN_STYLES
Definition imageop.h:185
@ IOP_FLAGS_SUPPORTS_BLENDING
Definition imageop.h:186
@ IOP_FLAGS_ALLOW_TILING
Definition imageop.h:188
void dt_iop_gui_enter_critical_section(dt_iop_module_t *const module)
Take the module's GUI lock, serialising access to its dt_iop_gui_data_t.
@ IOP_GROUP_COLOR
Definition imageop.h:158
GtkWidget * dt_bauhaus_toggle_from_params(dt_iop_module_t *self, const char *param)
GtkWidget * dt_bauhaus_slider_from_params(dt_iop_module_t *self, const char *param)
GtkWidget * dt_bauhaus_combobox_from_params(dt_iop_module_t *self, const char *param)
#define IOP_GUI_FREE
Definition imageop_gui.h:96
static dt_iop_gui_data_t * dt_iop_gui_data(const struct dt_iop_module_t *m)
The module's GUI data blob, NULL-safe for headless callers: IOP process() implementations read it for...
Definition imageop_gui.h:81
#define IOP_GUI_ALLOC(module)
Definition imageop_gui.h:93
void *const ovoid
static float kernel(const float *x, const float *y)
struct dt_iop_tonecurve_params_t preset
GtkWidget * dt_ui_section_label_new(const gchar *str)
Definition label.c:114
GtkWidget * dt_ui_label_new(const gchar *str)
Definition label.c:125
static float mix(const float a, const float b, const float t)
Definition liquify.c:751
_lib_location_type_t type
Definition location.c:1
@ DT_DEBUG_OPENCL
Definition logging.h:57
@ DT_DEBUG_ALWAYS
Definition logging.h:49
@ DT_DEBUG_DEV
Definition logging.h:53
void dt_print(dt_debug_thread_t thread, const char *msg,...) __attribute__((format(printf
Print to stdout when thread is enabled, prefixed with seconds since startup.
float *const restrict const size_t k
float *const restrict const size_t const size_t ch
#define IS_NULL_PTR(p)
C is way too permissive with !=, == and if(var) checks, which can mean too many things depending on w...
Definition macros.h:96
#define R
#define DT_FMA(x, y, z)
Definition math.h:64
#define NORM_MIN
Definition math.h:37
#define M_PI
Definition math.h:47
static void transpose_3x3_to_3xSSE(const float input[9], dt_colormatrix_t output)
Definition matrices.h:92
float DT_ALIGNED_ARRAY dt_colormatrix_t[4][4]
Definition matrices.h:34
static void transpose_3xSSE(const dt_colormatrix_t input, dt_colormatrix_t output)
Definition matrices.h:69
static void pack_3xSSE_to_3x4(const dt_colormatrix_t input, float output[12])
Definition matrices.h:150
static void repack_double3x3_to_3xSSE(const double input[9], dt_colormatrix_t output)
Definition matrices.h:114
static void pack_3xSSE_to_3x3(const dt_colormatrix_t input, float output[9])
Definition matrices.h:136
#define DT_ALIGNED_PIXEL
Align a 4-float pixel on 16 bytes, enough for SSE. Same struct-member caveat as DT_ALIGNED_ARRAY,...
Definition mem_alloc.h:85
#define dt_free_align(ptr)
Release memory from dt_alloc_align() and set ptr to NULL.
Definition mem_alloc.h:214
static void * dt_calloc_align(size_t size)
dt_alloc_align() followed by a zero fill.
Definition mem_alloc.h:225
static float * dt_alloc_align_float(size_t pixels)
Allocate pixels floats, cacheline-aligned and marked as such.
Definition mem_alloc.h:235
#define dt_free(ptr)
g_free() ptr and set it to NULL, skipping both if it is already NULL.
Definition mem_alloc.h:171
uint32_t width
Definition mipmap_cache.c:0
uint32_t height
Definition mipmap_cache.c:1
#define DT_MODULE_INTROSPECTION(MODVER, PARAMSTYPE)
DT_MODULE() for a module whose params struct is introspected.
GtkWidget * dt_ui_notebook_page(GtkNotebook *notebook, const char *text, const char *tooltip)
Definition notebook.c:88
GtkNotebook * dt_ui_notebook_new()
Definition notebook.c:83
void dt_ui_notebook_set_picker_owner(GtkNotebook *notebook, gpointer owner)
Register an opaque owner for a GtkNotebook's page switches, and report every "switch_page" to the hos...
Definition notebook.c:118
static double reference(const op_t op, const double x)
int dt_opencl_enqueue_kernel_2d(const int dev, const int kernel, const size_t *sizes)
Definition opencl.c:2554
int dt_opencl_create_kernel(const int prog, const char *name)
Definition opencl.c:2448
void * dt_opencl_copy_host_to_device_constant(const int devid, const size_t size, void *host)
Definition opencl.c:2750
void dt_opencl_free_kernel(const int kernel)
Definition opencl.c:2491
int dt_opencl_set_kernel_arg(const int dev, const int kernel, const int num, const size_t size, const void *arg)
Definition opencl.c:2545
void dt_opencl_release_mem_object(cl_mem mem)
Definition opencl.c:2805
#define ROUNDUPDHT(a, b)
Definition opencl.h:86
#define ROUNDUPDWD(a, b)
Definition opencl.h:85
#define dt_omploop_sfence()
Definition openmp.h:164
#define __OMP_DECLARE_SIMD__(...)
Definition openmp.h:100
#define __OMP_PARALLEL_FOR__(...)
Definition openmp.h:95
#define DT_PATH_MAX
Buffer size for a filesystem path anywhere in Ansel.
Definition paths.h:57
@ DT_DEV_PIXELPIPE_PREVIEW
Definition pixelpipe.h:44
@ DT_DEV_PIXELPIPE_FULL
Definition pixelpipe.h:43
#define dt_pixelpipe_cache_alloc_align_float_cache(pixels, id)
#define dt_pixelpipe_cache_free_align(mem)
static cairo_surface_t * dt_cairo_image_surface_create(cairo_format_t format, int width, int height)
#define DT_DEBUG_CONTROL_SIGNAL_DISCONNECT(ctlsig, cb, user_data)
Definition signal.h:407
struct dt_control_signal_t * dt_control_signal_get_global(void)
Definition darktable.c:616
@ DT_SIGNAL_DEVELOP_PREVIEW_PIPE_FINISHED
This signal is raised when develop preview pipe process is finished no param, no returned value.
Definition signal.h:177
@ DT_SIGNAL_DEVELOP_UI_PIPE_FINISHED
This signal is raised when pipe is finished and the gui is attached no param, no returned value.
Definition signal.h:182
#define DT_DEBUG_CONTROL_SIGNAL_CONNECT(ctlsig, signal, cb, user_data)
Definition signal.h:396
DT_ALIGNED_PIXEL float dt_aligned_pixel_t[4]
Definition simd.h:53
#define for_each_channel(_var,...)
Definition simd.h:87
float dt_aligned_pixel_simd_t __attribute__((vector_size(16), aligned(16)))
Apply one channel's tone curve to each of the three colour channels, or pass the channel through unto...
Definition simd.h:55
const float uint32_t state[4]
const float r
dt_aligned_pixel_t Lab
dt_color_checker_patch * values
dt_color_checker_targets type
dt_iop_buffer_dsc_t dsc_in
struct dt_iop_module_t *void * data
dt_dev_pixelpipe_type_t type
struct dt_develop_t * dev
The coherent set of numbers a darkroom pipe plans its ROI from.
int32_t gui_attached
Definition develop.h:167
dt_image_t image_storage
Definition develop.h:225
GList * iop
Definition develop.h:269
dt_aligned_pixel_t wb_coeffs
Definition develop.h:428
struct dt_iop_module_t * chroma_adaptation
Definition develop.h:424
struct dt_develop_t::@14 proxy
struct dt_dev_pixelpipe_t * pipe
Definition develop.h:214
int32_t flags
Definition image.h:401
float d65_color_matrix[9]
Definition image.h:421
float adobe_XYZ_to_CAM[4][3]
Definition image.h:449
dt_aligned_pixel_t coeffs
Definition format.h:110
unsigned int channels
Definition format.h:83
dt_iop_buffer_type_t datatype
Definition format.h:85
struct dt_iop_buffer_dsc_t::@56 temperature
dt_iop_channelmixer_rgb_version_t version
float DT_ALIGNED_PIXEL lightness[4]
float DT_ALIGNED_PIXEL grey[4]
float DT_ALIGNED_PIXEL saturation[4]
The affine map taking chart bounding-box coordinates to the buffer process() was handed.
dt_gui_collapsible_section_t csspot
dt_gui_collapsible_section_t cs
dt_iop_channelmixer_rgb_version_t version
White-preserving primaries model : per-primary rotation and saturation, no white freedom.
GtkWidget * widget
Definition imageop_gui.h:47
dt_dev_operation_t op
Definition imageop.h:235
dt_iop_global_data_t * data
Definition imageop.h:238
dt_dev_request_colorpick_flags_t request_color_pick
Definition imageop.h:272
dt_iop_params_t * default_params
Definition imageop.h:333
struct dt_iop_module_gui_t * gui
Definition imageop.h:346
struct dt_develop_t * dev
Definition imageop.h:311
dt_iop_global_data_t * global_data
Definition imageop.h:337
gboolean enabled
Definition imageop.h:313
dt_aligned_pixel_t picked_color_min
Definition imageop.h:287
dt_aligned_pixel_t picked_color_max
Definition imageop.h:287
dt_aligned_pixel_t picked_color
Definition imageop.h:287
dt_iop_params_t * params
Definition imageop.h:333
A profile reduced to the arithmetic the pixel loop can run: two matrices and six tone-curve LUTs,...
int nonlinearlut
Non-zero when the profile has tone curves at all; tested as a boolean everywhere, but it is really th...
int lutsize
Entry count of each of the six LUTs. Always 65536 in practice: both callers of dt_ioppr_init_profile_...
float grey
Luminance of 18.42% grey through this profile, used by the tone-mapping modules to place middle grey....
float * lut_out[3]
Per-channel linear -> encoded tone curve, same convention as lut_in.
dt_colormatrix_t matrix_out
XYZ (D50) -> RGB, row-major; the inverse of matrix_in.
float * lut_in[3]
Per-channel encoded -> linear tone curve, lutsize entries each, sampled over [0,1]....
dt_colormatrix_t matrix_in
RGB -> XYZ (D50), row-major. matrix_in[1][*] is the luminance row. NaN in [0][0] marks the whole prof...
Region of interest passed through the pixelpipe.
Definition format.h:49
int width
Definition format.h:50
int height
Definition format.h:50
size_t y
Definition censorize.c:78
size_t x
Definition censorize.c:78
#define __DT_CLONE_TARGETS__
gboolean dt_gui_widgets_suppressed(void)
#define dt_gui_freeze_begin()
#define dt_gui_freeze_end()
#define DT_GUI_BOX_SPACING
#define DT_PIXEL_APPLY_DPI(value)
void dtgtk_cairo_paint_refresh(cairo_t *cr, gint x, gint y, gint w, gint h, gint flags, void *data)
void dtgtk_cairo_paint_check_mark(cairo_t *cr, gint x, gint y, gint w, gint h, gint flags, void *data)
void dtgtk_cairo_paint_softproof(cairo_t *cr, gint x, gint y, gint w, gint h, gint flags, void *data)