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
324
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
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);
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 {
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 gboolean run_profile_callback(GtkWidget *widget __attribute__((unused)),
2978 GdkEventButton *event __attribute__((unused)), gpointer user_data)
2979{
2980 if(dt_gui_widgets_suppressed()) return FALSE;
2981 dt_iop_module_t *self = (dt_iop_module_t *)user_data;
2983
2985 g->run_profile = TRUE;
2989 return TRUE;
2990}
2991
2992static gboolean run_validation_callback(GtkWidget *widget __attribute__((unused)),
2993 GdkEventButton *event __attribute__((unused)), gpointer user_data)
2994{
2995 if(dt_gui_widgets_suppressed()) return FALSE;
2996 dt_iop_module_t *self = (dt_iop_module_t *)user_data;
2998
3000 g->run_validation = TRUE;
3004 return TRUE;
3005}
3006
3007static gboolean commit_profile_callback(GtkWidget *widget __attribute__((unused)),
3008 GdkEventButton *event __attribute__((unused)), gpointer user_data)
3009{
3010 if(dt_gui_widgets_suppressed()) return FALSE;
3011 dt_iop_module_t *self = (dt_iop_module_t *)user_data;
3014
3015 if(!g->profile_ready) return FALSE;
3016
3018
3019 p->x = g->xy[0];
3020 p->y = g->xy[1];
3021 p->illuminant = DT_ILLUMINANT_CUSTOM;
3022 check_if_close_to_daylight(p->x, p->y, &p->temperature, NULL, NULL);
3023
3024 p->red[0] = g->mix[0][0];
3025 p->red[1] = g->mix[0][1];
3026 p->red[2] = g->mix[0][2];
3027
3028 p->green[0] = g->mix[1][0];
3029 p->green[1] = g->mix[1][1];
3030 p->green[2] = g->mix[1][2];
3031
3032 p->blue[0] = g->mix[2][0];
3033 p->blue[1] = g->mix[2][1];
3034 p->blue[2] = g->mix[2][2];
3035
3037
3039 dt_bauhaus_combobox_set(g->illuminant, p->illuminant);
3040 dt_bauhaus_slider_set(g->temperature, p->temperature);
3041
3042 dt_aligned_pixel_t xyY = { p->x, p->y, 1.f };
3043 dt_aligned_pixel_t Lch = { 0 };
3044 dt_xyY_to_Lch(xyY, Lch);
3045 dt_bauhaus_slider_set(g->illum_x, Lch[2] / M_PI * 180.f);
3046 dt_bauhaus_slider_set(g->illum_y, Lch[1]);
3047
3048 dt_bauhaus_slider_set(g->scale_red_R, p->red[0]);
3049 dt_bauhaus_slider_set(g->scale_red_G, p->red[1]);
3050 dt_bauhaus_slider_set(g->scale_red_B, p->red[2]);
3051
3052 dt_bauhaus_slider_set(g->scale_green_R, p->green[0]);
3053 dt_bauhaus_slider_set(g->scale_green_G, p->green[1]);
3054 dt_bauhaus_slider_set(g->scale_green_B, p->green[2]);
3055
3056 dt_bauhaus_slider_set(g->scale_blue_R, p->blue[0]);
3057 dt_bauhaus_slider_set(g->scale_blue_G, p->blue[1]);
3058 dt_bauhaus_slider_set(g->scale_blue_B, p->blue[2]);
3059
3061
3062 gui_changed(self, NULL, NULL);
3063
3064 dt_print(DT_DEBUG_DEV, "[picker/channelmixerrgb] history commit source=commit_profile\n");
3065 dt_dev_add_history_item(self->dev, self, TRUE, TRUE);
3066 return TRUE;
3067}
3068
3069static void _develop_ui_pipe_finished_callback(gpointer instance, gpointer user_data)
3070{
3071 dt_iop_module_t *self = (dt_iop_module_t *)user_data;
3074
3075 if(IS_NULL_PTR(g)) return;
3076 if(p->illuminant != DT_ILLUMINANT_DETECT_EDGES && p->illuminant != DT_ILLUMINANT_DETECT_SURFACES)
3077 return;
3078
3080 p->x = g->XYZ[0];
3081 p->y = g->XYZ[1];
3083
3084 check_if_close_to_daylight(p->x, p->y, &p->temperature, &p->illuminant, &p->adaptation);
3085
3087
3088 dt_bauhaus_slider_set(g->temperature, p->temperature);
3089 dt_bauhaus_combobox_set(g->illuminant, p->illuminant);
3090 dt_bauhaus_combobox_set(g->adaptation, p->adaptation);
3091
3092 const dt_aligned_pixel_t xyY = { p->x, p->y, 1.f };
3094 dt_xyY_to_Lch(xyY, Lch);
3095 dt_bauhaus_slider_set(g->illum_x, Lch[2] / M_PI * 180.f);
3096 dt_bauhaus_slider_set(g->illum_y, Lch[1]);
3097
3098 update_illuminants(self);
3099 update_approx_cct(self);
3102
3104
3105 gui_changed(self, NULL, NULL);
3106
3107 dt_print(DT_DEBUG_DEV, "[picker/channelmixerrgb] history commit source=ui_pipe_finished\n");
3108 dt_dev_add_history_item(self->dev, self, TRUE, TRUE);
3109}
3110
3111static void _preview_pipe_finished_callback(gpointer instance, gpointer user_data)
3112{
3113 dt_iop_module_t *self = (dt_iop_module_t *)user_data;
3115
3117 gtk_label_set_markup(GTK_LABEL(g->label_delta_E), g->delta_E_label_text);
3119}
3120
3123{
3127
3128 d->version = p->version;
3129
3130 float norm_R = 1.0f;
3131 if(p->normalize_R) norm_R = p->red[0] + p->red[1] + p->red[2];
3132
3133 float norm_G = 1.0f;
3134 if(p->normalize_G) norm_G = p->green[0] + p->green[1] + p->green[2];
3135
3136 float norm_B = 1.0f;
3137 if(p->normalize_B) norm_B = p->blue[0] + p->blue[1] + p->blue[2];
3138
3139 float norm_sat = 0.0f;
3140 if(p->normalize_sat) norm_sat = (p->saturation[0] + p->saturation[1] + p->saturation[2]) / 3.f;
3141
3142 float norm_light = 0.0f;
3143 if(p->normalize_light) norm_light = (p->lightness[0] + p->lightness[1] + p->lightness[2]) / 3.f;
3144
3145 float norm_grey = p->grey[0] + p->grey[1] + p->grey[2];
3146 d->apply_grey = (p->grey[0] != 0.f) || (p->grey[1] != 0.f) || (p->grey[2] != 0.f);
3147 if(!p->normalize_grey || norm_grey == 0.f) norm_grey = 1.f;
3148
3149 for(int i = 0; i < 3; i++)
3150 {
3151 d->MIX[0][i] = p->red[i] / norm_R;
3152 d->MIX[1][i] = p->green[i] / norm_G;
3153 d->MIX[2][i] = p->blue[i] / norm_B;
3154 d->saturation[i] = -p->saturation[i] + norm_sat;
3155 d->lightness[i] = p->lightness[i] - norm_light;
3156 d->grey[i] = p->grey[i] / norm_grey; // = NaN if (norm_grey == 0.f) but we don't care since (d->apply_grey == FALSE)
3157 }
3158
3159 if(p->version == CHANNELMIXERRGB_V_1)
3160 {
3161 // for the v1 saturation algo, the effect of R and B coeffs is reversed
3162 d->saturation[0] = -p->saturation[2] + norm_sat;
3163 d->saturation[2] = -p->saturation[0] + norm_sat;
3164 }
3165
3166 // just in case compiler feels clever and uses SSE 4x1 dot product
3167 d->saturation[CHANNEL_SIZE - 1] = 0.0f;
3168 d->lightness[CHANNEL_SIZE - 1] = 0.0f;
3169 d->grey[CHANNEL_SIZE - 1] = 0.0f;
3170
3171 d->adaptation = p->adaptation;
3172 d->clip = p->clip;
3173 d->gamut = (p->gamut == 0.f) ? p->gamut : 1.f / p->gamut;
3174
3175 // find x y coordinates of illuminant for CIE 1931 2° observer
3176 float x = p->x;
3177 float y = p->y;
3178 dt_aligned_pixel_t custom_wb;
3179 // Pipeline thread: source temperature's WB from the per-piece descriptor propagated in pipe order
3180 // (temperature writes piece->dsc_out.temperature.coeffs upstream), NOT from dev->proxy.
3181 _custom_wb_from_coeffs(self, piece->dsc_in.temperature.coeffs, custom_wb);
3182 illuminant_to_xy(p->illuminant, &(self->dev->image_storage), custom_wb, &x, &y, p->temperature, p->illum_fluo, p->illum_led);
3183
3184 // if illuminant is set as camera, x and y are set on-the-fly at commit time, so we need to set adaptation too
3185 if(p->illuminant == DT_ILLUMINANT_CAMERA)
3186 check_if_close_to_daylight(x, y, NULL, NULL, &(d->adaptation));
3187
3188 d->illuminant_type = p->illuminant;
3189
3190 // Convert illuminant from xyY to XYZ
3193
3194 // Convert illuminant from XYZ to Bradford modified LMS
3195 dt_store_simd_aligned(d->illuminant, convert_any_XYZ_to_LMS(dt_load_simd_aligned(XYZ), d->adaptation));
3196 d->illuminant[3] = 0.f;
3197
3198 dt_iop_fmt_log(self, "commit: class=%s matrix_supported=%d illuminant=%d adaptation=%d custom_wb=[%.4f %.4f %.4f %.4f] xy=[%.4f %.4f]",
3201 p->illuminant, d->adaptation, custom_wb[0], custom_wb[1], custom_wb[2], custom_wb[3], x, y);
3202
3203 //fprintf(stdout, "illuminant: %i\n", p->illuminant);
3204 //fprintf(stdout, "x: %f, y: %f\n", x, y);
3205 //fprintf(stdout, "X: %f - Y: %f - Z: %f\n", XYZ[0], XYZ[1], XYZ[2]);
3206 //fprintf(stdout, "L: %f - M: %f - S: %f\n", d->illuminant[0], d->illuminant[1], d->illuminant[2]);
3207
3208 // blue compensation for Bradford transform = (test illuminant blue / reference illuminant blue)^0.0834
3209 // reference illuminant is hard-set D50 for darktable's pipeline
3210 // test illuminant is user params
3211 d->p = powf(0.818155f / d->illuminant[2], 0.0834f);
3212
3213 // Disable OpenCL path if we are in any kind of diagnose mode (only C path has diagnostics)
3214 if(self->dev->gui_attached && g)
3215 {
3216 if( (g->run_profile && pipe->type == DT_DEV_PIXELPIPE_PREVIEW) || // color checker extraction mode
3217 (g->run_validation && pipe->type == DT_DEV_PIXELPIPE_PREVIEW) || // delta E validation
3218 ( (d->illuminant_type == DT_ILLUMINANT_DETECT_EDGES ||
3219 d->illuminant_type == DT_ILLUMINANT_DETECT_SURFACES ) && // WB extraction mode
3221 {
3222 piece->process_cl_ready = 0;
3223
3224 // Same reason, and the one nobody wrote down: all three modes consume the WHOLE frame in a
3225 // single call -- the two colour-checker paths address it with chart-box coordinates, and
3226 // auto_detect_WB() averages it. Tiling calls process() once per tile with a tile-local
3227 // roi_in, so the chart box would be applied to a fragment and whichever tile ran last would
3228 // win. Whether that happens is a runtime memory decision (dt_tiling_piece_fits_host_memory(),
3229 // which reads dt_dev_pixelpipe_cache_get_available() and the pixelpipe cache's largest free
3230 // run), so it shows up on one machine and not another.
3231 piece->process_tiling_ready = 0;
3232 }
3233 }
3234}
3235
3236
3238{
3241
3242 if(p->adaptation == DT_ADAPTATION_RGB || p->adaptation == DT_ADAPTATION_LAST)
3243 {
3244 // user disabled CAT at all, hide everything and exit
3245 gtk_widget_set_visible(g->illuminant, FALSE);
3246 gtk_widget_set_visible(g->illum_color, FALSE);
3247 gtk_widget_set_visible(g->approx_cct, FALSE);
3248 gtk_widget_set_visible(g->color_picker, FALSE);
3249 gtk_widget_set_visible(g->temperature, FALSE);
3250 gtk_widget_set_visible(g->illum_fluo, FALSE);
3251 gtk_widget_set_visible(g->illum_led, FALSE);
3252 gtk_widget_set_visible(g->illum_x, FALSE);
3253 gtk_widget_set_visible(g->illum_y, FALSE);
3254 return;
3255 }
3256 else
3257 {
3258 // set everything visible again and carry on
3259 gtk_widget_set_visible(g->illuminant, TRUE);
3260 gtk_widget_set_visible(g->illum_color, TRUE);
3261 gtk_widget_set_visible(g->approx_cct, TRUE);
3262 gtk_widget_set_visible(g->color_picker, TRUE);
3263 gtk_widget_set_visible(g->temperature, TRUE);
3264 gtk_widget_set_visible(g->illum_fluo, TRUE);
3265 gtk_widget_set_visible(g->illum_led, TRUE);
3266 gtk_widget_set_visible(g->illum_x, TRUE);
3267 }
3268
3269 // Display only the relevant sliders
3270 switch(p->illuminant)
3271 {
3272 case DT_ILLUMINANT_PIPE:
3273 case DT_ILLUMINANT_A:
3274 case DT_ILLUMINANT_E:
3275 {
3276 gtk_widget_set_visible(g->adaptation, TRUE);
3277 gtk_widget_set_visible(g->temperature, FALSE);
3278 gtk_widget_set_visible(g->illum_fluo, FALSE);
3279 gtk_widget_set_visible(g->illum_led, FALSE);
3280 gtk_widget_set_visible(g->illum_x, FALSE);
3281 gtk_widget_set_visible(g->illum_y, FALSE);
3282 break;
3283 }
3284 case DT_ILLUMINANT_D:
3285 case DT_ILLUMINANT_BB:
3286 {
3287 gtk_widget_set_visible(g->adaptation, TRUE);
3288 gtk_widget_set_visible(g->temperature, TRUE);
3289 gtk_widget_set_visible(g->illum_fluo, FALSE);
3290 gtk_widget_set_visible(g->illum_led, FALSE);
3291 gtk_widget_set_visible(g->illum_x, FALSE);
3292 gtk_widget_set_visible(g->illum_y, FALSE);
3293 break;
3294 }
3295 case DT_ILLUMINANT_F:
3296 {
3297 gtk_widget_set_visible(g->adaptation, TRUE);
3298 gtk_widget_set_visible(g->temperature, FALSE);
3299 gtk_widget_set_visible(g->illum_fluo, TRUE);
3300 gtk_widget_set_visible(g->illum_led, FALSE);
3301 gtk_widget_set_visible(g->illum_x, FALSE);
3302 gtk_widget_set_visible(g->illum_y, FALSE);
3303 break;
3304 }
3305 case DT_ILLUMINANT_LED:
3306 {
3307 gtk_widget_set_visible(g->adaptation, TRUE);
3308 gtk_widget_set_visible(g->temperature, FALSE);
3309 gtk_widget_set_visible(g->illum_fluo, FALSE);
3310 gtk_widget_set_visible(g->illum_led, TRUE);
3311 gtk_widget_set_visible(g->illum_x, FALSE);
3312 gtk_widget_set_visible(g->illum_y, FALSE);
3313 break;
3314 }
3316 {
3317 gtk_widget_set_visible(g->adaptation, TRUE);
3318 gtk_widget_set_visible(g->temperature, FALSE);
3319 gtk_widget_set_visible(g->illum_fluo, FALSE);
3320 gtk_widget_set_visible(g->illum_led, FALSE);
3321 gtk_widget_set_visible(g->illum_x, TRUE);
3322 gtk_widget_set_visible(g->illum_y, TRUE);
3323 break;
3324 }
3326 {
3327 gtk_widget_set_visible(g->adaptation, TRUE);
3328 gtk_widget_set_visible(g->temperature, FALSE);
3329 gtk_widget_set_visible(g->illum_fluo, FALSE);
3330 gtk_widget_set_visible(g->illum_led, FALSE);
3331 gtk_widget_set_visible(g->illum_x, FALSE);
3332 gtk_widget_set_visible(g->illum_y, FALSE);
3333 break;
3334 }
3337 {
3338 gtk_widget_set_visible(g->adaptation, FALSE);
3339 gtk_widget_set_visible(g->temperature, FALSE);
3340 gtk_widget_set_visible(g->illum_fluo, FALSE);
3341 gtk_widget_set_visible(g->illum_led, FALSE);
3342 gtk_widget_set_visible(g->illum_x, FALSE);
3343 gtk_widget_set_visible(g->illum_y, FALSE);
3344 break;
3345 }
3346 case DT_ILLUMINANT_LAST:
3347 default:
3348 {
3349 break;
3350 }
3351 }
3352}
3353
3382{
3383 // update the fill background color of x, y sliders
3386
3387 if(IS_NULL_PTR(g) || IS_NULL_PTR(p) || IS_NULL_PTR(g->illum_x) || IS_NULL_PTR(g->illum_y)) return;
3388
3389 // Varies x in range around current y param
3390 for(int i = 0; i < DT_BAUHAUS_SLIDER_MAX_STOPS; i++)
3391 {
3392 const float stop = ((float)i / (float)(DT_BAUHAUS_SLIDER_MAX_STOPS - 1));
3393 const float x = stop * ILLUM_X_MAX;
3394 dt_aligned_pixel_t RGB = { 0 };
3395
3396 dt_aligned_pixel_t Lch = { 100.f, 50.f, x / 180.f * M_PI };
3397 dt_aligned_pixel_t xyY = { 0 };
3398 dt_Lch_to_xyY(Lch, xyY);
3400 dt_bauhaus_slider_set_stop(g->illum_x, stop, RGB[0], RGB[1], RGB[2]);
3401
3402 const float y = stop * ILLUM_Y_MAX / 2.0f;
3403
3404 // Find current hue
3405 const dt_aligned_pixel_t xyY2 = { p->x, p->y, 1.f };
3406 dt_xyY_to_Lch(xyY2, Lch);
3407
3408 // Replace chroma by current step
3409 Lch[0] = 75.f;
3410 Lch[1] = y;
3411
3412 // Go back to xyY
3413 dt_Lch_to_xyY(Lch, xyY);
3415 dt_bauhaus_slider_set_stop(g->illum_y, stop, RGB[0], RGB[1], RGB[2]);
3416 }
3417
3418 gtk_widget_queue_draw(g->illum_x);
3419 gtk_widget_queue_draw(g->illum_y);
3420}
3421
3422static void paint_hue(dt_iop_module_t *self)
3423{
3424 // update the fill background color of LCh sliders
3426
3427 const float hue = dt_bauhaus_slider_get(g->hue_spot);
3428
3429 for(int i = 0; i < DT_BAUHAUS_SLIDER_MAX_STOPS; i++)
3430 {
3431 const float stop = ((float)i / (float)(DT_BAUHAUS_SLIDER_MAX_STOPS - 1));
3432 dt_aligned_pixel_t RGB = { 0 }, Lab = { 0 }, XYZ = { 0 };
3433
3434 const dt_aligned_pixel_t Lch_hue = { 67.f, 96.f, (stop * HUE_MAX) / 360.f};
3435
3436 dt_LCH_2_Lab(Lch_hue, Lab);
3439
3440 dt_bauhaus_slider_set_stop(g->hue_spot, stop, RGB[0], RGB[1], RGB[2]);
3441
3442 const dt_aligned_pixel_t Lch_lightness = { stop * LIGHTNESS_MAX, 0.f, 0. };
3443
3444 dt_LCH_2_Lab(Lch_lightness, Lab);
3447
3448 dt_bauhaus_slider_set_stop(g->lightness_spot, stop, RGB[0], RGB[1], RGB[2]);
3449
3450 const dt_aligned_pixel_t Lch_chroma = { 50., stop * CHROMA_MAX, hue / 360.f };
3451
3452 dt_LCH_2_Lab(Lch_chroma, Lab);
3455
3456 dt_bauhaus_slider_set_stop(g->chroma_spot, stop, RGB[0], RGB[1], RGB[2]);
3457 }
3458
3459 gtk_widget_queue_draw(g->hue_spot);
3460 gtk_widget_queue_draw(g->lightness_spot);
3461 gtk_widget_queue_draw(g->chroma_spot);
3462 gtk_widget_queue_draw(g->target_spot);
3463}
3464
3465
3474{
3477 GtkWidget *const widgets[9]
3478 = { g->primaries_achromatic_hue, g->primaries_achromatic_purity, g->primaries_red_hue,
3479 g->primaries_red_purity, g->primaries_green_hue, g->primaries_green_purity,
3480 g->primaries_blue_hue, g->primaries_blue_purity, g->primaries_gain };
3484 const float rows[3][3] = { { p->red[0], p->red[1], p->red[2] },
3485 { p->green[0], p->green[1], p->green[2] },
3486 { p->blue[0], p->blue[1], p->blue[2] } };
3487 const gboolean normalize[3] = { p->normalize_R, p->normalize_G, p->normalize_B };
3488 float M[3][3] = { { 0.f } };
3489 float roundtrip[3][3] = { { 0.f } };
3490
3492 if(!dt_iop_channelmixer_shared_primaries_from_matrix(basis, M, &primaries)) return FALSE;
3493 if(!dt_iop_channelmixer_shared_primaries_to_matrix(basis, &primaries, roundtrip)) return FALSE;
3494
3495 const float roundtrip_error = dt_iop_channelmixer_shared_roundtrip_error(M, roundtrip);
3496 if(!IS_NULL_PTR(error)) *error = roundtrip_error;
3497
3501 return isfinite(roundtrip_error) && roundtrip_error <= DT_CHANNELMIXERRGB_SIMPLE_EPS;
3502}
3503
3505{
3509 const dt_iop_order_iccprofile_info_t *const display_profile
3511 GtkWidget *const widgets[9]
3512 = { g->primaries_achromatic_hue, g->primaries_achromatic_purity, g->primaries_red_hue,
3513 g->primaries_red_purity, g->primaries_green_hue, g->primaries_green_purity,
3514 g->primaries_blue_hue, g->primaries_blue_purity, g->primaries_gain };
3518
3520 dt_iop_channelmixer_shared_paint_primaries_sliders(p->adaptation, work_profile, display_profile, basis, &primaries,
3521 widgets);
3522}
3523
3532 GtkWidget *widgets[6])
3533{
3534 widgets[0] = g->white_preserving_red_rotation;
3535 widgets[1] = g->white_preserving_red_saturation;
3536 widgets[2] = g->white_preserving_green_rotation;
3537 widgets[3] = g->white_preserving_green_saturation;
3538 widgets[4] = g->white_preserving_blue_rotation;
3539 widgets[5] = g->white_preserving_blue_saturation;
3540
3541 for(int widget = 0; widget < 6; widget++)
3542 if(IS_NULL_PTR(widgets[widget])) return FALSE;
3543
3544 return TRUE;
3545}
3546
3556{
3559 GtkWidget *widgets[6] = { NULL };
3563 const float rows[3][3] = { { p->red[0], p->red[1], p->red[2] },
3564 { p->green[0], p->green[1], p->green[2] },
3565 { p->blue[0], p->blue[1], p->blue[2] } };
3566 const gboolean normalize[3] = { p->normalize_R, p->normalize_G, p->normalize_B };
3567 float M[3][3] = { { 0.f } };
3568 float roundtrip[3][3] = { { 0.f } };
3569
3572 if(!dt_iop_channelmixer_shared_white_preserving_from_matrix(basis, M, &white_preserving)) return FALSE;
3573 if(!dt_iop_channelmixer_shared_white_preserving_to_matrix(basis, &white_preserving, roundtrip)) return FALSE;
3574
3575 const float roundtrip_error = dt_iop_channelmixer_shared_roundtrip_error_relative(M, roundtrip);
3576 if(!IS_NULL_PTR(error)) *error = roundtrip_error;
3577
3581 return isfinite(roundtrip_error) && roundtrip_error <= DT_CHANNELMIXERRGB_SIMPLE_EPS;
3582}
3583
3602
3604 const struct dt_iop_order_iccprofile_info_t *const work_profile,
3605 const struct dt_iop_order_iccprofile_info_t *const display_profile,
3607{
3608 dt_aligned_pixel_t work_rgb = { LMS[0], LMS[1], LMS[2], 0.f };
3609
3610 if(p->adaptation != DT_ADAPTATION_RGB)
3611 {
3612 convert_any_LMS_to_RGB(LMS, work_rgb, p->adaptation);
3613 }
3614 else
3615 {
3616 for(size_t c = 0; c < 3; c++) work_rgb[c] = LMS[c];
3617 }
3618
3619 dt_iop_channelmixer_shared_work_rgb_to_display(work_rgb, work_profile, display_profile, RGB);
3620}
3621
3623 const struct dt_iop_order_iccprofile_info_t *const work_profile,
3624 const struct dt_iop_order_iccprofile_info_t *const display_profile,
3625 GtkWidget *w, float stop, float c, float r, float g, float b)
3626{
3627 const dt_aligned_pixel_t LMS = { 0.5f * (c * r + 1 - r),
3628 0.5f * (c * g + 1 - g),
3629 0.5f * (c * b + 1 - b)};
3630 dt_aligned_pixel_t RGB_t = { 0.5f };
3631 _convert_GUI_colors(p, work_profile, display_profile, LMS, RGB_t);
3632 dt_bauhaus_slider_set_stop(w, stop, RGB_t[0], RGB_t[1], RGB_t[2]);
3633}
3634
3635static void _update_RGB_colors(dt_iop_module_t *self, float r, float g, float b, gboolean normalize, float *a,
3636 GtkWidget *w_r, GtkWidget *w_g, GtkWidget *w_b)
3637{
3638 // update the fill background color of x, y sliders
3640 const struct dt_iop_order_iccprofile_info_t *const work_profile = dt_ioppr_get_pipe_work_profile_info(self->dev->pipe);
3641 const struct dt_iop_order_iccprofile_info_t *const display_profile = dt_ioppr_get_pipe_output_profile_info(self->dev->pipe);
3642
3643 // scale params if needed
3644 dt_aligned_pixel_t RGB = { a[0], a[1], a[2] };
3645
3646 if(normalize)
3647 {
3648 const float sum = RGB[0] + RGB[1] + RGB[2];
3649 if(sum != 0.f) for(int c = 0; c < 3; c++) RGB[c] /= sum;
3650 }
3651
3652 for(int i = 0; i < DT_BAUHAUS_SLIDER_MAX_STOPS; i++)
3653 {
3654 const float stop = ((float)i / (float)(DT_BAUHAUS_SLIDER_MAX_STOPS - 1));
3655 // Use the hard bounds of the sliders; drawing will take into account the possible soft rescaling
3656 const float range_x = COLOR_MAX - COLOR_MIN;
3657
3658 const float x = COLOR_MIN + stop * range_x;
3659
3660 _update_RGB_slider_stop(p, work_profile, display_profile, w_r, stop, x + RGB[1] + RGB[2], r, g, b);
3661 _update_RGB_slider_stop(p, work_profile, display_profile, w_g, stop, RGB[0] + x + RGB[2], r, g, b);
3662 _update_RGB_slider_stop(p, work_profile, display_profile, w_b, stop, RGB[0] + RGB[1] + x , r, g, b);
3663 }
3664
3665 gtk_widget_queue_draw(w_r);
3666 gtk_widget_queue_draw(w_b);
3667 gtk_widget_queue_draw(w_g);
3668}
3669
3678{
3681 GtkWidget *const widgets[6]
3682 = { g->simple_theta, g->simple_psi, g->simple_stretch_1, g->simple_stretch_2, g->simple_coupling_1,
3683 g->simple_coupling_2 };
3684 const float rows[3][3] = { { p->red[0], p->red[1], p->red[2] },
3685 { p->green[0], p->green[1], p->green[2] },
3686 { p->blue[0], p->blue[1], p->blue[2] } };
3687 const gboolean normalize[3] = { p->normalize_R, p->normalize_G, p->normalize_B };
3688 float M[3][3] = { { 0.f } };
3689 float roundtrip[3][3] = { { 0.f } };
3691
3692 if(!IS_NULL_PTR(error)) *error = INFINITY;
3695
3698
3699 const float roundtrip_error = dt_iop_channelmixer_shared_roundtrip_error(M, roundtrip);
3700 if(!IS_NULL_PTR(error)) *error = roundtrip_error;
3701
3705
3706 return isfinite(roundtrip_error) && roundtrip_error <= DT_CHANNELMIXERRGB_SIMPLE_EPS;
3707}
3708
3720{
3724 const dt_iop_order_iccprofile_info_t *const display_profile
3726 GtkWidget *const widgets[6]
3727 = { g->simple_theta, g->simple_psi, g->simple_stretch_1, g->simple_stretch_2, g->simple_coupling_1,
3728 g->simple_coupling_2 };
3730
3732 dt_iop_channelmixer_shared_paint_simple_sliders(p->adaptation, work_profile, display_profile, &simple, widgets);
3733}
3734
3735
3737{
3739 if(IS_NULL_PTR(g)) return;
3740
3741 if(!IS_NULL_PTR(g->illum_color))
3742 gtk_widget_queue_draw(g->illum_color);
3743
3744 update_xy_color(self);
3745}
3746
3747static gboolean illuminant_color_draw(GtkWidget *widget, cairo_t *crf, gpointer user_data)
3748{
3749 dt_iop_module_t *self = (dt_iop_module_t *)user_data;
3751
3752 // Init
3753 GtkAllocation allocation;
3754 gtk_widget_get_allocation(widget, &allocation);
3755 int width = allocation.width, height = allocation.height;
3756 cairo_surface_t *cst = dt_cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height);
3757 cairo_t *cr = cairo_create(cst);
3758
3759 // Margins
3760 const float margin = 2. * DT_PIXEL_APPLY_DPI(1.5);
3762 height -= 2 * margin;
3763
3764 // Paint illuminant color - we need to recompute it in full in case camera RAW is chosen
3765 float x = p->x;
3766 float y = p->y;
3767 dt_aligned_pixel_t RGB = { 0 };
3768 dt_aligned_pixel_t custom_wb;
3769 get_white_balance_coeff(self, custom_wb);
3770 illuminant_to_xy(p->illuminant, &(self->dev->image_storage), custom_wb,
3771 &x, &y, p->temperature, p->illum_fluo, p->illum_led);
3773 cairo_set_source_rgb(cr, RGB[0], RGB[1], RGB[2]);
3774 cairo_rectangle(cr, INNER_PADDING, margin, width, height);
3775 cairo_fill(cr);
3776
3777 // Clean
3778 cairo_stroke(cr);
3779 cairo_destroy(cr);
3780 cairo_set_source_surface(crf, cst, 0, 0);
3781 cairo_paint(crf);
3782 cairo_surface_destroy(cst);
3783 return TRUE;
3784}
3785
3786static gboolean target_color_draw(GtkWidget *widget, cairo_t *crf, gpointer user_data)
3787{
3788 dt_iop_module_t *self = (dt_iop_module_t *)user_data;
3790
3791 // Init
3792 GtkAllocation allocation;
3793 gtk_widget_get_allocation(widget, &allocation);
3794 int width = allocation.width, height = allocation.height;
3795 cairo_surface_t *cst = dt_cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height);
3796 cairo_t *cr = cairo_create(cst);
3797
3798 // Margins
3799 const float margin = 2. * DT_PIXEL_APPLY_DPI(1.5);
3801 height -= 2 * margin;
3802
3803 // Paint target color
3804 dt_aligned_pixel_t RGB = { 0 };
3805 dt_aligned_pixel_t Lch = { 0 };
3806 dt_aligned_pixel_t Lab = { 0 };
3807 dt_aligned_pixel_t XYZ = { 0 };
3808 Lch[0] = dt_bauhaus_slider_get(g->lightness_spot);
3809 Lch[1] = dt_bauhaus_slider_get(g->chroma_spot);
3810 Lch[2] = dt_bauhaus_slider_get(g->hue_spot) / 360.f;
3811 dt_LCH_2_Lab(Lch, Lab);
3814
3815 cairo_set_source_rgb(cr, RGB[0], RGB[1], RGB[2]);
3816 cairo_rectangle(cr, INNER_PADDING, margin, width, height);
3817 cairo_fill(cr);
3818
3819 // Clean
3820 cairo_stroke(cr);
3821 cairo_destroy(cr);
3822 cairo_set_source_surface(crf, cst, 0, 0);
3823 cairo_paint(crf);
3824 cairo_surface_destroy(cst);
3825 return TRUE;
3826}
3827
3828static gboolean origin_color_draw(GtkWidget *widget, cairo_t *crf, gpointer user_data)
3829{
3830 dt_iop_module_t *self = (dt_iop_module_t *)user_data;
3832
3833 // Init
3834 GtkAllocation allocation;
3835 gtk_widget_get_allocation(widget, &allocation);
3836 int width = allocation.width, height = allocation.height;
3837 cairo_surface_t *cst = dt_cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height);
3838 cairo_t *cr = cairo_create(cst);
3839
3840 // Margins
3841 const float margin = 2. * DT_PIXEL_APPLY_DPI(1.5);
3843 height -= 2 * margin;
3844
3845 cairo_set_source_rgb(cr, g->spot_RGB[0], g->spot_RGB[1], g->spot_RGB[2]);
3846 cairo_rectangle(cr, INNER_PADDING, margin, width, height);
3847 cairo_fill(cr);
3848
3849 // Clean
3850 cairo_stroke(cr);
3851 cairo_destroy(cr);
3852 cairo_set_source_surface(crf, cst, 0, 0);
3853 cairo_paint(crf);
3854 cairo_surface_destroy(cst);
3855 return TRUE;
3856}
3857
3859{
3862
3863 float x = p->x;
3864 float y = p->y;
3865 dt_aligned_pixel_t custom_wb;
3866 get_white_balance_coeff(self, custom_wb);
3867 illuminant_to_xy(p->illuminant, &(self->dev->image_storage), custom_wb, &x, &y, p->temperature, p->illum_fluo, p->illum_led);
3868
3869 dt_illuminant_t test_illuminant;
3870 float t = 5000.f;
3871 check_if_close_to_daylight(x, y, &t, &test_illuminant, NULL);
3872
3873 gchar *str;
3874 if(t > 1667.f && t < 25000.f)
3875 {
3876 if(test_illuminant == DT_ILLUMINANT_D)
3877 {
3878 str = g_strdup_printf(_("CCT: %.0f K (daylight)"), t);
3879 gtk_widget_set_tooltip_text(GTK_WIDGET(g->approx_cct),
3880 _("approximated correlated color temperature.\n"
3881 "this illuminant can be accurately modeled by a daylight spectrum,\n"
3882 "so its temperature is relevant and meaningful with a D illuminant."));
3883 }
3884 else if(test_illuminant == DT_ILLUMINANT_BB)
3885 {
3886 str = g_strdup_printf(_("CCT: %.0f K (black body)"), t);
3887 gtk_widget_set_tooltip_text(GTK_WIDGET(g->approx_cct),
3888 _("approximated correlated color temperature.\n"
3889 "this illuminant can be accurately modeled by a black body spectrum,\n"
3890 "so its temperature is relevant and meaningful with a Planckian illuminant."));
3891 }
3892 else
3893 {
3894 str = g_strdup_printf(_("CCT: %.0f K (invalid)"), t);
3895 gtk_widget_set_tooltip_text(GTK_WIDGET(g->approx_cct),
3896 _("approximated correlated color temperature.\n"
3897 "this illuminant cannot be accurately modeled by a daylight or black body spectrum,\n"
3898 "so its temperature is not relevant and meaningful and you need to use a custom illuminant."));
3899 }
3900 }
3901 else
3902 {
3903 str = g_strdup_printf(_("CCT: undefined"));
3904 gtk_widget_set_tooltip_text(GTK_WIDGET(g->approx_cct),
3905 _("the approximated correlated color temperature\n"
3906 "cannot be computed at all so you need to use a custom illuminant."));
3907 }
3908 gtk_label_set_text(GTK_LABEL(g->approx_cct), str);
3909 dt_free(str);
3910}
3911
3912
3913static void illum_xy_callback(GtkWidget *slider, gpointer user_data)
3914{
3915 dt_iop_module_t *self = (dt_iop_module_t *)user_data;
3916 if(dt_gui_widgets_suppressed()) return;
3919
3920 dt_aligned_pixel_t Lch = { 0 };
3921 Lch[0] = 100.f;
3922 Lch[2] = dt_bauhaus_slider_get(g->illum_x) / 180. * M_PI;
3923 Lch[1] = dt_bauhaus_slider_get(g->illum_y);
3924
3925 dt_aligned_pixel_t xyY = { 0 };
3926 dt_Lch_to_xyY(Lch, xyY);
3927 p->x = xyY[0];
3928 p->y = xyY[1];
3929
3930 float t = xy_to_CCT(p->x, p->y);
3931 // xy_to_CCT is valid only above 3000 K
3932 if(t < 3000.f) t = CCT_reverse_lookup(p->x, p->y);
3933 p->temperature = t;
3934
3936 dt_bauhaus_slider_set(g->temperature, p->temperature);
3937 update_approx_cct(self);
3941
3942 dt_print(DT_DEBUG_DEV, "[picker/channelmixerrgb] history commit source=illum_xy_callback slider=%p\n",
3943 (void *)slider);
3944 dt_dev_add_history_item(self->dev, self, TRUE, TRUE);
3945}
3946
3952
3954{
3955 self->dev->proxy.chroma_adaptation = NULL;
3956 dt_free_align(piece->data);
3957 piece->data = NULL;
3958}
3959
3961{
3963 g->is_profiling_started = FALSE;
3965 gui_changed(self, NULL, NULL);
3966}
3967
3968void gui_update(struct dt_iop_module_t *self)
3969{
3970 dt_iop_module_t *module = (dt_iop_module_t *)self;
3973 float simple_error = INFINITY;
3974 float primaries_error = INFINITY;
3975 float white_preserving_error = INFINITY;
3976
3978
3979 /* Restoring GUI state from params must not re-enter widget callbacks.
3980 Channelmixer's spot/color widgets can otherwise rerun auto-picking while
3981 a history refresh is already in flight, which feeds TOP_CHANGED/full-pipe
3982 loops when the picker is active. */
3984
3985 // always reset the mode the correct
3987
3988 // get the saved params
3990
3991 gboolean use_mixing = TRUE;
3992 if(dt_conf_key_exists("darkroom/modules/channelmixerrgb/use_mixing"))
3993 use_mixing = dt_conf_get_bool("darkroom/modules/channelmixerrgb/use_mixing");
3994 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->use_mixing), use_mixing);
3995
3996 float lightness = 50.f;
3997 if(dt_conf_key_exists("darkroom/modules/channelmixerrgb/lightness"))
3998 lightness = dt_conf_get_float("darkroom/modules/channelmixerrgb/lightness");
3999 dt_bauhaus_slider_set(g->lightness_spot, lightness);
4000
4001 float hue = 0.f;
4002 if(dt_conf_key_exists("darkroom/modules/channelmixerrgb/hue"))
4003 hue = dt_conf_get_float("darkroom/modules/channelmixerrgb/hue");
4004 dt_bauhaus_slider_set(g->hue_spot, hue);
4005
4006 float chroma = 0.f;
4007 if(dt_conf_key_exists("darkroom/modules/channelmixerrgb/chroma"))
4008 chroma = dt_conf_get_float("darkroom/modules/channelmixerrgb/chroma");
4009 dt_bauhaus_slider_set(g->chroma_spot, chroma);
4010
4012
4013 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->clip), p->clip);
4014 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->normalize_R), p->normalize_R);
4015 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->normalize_G), p->normalize_G);
4016 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->normalize_B), p->normalize_B);
4017
4018 const gboolean simple_ok = _channelmixerrgb_sync_simple_from_params(self, &simple_error);
4019 const gboolean primaries_ok = _channelmixerrgb_sync_primaries_from_params(self, &primaries_error);
4020 const gboolean white_preserving_ok
4021 = _channelmixerrgb_sync_white_preserving_from_params(self, &white_preserving_error);
4022 const dt_iop_channelmixer_rgb_mixer_mode_t requested_mode
4027 = requested_mode == DT_CHANNELMIXERRGB_MIXER_SIMPLE && simple_ok
4029 : requested_mode == DT_CHANNELMIXERRGB_MIXER_PRIMARIES && primaries_ok
4031 : requested_mode == DT_CHANNELMIXERRGB_MIXER_WHITE_PRESERVING && white_preserving_ok
4034 dt_bauhaus_combobox_set(g->mixer_mode, mixer_mode);
4036
4037 if(p->version != CHANNELMIXERRGB_V_3)
4038 dt_bauhaus_combobox_set(g->saturation_version, p->version);
4039 else
4040 gtk_widget_hide(GTK_WIDGET(g->saturation_version));
4041
4042 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->normalize_sat), p->normalize_sat);
4043 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->normalize_light), p->normalize_light);
4044 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->normalize_grey), p->normalize_grey);
4045
4047
4048 // Restore both selections by name: the chart list is rebuilt from scratch here, and the
4049 // stored name has to survive a built-in chart being added or a .cht file appearing.
4050 // The color list is filtered by the selected chart, so the chart combo box comes first.
4052
4053 gchar *selected_checker = dt_conf_get_string("darkroom/modules/channelmixerrgb/colorchecker");
4054 if(!dt_bauhaus_combobox_set_from_text(g->checkers_list, selected_checker))
4055 dt_bauhaus_combobox_set(g->checkers_list, 0);
4056 dt_free(selected_checker);
4057
4058 const dt_colorchecker_label_t *checker_label = _checker_label_from_combobox(g->checkers_list, g->colorcheckers);
4059
4061
4062 gchar *selected_color = dt_conf_get_string("darkroom/modules/channelmixerrgb/colorchecker_color");
4063 if(!dt_bauhaus_combobox_set_from_text(g->checkers_color_list, selected_color))
4064 dt_bauhaus_combobox_set(g->checkers_color_list, 0);
4065 dt_free(selected_color);
4066
4067 _set_checker_from_labels(g, checker_label,
4068 _checker_label_from_combobox(g->checkers_color_list, g->colorcheckers_color));
4069
4070 color_list_visibility(self, checker_label);
4071
4072 const int j = dt_conf_get_int("darkroom/modules/channelmixerrgb/optimization");
4073 dt_bauhaus_combobox_set(g->optimize, j);
4074 g->optimization = j;
4075
4076 g->safety_margin = dt_conf_get_float("darkroom/modules/channelmixerrgb/safety");
4077 dt_bauhaus_slider_set(g->safety, g->safety_margin);
4078
4080
4081 // always disable profiling mode by default
4082 g->is_profiling_started = FALSE;
4083
4086
4087 g->spot_RGB[0] = 0.f;
4088 g->spot_RGB[1] = 0.f;
4089 g->spot_RGB[2] = 0.f;
4090 g->spot_RGB[3] = 0.f;
4091
4092 // Widget defaults (reset values) and the image-dependent "as shot in camera" illuminant entry,
4093 // derived from the per-image defaults that reload_defaults() already computed into default_params.
4094 // This lives here, not in reload_defaults(), so it only touches widgets when they exist and on the
4095 // GUI thread (reload_defaults also runs on export/thumbnail devs that have no widgets).
4096 {
4098 const dt_image_t *img = &self->dev->image_storage;
4099 const dt_aligned_pixel_t xyY = { d->x, d->y, 1.f };
4100 dt_aligned_pixel_t Lch = { 0 };
4101 dt_xyY_to_Lch(xyY, Lch);
4102
4103 dt_bauhaus_slider_set_default(g->illum_x, Lch[2] / M_PI * 180.f);
4104 dt_bauhaus_slider_set_default(g->illum_y, Lch[1]);
4105 dt_bauhaus_slider_set_default(g->temperature, d->temperature);
4106 dt_bauhaus_combobox_set_default(g->illuminant, d->illuminant);
4107 dt_bauhaus_combobox_set_default(g->adaptation, d->adaptation);
4108 if(g->delta_E_label_text)
4109 {
4110 dt_free(g->delta_E_label_text);
4111 g->delta_E_label_text = NULL;
4112 }
4113
4115 {
4117 dt_bauhaus_combobox_add_full(g->illuminant, _("as shot in camera"), DT_BAUHAUS_COMBOBOX_ALIGN_RIGHT,
4118 GINT_TO_POINTER(DT_ILLUMINANT_CAMERA), NULL, TRUE);
4119 }
4120 else
4122 }
4123
4125
4126 gui_changed(self, NULL, NULL);
4127}
4128
4130{
4131 dt_iop_default_init(module);
4132
4134 d->red[0] = d->green[1] = d->blue[2] = 1.0;
4135 d->normalize_R = TRUE;
4136 d->normalize_G = TRUE;
4137 d->normalize_B = TRUE;
4138}
4139
4141{
4143
4144 // Our per-image defaults (illuminant chromaticity x/y, temperature) are derived from the white
4145 // balance temperature publishes into dev->proxy.wb_coeffs (see get_white_balance_coeff()).
4146 // temperature publishes the per-image DEFAULT WB from its own reload_defaults() (main thread, no
4147 // pipeline involved). For that seed to be visible here, temperature must run before us:
4148 // dt_dev_init_default_history() iterates modules in iop order, and temperature's iop_order is far
4149 // below ours, so this holds -- but it is a silent data dependency, so assert it. A future
4150 // iop-order change that put us first would otherwise regress our defaults on a brand-new history.
4151 // (The EFFECTIVE/history WB is published later via temperature's commit_proxy(); our defaults are
4152 // intentionally keyed on the metadata-default WB so a fresh init always matches a manual reset.)
4153 const dt_iop_module_t *const temp = dt_iop_get_module_from_list(module->dev->iop, "temperature");
4154 if(temp && temp->iop_order >= module->iop_order)
4155 fprintf(stderr, "[channelmixerrgb] BUG: temperature (iop_order %d) must run before channelmixerrgb "
4156 "(iop_order %d); white-balance-derived defaults will be wrong on a fresh history\n",
4157 temp->iop_order, module->iop_order);
4158 assert(!temp || temp->iop_order < module->iop_order);
4159
4160 d->normalize_R = TRUE;
4161 d->normalize_G = TRUE;
4162 d->normalize_B = TRUE;
4163
4164 d->x = module->get_f("x")->Float.Default;
4165 d->y = module->get_f("y")->Float.Default;
4166 d->temperature = module->get_f("temperature")->Float.Default;
4167 d->illuminant = module->get_f("illuminant")->Enum.Default;
4168 d->adaptation = module->get_f("adaptation")->Enum.Default;
4169
4170 // Note : this is not an user param anymore, and is set to modern by default
4171 // except for old histories using temperature without having an history entry for it.
4172 // see develop/develop.c/_dev_auto_apply_presets()
4173 const gboolean is_modern =
4174 dt_conf_is_equal("plugins/darkroom/chromatic-adaptation", "modern");
4175
4176 // note that if there is already an instance of this module with an
4177 // adaptation set we default to RGB (none) in this instance.
4178 // try to register the CAT here
4179 declare_cat_on_pipe(module, is_modern);
4180 const dt_image_t *img = &module->dev->image_storage;
4181
4182 // check if we could register
4183 gboolean CAT_already_applied =
4184 (!IS_NULL_PTR(module->dev->proxy.chroma_adaptation)) // CAT exists
4185 && (module->dev->proxy.chroma_adaptation != module) // and it is not us
4186 && (!dt_image_is_monochrome(img));
4187
4188 module->default_enabled = FALSE;
4189
4190 dt_aligned_pixel_t custom_wb = { 1.f, 1.f, 1.f, 1.f };
4191 if(!CAT_already_applied
4192 && is_modern
4193 && !get_white_balance_coeff(module, custom_wb)
4194 && !dt_image_is_monochrome(img))
4195 {
4196 // if workflow = modern and we find WB coeffs, take care of white balance here
4197 if(find_temperature_from_raw_coeffs(img, custom_wb, &(d->x), &(d->y)))
4198 d->illuminant = DT_ILLUMINANT_CAMERA;
4199
4200 check_if_close_to_daylight(d->x, d->y, &(d->temperature), &(d->illuminant), &(d->adaptation));
4201 module->workflow_enabled = dt_image_needs_rawprepare(img);
4202 }
4203 else
4204 {
4205 // otherwise, simple channel mixer
4206 d->illuminant = DT_ILLUMINANT_PIPE;
4207 d->adaptation = DT_ADAPTATION_RGB;
4208 }
4209
4210 // NOTE: widget state (slider/combo defaults, the image-dependent "as shot in camera" illuminant
4211 // entry) is intentionally NOT set here. reload_defaults() runs on export/thumbnail devs with no
4212 // widgets and off the GUI thread; touching bauhaus here re-entered callbacks against half-built
4213 // widgets and crashed. That work now lives in gui_update(), reading these same default_params.
4214}
4215
4216
4218{
4219 if(dt_gui_widgets_suppressed()) return;
4220
4222
4223 dt_aligned_pixel_t Lch_target = { 0.f };
4224
4226 Lch_target[0] = dt_bauhaus_slider_get(g->lightness_spot);
4227 Lch_target[1] = dt_bauhaus_slider_get(g->chroma_spot);
4228 Lch_target[2] = dt_bauhaus_slider_get(g->hue_spot) / 360.f;
4229 const gboolean use_mixing = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(g->use_mixing));
4231
4232 // Save the color on change
4233 dt_conf_set_float("darkroom/modules/channelmixerrgb/lightness", Lch_target[0]);
4234 dt_conf_set_float("darkroom/modules/channelmixerrgb/chroma", Lch_target[1]);
4235 dt_conf_set_float("darkroom/modules/channelmixerrgb/hue", Lch_target[2] * 360.f);
4236 dt_conf_set_bool("darkroom/modules/channelmixerrgb/use_mixing", use_mixing);
4237
4239 paint_hue(self);
4241
4242 // Re-run auto illuminant only for the module that currently owns the active picker.
4243 const dt_spot_mode_t mode = dt_bauhaus_combobox_get(g->spot_mode);
4244 const gboolean picker_active = dt_iop_color_picker_is_active_module(self);
4245 if(mode == DT_SPOT_MODE_CORRECT && picker_active)
4246 _auto_set_illuminant(self, self->dev->pipe);
4247 // else : just record new values and do nothing
4248}
4249
4252{
4253 gtk_stack_set_visible_child_name(GTK_STACK(g->mixer_stack),
4254 mode == DT_CHANNELMIXERRGB_MIXER_SIMPLE ? "simple"
4255 : mode == DT_CHANNELMIXERRGB_MIXER_PRIMARIES ? "primaries"
4256 : mode == DT_CHANNELMIXERRGB_MIXER_WHITE_PRESERVING ? "white-preserving"
4257 : "complete");
4258}
4259
4270static gboolean _channelmixerrgb_push_matrix_to_params(dt_iop_module_t *self, const float M[3][3])
4271{
4274 gboolean changed = p->normalize_R || p->normalize_G || p->normalize_B;
4275
4276 for(int col = 0; col < 3; col++)
4277 {
4278 changed = changed || p->red[col] != M[0][col] || p->green[col] != M[1][col] || p->blue[col] != M[2][col];
4279 p->red[col] = M[0][col];
4280 p->green[col] = M[1][col];
4281 p->blue[col] = M[2][col];
4282 }
4283
4284 p->normalize_R = FALSE;
4285 p->normalize_G = FALSE;
4286 p->normalize_B = FALSE;
4287
4289 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->normalize_R), FALSE);
4290 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->normalize_G), FALSE);
4291 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->normalize_B), FALSE);
4292 dt_bauhaus_slider_set(g->scale_red_R, p->red[0]);
4293 dt_bauhaus_slider_set(g->scale_red_G, p->red[1]);
4294 dt_bauhaus_slider_set(g->scale_red_B, p->red[2]);
4295 dt_bauhaus_slider_set(g->scale_green_R, p->green[0]);
4296 dt_bauhaus_slider_set(g->scale_green_G, p->green[1]);
4297 dt_bauhaus_slider_set(g->scale_green_B, p->green[2]);
4298 dt_bauhaus_slider_set(g->scale_blue_R, p->blue[0]);
4299 dt_bauhaus_slider_set(g->scale_blue_G, p->blue[1]);
4300 dt_bauhaus_slider_set(g->scale_blue_B, p->blue[2]);
4302
4303 return changed;
4304}
4305
4323
4324static void _channelmixerrgb_mixer_mode_callback(GtkWidget *combo, gpointer user_data)
4325{
4326 if(dt_gui_widgets_suppressed()) return;
4327
4328 dt_iop_module_t *self = (dt_iop_module_t *)user_data;
4332
4334
4336 {
4338 return;
4339 }
4340
4342 {
4343 float error = INFINITY;
4345 {
4346 dt_control_log(_("simple mixer mode requires all three output rows to be normalized with non-zero sums."));
4352 return;
4353 }
4354
4356 gui_changed(self, NULL, NULL);
4357 return;
4358 }
4359
4361 {
4362 const float rows[3][3] = { { p->red[0], p->red[1], p->red[2] },
4363 { p->green[0], p->green[1], p->green[2] },
4364 { p->blue[0], p->blue[1], p->blue[2] } };
4365 const gboolean normalize[3] = { p->normalize_R, p->normalize_G, p->normalize_B };
4366 float M[3][3] = { { 0.f } };
4367 float error = INFINITY;
4370 {
4371 dt_control_log(_("primaries mixer mode requires a non-singular 3x3 matrix with non-zero affine sums."));
4377 return;
4378 }
4379
4380 gboolean changed = p->normalize_R || p->normalize_G || p->normalize_B;
4381 for(int col = 0; col < 3; col++)
4382 {
4383 changed = changed || p->red[col] != M[0][col] || p->green[col] != M[1][col] || p->blue[col] != M[2][col];
4384 p->red[col] = M[0][col];
4385 p->green[col] = M[1][col];
4386 p->blue[col] = M[2][col];
4387 }
4388
4389 p->normalize_R = FALSE;
4390 p->normalize_G = FALSE;
4391 p->normalize_B = FALSE;
4392
4394 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->normalize_R), FALSE);
4395 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->normalize_G), FALSE);
4396 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->normalize_B), FALSE);
4397 dt_bauhaus_slider_set(g->scale_red_R, p->red[0]);
4398 dt_bauhaus_slider_set(g->scale_red_G, p->red[1]);
4399 dt_bauhaus_slider_set(g->scale_red_B, p->red[2]);
4400 dt_bauhaus_slider_set(g->scale_green_R, p->green[0]);
4401 dt_bauhaus_slider_set(g->scale_green_G, p->green[1]);
4402 dt_bauhaus_slider_set(g->scale_green_B, p->green[2]);
4403 dt_bauhaus_slider_set(g->scale_blue_R, p->blue[0]);
4404 dt_bauhaus_slider_set(g->scale_blue_G, p->blue[1]);
4405 dt_bauhaus_slider_set(g->scale_blue_B, p->blue[2]);
4407
4409 gui_changed(self, NULL, NULL);
4410
4411 if(changed)
4412 {
4413 dt_print(DT_DEBUG_DEV, "[channelmixerrgb] history commit source=mixer_mode_primaries\n");
4414 dt_dev_add_history_item(self->dev, self, TRUE, TRUE);
4415 }
4416 }
4417
4419 {
4420 const float rows[3][3] = { { p->red[0], p->red[1], p->red[2] },
4421 { p->green[0], p->green[1], p->green[2] },
4422 { p->blue[0], p->blue[1], p->blue[2] } };
4423 const gboolean normalize[3] = { p->normalize_R, p->normalize_G, p->normalize_B };
4424 float M[3][3] = { { 0.f } };
4425 float error = INFINITY;
4428 {
4430 self, _("white-preserving mixer mode requires a matrix that leaves the basis white unchanged."));
4431 return;
4432 }
4433
4434 const gboolean changed = _channelmixerrgb_push_matrix_to_params(self, M);
4435
4437 gui_changed(self, NULL, NULL);
4438
4439 if(changed)
4440 {
4441 dt_print(DT_DEBUG_DEV, "[channelmixerrgb] history commit source=mixer_mode_white_preserving\n");
4442 dt_dev_add_history_item(self->dev, self, TRUE, TRUE);
4443 }
4444 }
4445}
4446
4447static void _channelmixerrgb_simple_slider_callback(GtkWidget *slider, gpointer user_data)
4448{
4449 if(dt_gui_widgets_suppressed()) return;
4450
4451 dt_iop_module_t *self = (dt_iop_module_t *)user_data;
4454 GtkWidget *const widgets[6]
4455 = { g->simple_theta, g->simple_psi, g->simple_stretch_1, g->simple_stretch_2, g->simple_coupling_1,
4456 g->simple_coupling_2 };
4458 float M[3][3] = { { 0.f } };
4459
4462
4463 for(int col = 0; col < 3; col++)
4464 {
4465 p->red[col] = M[0][col];
4466 p->green[col] = M[1][col];
4467 p->blue[col] = M[2][col];
4468 }
4469
4471 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->normalize_R), p->normalize_R);
4472 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->normalize_G), p->normalize_G);
4473 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->normalize_B), p->normalize_B);
4474
4475 dt_bauhaus_slider_set(g->scale_red_R, p->red[0]);
4476 dt_bauhaus_slider_set(g->scale_red_G, p->red[1]);
4477 dt_bauhaus_slider_set(g->scale_red_B, p->red[2]);
4478 dt_bauhaus_slider_set(g->scale_green_R, p->green[0]);
4479 dt_bauhaus_slider_set(g->scale_green_G, p->green[1]);
4480 dt_bauhaus_slider_set(g->scale_green_B, p->green[2]);
4481 dt_bauhaus_slider_set(g->scale_blue_R, p->blue[0]);
4482 dt_bauhaus_slider_set(g->scale_blue_G, p->blue[1]);
4483 dt_bauhaus_slider_set(g->scale_blue_B, p->blue[2]);
4485
4486 gui_changed(self, slider, NULL);
4487
4488 dt_print(DT_DEBUG_DEV, "[channelmixerrgb] history commit source=simple_slider slider=%p\n", (void *)slider);
4489 dt_dev_add_history_item(self->dev, self, TRUE, TRUE);
4490}
4491
4492static void _channelmixerrgb_primaries_slider_callback(GtkWidget *slider, gpointer user_data)
4493{
4494 if(dt_gui_widgets_suppressed()) return;
4495
4496 dt_iop_module_t *self = (dt_iop_module_t *)user_data;
4499 GtkWidget *const widgets[9]
4500 = { g->primaries_achromatic_hue, g->primaries_achromatic_purity, g->primaries_red_hue,
4501 g->primaries_red_purity, g->primaries_green_hue, g->primaries_green_purity,
4502 g->primaries_blue_hue, g->primaries_blue_purity, g->primaries_gain };
4506 float M[3][3] = { { 0.f } };
4507
4509 if(!dt_iop_channelmixer_shared_primaries_to_matrix(basis, &primaries, M))
4510 {
4511 dt_control_log(_("primaries mixer mode requires a non-singular 3x3 matrix with non-zero affine sums."));
4512 return;
4513 }
4514
4515 for(int col = 0; col < 3; col++)
4516 {
4517 p->red[col] = M[0][col];
4518 p->green[col] = M[1][col];
4519 p->blue[col] = M[2][col];
4520 }
4521 p->normalize_R = FALSE;
4522 p->normalize_G = FALSE;
4523 p->normalize_B = FALSE;
4524
4526 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->normalize_R), FALSE);
4527 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->normalize_G), FALSE);
4528 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->normalize_B), FALSE);
4529
4530 dt_bauhaus_slider_set(g->scale_red_R, p->red[0]);
4531 dt_bauhaus_slider_set(g->scale_red_G, p->red[1]);
4532 dt_bauhaus_slider_set(g->scale_red_B, p->red[2]);
4533 dt_bauhaus_slider_set(g->scale_green_R, p->green[0]);
4534 dt_bauhaus_slider_set(g->scale_green_G, p->green[1]);
4535 dt_bauhaus_slider_set(g->scale_green_B, p->green[2]);
4536 dt_bauhaus_slider_set(g->scale_blue_R, p->blue[0]);
4537 dt_bauhaus_slider_set(g->scale_blue_G, p->blue[1]);
4538 dt_bauhaus_slider_set(g->scale_blue_B, p->blue[2]);
4540
4541 gui_changed(self, slider, NULL);
4542
4543 dt_print(DT_DEBUG_DEV, "[channelmixerrgb] history commit source=primaries_slider slider=%p\n", (void *)slider);
4544 dt_dev_add_history_item(self->dev, self, TRUE, TRUE);
4545}
4546
4547
4548static void _channelmixerrgb_white_preserving_slider_callback(GtkWidget *slider, gpointer user_data)
4549{
4550 if(dt_gui_widgets_suppressed()) return;
4551
4552 dt_iop_module_t *self = (dt_iop_module_t *)user_data;
4555 GtkWidget *widgets[6] = { NULL };
4559 float M[3][3] = { { 0.f } };
4560
4561 if(!_channelmixerrgb_white_preserving_widgets(g, widgets)) return;
4562
4564 if(!dt_iop_channelmixer_shared_white_preserving_to_matrix(basis, &white_preserving, M))
4565 {
4566 // Three primaries collapsed onto the white have no transform to describe. Snap the sliders
4567 // back to the last representable state rather than leaving the page showing a setting the
4568 // params do not hold -- the render would otherwise stop following the controls.
4569 dt_control_log(_("white-preserving mixer mode requires non-degenerate primaries."));
4571 return;
4572 }
4573
4575
4576 gui_changed(self, slider, NULL);
4577
4578 dt_print(DT_DEBUG_DEV, "[channelmixerrgb] history commit source=white_preserving_slider slider=%p\n",
4579 (void *)slider);
4580 dt_dev_add_history_item(self->dev, self, TRUE, TRUE);
4581}
4582
4583
4584void gui_changed(dt_iop_module_t *self, GtkWidget *w, void *previous)
4585{
4588 const gboolean simple_widget
4589 = w == g->simple_theta || w == g->simple_psi || w == g->simple_stretch_1 || w == g->simple_stretch_2
4590 || w == g->simple_coupling_1 || w == g->simple_coupling_2;
4591 const gboolean primaries_widget
4592 = w == g->primaries_achromatic_hue || w == g->primaries_achromatic_purity || w == g->primaries_red_hue
4593 || w == g->primaries_red_purity || w == g->primaries_green_hue || w == g->primaries_green_purity
4594 || w == g->primaries_blue_hue || w == g->primaries_blue_purity || w == g->primaries_gain;
4595 const gboolean white_preserving_widget
4596 = w == g->white_preserving_red_rotation || w == g->white_preserving_red_saturation
4597 || w == g->white_preserving_green_rotation || w == g->white_preserving_green_saturation
4598 || w == g->white_preserving_blue_rotation || w == g->white_preserving_blue_saturation;
4599 GtkWidget *white_preserving_widgets[6] = { NULL };
4600 // Bauhaus setters emit gui_changed() from gui_init(), before this page has been built.
4601 const gboolean white_preserving_ready = _channelmixerrgb_white_preserving_widgets(g, white_preserving_widgets);
4602 const gboolean normalize[3] = { p->normalize_R, p->normalize_G, p->normalize_B };
4603 const gboolean rows_are_normalized = dt_iop_channelmixer_shared_rows_are_normalized(normalize);
4604 const gboolean complete_widget
4605 = w == g->scale_red_R || w == g->scale_red_G || w == g->scale_red_B || w == g->scale_green_R
4606 || w == g->scale_green_G || w == g->scale_green_B || w == g->scale_blue_R || w == g->scale_blue_G
4607 || w == g->scale_blue_B || w == g->normalize_R || w == g->normalize_G || w == g->normalize_B;
4608 // Some Bauhaus setters emit during gui_init(), so synchronize the illuminant group only after
4609 // every widget sharing the same xyY/Lch state has been created.
4610 const gboolean illuminant_widgets_ready
4611 = !IS_NULL_PTR(g->illuminant) && !IS_NULL_PTR(g->illum_fluo) && !IS_NULL_PTR(g->illum_led)
4612 && !IS_NULL_PTR(g->temperature) && !IS_NULL_PTR(g->illum_color) && !IS_NULL_PTR(g->illum_x)
4613 && !IS_NULL_PTR(g->illum_y);
4614
4615 if(!IS_NULL_PTR(w) && w == g->illuminant)
4616 {
4617 if(!IS_NULL_PTR(previous))
4618 {
4619 dt_illuminant_t *prev_illuminant = (dt_illuminant_t *)previous;
4620 if(*prev_illuminant == DT_ILLUMINANT_CAMERA)
4621 {
4622 // If illuminant was previously set with "as set in camera",
4623 // when changing it, we need to ensure the temperature and chromaticity
4624 // are inited with the correct values taken from camera EXIF.
4625 // Otherwise, if using a preset defining illuminant = "as set in camera",
4626 // temperature and chromaticity are inited with the preset content when illuminant is changed.
4627 dt_aligned_pixel_t custom_wb;
4628 get_white_balance_coeff(self, custom_wb);
4629 find_temperature_from_raw_coeffs(&(self->dev->image_storage), custom_wb, &(p->x), &(p->y));
4630 check_if_close_to_daylight(p->x, p->y, &(p->temperature), NULL, &(p->adaptation));
4631 }
4632 }
4633 if(p->illuminant == DT_ILLUMINANT_CAMERA)
4634 {
4635 // Get camera WB and update illuminant
4636 dt_aligned_pixel_t custom_wb;
4637 get_white_balance_coeff(self, custom_wb);
4638 const int found = find_temperature_from_raw_coeffs(&(self->dev->image_storage), custom_wb, &(p->x), &(p->y));
4639 check_if_close_to_daylight(p->x, p->y, &(p->temperature), NULL, &(p->adaptation));
4640
4641 if(found)
4642 dt_control_log(_("white balance successfully extracted from raw image"));
4643 }
4644 else if(p->illuminant == DT_ILLUMINANT_DETECT_EDGES
4645 || p->illuminant == DT_ILLUMINANT_DETECT_SURFACES)
4646 {
4647 // We need to recompute only the full preview
4648 dt_control_log(_("auto-detection of white balance started..."));
4649 }
4650 }
4651
4652 if(!IS_NULL_PTR(w) && (w == g->illuminant || w == g->illum_fluo || w == g->illum_led || w == g->temperature))
4653 {
4654 // Convert and synchronize all the possible ways to define an illuminant to allow swapping modes
4655
4656 if(p->illuminant != DT_ILLUMINANT_CUSTOM && p->illuminant != DT_ILLUMINANT_CAMERA)
4657 {
4658 // We are in any mode defining (x, y) indirectly from an interface, so commit (x, y) explicitly
4659 illuminant_to_xy(p->illuminant, NULL, NULL, &(p->x), &(p->y), p->temperature, p->illum_fluo, p->illum_led);
4660 }
4661
4662 if(p->illuminant != DT_ILLUMINANT_D && p->illuminant != DT_ILLUMINANT_BB && p->illuminant != DT_ILLUMINANT_CAMERA)
4663 {
4664 // We are in any mode not defining explicitly a temperature, so find the the closest CCT and commit it
4665 check_if_close_to_daylight(p->x, p->y, &(p->temperature), NULL, NULL);
4666 }
4667 }
4668
4670
4671 if(IS_NULL_PTR(w) || w == g->hue_spot || w == g->chroma_spot || w == g->lightness_spot || w == g->spot_settings)
4672 {
4673 paint_hue(self);
4674 }
4675
4676 if((IS_NULL_PTR(w) || w == g->illuminant || w == g->illum_fluo || w == g->illum_led || w == g->temperature)
4677 && illuminant_widgets_ready)
4678 {
4679 update_illuminants(self);
4680 update_approx_cct(self);
4682
4683 // force-update all the illuminant sliders in case something above changed them
4684 // notice the hue/chroma of the illuminant has to be computed on-the-fly anyway
4685 dt_aligned_pixel_t xyY = { p->x, p->y, 1.f };
4687 dt_xyY_to_Lch(xyY, Lch);
4688
4689 // If the chroma is zero then there is not a meaningful hue angle. In this case
4690 // leave the hue slider where it was, so that if chroma is set to zero and then
4691 // set to a nonzero value, the hue setting will remain unchanged.
4692 if(Lch[1] > 0)
4693 dt_bauhaus_slider_set(g->illum_x, Lch[2] / M_PI * 180.f);
4694 dt_bauhaus_slider_set(g->illum_y, Lch[1]);
4695
4696 // Redraw the temperature background color taking new soft bounds into account
4697 dt_bauhaus_slider_set(g->temperature, p->temperature);
4699 }
4700
4701 if(w == g->adaptation)
4702 update_illuminants(self);
4703
4704 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)
4705 _update_RGB_colors(self, 1, 0, 0, p->normalize_R, p->red, g->scale_red_R, g->scale_red_G, g->scale_red_B);
4706 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)
4707 _update_RGB_colors(self, 0, 1, 0, p->normalize_G, p->green, g->scale_green_R, g->scale_green_G, g->scale_green_B);
4708 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)
4709 _update_RGB_colors(self, 0, 0, 1, p->normalize_B, p->blue, g->scale_blue_R, g->scale_blue_G, g->scale_blue_B);
4710
4711 if(rows_are_normalized && !simple_widget && (IS_NULL_PTR(w) || complete_widget))
4712 {
4713 float error = INFINITY;
4715 {
4717 {
4718 dt_print(DT_DEBUG_DEV, "[channelmixerrgb] simple mixer rejected error=%g normalized=%d%d%d\n",
4719 error, p->normalize_R, p->normalize_G, p->normalize_B);
4720 dt_control_log(_("simple mixer mode requires all three output rows to be normalized with non-zero sums."));
4724 }
4725 else
4726 dt_print(DT_DEBUG_DEV, "[channelmixerrgb] simple mixer roundtrip error=%g\n", error);
4727 }
4728 }
4729
4730 if(!primaries_widget && (IS_NULL_PTR(w) || complete_widget || simple_widget || w == g->adaptation))
4731 {
4732 float error = INFINITY;
4734 {
4736 {
4737 dt_print(DT_DEBUG_DEV, "[channelmixerrgb] primaries mixer roundtrip error=%g\n", error);
4741 }
4742 }
4743 }
4744
4745 if(white_preserving_ready && !white_preserving_widget
4746 && (IS_NULL_PTR(w) || complete_widget || simple_widget || primaries_widget || w == g->adaptation))
4747 {
4748 float error = INFINITY;
4750 {
4752 {
4753 dt_print(DT_DEBUG_DEV, "[channelmixerrgb] white-preserving mixer roundtrip error=%g\n", error);
4757 }
4758 }
4759 }
4760
4761 if(IS_NULL_PTR(w) || w == g->adaptation || complete_widget || simple_widget || primaries_widget
4762 || white_preserving_widget)
4764
4765 if(IS_NULL_PTR(w) || w == g->adaptation || complete_widget || simple_widget || primaries_widget
4766 || white_preserving_widget)
4768
4769 if(IS_NULL_PTR(w) || w == g->adaptation || complete_widget || simple_widget || primaries_widget
4770 || white_preserving_widget)
4772
4773 // if grey channel is used and norm = 0 and normalization = ON, we are going to have a division by zero
4774 // in commit_param, we avoid dividing by zero automatically, but user needs a notification
4775 if((p->grey[0] != 0.f) || (p->grey[1] != 0.f) || (p->grey[2] != 0.f))
4776 if((p->grey[0] + p->grey[1] + p->grey[2] == 0.f) && p->normalize_grey)
4777 dt_control_log(_("color calibration: the sum of the gray channel parameters is zero, normalization will be disabled."));
4778
4779 // If "as shot in camera" illuminant is used, CAT space is forced automatically
4780 // therefore, make the control insensitive
4781 gtk_widget_set_sensitive(g->adaptation, p->illuminant != DT_ILLUMINANT_CAMERA);
4782 gtk_widget_queue_draw(g->adaptation);
4783
4785
4787}
4788
4790{
4793 const dt_iop_channelmixer_rgb_params_t previous = *p;
4794
4795 // capture gui color picked event.
4796 if(self->picked_color_max[0] < self->picked_color_min[0])
4797 {
4798 dt_print(DT_DEBUG_DEV, "[picker/channelmixerrgb] rejected invalid min/max min=%g max=%g\n",
4799 self->picked_color_min[0], self->picked_color_max[0]);
4800 return;
4801 }
4802 const float *RGB = self->picked_color;
4803 if(!isfinite(RGB[0]) || !isfinite(RGB[1]) || !isfinite(RGB[2]))
4804 {
4805 dt_print(DT_DEBUG_DEV, "[picker/channelmixerrgb] rejected nonfinite RGB=(%g,%g,%g)\n",
4806 RGB[0], RGB[1], RGB[2]);
4807 return;
4808 }
4809 dt_print(DT_DEBUG_DEV, "[picker/channelmixerrgb] RGB=(%g,%g,%g) pipe=%p\n",
4810 RGB[0], RGB[1], RGB[2], (void *)pipe);
4811
4812 // Get the module-stage profile matching the sampled buffer.
4813 const dt_iop_order_iccprofile_info_t *const current_profile
4815 if(IS_NULL_PTR(current_profile))
4816 {
4817 dt_print(DT_DEBUG_DEV, "[picker/channelmixerrgb] missing current profile\n");
4818 return;
4819 }
4820
4821 // Convert the sampled linear RGB code values to XYZ in the module profile.
4822 // The picker already sampled the live module buffer, so keep the conversion explicit here.
4823 dt_aligned_pixel_t XYZ = { 0.f };
4824 dot_product(RGB, current_profile->matrix_in, XYZ);
4825 if(!isfinite(XYZ[0]) || !isfinite(XYZ[1]) || !isfinite(XYZ[2])) return;
4826 dt_XYZ_to_sRGB(XYZ, g->spot_RGB);
4827
4828 // Convert to Lch for GUI feedback (input)
4832 dt_Lab_2_LCH(Lab, Lch);
4833
4834 // Write report in GUI
4836 gtk_label_set_text(GTK_LABEL(g->Lch_origin),
4837 g_strdup_printf(_("L: \t%.1f %%\nh: \t%.1f \302\260\nc: \t%.1f"),
4838 Lch[0], Lch[2] * 360.f, Lch[1] ));
4839 gtk_widget_queue_draw(g->origin_spot);
4841
4842 const dt_spot_mode_t mode = dt_bauhaus_combobox_get(g->spot_mode);
4843 const gboolean use_mixing = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(g->use_mixing));
4844
4845 // build the channel mixing matrix - keep in synch with commit_params()
4846 dt_colormatrix_t MIX = { { 0.f } };
4847
4848 float norm_R = 1.0f;
4849 if(p->normalize_R) norm_R = p->red[0] + p->red[1] + p->red[2];
4850
4851 float norm_G = 1.0f;
4852 if(p->normalize_G) norm_G = p->green[0] + p->green[1] + p->green[2];
4853
4854 float norm_B = 1.0f;
4855 if(p->normalize_B) norm_B = p->blue[0] + p->blue[1] + p->blue[2];
4856
4857 for(int i = 0; i < 3; i++)
4858 {
4859 MIX[0][i] = p->red[i] / norm_R;
4860 MIX[1][i] = p->green[i] / norm_G;
4861 MIX[2][i] = p->blue[i] / norm_B;
4862 }
4863
4864 if(mode == DT_SPOT_MODE_MEASURE)
4865 {
4866 // Keep the following in sync with commit_params()
4867
4868 // find x y coordinates of illuminant for CIE 1931 2° observer
4869 float x = p->x;
4870 float y = p->y;
4871 dt_adaptation_t adaptation = p->adaptation;
4872 dt_aligned_pixel_t custom_wb;
4873 get_white_balance_coeff(self, custom_wb);
4874 illuminant_to_xy(p->illuminant, &(self->dev->image_storage), custom_wb, &x, &y, p->temperature, p->illum_fluo, p->illum_led);
4875
4876 // if illuminant is set as camera, x and y are set on-the-fly at commit time, so we need to set adaptation too
4877 if(p->illuminant == DT_ILLUMINANT_CAMERA) check_if_close_to_daylight(x, y, NULL, NULL, &adaptation);
4878
4879 // Convert illuminant from xyY to XYZ
4880 dt_aligned_pixel_t XYZ_illuminant = { 0.f };
4881 illuminant_xy_to_XYZ(x, y, XYZ_illuminant);
4882
4883 // Convert illuminant from XYZ to Bradford modified LMS
4884 dt_aligned_pixel_t LMS_illuminant = { 0.f };
4885 dt_store_simd_aligned(LMS_illuminant, convert_any_XYZ_to_LMS(dt_load_simd_aligned(XYZ_illuminant), adaptation));
4886
4887 // For the non-linear Bradford
4888 const float pp = powf(0.818155f / LMS_illuminant[2], 0.0834f);
4889
4890 //fprintf(stdout, "illuminant: %i\n", p->illuminant);
4891 //fprintf(stdout, "x: %f, y: %f\n", x, y);
4892 //fprintf(stdout, "X: %f - Y: %f - Z: %f\n", XYZ_illuminant[0], XYZ_illuminant[1], XYZ_illuminant[2]);
4893 //fprintf(stdout, "L: %f - M: %f - S: %f\n", LMS_illuminant[0], LMS_illuminant[1], LMS_illuminant[2]);
4894
4895 // Finally, chroma-adapt the pixel
4896 dt_aligned_pixel_t XYZ_output = { 0.f };
4897 dt_store_simd_aligned(XYZ_output, chroma_adapt_pixel(dt_load_simd_aligned(XYZ),
4898 dt_load_simd_aligned(LMS_illuminant),
4899 adaptation, pp));
4900
4901 // Optionaly, apply the channel mixing
4902 if(use_mixing)
4903 {
4904 dt_aligned_pixel_t LMS_output = { 0.f };
4905 dt_store_simd_aligned(LMS_output, convert_any_XYZ_to_LMS(dt_load_simd_aligned(XYZ_output), adaptation));
4906 dt_aligned_pixel_t temp = { 0.f };
4907 dot_product(LMS_output, MIX, temp);
4908 dt_store_simd_aligned(XYZ_output, convert_any_LMS_to_XYZ(dt_load_simd_aligned(temp), adaptation));
4909 }
4910
4911 // Convert to Lab and Lch for GUI feedback
4912 dt_aligned_pixel_t Lab_output = { 0.f };
4913 dt_aligned_pixel_t Lch_output = { 0.f };
4914 dt_XYZ_to_Lab(XYZ_output, Lab_output);
4915 dt_Lab_2_LCH(Lab_output, Lch_output);
4916
4917 // Return the values in sliders
4919 dt_bauhaus_slider_set(g->lightness_spot, Lch_output[0]);
4920 dt_bauhaus_slider_set(g->chroma_spot, Lch_output[1]);
4921 dt_bauhaus_slider_set(g->hue_spot, Lch_output[2] * 360.f);
4922 paint_hue(self);
4924
4925 dt_conf_set_float("darkroom/modules/channelmixerrgb/lightness", Lch_output[0]);
4926 dt_conf_set_float("darkroom/modules/channelmixerrgb/chroma", Lch_output[1]);
4927 dt_conf_set_float("darkroom/modules/channelmixerrgb/hue", Lch_output[2] * 360.f);
4928 dt_conf_set_bool("darkroom/modules/channelmixerrgb/use_mixing", use_mixing);
4929 }
4930 else if(mode == DT_SPOT_MODE_CORRECT)
4931 {
4932 // Get the target color in LMS space
4933 dt_aligned_pixel_t Lch_target = { 0.f };
4934 dt_aligned_pixel_t Lab_target = { 0.f };
4935 dt_aligned_pixel_t XYZ_target = { 0.f };
4936 dt_aligned_pixel_t LMS_target = { 0.f };
4937
4939 Lch_target[0] = dt_bauhaus_slider_get(g->lightness_spot);
4940 Lch_target[1] = dt_bauhaus_slider_get(g->chroma_spot);
4941 Lch_target[2] = dt_bauhaus_slider_get(g->hue_spot) / 360.f;
4943
4944 dt_LCH_2_Lab(Lch_target, Lab_target);
4945 dt_Lab_to_XYZ(Lab_target, XYZ_target);
4946 const float Y_target = XYZ_target[1];
4947 for(int c = 0; c < 3; c++) XYZ_target[c] /= Y_target;
4948 dt_store_simd_aligned(LMS_target, convert_any_XYZ_to_LMS(dt_load_simd_aligned(XYZ_target), p->adaptation));
4949
4950 // optionaly, apply the inverse mixing on the target
4951 if(use_mixing)
4952 {
4953 // Repack the MIX matrix to 3x3 to support the pseudoinverse function
4954 // I'm just too lazy to rewrite the pseudo-inverse for 3x4 padded input
4955 float MIX_3x3[9];
4956 pack_3xSSE_to_3x3(MIX, MIX_3x3);
4957
4958 /* DEBUG
4959 fprintf(stdout, "Repacked channel mixer matrix :\n");
4960 fprintf(stdout, "%f \t%f \t%f\n", MIX_3x3[0][0], MIX_3x3[0][1], MIX_3x3[0][2]);
4961 fprintf(stdout, "%f \t%f \t%f\n", MIX_3x3[1][0], MIX_3x3[1][1], MIX_3x3[1][2]);
4962 fprintf(stdout, "%f \t%f \t%f\n", MIX_3x3[2][0], MIX_3x3[2][1], MIX_3x3[2][2]);
4963 */
4964
4965 // Invert the matrix
4966 float MIX_INV_3x3[9];
4967 matrice_pseudoinverse((float (*)[3])MIX_3x3, (float (*)[3])MIX_INV_3x3, 3);
4968
4969 // Transpose and repack the inverse to SSE matrix because the inversion transposes too
4970 dt_colormatrix_t MIX_INV;
4971 transpose_3x3_to_3xSSE(MIX_INV_3x3, MIX_INV);
4972
4973 /* DEBUG
4974 fprintf(stdout, "Repacked inverted channel mixer matrix :\n");
4975 fprintf(stdout, "%f \t%f \t%f\n", MIX_INV[0][0], MIX_INV[0][1], MIX_INV[0][2]);
4976 fprintf(stdout, "%f \t%f \t%f\n", MIX_INV[1][0], MIX_INV[1][1], MIX_INV[1][2]);
4977 fprintf(stdout, "%f \t%f \t%f\n", MIX_INV[2][0], MIX_INV[2][1], MIX_INV[2][2]);
4978 */
4979
4980 // Undo the channel mixing on the reference color
4981 // So we get the expected target color after the CAT
4982 dt_aligned_pixel_t temp;
4983 dot_product(LMS_target, MIX_INV, temp);
4984
4985 //fprintf(stdout, "LMS before channel mixer inversion : \t%f \t%f \t%f\n", LMS_target[0], LMS_target[1], LMS_target[2]);
4986 //fprintf(stdout, "LMS after channel mixer inversion : \t%f \t%f \t%f\n", temp[0], temp[1], temp[2]);
4987
4988 // convert back to XYZ to normalize luminance again
4989 // in case the matrix is not normalized
4990 dt_store_simd_aligned(XYZ_target, convert_any_LMS_to_XYZ(dt_load_simd_aligned(temp), p->adaptation));
4991 const float Y_mix = XYZ_target[1];
4992 for(int c = 0; c < 3; c++) XYZ_target[c] /= Y_mix;
4993 dt_store_simd_aligned(LMS_target, convert_any_XYZ_to_LMS(dt_load_simd_aligned(XYZ_target), p->adaptation));
4994
4995 //fprintf(stdout, "LMS target after everything : %f \t%f \t%f\n", LMS_target[0], LMS_target[1], LMS_target[2]);
4996
4997 // So now we got the target color after CAT and before mixing
4998 // in LMS space
4999 }
5000
5001 // Get the input color in LMS space
5002 dt_aligned_pixel_t LMS = { 0.f };
5003 const float Y = XYZ[1];
5004 for(int c = 0; c < 3; c++) XYZ[c] /= Y;
5005 dt_store_simd_aligned(LMS, convert_any_XYZ_to_LMS(dt_load_simd_aligned(XYZ), p->adaptation));
5006
5007 // Find the illuminant
5009 convert_D50_to_LMS(p->adaptation, D50);
5010
5011 dt_aligned_pixel_t illuminant_LMS = { 0.f };
5012 dt_aligned_pixel_t illuminant_XYZ = { 0.f };
5013
5014 // We solve the equation : target color / input color = D50 / illuminant, for illuminant
5015 for(int c = 0; c < 3; c++) illuminant_LMS[c] = D50[c] * LMS[c] / LMS_target[c];
5016 dt_store_simd_aligned(illuminant_XYZ, convert_any_LMS_to_XYZ(dt_load_simd_aligned(illuminant_LMS), p->adaptation));
5017
5018 // Convert to xyY
5019 const float sum = fmaxf(illuminant_XYZ[0] + illuminant_XYZ[1] + illuminant_XYZ[2], NORM_MIN);
5020 illuminant_XYZ[0] /= sum; // x
5021 illuminant_XYZ[2] = illuminant_XYZ[1]; // Y
5022 illuminant_XYZ[1] /= sum; // y
5023
5024 p->x = illuminant_XYZ[0];
5025 p->y = illuminant_XYZ[1];
5026
5027 // Force illuminant to custom, the daylight/black body approximations are
5028 // not accurate enough for color matching
5029 p->illuminant = DT_ILLUMINANT_CUSTOM;
5030
5032
5033 check_if_close_to_daylight(p->x, p->y, &p->temperature, NULL, NULL);
5034
5035 dt_bauhaus_slider_set(g->temperature, p->temperature);
5036 dt_bauhaus_combobox_set(g->illuminant, p->illuminant);
5037 dt_bauhaus_combobox_set(g->adaptation, p->adaptation);
5038
5039 const dt_aligned_pixel_t xyY = { p->x, p->y, 1.f };
5040 dt_aligned_pixel_t Lch_illuminant = { 0 };
5041 dt_xyY_to_Lch(xyY, Lch_illuminant);
5042 dt_bauhaus_slider_set(g->illum_x, Lch_illuminant[2] / M_PI * 180.f);
5043 dt_bauhaus_slider_set(g->illum_y, Lch_illuminant[1]);
5044
5045 update_illuminants(self);
5046 update_approx_cct(self);
5048 paint_hue(self);
5050 gtk_widget_queue_draw(g->origin_spot);
5051
5053
5054 if(memcmp(&previous, p, sizeof(previous)) != 0)
5055 {
5056 /* Auto-illuminant is driven by live picker motion, so this commits once per sampled
5057 move. That used to outpace the preview worker and keep it permanently in
5058 TOP_CHANGED, which is why this call was routed through a throttle of its own; the
5059 history commit now batches the resync it triggers, so the picker stays live without
5060 any special handling here. */
5061 dt_print(DT_DEBUG_DEV, "[picker/channelmixerrgb] history commit source=auto_set_illuminant\n");
5062 dt_dev_add_history_item(self->dev, self, TRUE, TRUE);
5063 }
5064 }
5065}
5066
5067
5069{
5070 if(dt_gui_widgets_suppressed()) return;
5071 dt_print(DT_DEBUG_DEV, "[picker/channelmixerrgb] apply picker=%p pipe=%p\n", (void *)picker, (void *)pipe);
5073}
5074
5075
5076void autoset(struct dt_iop_module_t *self, const struct dt_dev_pixelpipe_t *pipe,
5077 const struct dt_dev_pixelpipe_iop_t *piece, const void *i)
5078{
5079 const dt_iop_roi_t *const roi_out = &piece->roi_out;
5081 if(piece->dsc_in.channels != 4) return;
5082
5083 const dt_iop_order_iccprofile_info_t *const current_profile
5085 if(IS_NULL_PTR(current_profile)) return;
5086
5087 dt_colormatrix_t MIX = { { 0.f } };
5088
5089 float norm_R = 1.0f;
5090 if(p->normalize_R) norm_R = p->red[0] + p->red[1] + p->red[2];
5091
5092 float norm_G = 1.0f;
5093 if(p->normalize_G) norm_G = p->green[0] + p->green[1] + p->green[2];
5094
5095 float norm_B = 1.0f;
5096 if(p->normalize_B) norm_B = p->blue[0] + p->blue[1] + p->blue[2];
5097
5098 for(int c = 0; c < 3; c++)
5099 {
5100 MIX[0][c] = p->red[c] / norm_R;
5101 MIX[1][c] = p->green[c] / norm_G;
5102 MIX[2][c] = p->blue[c] / norm_B;
5103 }
5104
5105 float lightness = 50.f;
5106 if(dt_conf_key_exists("darkroom/modules/channelmixerrgb/lightness"))
5107 lightness = dt_conf_get_float("darkroom/modules/channelmixerrgb/lightness");
5108
5109 float hue = 0.f;
5110 if(dt_conf_key_exists("darkroom/modules/channelmixerrgb/hue"))
5111 hue = dt_conf_get_float("darkroom/modules/channelmixerrgb/hue");
5112
5113 float chroma = 0.f;
5114 if(dt_conf_key_exists("darkroom/modules/channelmixerrgb/chroma"))
5115 chroma = dt_conf_get_float("darkroom/modules/channelmixerrgb/chroma");
5116
5117 dt_aligned_pixel_t Lch_target = { lightness, chroma, hue / 360.f, 0.f };
5118 dt_aligned_pixel_t Lab_target = { 0.f };
5119 dt_aligned_pixel_t XYZ_target = { 0.f };
5120 dt_aligned_pixel_t LMS_target = { 0.f };
5121 dt_LCH_2_Lab(Lch_target, Lab_target);
5122 dt_Lab_to_XYZ(Lab_target, XYZ_target);
5123
5124 // Normalize for unit luminance (illuminant)
5125 const float Y_target = XYZ_target[1];
5126 for(int c = 0; c < 3; c++) XYZ_target[c] /= Y_target;
5127 dt_store_simd_aligned(LMS_target, convert_any_XYZ_to_LMS(dt_load_simd_aligned(XYZ_target), p->adaptation));
5128
5129 float MIX_3x3[9];
5130 pack_3xSSE_to_3x3(MIX, MIX_3x3);
5131
5132 float MIX_INV_3x3[9];
5133 matrice_pseudoinverse((float (*)[3])MIX_3x3, (float (*)[3])MIX_INV_3x3, 3);
5134
5135 dt_colormatrix_t MIX_INV;
5136 transpose_3x3_to_3xSSE(MIX_INV_3x3, MIX_INV);
5137
5138 dt_aligned_pixel_t temp = { 0.f };
5139 dot_product(LMS_target, MIX_INV, temp);
5140
5141 dt_store_simd_aligned(XYZ_target, convert_any_LMS_to_XYZ(dt_load_simd_aligned(temp), p->adaptation));
5142 const float Y_mix = XYZ_target[1];
5143 if(Y_mix <= NORM_MIN) return;
5144 for(int c = 0; c < 3; c++) XYZ_target[c] /= Y_mix;
5145 dt_store_simd_aligned(LMS_target, convert_any_XYZ_to_LMS(dt_load_simd_aligned(XYZ_target), p->adaptation));
5146
5147 const float *const restrict in = (float *)i;
5148 float average_L = 0.f;
5149 float average_M = 0.f;
5150 float average_S = 0.f;
5151 size_t valid_pixels = 0;
5152
5153 // Compute average LMS in the image
5154 __OMP_PARALLEL_FOR__(reduction(+:average_L, average_M, average_S, valid_pixels))
5155 for(size_t k = 0; k < roi_out->width * roi_out->height * 4; k += 4)
5156 {
5157 // Convert each input pixel to the module CAT space first, then average the chromaticity there.
5158 dt_aligned_pixel_t XYZ = { 0.f };
5159 dt_aligned_pixel_t LMS = { 0.f };
5160 dt_ioppr_rgb_matrix_to_xyz(in + k, XYZ, current_profile->matrix_in_transposed, current_profile->lut_in,
5161 current_profile->unbounded_coeffs_in, current_profile->lutsize,
5162 current_profile->nonlinearlut);
5163
5164 dt_store_simd_aligned(LMS, convert_any_XYZ_to_LMS(dt_load_simd_aligned(XYZ), p->adaptation));
5165 if(!isfinite(LMS[0]) || !isfinite(LMS[1]) || !isfinite(LMS[2])) continue;
5166
5167 average_L += LMS[0];
5168 average_M += LMS[1];
5169 average_S += LMS[2];
5170 valid_pixels++;
5171 }
5172
5173 if(valid_pixels == 0) return;
5174
5175 const float norm = 1.f / (float)valid_pixels;
5176 dt_aligned_pixel_t average_LMS = { average_L * norm, average_M * norm, average_S * norm, 0.f };
5177
5178 // Normalize for unit luminance
5179 dt_aligned_pixel_t average_XYZ = { 0.f };
5180 dt_store_simd_aligned(average_XYZ, convert_any_LMS_to_XYZ(dt_load_simd_aligned(average_LMS), p->adaptation));
5181 const float Y_average_lms = average_XYZ[1];
5182 for(int c = 0; c < 3; c++) average_XYZ[c] /= Y_average_lms;
5183 dt_store_simd_aligned(average_LMS, convert_any_XYZ_to_LMS(dt_load_simd_aligned(average_XYZ), p->adaptation));
5184
5185 dt_aligned_pixel_t D50 = { 0.f };
5186 dt_aligned_pixel_t illuminant_LMS = { 0.f };
5187 dt_aligned_pixel_t illuminant_XYZ = { 0.f };
5188 convert_D50_to_LMS(p->adaptation, D50);
5189
5190 for(int c = 0; c < 3; c++)
5191 {
5192 const float target = copysignf(fmaxf(fabsf(LMS_target[c]), NORM_MIN), LMS_target[c]);
5193 illuminant_LMS[c] = D50[c] * average_LMS[c] / target;
5194 }
5195
5196 dt_store_simd_aligned(illuminant_XYZ, convert_any_LMS_to_XYZ(dt_load_simd_aligned(illuminant_LMS), p->adaptation));
5197
5198 const float sum = fmaxf(illuminant_XYZ[0] + illuminant_XYZ[1] + illuminant_XYZ[2], NORM_MIN);
5199 p->x = illuminant_XYZ[0] / sum;
5200 p->y = illuminant_XYZ[1] / sum;
5201 p->illuminant = DT_ILLUMINANT_CUSTOM;
5202 check_if_close_to_daylight(p->x, p->y, &p->temperature, NULL, NULL);
5203}
5204
5205
5206void gui_init(struct dt_iop_module_t *self)
5207{
5209
5210 // Init the color checker UI
5211 for(size_t k = 0; k < 4; k++)
5212 {
5213 g->box[k].x = g->box[k].y = -1.;
5214 g->active_node[k] = FALSE;
5215 }
5216 g->is_cursor_close = FALSE;
5217 g->drag_drop = FALSE;
5218 g->is_profiling_started = FALSE;
5219 g->run_profile = FALSE;
5220 g->run_validation = FALSE;
5221 g->profile_ready = FALSE;
5222 g->checker_ready = FALSE;
5223 g->delta_E_in = NULL;
5224 g->delta_E_label_text = NULL;
5225 g->colorcheckers = NULL;
5226
5227 g->XYZ[0] = NAN;
5228
5230 G_CALLBACK(_develop_ui_pipe_finished_callback), self);
5232 G_CALLBACK(_preview_pipe_finished_callback), self);
5233
5234 // Init GTK notebook
5235 g->notebook = dt_ui_notebook_new();
5236 // The CAT page's illuminant picker is read at apply time; reset it if it is
5237 // still active once the user switches away from that page.
5238 dt_ui_notebook_set_picker_owner(g->notebook, self);
5239
5240 // Page CAT
5241 self->gui->widget = dt_ui_notebook_page(g->notebook, N_("CAT"), _("chromatic adaptation transform"));
5242
5243 g->adaptation = dt_bauhaus_combobox_from_params(self, N_("adaptation"));
5244 gtk_widget_set_tooltip_text(GTK_WIDGET(g->adaptation),
5245 _("choose the method to adapt the illuminant\n"
5246 "and the colorspace in which the module works: \n"
5247 "- Linear Bradford (1985) is consistent with ICC v4 toolchain.\n"
5248 "- CAT16 (2016) is more robust and accurate.\n"
5249 "- Non-linear Bradford (1985) is the original Bradford,\n"
5250 " it can produce better results than the linear version, but is unreliable.\n"
5251 "- XYZ is a simple scaling in XYZ space. It is not recommended in general.\n"
5252 "- none disables any adaptation and uses pipeline working RGB."));
5253
5254 GtkWidget *hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, DT_GUI_BOX_SPACING);
5255
5256 g->approx_cct = dt_ui_label_new("CCT:");
5257 gtk_box_pack_start(GTK_BOX(hbox), g->approx_cct, FALSE, FALSE, 0);
5258
5259 g->illum_color = GTK_WIDGET(gtk_drawing_area_new());
5260 gtk_widget_set_size_request(g->illum_color, 2 * DT_PIXEL_APPLY_DPI(dt_bauhaus_get_global()->quad_width),
5262 gtk_widget_set_tooltip_text(GTK_WIDGET(g->illum_color),
5263 _("this is the color of the scene illuminant before chromatic adaptation\n"
5264 "this color will be turned into pure white by the adaptation."));
5265
5266 g_signal_connect(G_OBJECT(g->illum_color), "draw", G_CALLBACK(illuminant_color_draw), self);
5267 gtk_box_pack_start(GTK_BOX(hbox), g->illum_color, TRUE, TRUE, 0);
5268
5269 g->color_picker = dt_color_picker_new(self, DT_COLOR_PICKER_AREA, hbox);
5270 gtk_widget_set_tooltip_text(g->color_picker, _("set white balance to detected from area"));
5271
5272 gtk_box_pack_start(GTK_BOX(self->gui->widget), GTK_WIDGET(hbox), FALSE, FALSE, 0);
5273
5274 g->illuminant = dt_bauhaus_combobox_from_params(self, N_("illuminant"));
5275
5276 g->illum_fluo = dt_bauhaus_combobox_from_params(self, "illum_fluo");
5277
5278 g->illum_led = dt_bauhaus_combobox_from_params(self, "illum_led");
5279
5280 g->temperature = dt_bauhaus_slider_from_params(self, N_("temperature"));
5281
5283 dt_bauhaus_widget_set_label(g->illum_x, N_("hue"));
5284 dt_bauhaus_slider_set_format(g->illum_x, "\302\260");
5285 g_signal_connect(G_OBJECT(g->illum_x), "value-changed", G_CALLBACK(illum_xy_callback), self);
5286 gtk_box_pack_start(GTK_BOX(self->gui->widget), GTK_WIDGET(g->illum_x), FALSE, FALSE, 0);
5287
5288 g->illum_y = dt_bauhaus_slider_new_with_range(dt_bauhaus_get_global(), DT_GUI_MODULE(self), 0., 100., 0, 0, 1);
5289 dt_bauhaus_widget_set_label(g->illum_y, N_("chroma"));
5290 dt_bauhaus_slider_set_format(g->illum_y, "%");
5292 g_signal_connect(G_OBJECT(g->illum_y), "value-changed", G_CALLBACK(illum_xy_callback), self);
5293 gtk_box_pack_start(GTK_BOX(self->gui->widget), GTK_WIDGET(g->illum_y), FALSE, FALSE, 0);
5294
5295 dt_bauhaus_slider_set_soft_range(g->temperature, 3000., 7000.);
5296 dt_bauhaus_slider_set_digits(g->temperature, 0);
5297 dt_bauhaus_slider_set_format(g->temperature, " K");
5298
5299 g->gamut = dt_bauhaus_slider_from_params(self, "gamut");
5300 dt_bauhaus_slider_set_soft_max(g->gamut, 4.f);
5301
5302 g->clip = dt_bauhaus_toggle_from_params(self, "clip");
5303
5304 // Add the color mapping collapsible panel
5305
5307 (&g->csspot,
5308 "plugins/darkroom/channelmixerrgb/expand_picker_mapping",
5309 _("spot color mapping"),
5310 GTK_BOX(self->gui->widget), GTK_PACK_END);
5311
5312 gtk_widget_set_tooltip_text(g->csspot.expander, _("use a color checker target to autoset CAT and channels"));
5313
5314 DT_BAUHAUS_COMBOBOX_NEW_FULL(dt_bauhaus_get_global(), g->spot_mode, DT_GUI_MODULE(self), N_("spot mode"),
5315 _("\"correction\" automatically adjust the illuminant\n"
5316 "such that the input color is mapped to the target.\n"
5317 "\"measure\" simply shows how an input color is mapped by the CAT\n"
5318 "and can be used to sample a target."),
5319 0, NULL, self,
5320 N_("correction"),
5321 N_("measure"));
5322 gtk_box_pack_start(GTK_BOX(g->csspot.container), GTK_WIDGET(g->spot_mode), TRUE, TRUE, 0);
5323 g_signal_connect(G_OBJECT(g->spot_mode), "value-changed", G_CALLBACK(_spot_settings_changed_callback), self);
5324
5325 gchar *label = N_("take channel mixing into account");
5326 g->use_mixing = gtk_check_button_new_with_label(_(label));
5327 gtk_label_set_ellipsize(GTK_LABEL(gtk_bin_get_child(GTK_BIN(g->use_mixing))), PANGO_ELLIPSIZE_END);
5328 gtk_widget_set_tooltip_text(g->use_mixing,
5329 _("compute the target by taking the channel mixing into account.\n"
5330 "if disabled, only the CAT is considered."));
5331 gtk_box_pack_start(GTK_BOX(g->csspot.container), GTK_WIDGET(g->use_mixing), TRUE, TRUE, 0);
5332 g_signal_connect(G_OBJECT(g->use_mixing), "toggled", G_CALLBACK(_spot_settings_changed_callback), self);
5333
5334 GtkWidget *hhbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, DT_GUI_BOX_SPACING);
5335 GtkWidget *vvbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, DT_GUI_BOX_SPACING);
5336
5337 gtk_box_pack_start(GTK_BOX(vvbox), dt_ui_section_label_new(_("input")), FALSE, FALSE, 0);
5338
5339 g->origin_spot = GTK_WIDGET(gtk_drawing_area_new());
5340 gtk_widget_set_size_request(g->origin_spot, 2 * DT_PIXEL_APPLY_DPI(dt_bauhaus_get_global()->quad_width),
5342 gtk_widget_set_tooltip_text(GTK_WIDGET(g->origin_spot),
5343 _("the input color that should be mapped to the target"));
5344
5345 g_signal_connect(G_OBJECT(g->origin_spot), "draw", G_CALLBACK(origin_color_draw), self);
5346 gtk_box_pack_start(GTK_BOX(vvbox), g->origin_spot, TRUE, TRUE, 0);
5347
5348 g->Lch_origin = gtk_label_new(_("L: \tN/A\nh: \tN/A\nc: \tN/A"));
5349 gtk_widget_set_tooltip_text(GTK_WIDGET(g->Lch_origin),
5350 _("these LCh coordinates are computed from CIE Lab 1976 coordinates"));
5351 gtk_box_pack_start(GTK_BOX(vvbox), GTK_WIDGET(g->Lch_origin), FALSE, FALSE, 0);
5352
5353 gtk_box_pack_start(GTK_BOX(hhbox), GTK_WIDGET(vvbox), FALSE, FALSE, DT_BAUHAUS_SPACE);
5354
5355 vvbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, DT_GUI_BOX_SPACING);
5356
5357 gtk_box_pack_start(GTK_BOX(vvbox), dt_ui_section_label_new(_("target")), TRUE, TRUE, 0);
5358
5359 g->target_spot = GTK_WIDGET(gtk_drawing_area_new());
5360 gtk_widget_set_size_request(g->target_spot, 2 * DT_PIXEL_APPLY_DPI(dt_bauhaus_get_global()->quad_width),
5362 gtk_widget_set_tooltip_text(GTK_WIDGET(g->target_spot),
5363 _("the desired target color after mapping"));
5364
5365 g_signal_connect(G_OBJECT(g->target_spot), "draw", G_CALLBACK(target_color_draw), self);
5366 gtk_box_pack_start(GTK_BOX(vvbox), g->target_spot, TRUE, TRUE, 0);
5367
5369 dt_bauhaus_widget_set_label(g->lightness_spot, N_("lightness"));
5370 dt_bauhaus_slider_set_format(g->lightness_spot, "%");
5371 dt_bauhaus_slider_set_default(g->lightness_spot, 50.f);
5372 gtk_box_pack_start(GTK_BOX(vvbox), GTK_WIDGET(g->lightness_spot), TRUE, TRUE, 0);
5373 g_signal_connect(G_OBJECT(g->lightness_spot), "value-changed", G_CALLBACK(_spot_settings_changed_callback), self);
5374
5376 dt_bauhaus_widget_set_label(g->hue_spot, N_("hue"));
5377 dt_bauhaus_slider_set_format(g->hue_spot, "\302\260");
5378 dt_bauhaus_slider_set_default(g->hue_spot, 0.f);
5379 gtk_box_pack_start(GTK_BOX(vvbox), GTK_WIDGET(g->hue_spot), TRUE, TRUE, 0);
5380 g_signal_connect(G_OBJECT(g->hue_spot), "value-changed", G_CALLBACK(_spot_settings_changed_callback), self);
5381
5383 dt_bauhaus_widget_set_label(g->chroma_spot, N_("chroma"));
5384 dt_bauhaus_slider_set_default(g->chroma_spot, 0.f);
5385 gtk_box_pack_start(GTK_BOX(vvbox), GTK_WIDGET(g->chroma_spot), TRUE, TRUE, 0);
5386 g_signal_connect(G_OBJECT(g->chroma_spot), "value-changed", G_CALLBACK(_spot_settings_changed_callback), self);
5387
5388 gtk_box_pack_start(GTK_BOX(hhbox), GTK_WIDGET(vvbox), TRUE, TRUE, DT_BAUHAUS_SPACE);
5389
5390 gtk_box_pack_start(GTK_BOX(g->csspot.container), GTK_WIDGET(hhbox), FALSE, FALSE, 0);
5391
5392 GtkWidget *mixer_page = dt_ui_notebook_page(g->notebook, N_("Mixer"), _("channel mixing"));
5394 dt_bauhaus_widget_set_label(g->mixer_mode, N_("mode"));
5395 dt_bauhaus_combobox_add(g->mixer_mode, _("Complete"));
5396 dt_bauhaus_combobox_add(g->mixer_mode, _("Simple"));
5397 dt_bauhaus_combobox_add(g->mixer_mode, _("Primaries"));
5398 dt_bauhaus_combobox_add(g->mixer_mode, _("White-preserving"));
5399 gtk_widget_set_tooltip_text(g->mixer_mode,
5400 _("complete exposes the original nine mixer coefficients.\n"
5401 "simple rebuilds the normalized mixer as an exact chroma-plane rotation,\n"
5402 "two signed stretches and two neutral couplings.\n"
5403 "primaries rebuilds the mixer as a generalized primaries, white tint and gain model.\n"
5404 "white-preserving rotates and saturates each primary around the white,\n"
5405 "which stays exactly where it is."));
5406 gtk_box_pack_start(GTK_BOX(mixer_page), GTK_WIDGET(g->mixer_mode), FALSE, FALSE, 0);
5407 g_signal_connect(G_OBJECT(g->mixer_mode), "value-changed", G_CALLBACK(_channelmixerrgb_mixer_mode_callback), self);
5408
5409 g->mixer_stack = gtk_stack_new();
5410 gtk_stack_set_transition_type(GTK_STACK(g->mixer_stack), GTK_STACK_TRANSITION_TYPE_NONE);
5411 gtk_box_pack_start(GTK_BOX(mixer_page), GTK_WIDGET(g->mixer_stack), FALSE, FALSE, 0);
5412
5413 GtkWidget *mixer_complete = gtk_box_new(GTK_ORIENTATION_VERTICAL, DT_GUI_BOX_SPACING);
5414 gtk_stack_add_named(GTK_STACK(g->mixer_stack), mixer_complete, "complete");
5415
5416 GtkWidget *first, *second, *third;
5417#define MIXER_ROW(var, short, section, swap) \
5418 gtk_box_pack_start(GTK_BOX(mixer_complete), dt_ui_section_label_new(section), FALSE, FALSE, 0); \
5419 self->gui->widget = mixer_complete; \
5420 first = dt_bauhaus_slider_from_params(self, swap ? #var "[2]" : #var "[0]");\
5421 dt_bauhaus_slider_set_digits(first, 3); \
5422 dt_bauhaus_widget_set_label(first, N_("input R")); \
5423 second = dt_bauhaus_slider_from_params(self, #var "[1]"); \
5424 dt_bauhaus_slider_set_digits(second, 3); \
5425 dt_bauhaus_widget_set_label(second, N_("input G")); \
5426 third = dt_bauhaus_slider_from_params(self, swap ? #var "[0]" : #var "[2]");\
5427 dt_bauhaus_slider_set_digits(third, 3); \
5428 dt_bauhaus_widget_set_label(third, N_("input B")); \
5429 g->scale_##var##_R = swap ? third : first; \
5430 g->scale_##var##_G = second; \
5431 g->scale_##var##_B = swap ? first : third; \
5432 g->normalize_##short = dt_bauhaus_toggle_from_params(self, "normalize_" #short);
5433
5434 MIXER_ROW(red, R, _("output red"), FALSE)
5435 MIXER_ROW(green, G, _("output green"), FALSE)
5436 MIXER_ROW(blue, B, _("output blue"), FALSE)
5437
5438 GtkWidget *mixer_simple = gtk_box_new(GTK_ORIENTATION_VERTICAL, DT_GUI_BOX_SPACING);
5439 gtk_stack_add_named(GTK_STACK(g->mixer_stack), mixer_simple, "simple");
5440
5441 g->simple_theta = dt_bauhaus_slider_new_with_range(dt_bauhaus_get_global(), DT_GUI_MODULE(self), -1.f, 1.f, 0, 0, 3);
5442 dt_bauhaus_widget_set_label(g->simple_theta, N_("global hue rotation"));
5443 dt_bauhaus_slider_set_factor(g->simple_theta, 180.f);
5444 dt_bauhaus_slider_set_format(g->simple_theta, "\302\260");
5445 gtk_widget_set_tooltip_text(g->simple_theta, _("global rotation of the normalized chroma plane."));
5446 gtk_box_pack_start(GTK_BOX(mixer_simple), GTK_WIDGET(g->simple_theta), FALSE, FALSE, 0);
5447 g_signal_connect(G_OBJECT(g->simple_theta), "value-changed", G_CALLBACK(_channelmixerrgb_simple_slider_callback), self);
5448
5449 gtk_box_pack_start(GTK_BOX(mixer_simple), dt_ui_section_label_new(_("chroma")), FALSE, FALSE, 0);
5450
5451 g->simple_psi = dt_bauhaus_slider_new_with_range(dt_bauhaus_get_global(), DT_GUI_MODULE(self), -1.f, 1.f, 0, 0, 3);
5452 dt_bauhaus_widget_set_label(g->simple_psi, N_("chroma (u,v) axes orientation"));
5453 dt_bauhaus_slider_set_factor(g->simple_psi, 90.f);
5454 dt_bauhaus_slider_set_format(g->simple_psi, "\302\260");
5455 gtk_widget_set_tooltip_text(g->simple_psi, _("orientation of the principal stretch axes in the chroma plane."));
5456 gtk_box_pack_start(GTK_BOX(mixer_simple), GTK_WIDGET(g->simple_psi), FALSE, FALSE, 0);
5457 g_signal_connect(G_OBJECT(g->simple_psi), "value-changed", G_CALLBACK(_channelmixerrgb_simple_slider_callback), self);
5458
5459 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);
5460 dt_bauhaus_widget_set_label(g->simple_stretch_1, N_("u stretch"));
5461 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."));
5462 gtk_box_pack_start(GTK_BOX(mixer_simple), GTK_WIDGET(g->simple_stretch_1), FALSE, FALSE, 0);
5463 g_signal_connect(G_OBJECT(g->simple_stretch_1), "value-changed", G_CALLBACK(_channelmixerrgb_simple_slider_callback), self);
5464
5465 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);
5466 dt_bauhaus_widget_set_label(g->simple_stretch_2, N_("v stretch"));
5467 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."));
5468 gtk_box_pack_start(GTK_BOX(mixer_simple), GTK_WIDGET(g->simple_stretch_2), FALSE, FALSE, 0);
5469 g_signal_connect(G_OBJECT(g->simple_stretch_2), "value-changed", G_CALLBACK(_channelmixerrgb_simple_slider_callback), self);
5470
5471 gtk_box_pack_start(GTK_BOX(mixer_simple), dt_ui_section_label_new(_("achromatic coupling")), FALSE, FALSE, 0);
5472
5473 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);
5474 dt_bauhaus_widget_set_label(g->simple_coupling_2, N_("achromatic coupling hue"));
5475 dt_bauhaus_slider_set_factor(g->simple_coupling_2, 180.f);
5476 dt_bauhaus_slider_set_format(g->simple_coupling_2, "\302\260");
5477 gtk_widget_set_tooltip_text(g->simple_coupling_2, _("chroma direction, in the fixed chroma basis, that is coupled into the achromatic axis."));
5478 gtk_box_pack_start(GTK_BOX(mixer_simple), GTK_WIDGET(g->simple_coupling_2), FALSE, FALSE, 0);
5479 g_signal_connect(G_OBJECT(g->simple_coupling_2), "value-changed", G_CALLBACK(_channelmixerrgb_simple_slider_callback), self);
5480
5481 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);
5482 dt_bauhaus_widget_set_label(g->simple_coupling_1, N_("achromatic coupling amount"));
5483 gtk_widget_set_tooltip_text(g->simple_coupling_1, _("strength of the chroma-to-achromatic coupling in the fixed chroma basis."));
5484 gtk_box_pack_start(GTK_BOX(mixer_simple), GTK_WIDGET(g->simple_coupling_1), FALSE, FALSE, 0);
5485 g_signal_connect(G_OBJECT(g->simple_coupling_1), "value-changed", G_CALLBACK(_channelmixerrgb_simple_slider_callback), self);
5486
5487 GtkWidget *mixer_primaries = gtk_box_new(GTK_ORIENTATION_VERTICAL, DT_GUI_BOX_SPACING);
5488 gtk_stack_add_named(GTK_STACK(g->mixer_stack), mixer_primaries, "primaries");
5489
5490 gtk_box_pack_start(GTK_BOX(mixer_primaries), dt_ui_section_label_new(_("achromatic axis")), FALSE, FALSE, 0);
5491
5492 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);
5493 dt_bauhaus_widget_set_label(g->primaries_achromatic_hue, N_("white hue"));
5494 dt_bauhaus_slider_set_factor(g->primaries_achromatic_hue, 90.f);
5495 dt_bauhaus_slider_set_format(g->primaries_achromatic_hue, "\302\260");
5496 gtk_widget_set_tooltip_text(g->primaries_achromatic_hue,
5497 _("rotate the custom white vector around the D50 white of the current mixer basis."));
5498 gtk_box_pack_start(GTK_BOX(mixer_primaries), GTK_WIDGET(g->primaries_achromatic_hue), FALSE, FALSE, 0);
5499 g_signal_connect(G_OBJECT(g->primaries_achromatic_hue), "value-changed",
5501
5502 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);
5503 dt_bauhaus_widget_set_label(g->primaries_achromatic_purity, N_("white purity"));
5504 gtk_widget_set_tooltip_text(g->primaries_achromatic_purity,
5505 _("distance of the custom white vector from the D50 white within the current mixer basis."));
5506 gtk_box_pack_start(GTK_BOX(mixer_primaries), GTK_WIDGET(g->primaries_achromatic_purity), FALSE, FALSE, 0);
5507 g_signal_connect(G_OBJECT(g->primaries_achromatic_purity), "value-changed",
5509
5510 gtk_box_pack_start(GTK_BOX(mixer_primaries), dt_ui_section_label_new(_("red primary")), FALSE, FALSE, 0);
5511
5512 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);
5513 dt_bauhaus_widget_set_label(g->primaries_red_hue, N_("red hue"));
5514 dt_bauhaus_slider_set_factor(g->primaries_red_hue, 90.f);
5515 dt_bauhaus_slider_set_format(g->primaries_red_hue, "\302\260");
5516 gtk_widget_set_tooltip_text(g->primaries_red_hue,
5517 _("rotate the first basis vector around the D50 white of the current mixer basis."));
5518 gtk_box_pack_start(GTK_BOX(mixer_primaries), GTK_WIDGET(g->primaries_red_hue), FALSE, FALSE, 0);
5519 g_signal_connect(G_OBJECT(g->primaries_red_hue), "value-changed",
5521
5522 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);
5523 dt_bauhaus_widget_set_label(g->primaries_red_purity, N_("red purity"));
5524 gtk_widget_set_tooltip_text(g->primaries_red_purity,
5525 _("radial scaling of the first basis vector inside the affine primaries footprint."));
5526 gtk_box_pack_start(GTK_BOX(mixer_primaries), GTK_WIDGET(g->primaries_red_purity), FALSE, FALSE, 0);
5527 g_signal_connect(G_OBJECT(g->primaries_red_purity), "value-changed",
5529
5530 gtk_box_pack_start(GTK_BOX(mixer_primaries), dt_ui_section_label_new(_("green primary")), FALSE, FALSE, 0);
5531
5532 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);
5533 dt_bauhaus_widget_set_label(g->primaries_green_hue, N_("green hue"));
5534 dt_bauhaus_slider_set_factor(g->primaries_green_hue, 90.f);
5535 dt_bauhaus_slider_set_format(g->primaries_green_hue, "\302\260");
5536 gtk_widget_set_tooltip_text(g->primaries_green_hue,
5537 _("rotate the second basis vector around the D50 white of the current mixer basis."));
5538 gtk_box_pack_start(GTK_BOX(mixer_primaries), GTK_WIDGET(g->primaries_green_hue), FALSE, FALSE, 0);
5539 g_signal_connect(G_OBJECT(g->primaries_green_hue), "value-changed",
5541
5542 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);
5543 dt_bauhaus_widget_set_label(g->primaries_green_purity, N_("green purity"));
5544 gtk_widget_set_tooltip_text(g->primaries_green_purity,
5545 _("radial scaling of the second basis vector inside the affine primaries footprint."));
5546 gtk_box_pack_start(GTK_BOX(mixer_primaries), GTK_WIDGET(g->primaries_green_purity), FALSE, FALSE, 0);
5547 g_signal_connect(G_OBJECT(g->primaries_green_purity), "value-changed",
5549
5550 gtk_box_pack_start(GTK_BOX(mixer_primaries), dt_ui_section_label_new(_("blue primary")), FALSE, FALSE, 0);
5551
5552 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);
5553 dt_bauhaus_widget_set_label(g->primaries_blue_hue, N_("blue hue"));
5554 dt_bauhaus_slider_set_factor(g->primaries_blue_hue, 90.f);
5555 dt_bauhaus_slider_set_format(g->primaries_blue_hue, "\302\260");
5556 gtk_widget_set_tooltip_text(g->primaries_blue_hue,
5557 _("rotate the third basis vector around the D50 white of the current mixer basis."));
5558 gtk_box_pack_start(GTK_BOX(mixer_primaries), GTK_WIDGET(g->primaries_blue_hue), FALSE, FALSE, 0);
5559 g_signal_connect(G_OBJECT(g->primaries_blue_hue), "value-changed",
5561
5562 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);
5563 dt_bauhaus_widget_set_label(g->primaries_blue_purity, N_("blue purity"));
5564 gtk_widget_set_tooltip_text(g->primaries_blue_purity,
5565 _("radial scaling of the third basis vector inside the affine primaries footprint."));
5566 gtk_box_pack_start(GTK_BOX(mixer_primaries), GTK_WIDGET(g->primaries_blue_purity), FALSE, FALSE, 0);
5567 g_signal_connect(G_OBJECT(g->primaries_blue_purity), "value-changed",
5569
5570 gtk_box_pack_start(GTK_BOX(mixer_primaries), dt_ui_section_label_new(_("gain correction")), FALSE, FALSE, 0);
5571
5572 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);
5573 dt_bauhaus_widget_set_label(g->primaries_gain, N_("gain"));
5574 gtk_widget_set_tooltip_text(g->primaries_gain,
5575 _("global gain multiplying the custom white vector after the affine primaries transform."));
5576 gtk_box_pack_start(GTK_BOX(mixer_primaries), GTK_WIDGET(g->primaries_gain), FALSE, FALSE, 0);
5577 g_signal_connect(G_OBJECT(g->primaries_gain), "value-changed",
5579
5580 GtkWidget *mixer_white_preserving = gtk_box_new(GTK_ORIENTATION_VERTICAL, DT_GUI_BOX_SPACING);
5581 gtk_stack_add_named(GTK_STACK(g->mixer_stack), mixer_white_preserving, "white-preserving");
5582
5583#define WHITE_PRESERVING_PRIMARY(var, section, rotation_label, rotation_tooltip, saturation_label, \
5584 saturation_tooltip) \
5585 gtk_box_pack_start(GTK_BOX(mixer_white_preserving), dt_ui_section_label_new(section), FALSE, FALSE, 0); \
5586 g->white_preserving_##var##_rotation \
5587 = dt_bauhaus_slider_new_with_range(dt_bauhaus_get_global(), DT_GUI_MODULE(self), -1.f, 1.f, 0, 0, 3); \
5588 dt_bauhaus_widget_set_label(g->white_preserving_##var##_rotation, rotation_label); \
5589 dt_bauhaus_slider_set_factor(g->white_preserving_##var##_rotation, 90.f); \
5590 dt_bauhaus_slider_set_format(g->white_preserving_##var##_rotation, "\302\260"); \
5591 gtk_widget_set_tooltip_text(g->white_preserving_##var##_rotation, rotation_tooltip); \
5592 gtk_box_pack_start(GTK_BOX(mixer_white_preserving), GTK_WIDGET(g->white_preserving_##var##_rotation), FALSE,\
5593 FALSE, 0); \
5594 g_signal_connect(G_OBJECT(g->white_preserving_##var##_rotation), "value-changed", \
5595 G_CALLBACK(_channelmixerrgb_white_preserving_slider_callback), self); \
5596 /* -100%/+100% is the travel this control is meant to be used over, but a matrix hand-built */ \
5597 /* in complete mode can legitimately push a primary further out. Keep the hard range wider so */ \
5598 /* the page reports such a matrix instead of clamping and quietly lying about it. */ \
5599 g->white_preserving_##var##_saturation \
5600 = dt_bauhaus_slider_new_with_range(dt_bauhaus_get_global(), DT_GUI_MODULE(self), -4.f, 4.f, 0, 0, 3); \
5601 dt_bauhaus_slider_set_soft_range(g->white_preserving_##var##_saturation, -1.f, 1.f); \
5602 dt_bauhaus_widget_set_label(g->white_preserving_##var##_saturation, saturation_label); \
5603 /* On a range under 10, a "%" format makes bauhaus scale by 100 and take two digits off by */ \
5604 /* itself. Do not also set a factor, and leave it enough digits to take : it subtracts them */ \
5605 /* unconditionally, and a negative digit count used to hang the whole GUI in ipow(). */ \
5606 dt_bauhaus_slider_set_format(g->white_preserving_##var##_saturation, "%"); \
5607 gtk_widget_set_tooltip_text(g->white_preserving_##var##_saturation, saturation_tooltip); \
5608 gtk_box_pack_start(GTK_BOX(mixer_white_preserving), GTK_WIDGET(g->white_preserving_##var##_saturation), \
5609 FALSE, FALSE, 0); \
5610 g_signal_connect(G_OBJECT(g->white_preserving_##var##_saturation), "value-changed", \
5611 G_CALLBACK(_channelmixerrgb_white_preserving_slider_callback), self);
5612
5613 WHITE_PRESERVING_PRIMARY(red, _("red primary"), N_("red rotation"),
5614 _("rotate the red primary around the white of the current mixer basis.\n"
5615 "the white itself is left exactly where it is."),
5616 N_("red saturation"),
5617 _("scale the red 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(green, _("green primary"), N_("green rotation"),
5620 _("rotate the green primary around the white of the current mixer basis.\n"
5621 "the white itself is left exactly where it is."),
5622 N_("green saturation"),
5623 _("scale the green primary's distance to the white of the current mixer basis.\n"
5624 "-100% collapses it onto the white, +100% doubles it."))
5625 WHITE_PRESERVING_PRIMARY(blue, _("blue primary"), N_("blue rotation"),
5626 _("rotate the blue primary around the white of the current mixer basis.\n"
5627 "the white itself is left exactly where it is."),
5628 N_("blue saturation"),
5629 _("scale the blue primary's distance to the white of the current mixer basis.\n"
5630 "-100% collapses it onto the white, +100% doubles it."))
5631
5632#undef WHITE_PRESERVING_PRIMARY
5633
5634 GtkWidget *outputs_page = dt_ui_notebook_page(g->notebook, N_("Outputs"),
5635 _("output colorfulness, brightness and B&W mixing"));
5636 self->gui->widget = outputs_page;
5637
5638#define OUTPUT_SECTION(var, short, section, swap) \
5639 gtk_box_pack_start(GTK_BOX(outputs_page), dt_ui_section_label_new(section), FALSE, FALSE, 0); \
5640 \
5641 first = dt_bauhaus_slider_from_params(self, swap ? #var "[2]" : #var "[0]");\
5642 dt_bauhaus_slider_set_digits(first, 3); \
5643 dt_bauhaus_widget_set_label(first, N_("input R")); \
5644 \
5645 second = dt_bauhaus_slider_from_params(self, #var "[1]"); \
5646 dt_bauhaus_slider_set_digits(second, 3); \
5647 dt_bauhaus_widget_set_label(second, N_("input G")); \
5648 \
5649 third = dt_bauhaus_slider_from_params(self, swap ? #var "[0]" : #var "[2]");\
5650 dt_bauhaus_slider_set_digits(third, 3); \
5651 dt_bauhaus_widget_set_label(third, N_("input B")); \
5652 \
5653 g->scale_##var##_R = swap ? third : first; \
5654 g->scale_##var##_G = second; \
5655 g->scale_##var##_B = swap ? first : third; \
5656 \
5657 g->normalize_##short = dt_bauhaus_toggle_from_params(self, "normalize_" #short);
5658
5659 OUTPUT_SECTION(saturation, sat, _("colorfulness"), FALSE)
5660 g->saturation_version = dt_bauhaus_combobox_from_params(self, "version");
5661 OUTPUT_SECTION(lightness, light, _("brightness"), FALSE)
5662 OUTPUT_SECTION(grey, grey, _("B&W"), FALSE)
5663
5664 // start building top level widget
5665 self->gui->widget = gtk_box_new(GTK_ORIENTATION_VERTICAL, DT_GUI_BOX_SPACING);
5666
5667 gtk_box_pack_start(GTK_BOX(self->gui->widget), GTK_WIDGET(g->notebook), FALSE, FALSE, 0);
5668 const int saved_page = dt_conf_get_int("plugins/darkroom/channelmixerrgb/gui_page");
5669 const int active_page = saved_page > 2 ? 2 : CLAMP(saved_page, 0, 2);
5670 gtk_widget_show(gtk_notebook_get_nth_page(g->notebook, active_page));
5671 gtk_notebook_set_current_page(g->notebook, active_page);
5672
5673 // Add the color checker collapsible panel
5674
5676 (&g->cs,
5677 "plugins/darkroom/channelmixerrgb/expand_values",
5678 _("calibrate with a color checker"),
5679 GTK_BOX(self->gui->widget), GTK_PACK_END);
5680
5681 gtk_widget_set_tooltip_text(g->cs.toggle,
5682 _("use a color checker target to autoset CAT and channels"));
5683 g_signal_connect(G_OBJECT(g->cs.toggle), "toggled", G_CALLBACK(start_profiling_callback), self);
5684
5685 GtkWidget *collapsible = GTK_WIDGET(g->cs.container);
5686
5687 gchar *tip_files_loc = NULL;
5688 {
5689 char confdir[DT_PATH_MAX] = { 0 };
5690 dt_loc_get_user_config_dir(confdir, sizeof(confdir));
5691
5692 gchar *user_CGATS_dir = g_build_filename(confdir, "color", "checker", NULL);
5693 tip_files_loc = g_strdup_printf(_("'%s'"), user_CGATS_dir);
5694 dt_free(user_CGATS_dir);
5695 }
5696
5698 dt_bauhaus_widget_set_label(g->checkers_list, N_("Chart"));
5699 gtk_box_pack_start(GTK_BOX(collapsible), GTK_WIDGET(g->checkers_list), TRUE, TRUE, 0);
5700 dt_bauhaus_combobox_set(g->checkers_list, 0);
5701 gchar *tooltip = g_strdup_printf(_("Choose the vendor and the type of your chart.\n"
5702 ".cht files in %s."), tip_files_loc);
5703 gtk_widget_set_tooltip_text(g->checkers_list, tooltip);
5705 g_signal_connect(G_OBJECT(g->checkers_list), "value-changed", G_CALLBACK(checker_changed_callback), (gpointer)self);
5706
5707 g->checkers_color_list = dt_bauhaus_combobox_new(dt_bauhaus_get_global(), DT_GUI_MODULE(self));
5708 dt_bauhaus_widget_set_label(g->checkers_color_list, N_("Chart color"));
5709 gtk_box_pack_start(GTK_BOX(collapsible), GTK_WIDGET(g->checkers_color_list), TRUE, TRUE, 0);
5710
5711 dt_bauhaus_combobox_set(g->checkers_color_list, 0);
5712 tooltip = g_strdup_printf(_("Choose the definition of your chart.\n"
5713 "CGATS.17 files in %s."), tip_files_loc);
5714 gtk_widget_set_tooltip_text(g->checkers_color_list, tooltip);
5716 g_signal_connect(G_OBJECT(g->checkers_color_list), "value-changed", G_CALLBACK(checker_color_changed_callback), (gpointer)self);
5717
5718 tooltip = g_markup_printf_escaped(_("<i>No CGATS.17 color file matches the selected chart.\n"
5719 "Add a compatible CGATS.17 file to <b>%s</b>.</i>"), tip_files_loc);
5720 g->checker_msg = gtk_label_new(NULL);
5721 gtk_label_set_markup(GTK_LABEL(g->checker_msg), tooltip);
5723 gtk_label_set_line_wrap(GTK_LABEL(g->checker_msg), TRUE);
5724 gtk_label_set_line_wrap_mode(GTK_LABEL(g->checker_msg), PANGO_WRAP_WORD);
5725 gtk_box_pack_start(GTK_BOX(collapsible), GTK_WIDGET(g->checker_msg), FALSE, TRUE, 0);
5726 gtk_widget_set_visible(g->checker_msg, FALSE);
5727 dt_free(tip_files_loc);
5728
5729
5730
5731 DT_BAUHAUS_COMBOBOX_NEW_FULL(dt_bauhaus_get_global(), g->optimize, DT_GUI_MODULE(self), N_("optimize for"),
5732 _("choose the colors that will be optimized with higher priority.\n"
5733 "neutral colors gives the lowest average delta E but a high maximum delta E\n"
5734 "saturated colors gives the lowest maximum delta E but a high average delta E\n"
5735 "none is a trade-off between both\n"
5736 "the others are special behaviours to protect some hues"),
5738 N_("none"),
5739 N_("neutral colors"),
5740 N_("saturated colors"),
5741 N_("skin and soil colors"),
5742 N_("foliage colors"),
5743 N_("sky and water colors"),
5744 N_("average delta E"),
5745 N_("maximum delta E"));
5746 gtk_box_pack_start(GTK_BOX(collapsible), GTK_WIDGET(g->optimize), TRUE, TRUE, 0);
5747
5749 dt_bauhaus_widget_set_label(g->safety, N_("patch scale"));
5750 gtk_widget_set_tooltip_text(g->safety, _("reduce the radius of the patches to select the more or less central part.\n"
5751 "useful when the perspective correction is sloppy or\n"
5752 "the patches frame cast a shadows on the edges of the patch." ));
5753 g_signal_connect(G_OBJECT(g->safety), "value-changed", G_CALLBACK(safety_changed_callback), self);
5754 gtk_box_pack_start(GTK_BOX(collapsible), GTK_WIDGET(g->safety), TRUE, TRUE, 0);
5755
5756 g->label_delta_E = dt_ui_label_new("");
5757 gtk_box_pack_start(GTK_BOX(collapsible), GTK_WIDGET(g->label_delta_E), TRUE, TRUE, 0);
5758 gtk_widget_set_tooltip_text(g->label_delta_E, _("the delta E is using the CIE 2000 formula"));
5759
5760 GtkWidget *toolbar = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, DT_GUI_BOX_SPACING);
5761
5762 g->button_commit = dtgtk_button_new(dtgtk_cairo_paint_check_mark, 0, NULL);
5763 gtk_box_pack_end(GTK_BOX(toolbar), GTK_WIDGET(g->button_commit), FALSE, FALSE, 0);
5764 gtk_widget_set_tooltip_text(g->button_commit, _("accept the computed profile and set it in the module"));
5765 g_signal_connect(G_OBJECT(g->button_commit), "button-press-event", G_CALLBACK(commit_profile_callback), (gpointer)self);
5766
5767 g->button_profile = dtgtk_button_new(dtgtk_cairo_paint_refresh, 0, NULL);
5768 g_signal_connect(G_OBJECT(g->button_profile), "button-press-event", G_CALLBACK(run_profile_callback), (gpointer)self);
5769 gtk_widget_set_tooltip_text(g->button_profile, _("recompute the profile"));
5770 gtk_box_pack_end(GTK_BOX(toolbar), GTK_WIDGET(g->button_profile), FALSE, FALSE, 0);
5771
5772 g->button_validate = dtgtk_button_new(dtgtk_cairo_paint_softproof, 0, NULL);
5773 g_signal_connect(G_OBJECT(g->button_validate), "button-press-event", G_CALLBACK(run_validation_callback), (gpointer)self);
5774 gtk_widget_set_tooltip_text(g->button_validate, _("check the output delta E"));
5775 gtk_box_pack_end(GTK_BOX(toolbar), GTK_WIDGET(g->button_validate), FALSE, FALSE, 0);
5776
5777 gtk_box_pack_start(GTK_BOX(collapsible), GTK_WIDGET(toolbar), FALSE, FALSE, 0);
5778}
5779
5781{
5784 G_CALLBACK(_develop_ui_pipe_finished_callback), self);
5786
5788 dt_conf_set_int("plugins/darkroom/channelmixerrgb/gui_page", gtk_notebook_get_current_page (g->notebook));
5789
5790 if(g->delta_E_in)
5791 {
5792 dt_free_align(g->delta_E_in);
5793 g->delta_E_in = NULL;
5794 }
5795
5796 dt_free(g->delta_E_label_text);
5797
5798 dt_colorchecker_label_list_cleanup(&(g->colorcheckers));
5799 dt_colorchecker_label_list_cleanup(&(g->colorcheckers_all_color));
5800 g_list_free(g->colorcheckers_color); // data pointers are already freed by the cleanup function for colorcheckers_all_color
5801
5802 dt_colorchecker_cleanup(g->checker);
5803
5805}
5806
5807// clang-format off
5808// modelines: These editor modelines have been set for all relevant files by tools/update_modelines.py
5809// vim: shiftwidth=2 expandtab tabstop=2 cindent
5810// kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified;
5811// 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:3346
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:2000
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:2010
void dt_bauhaus_slider_set_stop(GtkWidget *widget, float stop, float r, float g, float b)
Definition bauhaus.c:2164
float dt_bauhaus_slider_get(GtkWidget *widget)
Definition bauhaus.c:3283
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:1641
int dt_bauhaus_combobox_get(GtkWidget *widget)
Definition bauhaus.c:2139
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:1966
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:1849
void dt_bauhaus_combobox_add_separator(GtkWidget *widget)
Definition bauhaus.c:1861
void dt_bauhaus_slider_set(GtkWidget *widget, float pos)
Definition bauhaus.c:3334
void dt_bauhaus_combobox_set(GtkWidget *widget, const int pos)
Definition bauhaus.c:2093
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:1635
void dt_bauhaus_combobox_remove_at(GtkWidget *widget, int pos)
Definition bauhaus.c:1912
GtkWidget * dt_bauhaus_combobox_new(dt_bauhaus_t *bh, dt_gui_module_t *self)
Definition bauhaus.c:1701
gboolean dt_bauhaus_combobox_set_from_text(GtkWidget *widget, const char *text)
Definition bauhaus.c:2103
void dt_bauhaus_slider_set_format(GtkWidget *widget, const char *format)
Definition bauhaus.c:3410
void dt_bauhaus_combobox_add(GtkWidget *widget, const char *text)
Definition bauhaus.c:1827
void dt_bauhaus_slider_set_factor(GtkWidget *widget, float factor)
Definition bauhaus.c:3426
@ 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:352
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
static gboolean run_profile_callback(GtkWidget *widget __attribute__((unused)), GdkEventButton *event __attribute__((unused)), gpointer user_data)
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 gboolean commit_profile_callback(GtkWidget *widget __attribute__((unused)), GdkEventButton *event __attribute__((unused)), gpointer user_data)
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 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 gboolean run_validation_callback(GtkWidget *widget __attribute__((unused)), GdkEventButton *event __attribute__((unused)), gpointer user_data)
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
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)
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:839
void dt_control_queue_redraw_center()
Request a redraw of the centre view.
Definition control.c:970
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:1015
#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:511
struct dt_bauhaus_t * dt_bauhaus_get_global(void)
Definition darktable.c:649
#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:2026
void dt_dev_coordinates_image_norm_to_preview_abs(dt_develop_t *dev, float *points, size_t num_points)
Definition develop.c:1358
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:2138
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:1211
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:319
@ DT_IMAGE_4BAYER
Definition image.h:149
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:1713
gboolean dt_iop_is_first_instance(GList *modules, dt_iop_module_t *module)
Definition imageop.c:1891
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:1909
void dt_iop_set_cache_bypass(dt_iop_module_t *module, gboolean state)
Definition imageop.c:1685
@ 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 const dt_dev_pixelpipe_t * pipe
Definition layers.h:49
static const dt_dev_pixelpipe_t const dt_dev_pixelpipe_iop_t * piece
Definition layers.h:50
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:2612
int dt_opencl_create_kernel(const int prog, const char *name)
Definition opencl.c:2506
void * dt_opencl_copy_host_to_device_constant(const int devid, const size_t size, void *host)
Definition opencl.c:2808
void dt_opencl_free_kernel(const int kernel)
Definition opencl.c:2549
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:2603
void dt_opencl_release_mem_object(cl_mem mem)
Definition opencl.c:2863
#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:411
struct dt_control_signal_t * dt_control_signal_get_global(void)
Definition darktable.c:619
@ 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:178
@ 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:183
#define DT_DEBUG_CONTROL_SIGNAL_CONNECT(ctlsig, signal, cb, user_data)
Definition signal.h:400
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_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:410
float d65_color_matrix[9]
Definition image.h:430
float adobe_XYZ_to_CAM[4][3]
Definition image.h:458
unsigned int channels
Definition format.h:83
dt_iop_buffer_type_t datatype
Definition format.h:85
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)