Ansel 0.0
A darktable fork - bloat + design vision
Loading...
Searching...
No Matches
toneequal.c
Go to the documentation of this file.
1/*
2 This file is part of the Ansel project.
3 Copyright (C) 2018-2020, 2022-2026 Aurélien PIERRE.
4 Copyright (C) 2019 Andreas Schneider.
5 Copyright (C) 2019, 2021 luzpaz.
6 Copyright (C) 2019-2022 Pascal Obry.
7 Copyright (C) 2019-2020 rawfiner.
8 Copyright (C) 2019 Tobias Ellinghaus.
9 Copyright (C) 2020, 2022 Aldric Renaudin.
10 Copyright (C) 2020-2021 Chris Elston.
11 Copyright (C) 2020-2022 Diederik Ter Rahe.
12 Copyright (C) 2020 hatsunearu.
13 Copyright (C) 2020-2021 Hubert Kowalski.
14 Copyright (C) 2020 Matthieu Moy.
15 Copyright (C) 2020-2021 Ralf Brown.
16 Copyright (C) 2020 U-DESKTOP-TRPCBD3\Matthijs.
17 Copyright (C) 2021 Dan Torop.
18 Copyright (C) 2021 Heiko Bauke.
19 Copyright (C) 2021 lhietal.
20 Copyright (C) 2021 Marco Carrarini.
21 Copyright (C) 2021 Mark-64.
22 Copyright (C) 2021 Paolo DePetrillo.
23 Copyright (C) 2022 Hanno Schwalm.
24 Copyright (C) 2022 Martin Bařinka.
25 Copyright (C) 2022 Nicolas Auffray.
26 Copyright (C) 2022 Philipp Lutz.
27 Copyright (C) 2022 Sakari Kapanen.
28 Copyright (C) 2022 Victor Forsiuk.
29 Copyright (C) 2023-2024 Alynx Zhou.
30 Copyright (C) 2023 Luca Zulberti.
31 Copyright (C) 2025-2026 Guillaume Stutin.
32
33 Ansel is free software: you can redistribute it and/or modify
34 it under the terms of the GNU General Public License as published by
35 the Free Software Foundation, either version 3 of the License, or
36 (at your option) any later version.
37
38 Ansel is distributed in the hope that it will be useful,
39 but WITHOUT ANY WARRANTY; without even the implied warranty of
40 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
41 GNU General Public License for more details.
42
43 You should have received a copy of the GNU General Public License
44 along with Ansel. If not, see <http://www.gnu.org/licenses/>.
45*/
46
47/*** DOCUMENTATION
48 *
49 * This module aims at relighting the scene by performing an exposure compensation
50 * selectively on specified exposures octaves, the same way HiFi audio equalizers allow to set
51 * a gain for each octave.
52 *
53 * It is intended to work in scene-linear camera RGB, to behave as if light was physically added
54 * or removed from the scene. As such, it should be put before input profile in the pipe, but preferably
55 * after exposure. It also need to be placed after the rotation, perspective and cropping modules
56 * for the interactive editing to work properly (so the image buffer overlap perfectly with the
57 * image preview).
58 *
59 * Because it works before camera RGB -> XYZ conversion, the exposure cannot be computed from
60 * any human-based perceptual colour model (Y channel), hence why several RGB norms are provided as estimators of
61 * the pixel energy to compute a luminance map. None of them is perfect, and I'm still
62 * looking forward to a real spectral energy estimator. The best physically-accurate norm should be the euclidean
63 * norm, but the best looking is often the power norm, which has no theoretical background.
64 * The geometric mean also display interesting properties as it interprets saturated colours
65 * as low-lights, allowing to lighten and desaturate them in a realistic way.
66 *
67 * The exposure correction is computed as a series of each octave's gain weighted by the
68 * gaussian of the radial distance between the current pixel exposure and each octave's center.
69 * This allows for a smooth and continuous infinite-order interpolation, preserving exposure gradients
70 * as best as possible. The radius of the kernel is user-defined and can be tweaked to get
71 * a smoother interpolation (possibly generating oscillations), or a more monotonous one
72 * (possibly less smooth). The actual factors of the gaussian series are computed by
73 * solving the linear system taking the user-input parameters as target exposures compensations.
74 *
75 * Notice that every pixel operation is performed in linear space. The exposures in log2 (EV)
76 * are only used for user-input parameters and for the gaussian weights of the radial distance
77 * between pixel exposure and octave's centers.
78 *
79 * The details preservation modes make use of a fast guided filter optimized to perform
80 * an edge-aware surface blur on the luminance mask, in the same spirit as the bilateral
81 * filter, but without its classic issues of gradient reversal around sharp edges. This
82 * surface blur will allow to perform piece-wise smooth exposure compensation, so local contrast
83 * will be preserved inside contiguous regions. Various mask refinements are provided to help
84 * the edge-taping of the filter (feathering parameter) while keeping smooth contiguous region
85 * (quantization parameter), but also to translate (exposure boost) and dilate (contrast boost)
86 * the exposure histogram through the control octaves, to center it on the control view
87 * and make maximum use of the available channels.
88 *
89 * Users should be aware that not all the available octaves will be useful on every pictures.
90 * Some automatic options will help them to optimize the luminance mask, performing histogram
91 * analyse, mapping the average exposure to -4EV, and mapping the first and last deciles of
92 * the histogram on its average ± 4EV. These automatic helpers usually fail on X-Trans sensors,
93 * maybe because of bad demosaicing, possibly resulting in outliers\negative RGB values.
94 * Since they fail the same way on filmic's auto-tuner, we might need to investigate X-Trans
95 * algos at some point.
96 *
97***/
98
99#ifdef HAVE_CONFIG_H
100#include "config.h"
102#include "widgets/accelerators.h"
103#endif
104#include "develop/masks_gui.h"
105#include <assert.h>
106#include <math.h>
107#include <stdlib.h>
108#include <stdio.h>
109#include <string.h>
110#include <time.h>
111
112#include "widgets/bauhaus.h"
113#include "system/macros.h"
114#include "system/openmp.h"
115#include "system/target_clones.h"
116#include "system/mem_alloc.h"
117#include "system/simd.h"
118#include "common/hash.h"
119#include "common/logging.h"
122#include "develop/masks.h"
124#include "pixel/eigf.h"
125#include "pixel/luminance_mask.h"
126#include "common/collection.h"
127#include "common/conf.h"
128#include "control/control.h"
129#include "develop/develop.h"
130#include "develop/imageop.h"
131#include "develop/imageop_gui.h"
133
135#include "widgets/draw.h"
136#include "gui/application.h"
137#include "gui/presets.h"
139#include "iop/iop_api.h"
140#include "math/choleski.h"
141#include "libs/colorpicker.h"
142#include "widgets/label.h"
143#include "widgets/notebook.h"
144#include "widgets/scroll_wrap.h"
145#include "widgets/widget_style.h"
146#include "gui/screen_metrics.h"
147
148#include "control/signal.h"
149#include "widgets/togglebutton.h"
150
151#ifdef _OPENMP
152#include <omp.h>
153#endif
154
155
157
158
159#define UI_SAMPLES 256 // 128 is a bit small for 4K resolution
160#define CONTRAST_FULCRUM exp2f(-4.0f)
161#define MIN_FLOAT exp2f(-16.0f)
162
168#define CHANNELS 9
169#define PIXEL_CHAN 8
170#define LUT_RESOLUTION 10000
171
172// radial distances used for pixel ops
173static const float centers_ops[PIXEL_CHAN] DT_ALIGNED_ARRAY = {-56.0f / 7.0f, // = -8.0f
174 -48.0f / 7.0f,
175 -40.0f / 7.0f,
176 -32.0f / 7.0f,
177 -24.0f / 7.0f,
178 -16.0f / 7.0f,
179 -8.0f / 7.0f,
180 0.0f / 7.0f}; // split 8 EV into 7 evenly-spaced channels
181
182static const float centers_params[CHANNELS] DT_ALIGNED_ARRAY = { -8.0f, -7.0f, -6.0f, -5.0f,
183 -4.0f, -3.0f, -2.0f, -1.0f, 0.0f};
184
185
187{
188 DT_TONEEQ_NONE = 0, // $DESCRIPTION: "no"
189 DT_TONEEQ_AVG_GUIDED, // $DESCRIPTION: "averaged guided filter"
190 DT_TONEEQ_GUIDED, // $DESCRIPTION: "guided filter"
191 DT_TONEEQ_AVG_EIGF, // $DESCRIPTION: "averaged eigf"
192 DT_TONEEQ_EIGF // $DESCRIPTION: "eigf"
194
195
197{
198 float noise; // $MIN: -2.0 $MAX: 2.0 $DEFAULT: 0.0 $DESCRIPTION: "blacks"
199 float ultra_deep_blacks; // $MIN: -2.0 $MAX: 2.0 $DEFAULT: 0.0 $DESCRIPTION: "deep shadows"
200 float deep_blacks; // $MIN: -2.0 $MAX: 2.0 $DEFAULT: 0.0 $DESCRIPTION: "shadows"
201 float blacks; // $MIN: -2.0 $MAX: 2.0 $DEFAULT: 0.0 $DESCRIPTION: "light shadows"
202 float shadows; // $MIN: -2.0 $MAX: 2.0 $DEFAULT: 0.0 $DESCRIPTION: "mid-tones"
203 float midtones; // $MIN: -2.0 $MAX: 2.0 $DEFAULT: 0.0 $DESCRIPTION: "dark highlights"
204 float highlights; // $MIN: -2.0 $MAX: 2.0 $DEFAULT: 0.0 $DESCRIPTION: "highlights"
205 float whites; // $MIN: -2.0 $MAX: 2.0 $DEFAULT: 0.0 $DESCRIPTION: "whites"
206 float speculars; // $MIN: -2.0 $MAX: 2.0 $DEFAULT: 0.0 $DESCRIPTION: "speculars"
207 float blending; // $MIN: 0.01 $MAX: 100.0 $DEFAULT: 5.0 $DESCRIPTION: "smoothing diameter"
208 float smoothing; // $DEFAULT: 1.414213562 sqrtf(2.0f)
209 float feathering; // $MIN: 0.01 $MAX: 10000.0 $DEFAULT: 1.0 $DESCRIPTION: "edges refinement (feathering)"
210 float quantization; // $MIN: 0.0 $MAX: 2.0 $DEFAULT: 0.0 $DESCRIPTION: "mask quantization"
211 float contrast_boost; // $MIN: -16.0 $MAX: 16.0 $DEFAULT: 0.0 $DESCRIPTION: "mask contrast compensation"
212 float exposure_boost; // $MIN: -16.0 $MAX: 16.0 $DEFAULT: 0.0 $DESCRIPTION: "mask exposure compensation"
213 dt_iop_toneequalizer_filter_t details; // $DEFAULT: DT_TONEEQ_EIGF
214 dt_iop_luminance_mask_method_t method; // $DEFAULT: DT_TONEEQ_NORM_2 $DESCRIPTION: "luminance estimator"
215 int iterations; // $MIN: 1 $MAX: 20 $DEFAULT: 1 $DESCRIPTION: "filter diffusion"
217
218
230
231
233{
234 // TODO: put OpenCL kernels here at some point
236
237
239{
240 // Mem arrays 64-bytes aligned - contiguous memory
242 float gui_lut[UI_SAMPLES] DT_ALIGNED_ARRAY; // LUT for the UI graph
243 float interpolation_matrix[CHANNELS * PIXEL_CHAN] DT_ALIGNED_ARRAY;
244 int histogram[UI_SAMPLES] DT_ALIGNED_ARRAY; // histogram for the UI graph
245 float temp_user_params[CHANNELS] DT_ALIGNED_ARRAY;
246 float cursor_exposure; // store the exposure value at current cursor position
247 float step; // scrolling step
248
249 // 14 int to pack - contiguous memory
255
256 // Cursor position over the image, NORMALIZED in [0, 1[. The luminance mask the GUI samples is
257 // the pipeline's own buffer, so its size is whatever ROI the pipe planned for this module:
258 // the preview size when darkroom renders scaled, the full sensor resolution when it renders
259 // at 1:1 (`darkroom/render_size`). Storing pixel coordinates of one of those two spaces
260 // sampled the wrong pixels in the other. Every sampler resolves these against the dimensions
261 // of the buffer it just attached, the way the color picker already resolves its own box.
264
265 // Preview luminance cache state shared with the GUI.
266 // The GUI never owns raw luminance buffers directly anymore: it only keeps the
267 // cache hash, dimensions and one retained cache entry reference so every reader
268 // goes through the pixelpipe cache locking API before sampling.
272
273 // Misc stuff, contiguity, length and alignment unknown
274 float scale;
275 float sigma;
279
280 // Preview luminance cache entry retained across pipe runs for GUI sampling.
281 // Lifetime is explicit: process() transfers one ref to the GUI when the current
282 // preview-sized output matches darkroom preview, and invalidation/cleanup drop it.
284
285 // GTK garbage, nobody cares, no SIMD here
287 GtkDrawingArea *area;
291 GtkNotebook *notebook;
293
294 // Cache Pango and Cairo stuff for the equalizer drawing
297 float graph_left_space; // used to center the circle on the mouse.
305 float x_label;
306 int inset;
308
309 GtkAllocation allocation;
310 cairo_surface_t *cst;
311 cairo_t *cr;
312 PangoLayout *layout;
313 PangoRectangle ink;
314 PangoFontDescription *desc;
315 GtkStyleContext *context;
316
317 // Event for equalizer drawing
320 float area_x; // x coordinate of cursor over graph/drawing area
321 float area_y; // y coordinate
323
324 // Flags for UI events
325 int valid_nodes_x; // TRUE if x coordinates of graph nodes have been inited
326 int valid_nodes_y; // TRUE if y coordinates of graph nodes have been inited
327 int area_cursor_valid; // TRUE if mouse cursor is over the graph area
328 int area_dragging; // TRUE if left-button has been pushed but not released and cursor motion is recorded
329 int cursor_valid; // TRUE if mouse cursor is over the preview image
330 int has_focus; // TRUE if the widget has the focus from GTK
331
332 // Flags for buffer caches invalidation
333 int interpolation_valid; // TRUE if the interpolation_matrix is ready
334 int luminance_valid; // TRUE if the luminance cache is ready
335 int histogram_valid; // TRUE if the histogram cache and stats are ready
336 int lut_valid; // TRUE if the gui_lut is ready
337 int graph_valid; // TRUE if the UI graph view is ready
338 int user_param_valid; // TRUE if users params set in interactive view are in bounds
339 int factors_valid; // TRUE if radial-basis coeffs are ready
340
342
343
344const char *name()
345{
346 return _("tone e_qualizer");
347}
348
349const char *aliases()
350{
351 return _("tone curve|tone mapping|relight|background light|shadows highlights");
352}
353
354
355const char **description(struct dt_iop_module_t *self)
356{
357 return dt_iop_set_description(self, _("relight the scene as if the lighting was done directly on the scene"),
358 _("corrective and creative"),
359 _("linear, RGB, scene-referred"),
360 _("quasi-linear, RGB"),
361 _("quasi-linear, RGB, scene-referred"));
362}
363
365{
366 return IOP_GROUP_TONES;
367}
368
373
375{
376 return IOP_CS_RGB;
377}
378
381{
382 default_input_format(self, pipe, piece, dsc);
383 dsc->channels = 4;
384 dsc->datatype = TYPE_FLOAT;
385}
386
387int legacy_params(dt_iop_module_t *self, const void *const old_params, const int old_version, void *new_params,
388 const int new_version)
389{
390 if(old_version == 1 && new_version == 2)
391 {
392 typedef struct dt_iop_toneequalizer_params_v1_t
393 {
394 float noise, ultra_deep_blacks, deep_blacks, blacks, shadows, midtones, highlights, whites, speculars;
395 float blending, feathering, contrast_boost, exposure_boost;
397 int iterations;
399 } dt_iop_toneequalizer_params_v1_t;
400
401 dt_iop_toneequalizer_params_v1_t *o = (dt_iop_toneequalizer_params_v1_t *)old_params;
404
405 *n = *d; // start with a fresh copy of default parameters
406
407 // Olds params
408 n->noise = o->noise;
409 n->ultra_deep_blacks = o->ultra_deep_blacks;
410 n->deep_blacks = o->deep_blacks;
411 n->blacks = o->blacks;
412 n->shadows = o->shadows;
413 n->midtones = o->midtones;
414 n->highlights = o->highlights;
415 n->whites = o->whites;
416 n->speculars = o->speculars;
417
418 n->blending = o->blending;
419 n->feathering = o->feathering;
420 n->contrast_boost = o->contrast_boost;
421 n->exposure_boost = o->exposure_boost;
422
423 n->details = o->details;
424 n->iterations = o->iterations;
425 n->method = o->method;
426
427 // New params
428 n->quantization = 0.01f;
429 n->smoothing = sqrtf(2.0f);
430 return 0;
431 }
432 return 1;
433}
434
436{
437 // this function is used to set the exposure params for the 4 "compress shadows
438 // highlights" presets, which use basically the same curve, centered around
439 // -4EV with an exposure compensation that puts middle-grey at -4EV.
440 p->noise = step;
441 p->ultra_deep_blacks = 5.f / 3.f * step;
442 p->deep_blacks = 5.f / 3.f * step;
443 p->blacks = step;
444 p->shadows = 0.0f;
445 p->midtones = -step;
446 p->highlights = -5.f / 3.f * step;
447 p->whites = -5.f / 3.f * step;
448 p->speculars = -step;
449}
450
451
453{
454 // create a tone curve meant to be used without filter (as a flat, non-local, 1D tone curve) that reverts
455 // the local settings above.
456 p->noise = -15.f / 9.f * step;
457 p->ultra_deep_blacks = -14.f / 9.f * step;
458 p->deep_blacks = -12.f / 9.f * step;
459 p->blacks = -8.f / 9.f * step;
460 p->shadows = 0.f;
461 p->midtones = 8.f / 9.f * step;
462 p->highlights = 12.f / 9.f * step;
463 p->whites = 14.f / 9.f * step;
464 p->speculars = 15.f / 9.f * step;
465}
466
468{
470 memset(&p, 0, sizeof(p));
471
472 p.method = DT_TONEEQ_NORM_POWER;
473 p.contrast_boost = 0.0f;
474 p.details = DT_TONEEQ_NONE;
475 p.exposure_boost = -0.5f;
476 p.feathering = 1.0f;
477 p.iterations = 1;
478 p.smoothing = sqrtf(2.0f);
479 p.quantization = 0.0f;
480
481 // Init exposure settings
482 p.noise = p.ultra_deep_blacks = p.deep_blacks = p.blacks = p.shadows = p.midtones = p.highlights = p.whites = p. speculars = 0.0f;
483
484 // No blending
485 dt_gui_presets_add_generic(_("simple tone curve"), self->op,
486 self->version(), &p, sizeof(p), 1);
487
488 // Simple utils blendings
489 p.details = DT_TONEEQ_EIGF;
490 p.method = DT_TONEEQ_NORM_2;
491
492 p.blending = 5.0f;
493 p.feathering = 1.0f;
494 p.iterations = 1;
495 p.quantization = 0.0f;
496 p.exposure_boost = 0.0f;
497 p.contrast_boost = 0.0f;
498 dt_gui_presets_add_generic(_("mask blending: all purposes"), self->op,
499 self->version(), &p, sizeof(p), 1);
500
501 p.blending = 1.0f;
502 p.feathering = 10.0f;
503 p.iterations = 3;
504 dt_gui_presets_add_generic(_("mask blending: people with backlight"), self->op,
505 self->version(), &p, sizeof(p), 1);
506
507 // Shadows/highlights presets
508 // move middle-grey to the center of the range
509 p.exposure_boost = -1.57f;
510 p.contrast_boost = 0.0f;
511 p.blending = 2.0f;
512 p.feathering = 50.0f;
513 p.iterations = 5;
514 p.quantization = 0.0f;
515
516 // slight modification to give higher compression
517 p.details = DT_TONEEQ_EIGF;
518 p.feathering = 20.0f;
520 dt_gui_presets_add_generic(_("compress shadows/highlights (eigf): strong"), self->op,
521 self->version(), &p, sizeof(p), 1);
522 p.details = DT_TONEEQ_GUIDED;
523 p.feathering = 500.0f;
524 dt_gui_presets_add_generic(_("compress shadows/highlights (gf): strong"), self->op,
525 self->version(), &p, sizeof(p), 1);
526
527 p.details = DT_TONEEQ_EIGF;
528 p.blending = 3.0f;
529 p.feathering = 7.0f;
530 p.iterations = 3;
532 dt_gui_presets_add_generic(_("compress shadows/highlights (eigf): medium"), self->op,
533 self->version(), &p, sizeof(p), 1);
534 p.details = DT_TONEEQ_GUIDED;
535 p.feathering = 500.0f;
536 dt_gui_presets_add_generic(_("compress shadows/highlights (gf): medium"), self->op,
537 self->version(), &p, sizeof(p), 1);
538
539 p.details = DT_TONEEQ_EIGF;
540 p.blending = 5.0f;
541 p.feathering = 1.0f;
542 p.iterations = 1;
544 dt_gui_presets_add_generic(_("compress shadows/highlights (eigf): soft"), self->op,
545 self->version(), &p, sizeof(p), 1);
546 p.details = DT_TONEEQ_GUIDED;
547 p.feathering = 500.0f;
548 dt_gui_presets_add_generic(_("compress shadows/highlights (gf): soft"), self->op,
549 self->version(), &p, sizeof(p), 1);
550
551 // build the 1D contrast curves that revert the local compression of contrast above
552 p.details = DT_TONEEQ_NONE;
554 dt_gui_presets_add_generic(_("contrast tone curve: soft"), self->op,
555 self->version(), &p, sizeof(p), 1);
556
558 dt_gui_presets_add_generic(_("contrast tone curve: medium"), self->op,
559 self->version(), &p, sizeof(p), 1);
560
562 dt_gui_presets_add_generic(_("contrast tone curve: strong"), self->op,
563 self->version(), &p, sizeof(p), 1);
564
565 // relight
566 p.details = DT_TONEEQ_EIGF;
567 p.blending = 5.0f;
568 p.feathering = 1.0f;
569 p.iterations = 1;
570 p.quantization = 0.0f;
571 p.exposure_boost = -0.5f;
572 p.contrast_boost = 0.0f;
573
574 p.noise = 0.0f;
575 p.ultra_deep_blacks = 0.15f;
576 p.deep_blacks = 0.6f;
577 p.blacks = 1.15f;
578 p.shadows = 1.33f;
579 p.midtones = 1.15f;
580 p.highlights = 0.6f;
581 p.whites = 0.15f;
582 p.speculars = 0.0f;
583
584 dt_gui_presets_add_generic(_("relight: fill-in"), self->op,
585 self->version(), &p, sizeof(p), 1);
586}
587
588
593static gboolean in_mask_editing(dt_iop_module_t *self)
594{
595 const dt_develop_t *dev = self->dev;
596 return dev->form_gui && dt_masks_get_visible_form(dev);
597}
598
600{
601 // Invalidate the preview luminance cache and histogram when
602 // the luminance mask extraction parameters have changed.
603 // Keep the ref hand-off visible here: we detach the GUI from the shared cache
604 // entry under the GUI lock, then release the retained cache ref afterwards.
605 // This is one of the cases that used to go wrong when tone equalizer stored
606 // ad-hoc GUI buffers outside the pixelpipe cache.
608 if(IS_NULL_PTR(g)) return;
609
610 dt_pixel_cache_entry_t *preview_entry = NULL;
612 g->max_histogram = 1;
613 g->luminance_valid = FALSE;
614 g->histogram_valid = 0;
615 g->thumb_preview_hash = DT_PIXELPIPE_CACHE_HASH_INVALID;
616 g->pending_preview_hash = DT_PIXELPIPE_CACHE_HASH_INVALID;
617 g->thumb_preview_buf_width = 0;
618 g->thumb_preview_buf_height = 0;
619 preview_entry = g->thumb_preview_entry;
620 g->thumb_preview_entry = NULL;
622
623 if(!IS_NULL_PTR(preview_entry))
625}
626
627
628static inline __attribute__((always_inline)) int sanity_check(dt_iop_module_t *self)
629{
630 // If tone equalizer is put after flip/orientation module,
631 // the pixel buffer will be in landscape orientation even for pictures displayed in portrait orientation
632 // so the interactive editing will fail. Disable the module and issue a warning then.
633
634 const double position_self = self->iop_order;
635 const double position_min = dt_ioppr_get_iop_order(self->dev->iop_order_list, "flip", 0);
636
637 if(position_self < position_min && self->enabled)
638 {
639 dt_control_log(_("tone equalizer needs to be after distortion modules in the pipeline - disabled"));
640 fprintf(stdout, "tone equalizer needs to be after distortion modules in the pipeline - disabled\n");
641 self->enabled = 0;
642 dt_dev_add_history_item(self->dev, self, FALSE, TRUE);
643
644 if(self->dev->gui_attached)
645 {
646 // Repaint the on/off icon
647 if(self->gui->off)
648 {
650 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(self->gui->off), self->enabled);
652 }
653 }
654 return 0;
655 }
656
657 return 1;
658}
659
660// gaussian-ish kernel - sum is == 1.0f so we don't care much about actual coeffs
662 { { 0.076555024f, 0.124401914f, 0.076555024f },
663 { 0.124401914f, 0.196172249f, 0.124401914f },
664 { 0.076555024f, 0.124401914f, 0.076555024f } };
665
666static float get_luminance_from_buffer(const float *const buffer,
667 const size_t width, const size_t height,
668 const size_t x, const size_t y)
669{
670 // Get the weighted average luminance of the 3x3 pixels region centered in (x, y)
671 // x and y are ratios in [0, 1] of the width and height
672
673 if(y >= height || x >= width) return NAN;
674
675 const size_t y_abs[4] DT_ALIGNED_PIXEL =
676 { MAX(y, 1) - 1, // previous line
677 y, // center line
678 MIN(y + 1, height - 1), // next line
679 y }; // padding for vectorization
680
681 float luminance = 0.0f;
682 if (x > 0 && x < width - 2)
683 {
684 // no clamping needed on x, which allows us to vectorize
685 // apply the convolution
686 for(int i = 0; i < 3; ++i)
687 {
688 const size_t y_i = y_abs[i];
690 luminance += buffer[width * y_i + x-1 + j] * gauss_kernel[i][j];
691 }
692 return luminance;
693 }
694
695 const size_t x_abs[4] DT_ALIGNED_PIXEL =
696 { MAX(x, 1) - 1, // previous column
697 x, // center column
698 MIN(x + 1, width - 1), // next column
699 x }; // padding for vectorization
700
701 // convolution
702 for(int i = 0; i < 3; ++i)
703 {
704 const size_t y_i = y_abs[i];
706 luminance += buffer[width * y_i + x_abs[j]] * gauss_kernel[i][j];
707 }
708 return luminance;
709}
710
711
721static inline float get_luminance_at_norm(const float *const buffer,
722 const size_t width, const size_t height,
723 const float norm_x, const float norm_y)
724{
725 if(IS_NULL_PTR(buffer) || width < 1 || height < 1) return NAN;
726 if(!(norm_x >= 0.f) || !(norm_y >= 0.f) || norm_x >= 1.f || norm_y >= 1.f) return NAN;
727
728 const size_t x = MIN((size_t)(norm_x * (float)width), width - 1);
729 const size_t y = MIN((size_t)(norm_y * (float)height), height - 1);
730 return get_luminance_from_buffer(buffer, width, height, x, y);
731}
732
733
745static inline gboolean luminance_entry_fits(dt_pixel_cache_entry_t *entry,
746 const size_t width, const size_t height)
747{
748 if(IS_NULL_PTR(entry) || width < 1 || height < 1) return FALSE;
749 if(width > SIZE_MAX / height) return FALSE;
750 return dt_pixel_cache_entry_get_size(entry) >= width * height * sizeof(float);
751}
752
753
754/***
755 * Exposure compensation computation
756 *
757 * Construct the final correction factor by summing the octaves channels gains weighted by
758 * the gaussian of the radial distance (pixel exposure - octave center)
759 *
760 ***/
761
763static inline __attribute__((always_inline)) float gaussian_denom(const float sigma)
764{
765 // Gaussian function denominator such that y = exp(- radius^2 / denominator)
766 // this is the constant factor of the exponential, so we don't need to recompute it
767 // for every single pixel
768 return 2.0f * sigma * sigma;
769}
770
771
773static inline __attribute__((always_inline)) float gaussian_func(const float radius, const float denominator)
774{
775 // Gaussian function without normalization
776 // this is the variable part of the exponential
777 // the denominator should be evaluated with `gaussian_denom`
778 // ahead of the array loop for optimal performance
779 return expf(- radius * radius / denominator);
780}
781
782#define DT_TONEEQ_USE_LUT TRUE
783#if DT_TONEEQ_USE_LUT
784
785// this is the version currently used, as using a lut gives a
786// big performance speedup on some cpus
788static inline void apply_toneequalizer(const float *const restrict in,
789 const float *const restrict luminance,
790 float *const restrict out,
791 const dt_iop_roi_t *const roi_in, const dt_iop_roi_t *const roi_out,
792 const size_t ch,
793 const dt_iop_toneequalizer_data_t *const d)
794{
795 const size_t num_elem = (size_t)roi_in->width * roi_in->height;
796 const int min_ev = -8;
797 const int max_ev = 0;
798 const float* restrict lut = d->correction_lut;
800 for(size_t k = 0; k < num_elem; ++k)
801 {
802 // The radial-basis interpolation is valid in [-8; 0] EV and can quickely diverge outside
803 const float exposure = fast_clamp(log2f(luminance[k]), min_ev, max_ev);
804 const float correction = lut[(unsigned)roundf((exposure - min_ev) * LUT_RESOLUTION)];
805 const size_t idx = k * ch;
806 const dt_aligned_pixel_simd_t pix_in = dt_load_simd_aligned(in + idx);
807 const dt_aligned_pixel_simd_t correction_v = { correction, correction, correction, 1.0f };
808 dt_store_simd_nontemporal(out + idx, pix_in * correction_v);
809 }
810 dt_omploop_sfence(); // ensure that nontemporal writes complete before the caller reads output
811}
812
813#else
814
815// we keep this version for further reference (e.g. for implementing
816// a gpu version)
818static inline void apply_toneequalizer(const float *const restrict in,
819 const float *const restrict luminance,
820 float *const restrict out,
821 const dt_iop_roi_t *const roi_in, const dt_iop_roi_t *const roi_out,
822 const size_t ch,
823 const dt_iop_toneequalizer_data_t *const d)
824{
825 const size_t num_elem = roi_in->width * roi_in->height;
826 const float *const restrict factors = d->factors;
827 const float sigma = d->smoothing;
828 const float gauss_denom = gaussian_denom(sigma);
830 for(size_t k = 0; k < num_elem; ++k)
831 {
832 // build the correction for the current pixel
833 // as the sum of the contribution of each luminance channelcorrection
834 float result = 0.0f;
835
836 // The radial-basis interpolation is valid in [-8; 0] EV and can quickely diverge outside
837 const float exposure = fast_clamp(log2f(luminance[k]), -8.0f, 0.0f);
838 __OMP_SIMD__(aligned(luminance, centers_ops, factors:64) safelen(PIXEL_CHAN) reduction(+:result))
839 for(int i = 0; i < PIXEL_CHAN; ++i)
840 result += gaussian_func(exposure - centers_ops[i], gauss_denom) * factors[i];
841
842 // the user-set correction is expected in [-2;+2] EV, so is the interpolated one
843 const float correction = fast_clamp(result, 0.25f, 4.0f);
844 const size_t idx = k * ch;
845 const dt_aligned_pixel_simd_t pix_in = dt_load_simd_aligned(in + idx);
846 const dt_aligned_pixel_simd_t correction_v = { correction, correction, correction, 1.0f };
847 dt_store_simd_nontemporal(out + idx, pix_in * correction_v);
848 }
849 dt_omploop_sfence(); // ensure that nontemporal writes complete before the caller reads output
850}
851#endif // USE_LUT
852
853static inline float pixel_correction(const float exposure,
854 const float *const restrict factors,
855 const float sigma)
856{
857 // build the correction for the current pixel
858 // as the sum of the contribution of each luminance channel
859 float result = 0.0f;
860 const float gauss_denom = gaussian_denom(sigma);
861 const float expo = fast_clamp(exposure, -8.0f, 0.0f);
862 __OMP_SIMD__(aligned(centers_ops, factors:64) safelen(PIXEL_CHAN) reduction(+:result))
863 for(int i = 0; i < PIXEL_CHAN; ++i)
864 result += gaussian_func(expo - centers_ops[i], gauss_denom) * factors[i];
865
866 return fast_clamp(result, 0.25f, 4.0f);
867}
868
869
870static inline int compute_luminance_mask(const float *const restrict in, float *const restrict luminance,
871 const size_t width, const size_t height, const size_t ch,
872 const dt_iop_toneequalizer_data_t *const d)
873{
874 switch(d->details)
875 {
876 case(DT_TONEEQ_NONE):
877 {
878 // No contrast boost here
879 luminance_mask(in, luminance, width, height, ch, d->method, d->exposure_boost, 0.0f, 1.0f);
880 break;
881 }
882
884 {
885 // Still no contrast boost
886 luminance_mask(in, luminance, width, height, ch, d->method, d->exposure_boost, 0.0f, 1.0f);
887 if(fast_surface_blur(luminance, width, height, d->radius, d->feathering, d->iterations,
888 DT_GF_BLENDING_GEOMEAN, d->scale, d->quantization, exp2f(-14.0f), 4.0f) != 0)
889 return 1;
890 break;
891 }
892
893 case(DT_TONEEQ_GUIDED):
894 {
895 // Contrast boosting is done around the average luminance of the mask.
896 // This is to make exposure corrections easier to control for users, by spreading
897 // the dynamic range along all exposure channels, because guided filters
898 // tend to flatten the luminance mask a lot around an average ± 2 EV
899 // which makes only 2-3 channels usable.
900 // we assume the distribution is centered around -4EV, e.g. the center of the nodes
901 // the exposure boost should be used to make this assumption true
902 luminance_mask(in, luminance, width, height, ch, d->method, d->exposure_boost,
903 CONTRAST_FULCRUM, d->contrast_boost);
904 if(fast_surface_blur(luminance, width, height, d->radius, d->feathering, d->iterations,
905 DT_GF_BLENDING_LINEAR, d->scale, d->quantization, exp2f(-14.0f), 4.0f) != 0)
906 return 1;
907 break;
908 }
909
910 case(DT_TONEEQ_AVG_EIGF):
911 {
912 // Still no contrast boost
913 luminance_mask(in, luminance, width, height, ch, d->method, d->exposure_boost, 0.0f, 1.0f);
914 if(fast_eigf_surface_blur(luminance, width, height, d->radius, d->feathering, d->iterations,
915 DT_GF_BLENDING_GEOMEAN, d->scale, d->quantization, exp2f(-14.0f), 4.0f) != 0)
916 return 1;
917 break;
918 }
919
920 case(DT_TONEEQ_EIGF):
921 {
922 luminance_mask(in, luminance, width, height, ch, d->method, d->exposure_boost,
923 CONTRAST_FULCRUM, d->contrast_boost);
924 if(fast_eigf_surface_blur(luminance, width, height, d->radius, d->feathering, d->iterations,
925 DT_GF_BLENDING_LINEAR, d->scale, d->quantization, exp2f(-14.0f), 4.0f) != 0)
926 return 1;
927 break;
928 }
929
930 default:
931 {
932 luminance_mask(in, luminance, width, height, ch, d->method, d->exposure_boost, 0.0f, 1.0f);
933 break;
934 }
935 }
936 return 0;
937}
938
939
940/***
941 * Actual transfer functions
942 **/
943
945static inline void display_luminance_mask(const float *const restrict in,
946 const float *const restrict luminance,
947 float *const restrict out,
948 const dt_iop_roi_t *const roi_in, const dt_iop_roi_t *const roi_out,
949 const dt_dev_pixelpipe_t *pipe,
950 const size_t ch)
951{
952 const size_t offset_x = (roi_in->x < roi_out->x) ? -roi_in->x + roi_out->x : 0;
953 const size_t offset_y = (roi_in->y < roi_out->y) ? -roi_in->y + roi_out->y : 0;
954
955 // The output dimensions need to be smaller or equal to the input ones
956 // there is no logical reason they shouldn't, except some weird bug in the pipe
957 // in this case, ensure we don't segfault
958 const size_t in_width = roi_in->width;
959 const size_t out_width = (roi_in->width > roi_out->width) ? roi_out->width : roi_in->width;
960 const size_t out_height = (roi_in->height > roi_out->height) ? roi_out->height : roi_in->height;
961 __OMP_PARALLEL_FOR__(collapse(2))
962 for(size_t i = 0 ; i < out_height; ++i)
963 for(size_t j = 0; j < out_width; ++j)
964 {
965 // normalize the mask intensity between -8 EV and 0 EV for clarity,
966 // and add a "gamma" 2.0 for better legibility in shadows
967 const float intensity = sqrtf(fminf(fmaxf(luminance[(i + offset_y) * in_width + (j + offset_x)] - 0.00390625f, 0.f) / 0.99609375f, 1.f));
968 const size_t index = (i * out_width + j) * ch;
969 dt_aligned_pixel_simd_t intensity_v = dt_simd_set1(intensity);
970
971 // Keep mask-display alpha consistent with the input while showing a grayscale mask.
973 {
974 const size_t in_index = ((i + offset_y) * in_width + (j + offset_x)) * ch;
975 intensity_v[3] = in[in_index + 3];
976 }
977
978 dt_store_simd_nontemporal(out + index, intensity_v);
979 }
980 dt_omploop_sfence(); // ensure that nontemporal writes complete before the caller reads output
981}
982
983
984static inline __attribute__((always_inline)) int toneeq_process(struct dt_iop_module_t *self, const dt_dev_pixelpipe_t *pipe,
985 const dt_dev_pixelpipe_iop_t *piece, const void *const restrict ivoid,
986 void *const restrict ovoid, const dt_iop_roi_t *const roi_in,
987 const dt_iop_roi_t *const roi_out)
988{
989 const dt_iop_toneequalizer_data_t *const d = (const dt_iop_toneequalizer_data_t *const)piece->data;
991
992 const float *const restrict in = dt_check_sse_aligned((float *const)ivoid);
993 float *const restrict out = dt_check_sse_aligned((float *const)ovoid);
994 float *restrict luminance = NULL;
995 dt_pixel_cache_entry_t *luminance_entry = NULL;
996 gboolean created_luminance_entry = FALSE;
998
999 if(IS_NULL_PTR(in) || IS_NULL_PTR(out))
1000 {
1001 // Pointers are not 64-bits aligned, and SSE code will segfault
1002 dt_control_log(_("tone equalizer in/out buffer are ill-aligned, please report the bug to the developers"));
1003 fprintf(stdout, "tone equalizer in/out buffer are ill-aligned, please report the bug to the developers\n");
1004 return 1;
1005 }
1006
1007 const size_t width = roi_in->width;
1008 const size_t height = roi_in->height;
1009 const size_t num_elem = width * height;
1010 const size_t ch = 4;
1011
1012 // Get the hash of the upstream pipe to track changes
1013 const int position = self->iop_order;
1014 const gboolean preview_output = dt_dev_pixelpipe_has_preview_output(self->dev, pipe, roi_out);
1015
1016 // Sanity checks
1017 if(width < 1 || height < 1) return 1;
1018 if(roi_in->width < roi_out->width || roi_in->height < roi_out->height) return 0; // input should be at least as large as output
1019 if(!sanity_check(self))
1020 {
1021 // if module just got disabled by sanity checks, due to pipe position, just pass input through
1022 dt_simd_memcpy(in, out, num_elem * ch);
1023 return 0;
1024 }
1025
1026 if(self->dev->gui_attached)
1027 {
1028 // If the module instance has changed order in the pipe, invalidate the caches
1029 if(!IS_NULL_PTR(g) && g->pipe_order != position)
1030 {
1031 dt_pixel_cache_entry_t *preview_entry = NULL;
1033 g->pipe_order = position;
1034 g->thumb_preview_hash = DT_PIXELPIPE_CACHE_HASH_INVALID;
1035 g->pending_preview_hash = DT_PIXELPIPE_CACHE_HASH_INVALID;
1036 g->thumb_preview_buf_width = 0;
1037 g->thumb_preview_buf_height = 0;
1038 g->luminance_valid = FALSE;
1039 g->histogram_valid = FALSE;
1040 preview_entry = g->thumb_preview_entry;
1041 g->thumb_preview_entry = NULL;
1043
1044 if(preview_entry)
1046 }
1047 }
1048
1049 if(self->dev->gui_attached)
1050 {
1051 // Cache the luminance mask in the shared pixelpipe cache so the key follows
1052 // the exact module state and GUI readers can reuse the same lifetime/locking model.
1053 // `piece->global_hash` already includes the upstream image state and the module
1054 // params committed to that run, so the luminance cacheline stays coherent with
1055 // both preview and main pipelines without adding toneequal-specific validity rules.
1056 //
1057 // The fixes we rely on here were exercised with:
1058 // - history edits and parameter edits invalidating/rebuilding the mask,
1059 // - opening the module on an already computed preview and reattaching to the
1060 // existing cacheline without waiting for a fresh process(),
1061 // - GUI histogram/cursor sampling while the worker threads are running,
1062 // - mask display staying restricted to the full pipe while the luminance data
1063 // itself comes from whichever pipe produced the preview-sized output.
1064 void *cache_data = NULL;
1065 static const char cache_tag[] = "toneequal:luminance";
1066 luminance_hash = dt_hash(piece->global_hash, cache_tag, sizeof(cache_tag));
1067
1068 created_luminance_entry = dt_dev_pixelpipe_cache_get(luminance_hash,
1069 num_elem * sizeof(float), "toneequal luminance",
1070 pipe->type, TRUE, &cache_data,
1071 &luminance_entry);
1072 luminance = (float *)cache_data;
1073 if(IS_NULL_PTR(luminance) || IS_NULL_PTR(luminance_entry))
1074 {
1075 if(luminance_entry)
1076 {
1077 if(created_luminance_entry)
1080 }
1081 return 1;
1082 }
1083
1084 if(created_luminance_entry)
1085 {
1087 {
1090 dt_dev_pixelpipe_cache_remove(TRUE, luminance_entry);
1091 return 1;
1092 }
1093
1095 }
1096 }
1097 else
1098 {
1099 // Export/thumbnail pipes don't need persistent GUI sampling, so a local temp buffer is enough.
1101 if(IS_NULL_PTR(luminance)) return 1;
1102
1104 {
1106 return 1;
1107 }
1108 }
1109
1110 // Display output
1111 if(self->dev->gui_attached && pipe->type == DT_DEV_PIXELPIPE_FULL)
1112 {
1113 if(!IS_NULL_PTR(g) && g->mask_display)
1114 {
1115 display_luminance_mask(in, luminance, out, roi_in, roi_out, pipe, ch);
1116 ((dt_dev_pixelpipe_t *)pipe)->mask_display = DT_DEV_PIXELPIPE_DISPLAY_PASSTHRU;
1117 ((dt_dev_pixelpipe_t *)pipe)->bypass_blendif = 1;
1118 }
1119 else
1120 apply_toneequalizer(in, luminance, out, roi_in, roi_out, ch, d);
1121 }
1122 else
1123 {
1124 apply_toneequalizer(in, luminance, out, roi_in, roi_out, ch, d);
1125 }
1126
1127 if(preview_output && self->dev->gui_attached && !IS_NULL_PTR(g)
1128 && luminance_entry_fits(luminance_entry, width, height))
1129 {
1130 dt_pixel_cache_entry_t *old_entry = NULL;
1131 gboolean keep_process_ref = FALSE;
1132
1133 // Transfer one cache ref from this process run to the GUI if this run produced
1134 // the preview-sized image darkroom is sampling from. Keeping this explicit avoids
1135 // guessing whether FULL/PREVIEW pipe type owns the GUI state when both pipes can
1136 // share the same output size.
1138 if(g->thumb_preview_entry != luminance_entry || g->thumb_preview_hash != luminance_hash)
1139 {
1140 old_entry = g->thumb_preview_entry;
1141 g->thumb_preview_entry = luminance_entry;
1142 g->thumb_preview_hash = luminance_hash;
1143 g->pending_preview_hash = DT_PIXELPIPE_CACHE_HASH_INVALID;
1144 g->thumb_preview_buf_width = width;
1145 g->thumb_preview_buf_height = height;
1146 g->luminance_valid = TRUE;
1147 g->histogram_valid = FALSE;
1148 keep_process_ref = TRUE;
1149 }
1151
1152 if(old_entry)
1154
1155 if(!keep_process_ref)
1157 }
1158 else if(luminance_entry)
1159 {
1161 }
1162 else
1163 {
1165 }
1166
1167 return 0;
1168}
1169
1170int process(struct dt_iop_module_t *self, const dt_dev_pixelpipe_t *pipe, const dt_dev_pixelpipe_iop_t *piece,
1171 const void *const restrict ivoid, void *const restrict ovoid)
1172{
1173 const dt_iop_roi_t *const roi_in = &piece->roi_in;
1174 const dt_iop_roi_t *const roi_out = &piece->roi_out;
1175 return toneeq_process(self, pipe, piece, ivoid, ovoid, roi_in, roi_out);
1176}
1177
1178
1179void modify_roi_in(struct dt_iop_module_t *self, const struct dt_dev_pixelpipe_t *pipe,
1180 struct dt_dev_pixelpipe_iop_t *piece,
1181 const dt_iop_roi_t *roi_out, dt_iop_roi_t *roi_in)
1182{
1183 // Pad the zoomed-in view to avoid weird stuff with local averages at the borders of
1184 // the preview
1185
1187
1188 // Get the scaled window radius for the box average
1189 const int max_size = (piece->iwidth > piece->iheight) ? piece->iwidth : piece->iheight;
1190 const float diameter = d->blending * max_size * roi_in->scale;
1191 const int radius = (int)((diameter - 1.0f) / ( 2.0f));
1192 d->radius = radius;
1193
1194 /*
1195 // Enlarge the preview roi with padding if needed
1196 if(self->dev->gui_attached && sanity_check(self))
1197 {
1198 int roiy = fmaxf(roi_in->y - radius, 0.0f);
1199 int roix = fmaxf(roi_in->x - radius, 0.0f);
1200 int roir = fminf(roix + roi_in->width + 2 * radius, piece->buf_in.width * roi_in->scale);
1201 int roib = fminf(roiy + roi_in->height + 2 * radius, piece->buf_in.height * roi_in->scale);
1202
1203 // Set the values and check
1204 roi_in->x = roix;
1205 roi_in->y = roiy;
1206 roi_in->width = roir - roi_in->x;
1207 roi_in->height = roib - roi_in->y;
1208 }
1209 */
1210}
1211
1212
1213/***
1214 * Setters and Getters for parameters
1215 *
1216 * Remember the user params split the [-8; 0] EV range in 9 channels and define a set of (x, y)
1217 * coordinates, where x are the exposure channels (evenly-spaced by 1 EV in [-8; 0] EV)
1218 * and y are the desired exposure compensation for each channel.
1219 *
1220 * This (x, y) set is interpolated by radial-basis function using a series of 8 gaussians.
1221 * Losing 1 degree of freedom makes it an approximation rather than an interpolation but
1222 * helps reducing a bit the oscillations and fills a full AVX vector.
1223 *
1224 * The coefficients/factors used in the interpolation/approximation are linear, but keep in
1225 * mind that users params are expressed as log2 gains, so we always need to do the log2/exp2
1226 * flip/flop between both.
1227 *
1228 * User params of exposure compensation are expected between [-2 ; +2] EV for practical UI reasons
1229 * and probably numerical stability reasons, but there is no theoretical obstacle to enlarge
1230 * this range. The main reason for not allowing it is tone equalizer is mostly intended
1231 * to do local changes, and these don't look so well if you are too harsh on the changes.
1232 * For heavier tonemapping, it should be used in combination with a tone curve or filmic.
1233 *
1234 ***/
1235
1236static void compute_correction_lut(float* restrict lut, const float sigma, const float *const restrict factors)
1237{
1238 const float gauss_denom = gaussian_denom(sigma);
1239 const int min_ev = -8;
1240 assert(PIXEL_CHAN == -min_ev);
1241 for(int j = 0; j <= LUT_RESOLUTION * PIXEL_CHAN; j++)
1242 {
1243 // build the correction for each pixel
1244 // as the sum of the contribution of each luminance channelcorrection
1245 float exposure = (float)j / (float)LUT_RESOLUTION + min_ev;
1246 float result = 0.0f;
1247 for(int i = 0; i < PIXEL_CHAN; ++i)
1248 result += gaussian_func(exposure - centers_ops[i], gauss_denom) * factors[i];
1249 // the user-set correction is expected in [-2;+2] EV, so is the interpolated one
1250 lut[j] = fast_clamp(result, 0.25f, 4.0f);
1251 }
1252}
1253
1255{
1256 assert(CHANNELS == 9);
1257
1258 // Get user-set channels gains in EV (log2)
1259 factors[0] = p->noise; // -8 EV
1260 factors[1] = p->ultra_deep_blacks; // -7 EV
1261 factors[2] = p->deep_blacks; // -6 EV
1262 factors[3] = p->blacks; // -5 EV
1263 factors[4] = p->shadows; // -4 EV
1264 factors[5] = p->midtones; // -3 EV
1265 factors[6] = p->highlights; // -2 EV
1266 factors[7] = p->whites; // -1 EV
1267 factors[8] = p->speculars; // +0 EV
1268}
1269
1270
1272{
1273 assert(CHANNELS == 9);
1274
1275 // Get user-set channels gains in EV (log2)
1276 get_channels_gains(factors, p);
1277
1278 // Convert from EV offsets to linear factors
1279 __OMP_SIMD__(aligned(factors:64))
1280 for(int c = 0; c < CHANNELS; ++c)
1281 factors[c] = exp2f(factors[c]);
1282}
1283
1284
1285static int compute_channels_factors(const float factors[PIXEL_CHAN], float out[CHANNELS], const float sigma)
1286{
1287 // Input factors are the weights for the radial-basis curve approximation of user params
1288 // Output factors are the gains of the user parameters channels
1289 // aka the y coordinates of the approximation for x = { CHANNELS }
1290 assert(PIXEL_CHAN == 8);
1291
1292 int valid = 1;
1293 __OMP_PARALLEL_FOR_SIMD__(aligned(factors, out, centers_params:64) shared(valid) firstprivate(centers_params))
1294 for(int i = 0; i < CHANNELS; ++i)
1295 {
1296 // Compute the new channels factors
1297 out[i] = pixel_correction(centers_params[i], factors, sigma);
1298
1299 // check they are in [-2, 2] EV and not NAN
1300 if(isnan(out[i]) || out[i] < 0.25f || out[i] > 4.0f) valid = 0;
1301 }
1302
1303 return valid;
1304}
1305
1306
1307static int compute_channels_gains(const float in[CHANNELS], float out[CHANNELS])
1308{
1309 // Helper function to compute the new channels gains (log) from the factors (linear)
1310 assert(PIXEL_CHAN == 8);
1311
1312 const int valid = 1;
1313
1314 for(int i = 0; i < CHANNELS; ++i)
1315 out[i] = log2f(in[i]);
1316
1317 return valid;
1318}
1319
1320
1322{
1323 p->noise = factors[0];
1324 p->ultra_deep_blacks = factors[1];
1325 p->deep_blacks = factors[2];
1326 p->blacks = factors[3];
1327 p->shadows = factors[4];
1328 p->midtones = factors[5];
1329 p->highlights = factors[6];
1330 p->whites = factors[7];
1331 p->speculars = factors[8];
1332
1333 return 1;
1334}
1335
1336
1337/***
1338 * Cache invalidation and initializatiom
1339 ***/
1340
1341
1342static void gui_cache_init(struct dt_iop_module_t *self)
1343{
1345 if(IS_NULL_PTR(g)) return;
1346
1348 g->thumb_preview_hash = DT_PIXELPIPE_CACHE_HASH_INVALID;
1349 g->pending_preview_hash = DT_PIXELPIPE_CACHE_HASH_INVALID;
1350 g->max_histogram = 1;
1351 g->scale = 1.0f;
1352 g->sigma = sqrtf(2.0f);
1353 g->mask_display = FALSE;
1354
1355 g->interpolation_valid = FALSE; // TRUE if the interpolation_matrix is ready
1356 g->luminance_valid = FALSE; // TRUE if the luminance cache is ready
1357 g->histogram_valid = FALSE; // TRUE if the histogram cache and stats are ready
1358 g->lut_valid = FALSE; // TRUE if the gui_lut is ready
1359 g->graph_valid = FALSE; // TRUE if the UI graph view is ready
1360 g->user_param_valid = FALSE; // TRUE if users params set in interactive view are in bounds
1361 g->factors_valid = TRUE; // TRUE if radial-basis coeffs are ready
1362
1363 g->valid_nodes_x = FALSE; // TRUE if x coordinates of graph nodes have been inited
1364 g->valid_nodes_y = FALSE; // TRUE if y coordinates of graph nodes have been inited
1365 g->area_cursor_valid = FALSE; // TRUE if mouse cursor is over the graph area
1366 g->area_dragging = FALSE; // TRUE if left-button has been pushed but not released and cursor motion is recorded
1367 g->cursor_valid = FALSE; // TRUE if mouse cursor is over the preview image
1368
1369 g->thumb_preview_entry = NULL;
1370 g->thumb_preview_buf_width = 0;
1371 g->thumb_preview_buf_height = 0;
1372
1373 g->desc = NULL;
1374 g->layout = NULL;
1375 g->cr = NULL;
1376 g->cst = NULL;
1377 g->context = NULL;
1378
1379 g->pipe_order = 0;
1381}
1382
1384{
1385 if(width) *width = 0;
1386 if(height) *height = 0;
1388
1390 if(IS_NULL_PTR(piece) || !piece->enabled || piece->roi_in.width <= 0 || piece->roi_in.height <= 0)
1392
1393 if(width) *width = piece->roi_in.width;
1394 if(height) *height = piece->roi_in.height;
1395
1396 static const char cache_tag[] = "toneequal:luminance";
1397 return dt_hash(piece->global_hash, cache_tag, sizeof(cache_tag));
1398}
1399
1400
1402 const float sigma)
1403{
1404 // Build the symmetrical definite positive part of the augmented matrix
1405 // of the radial-basis interpolation weights
1406
1407 const float gauss_denom = gaussian_denom(sigma);
1408 __OMP_SIMD__(aligned(A, centers_ops, centers_params:64) collapse(2))
1409 for(int i = 0; i < CHANNELS; ++i)
1410 for(int j = 0; j < PIXEL_CHAN; ++j)
1411 A[i * PIXEL_CHAN + j] = gaussian_func(centers_params[i] - centers_ops[j], gauss_denom);
1412}
1413
1414
1416static inline void compute_log_histogram_and_stats(const float *const restrict luminance,
1417 int histogram[UI_SAMPLES],
1418 const size_t num_elem,
1419 int *max_histogram,
1420 float *first_decile, float *last_decile)
1421{
1422 // (Re)init the histogram
1423 memset(histogram, 0, sizeof(int) * UI_SAMPLES);
1424
1425 // we first calculate an extended histogram for better accuracy
1426 #define TEMP_SAMPLES 2 * UI_SAMPLES
1427 int temp_hist[TEMP_SAMPLES];
1428 memset(temp_hist, 0, sizeof(int) * TEMP_SAMPLES);
1429
1430 // Split exposure in bins
1431 __OMP_PARALLEL_FOR__(reduction(+:temp_hist[:TEMP_SAMPLES]))
1432 for(size_t k = 0; k < num_elem; k++)
1433 {
1434 // extended histogram bins between [-10; +6] EV remapped between [0 ; 2 * UI_SAMPLES]
1435 const int index = CLAMP((int)(((log2f(luminance[k]) + 10.0f) / 16.0f) * (float)TEMP_SAMPLES), 0, TEMP_SAMPLES - 1);
1436 temp_hist[index] += 1;
1437 }
1438
1439 const int first = (int)((float)num_elem * 0.05f);
1440 const int last = (int)((float)num_elem * (1.0f - 0.95f));
1441 int population = 0;
1442 int first_pos = 0;
1443 int last_pos = 0;
1444
1445 // scout the extended histogram bins looking for deciles
1446 // these would not be accurate with the regular histogram
1447 for(int k = 0; k < TEMP_SAMPLES; ++k)
1448 {
1449 const size_t prev_population = population;
1450 population += temp_hist[k];
1451 if(prev_population < first && first <= population)
1452 {
1453 first_pos = k;
1454 break;
1455 }
1456 }
1457 population = 0;
1458 for(int k = TEMP_SAMPLES - 1; k >= 0; --k)
1459 {
1460 const size_t prev_population = population;
1461 population += temp_hist[k];
1462 if(prev_population < last && last <= population)
1463 {
1464 last_pos = k;
1465 break;
1466 }
1467 }
1468
1469 // Convert decile positions to exposures
1470 *first_decile = 16.0 * (float)first_pos / (float)(TEMP_SAMPLES - 1) - 10.0;
1471 *last_decile = 16.0 * (float)last_pos / (float)(TEMP_SAMPLES - 1) - 10.0;
1472
1473 // remap the extended histogram into the normal one
1474 // bins between [-8; 0] EV remapped between [0 ; UI_SAMPLES]
1475 for(size_t k = 0; k < TEMP_SAMPLES; ++k)
1476 {
1477 float EV = 16.0 * (float)k / (float)(TEMP_SAMPLES - 1) - 10.0;
1478 const int i = CLAMP((int)(((EV + 8.0f) / 8.0f) * (float)UI_SAMPLES), 0, UI_SAMPLES - 1);
1479 histogram[i] += temp_hist[k];
1480
1481 // store the max numbers of elements in bins for later normalization
1482 *max_histogram = histogram[i] > *max_histogram ? histogram[i] : *max_histogram;
1483 }
1484}
1485
1486static inline void update_histogram(struct dt_iop_module_t *const self)
1487{
1489 if(IS_NULL_PTR(g)) return;
1490
1491 dt_pixel_cache_entry_t *preview_entry = NULL;
1492 size_t width = 0;
1493 size_t height = 0;
1495 gboolean needs_histogram = FALSE;
1496
1497 // Readers take a temporary cache ref while copying the GUI-visible entry pointer,
1498 // then read-lock the cacheline only around the actual sampling. This keeps both
1499 // ownership transfer and lock lifetime visible at the call site.
1501 if(!g->histogram_valid && g->luminance_valid && g->thumb_preview_entry)
1502 {
1503 preview_entry = g->thumb_preview_entry;
1504 width = g->thumb_preview_buf_width;
1505 height = g->thumb_preview_buf_height;
1506 preview_hash = g->thumb_preview_hash;
1508 needs_histogram = TRUE;
1509 }
1511
1512 if(!needs_histogram || width == 0 || height == 0)
1513 {
1514 if(!IS_NULL_PTR(preview_entry))
1516 return;
1517 }
1518
1519 int histogram[UI_SAMPLES];
1520 int max_histogram = 1;
1521 float first_decile = 0.0f;
1522 float last_decile = 0.0f;
1523
1525 const float *const preview_buf = (const float *const)dt_pixel_cache_entry_get_data(preview_entry);
1526 if(preview_buf)
1527 compute_log_histogram_and_stats(preview_buf, histogram, width * height, &max_histogram, &first_decile,
1528 &last_decile);
1530
1531 if(IS_NULL_PTR(preview_buf))
1532 {
1534 return;
1535 }
1536
1538 if(g->thumb_preview_entry == preview_entry && g->thumb_preview_hash == preview_hash && !g->histogram_valid)
1539 {
1540 memcpy(g->histogram, histogram, sizeof(histogram));
1541 g->max_histogram = max_histogram;
1542 g->histogram_first_decile = first_decile;
1543 g->histogram_last_decile = last_decile;
1544 g->histogram_average = (first_decile + last_decile) / 2.0f;
1545 g->histogram_valid = TRUE;
1546 }
1548
1550}
1551
1552
1555 const float offset,
1556 const float scaling)
1557{
1558 // Compute the LUT of the exposure corrections in EV,
1559 // offset and scale it for display in GUI widget graph
1560
1561 float *const restrict LUT = g->gui_lut;
1562 const float *const restrict factors = g->factors;
1563 const float sigma = g->sigma;
1564 __OMP_FOR_SIMD__(aligned(LUT, factors:64))
1565 for(int k = 0; k < UI_SAMPLES; k++)
1566 {
1567 // build the inset graph curve LUT
1568 // the x range is [-14;+2] EV
1569 const float x = (8.0f * (((float)k) / ((float)(UI_SAMPLES - 1)))) - 8.0f;
1570 LUT[k] = offset - log2f(pixel_correction(x, factors, sigma)) / scaling;
1571 }
1572}
1573
1574
1575
1576static inline gboolean update_curve_lut(struct dt_iop_module_t *self)
1577{
1580
1581 if(IS_NULL_PTR(g)) return FALSE;
1582
1583 gboolean valid = TRUE;
1584
1585 if(!g->interpolation_valid)
1586 {
1587 build_interpolation_matrix(g->interpolation_matrix, g->sigma);
1588 g->interpolation_valid = TRUE;
1589 g->factors_valid = FALSE;
1590 }
1591
1592 if(!g->user_param_valid)
1593 {
1594 float factors[CHANNELS] DT_ALIGNED_ARRAY;
1595 get_channels_factors(factors, p);
1596 dt_simd_memcpy(factors, g->temp_user_params, CHANNELS);
1597 g->user_param_valid = TRUE;
1598 g->factors_valid = FALSE;
1599 }
1600
1601 if(!g->factors_valid && g->user_param_valid)
1602 {
1603 float factors[CHANNELS] DT_ALIGNED_ARRAY;
1604 dt_simd_memcpy(g->temp_user_params, factors, CHANNELS);
1605 if(pseudo_solve(g->interpolation_matrix, factors, CHANNELS, PIXEL_CHAN, 1) != 0)
1606 {
1607 valid = FALSE;
1608 }
1609 else
1610 {
1611 dt_simd_memcpy(factors, g->factors, PIXEL_CHAN);
1612 g->factors_valid = TRUE;
1613 g->lut_valid = FALSE;
1614 }
1615 }
1616
1617 if(!g->lut_valid && g->factors_valid)
1618 {
1619 compute_lut_correction(g, 0.5f, 4.0f);
1620 g->lut_valid = TRUE;
1621 }
1622
1623 return valid;
1624}
1625
1626
1628{
1631
1632 module->data = gd;
1633}
1634
1635
1637{
1638 dt_free(module->data);
1639}
1640
1641
1644{
1648
1649 // Trivial params passing
1650 d->method = p->method;
1651 d->details = p->details;
1652 d->iterations = p->iterations;
1653 d->smoothing = p->smoothing;
1654 d->quantization = p->quantization;
1655
1656 // UI blending param is set in % of the largest image dimension
1657 d->blending = p->blending / 100.0f;
1658
1659 // UI guided filter feathering param increases the edges taping
1660 // but the actual regularization params applied in guided filter behaves the other way
1661 d->feathering = 1.f / (p->feathering);
1662
1663 // UI params are in log2 offsets (EV) : convert to linear factors
1664 d->contrast_boost = exp2f(p->contrast_boost);
1665 d->exposure_boost = exp2f(p->exposure_boost);
1666
1667 /*
1668 * Perform a radial-based interpolation using a series gaussian functions
1669 */
1670 // FIXME: trying to spare some CPU cycles by mixing GUI params update
1671 // with pipeline code is not worth the thread-safety issues (solved only by deadlocks).
1672 // Move that to GUI code.
1673 if(self->dev->gui_attached && !IS_NULL_PTR(g))
1674 {
1675 if(g->sigma != p->smoothing) g->interpolation_valid = FALSE;
1676 g->sigma = p->smoothing;
1677 g->user_param_valid = FALSE; // force updating channels factors
1678
1679 update_curve_lut(self);
1680 dt_simd_memcpy(g->factors, d->factors, PIXEL_CHAN);
1681 }
1682 else
1683 {
1684 // No cache : Build / Solve interpolation matrix
1685 float factors[CHANNELS] DT_ALIGNED_ARRAY;
1686 get_channels_factors(factors, p);
1687
1689 build_interpolation_matrix(A, p->smoothing);
1690 if(pseudo_solve(A, factors, CHANNELS, PIXEL_CHAN, 0) != 0) return;
1691
1692 dt_simd_memcpy(factors, d->factors, PIXEL_CHAN);
1693 }
1694
1695 // compute the correction LUT here to spare some time in process
1696 // when computing several times toneequalizer with same parameters
1697 compute_correction_lut(d->correction_lut, d->smoothing, d->factors);
1698}
1699
1700
1702{
1704 piece->data_size = sizeof(dt_iop_toneequalizer_data_t);
1705}
1706
1707
1709{
1710 dt_free_align(piece->data);
1711 piece->data = NULL;
1712}
1713
1715{
1716 dt_iop_module_t *module = (dt_iop_module_t *)self;
1718 const dt_iop_toneequalizer_params_t *p = (const dt_iop_toneequalizer_params_t *)module->params;
1719
1720 switch(p->details)
1721 {
1722 case(DT_TONEEQ_NONE):
1723 {
1724 gtk_widget_set_visible(g->blending, FALSE);
1725 gtk_widget_set_visible(g->feathering, FALSE);
1726 gtk_widget_set_visible(g->iterations, FALSE);
1727 gtk_widget_set_visible(g->contrast_boost, FALSE);
1728 gtk_widget_set_visible(g->quantization, FALSE);
1729 break;
1730 }
1731
1733 case(DT_TONEEQ_AVG_EIGF):
1734 {
1735 gtk_widget_set_visible(g->blending, TRUE);
1736 gtk_widget_set_visible(g->feathering, TRUE);
1737 gtk_widget_set_visible(g->iterations, TRUE);
1738 gtk_widget_set_visible(g->contrast_boost, FALSE);
1739 gtk_widget_set_visible(g->quantization, TRUE);
1740 break;
1741 }
1742
1743 case(DT_TONEEQ_GUIDED):
1744 case(DT_TONEEQ_EIGF):
1745 {
1746 gtk_widget_set_visible(g->blending, TRUE);
1747 gtk_widget_set_visible(g->feathering, TRUE);
1748 gtk_widget_set_visible(g->iterations, TRUE);
1749 gtk_widget_set_visible(g->contrast_boost, TRUE);
1750 gtk_widget_set_visible(g->quantization, TRUE);
1751 break;
1752 }
1753 }
1754}
1755
1757{
1759 dt_bauhaus_slider_set(g->noise, p->noise);
1760 dt_bauhaus_slider_set(g->ultra_deep_blacks, p->ultra_deep_blacks);
1761 dt_bauhaus_slider_set(g->deep_blacks, p->deep_blacks);
1762 dt_bauhaus_slider_set(g->blacks, p->blacks);
1763 dt_bauhaus_slider_set(g->shadows, p->shadows);
1764 dt_bauhaus_slider_set(g->midtones, p->midtones);
1765 dt_bauhaus_slider_set(g->highlights, p->highlights);
1766 dt_bauhaus_slider_set(g->whites, p->whites);
1767 dt_bauhaus_slider_set(g->speculars, p->speculars);
1769}
1770
1771
1772void gui_update(struct dt_iop_module_t *self)
1773{
1776
1777 dt_bauhaus_slider_set(g->smoothing, logf(p->smoothing) / logf(sqrtf(2.0f)) - 1.0f);
1778
1781
1782 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->show_luminance_mask), g->mask_display);
1783}
1784
1785void gui_changed(dt_iop_module_t *self, GtkWidget *w, void *previous)
1786{
1788 if(w == g->method ||
1789 w == g->blending ||
1790 w == g->feathering ||
1791 w == g->iterations ||
1792 w == g->quantization)
1793 {
1795 }
1796 else if (w == g->details)
1797 {
1800 }
1801 else if (w == g->contrast_boost || w == g->exposure_boost)
1802 {
1805 }
1806}
1807
1808static void smoothing_callback(GtkWidget *slider, gpointer user_data)
1809{
1810 dt_iop_module_t *self = (dt_iop_module_t *)user_data;
1811 if(dt_gui_widgets_suppressed()) return;
1814
1815 p->smoothing= powf(sqrtf(2.0f), 1.0f + dt_bauhaus_slider_get(slider));
1816
1817 float factors[CHANNELS] DT_ALIGNED_ARRAY;
1818 get_channels_factors(factors, p);
1819
1820 // Solve the interpolation by least-squares to check the validity of the smoothing param
1821 const int valid = update_curve_lut(self);
1822 if(!valid) dt_control_log(_("the interpolation is unstable, decrease the curve smoothing"));
1823
1824 // Redraw graph before launching computation
1825 update_curve_lut(self);
1826 gtk_widget_queue_draw(GTK_WIDGET(g->area));
1827 dt_dev_add_history_item(self->dev, self, TRUE, TRUE);
1828
1829 // Unlock the colour picker so we can display our own custom cursor
1831}
1832
1833static void show_luminance_mask_callback(GtkWidget *togglebutton, GdkEventButton *event, dt_iop_module_t *self)
1834{
1835 if(dt_gui_widgets_suppressed()) return;
1837
1838 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(self->gui->off), TRUE);
1839
1841
1842 // if blend module is displaying mask do not display it here
1845
1846 g->mask_display = !g->mask_display;
1847
1848 if(g->mask_display)
1850
1851 dt_iop_set_cache_bypass(self, g->mask_display);
1852 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->show_luminance_mask), g->mask_display);
1854
1855 // Unlock the colour picker so we can display our own custom cursor
1857}
1858
1859
1860/***
1861 * GUI Interactivity
1862 **/
1863
1864static void _switch_cursors(struct dt_iop_module_t *self)
1865{
1867 if(IS_NULL_PTR(g) || !self->dev->gui_attached) return;
1868
1869 if(!self->gui->expanded)
1870 {
1871 // If the module lost focus, do nothing and let the app decide.
1872 return;
1873 }
1874 else if(!self->enabled)
1875 {
1876 // A disabled module does not own the cursor state: leave whatever the view
1877 // or another tool has currently selected, and most importantly do not hide it.
1878 return;
1879 }
1880 else if(!sanity_check(self) || in_mask_editing(self) || dt_iop_color_picker_is_visible(self->dev))
1881 {
1882 // if we are editing masks or using colour-pickers, do not display our own custom cursor,
1883 // but do not force a specific shape either: the mouse can still be over the image here,
1884 // and darkroom's own default cursor logic (dot/crosshair/left_ptr, picked per position in
1885 // _darkroom_set_default_cursor) already queued the right one before this module's
1886 // mouse_moved ran. Un-hide it (it may have been hidden by the branch below) and leave it.
1888 return;
1889 }
1890 else if((self->dev->pipe->processing || self->dev->preview_pipe->processing) && g->cursor_valid)
1891 {
1892 // if pipe is busy or dirty but cursor is on preview,
1893 // display waiting cursor while pipe reprocesses
1896
1898 }
1899 else if(self->enabled && g->cursor_valid && !self->dev->pipe->processing)
1900 {
1901 // if pipe is clean and idle and cursor is on preview,
1902 // hide GTK cursor because we display our custom one
1905 _("scroll over image to change tone exposure\n"
1906 "shift+scroll for large steps; "
1907 "ctrl+scroll for small steps"));
1908
1910 }
1911 else
1912 {
1913 // Cursor is out of the preview (off the image entirely): same reasoning as above, let
1914 // darkroom's own default cursor stand instead of forcing a specific shape.
1917 }
1918}
1919
1920
1921int mouse_moved(struct dt_iop_module_t *self, double x, double y, double pressure, int which)
1922{
1923 // Whenever the mouse moves over the picture preview, store its coordinates in the GUI struct
1924 // for later use. This works only if dev->preview_pipe perfectly overlaps with the UI preview
1925 // meaning all distortions, cropping, rotations etc. are applied before this module in the pipe.
1926
1927 dt_develop_t *dev = self->dev;
1929
1930 const int fail = !sanity_check(self);
1931 if(fail) return 0;
1932
1934 {
1935 g->cursor_valid = FALSE;
1936 g->area_active_node = -1;
1937 _switch_cursors(self);
1938 gtk_widget_queue_draw(GTK_WIDGET(g->area));
1939 return 0;
1940 }
1941
1942 if(IS_NULL_PTR(g)) return 0;
1943
1944 // Stop at the normalized image coordinates: they describe the position on the picture itself,
1945 // which is the only space both the preview-sized and the full-resolution luminance mask share.
1946 float pzxpy[2] = { (float)x, (float)y };
1948
1949 const float x_pointer = pzxpy[0];
1950 const float y_pointer = pzxpy[1];
1951
1952 // Cursor is valid if it's inside the picture frame
1953 if(x_pointer >= 0.f && x_pointer < 1.f && y_pointer >= 0.f && y_pointer < 1.f)
1954 {
1955 g->cursor_valid = TRUE;
1956 g->cursor_pos_x = x_pointer;
1957 g->cursor_pos_y = y_pointer;
1958 }
1959 else
1960 {
1961 g->cursor_valid = FALSE;
1962 g->cursor_pos_x = 0.f;
1963 g->cursor_pos_y = 0.f;
1964 }
1965
1966 // Store the current preview exposure too, to spare recomputing it in the UI callbacks.
1967 if(g->cursor_valid && !dev->pipe->processing)
1968 {
1969 dt_pixel_cache_entry_t *preview_entry = NULL;
1970 size_t preview_width = 0;
1971 size_t preview_height = 0;
1972
1973 // Keep the GUI cache entry alive before releasing the GUI state lock.
1974 // Pipe workers can replace the retained entry while mouse motion keeps sampling it.
1976 if(g->luminance_valid && !IS_NULL_PTR(g->thumb_preview_entry))
1977 {
1978 preview_entry = g->thumb_preview_entry;
1979 preview_width = g->thumb_preview_buf_width;
1980 preview_height = g->thumb_preview_buf_height;
1982 }
1984
1985 if(!IS_NULL_PTR(preview_entry) && preview_width > 0 && preview_height > 0)
1986 {
1988 const float *const preview_buf = (const float *const)dt_pixel_cache_entry_get_data(preview_entry);
1989 const float cursor_exposure
1990 = log2f(get_luminance_at_norm(preview_buf, preview_width, preview_height,
1991 x_pointer, y_pointer));
1993
1994 if(!isnan(cursor_exposure))
1995 {
1996 g->cursor_exposure = cursor_exposure;
1997 }
1998
1999 }
2000
2001 if(preview_entry)
2003 }
2004
2005 _switch_cursors(self);
2006 return 1;
2007}
2008
2009
2011{
2013
2014 if(IS_NULL_PTR(g)) return 0;
2015
2016 g->cursor_valid = FALSE;
2017 g->area_active_node = -1;
2018
2019 // display default cursor
2022 gtk_widget_queue_draw(GTK_WIDGET(g->area));
2023
2024 return 1;
2025}
2026
2027
2028static inline int set_new_params_interactive(const float control_exposure, const float exposure_offset, const float blending_sigma,
2030{
2031 // Apply an exposure offset optimized smoothly over all the exposure channels,
2032 // taking user instruction to apply exposure_offset EV at control_exposure EV,
2033 // and commit the new params is the solution is valid.
2034
2035 // Raise the user params accordingly to control correction and distance from cursor exposure
2036 // to blend smoothly the desired correction
2037 const float std = gaussian_denom(blending_sigma);
2038 if(g->user_param_valid)
2039 {
2040 for(int i = 0; i < CHANNELS; ++i)
2041 g->temp_user_params[i] *= exp2f(gaussian_func(centers_params[i] - control_exposure, std) * exposure_offset);
2042 }
2043
2044 // Get the new weights for the radial-basis approximation
2045 float factors[CHANNELS] DT_ALIGNED_ARRAY;
2046 dt_simd_memcpy(g->temp_user_params, factors, CHANNELS);
2047 if(g->user_param_valid)
2048 g->user_param_valid = (pseudo_solve(g->interpolation_matrix, factors, CHANNELS, PIXEL_CHAN, 1) == 0);
2049 if(!g->user_param_valid) dt_control_log(_("the interpolation is unstable, decrease the curve smoothing"));
2050
2051 // Compute new user params for channels and store them locally
2052 if(g->user_param_valid)
2053 g->user_param_valid = compute_channels_factors(factors, g->temp_user_params, g->sigma);
2054 if(!g->user_param_valid) dt_control_log(_("some parameters are out-of-bounds"));
2055
2056 const int commit = g->user_param_valid;
2057
2058 if(commit)
2059 {
2060 // Accept the solution
2061 dt_simd_memcpy(factors, g->factors, PIXEL_CHAN);
2062 g->lut_valid = 0;
2063
2064 // Convert the linear temp parameters to log gains and commit
2065 float gains[CHANNELS] DT_ALIGNED_ARRAY;
2066 compute_channels_gains(g->temp_user_params, gains);
2067 commit_channels_gains(gains, p);
2068 }
2069 else
2070 {
2071 // Reset the GUI copy of user params
2072 get_channels_factors(factors, p);
2073 dt_simd_memcpy(factors, g->temp_user_params, CHANNELS);
2074 g->user_param_valid = 1;
2075 }
2076
2077 return commit;
2078}
2079
2080
2081int scrolled(struct dt_iop_module_t *self, double x, double y, int up, uint32_t state)
2082{
2083 dt_develop_t *dev = self->dev;
2086
2087 if(!sanity_check(self)) return 0;
2088 if(dt_gui_widgets_suppressed()) return 1;
2089 if(IS_NULL_PTR(g)) return 0;
2090 if(!self->gui->expanded) return 0;
2091 if(in_mask_editing(self) || dt_iop_color_picker_is_visible(dev)) return 0;
2092
2093 // turn-on the module if off
2094 if(!self->enabled)
2095 if(self->gui->off) gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(self->gui->off), 1);
2096
2097 // if GUI buffers not ready, exit but still handle the cursor
2098 const int fail = (!g->cursor_valid || !g->interpolation_valid || !g->user_param_valid || dev->pipe->processing || !self->gui->expanded);
2099 if(fail) return 1;
2100
2101 // Re-read the exposure in case the preview changed after the mouse moved.
2102 dt_pixel_cache_entry_t *preview_entry = NULL;
2103 size_t preview_width = 0;
2104 size_t preview_height = 0;
2105 float cursor_x = 0.f;
2106 float cursor_y = 0.f;
2107
2108 // Copy the cursor sample source while holding the GUI state lock, then keep the
2109 // cacheline alive with an explicit ref until the sampling read lock is released.
2111 if(g->luminance_valid && !IS_NULL_PTR(g->thumb_preview_entry))
2112 {
2113 preview_entry = g->thumb_preview_entry;
2114 preview_width = g->thumb_preview_buf_width;
2115 preview_height = g->thumb_preview_buf_height;
2116 cursor_x = g->cursor_pos_x;
2117 cursor_y = g->cursor_pos_y;
2119 }
2121
2122 if(IS_NULL_PTR(preview_entry) || preview_width == 0 || preview_height == 0)
2123 {
2124 if(!IS_NULL_PTR(preview_entry))
2126 return 1;
2127 }
2128
2129 if(!IS_NULL_PTR(preview_entry) && preview_width > 0 && preview_height > 0)
2130 {
2132 const float *const preview_buf = (const float *const)dt_pixel_cache_entry_get_data(preview_entry);
2133 const float cursor_exposure
2134 = log2f(get_luminance_at_norm(preview_buf, preview_width, preview_height,
2135 cursor_x, cursor_y));
2137
2138 if(!isnan(cursor_exposure))
2139 {
2140 g->cursor_exposure = cursor_exposure;
2141 }
2142 }
2143
2144 if(!IS_NULL_PTR(preview_entry))
2146
2147 // Set the correction from mouse scroll input
2148 const float increment = (up) ? +1.0f : -1.0f;
2149
2150 float step;
2151 if(dt_modifier_is(state, GDK_SHIFT_MASK))
2152 step = 1.0f; // coarse
2154 step = 0.1f; // fine
2155 else
2156 step = 0.25f; // standard
2157
2158 const float offset = step * ((float)increment);
2159
2160 // Get the desired correction on exposure channels
2161 const int commit = set_new_params_interactive(g->cursor_exposure, offset, g->sigma * g->sigma / 2.0f, g, p);
2162
2163 gtk_widget_queue_draw(GTK_WIDGET(g->area));
2164
2165 if(commit)
2166 {
2167 // Update GUI with new params
2169
2170 dt_dev_add_history_item(self->dev, self, FALSE, TRUE);
2171 }
2172
2173 return 1;
2174}
2175
2176/***
2177 * GTK/Cairo drawings and custom widgets
2178 **/
2179
2180static inline gboolean _init_drawing(dt_iop_module_t *const restrict self, GtkWidget *widget,
2181 dt_iop_toneequalizer_gui_data_t *const restrict g);
2182
2183
2184void cairo_draw_hatches(cairo_t *cr, double center[2], double span[2], int instances, double line_width, double shade)
2185{
2186 // center is the (x, y) coordinates of the region to draw
2187 // span is the distance of the region's bounds to the center, over (x, y) axes
2188
2189 // Get the coordinates of the corners of the bounding box of the region
2190 double C0[2] = { center[0] - span[0], center[1] - span[1] };
2191 double C2[2] = { center[0] + span[0], center[1] + span[1] };
2192
2193 double delta[2] = { 2.0 * span[0] / (double)instances,
2194 2.0 * span[1] / (double)instances };
2195
2196 cairo_set_line_width(cr, line_width);
2197 cairo_set_source_rgb(cr, shade, shade, shade);
2198
2199 for(int i = -instances / 2 - 1; i <= instances / 2 + 1; i++)
2200 {
2201 cairo_move_to(cr, C0[0] + (double)i * delta[0], C0[1]);
2202 cairo_line_to(cr, C2[0] + (double)i * delta[0], C2[1]);
2203 cairo_stroke(cr);
2204 }
2205}
2206
2207static void get_shade_from_luminance(cairo_t *cr, const float luminance, const float alpha)
2208{
2209 // TODO: fetch screen gamma from ICC display profile
2210 const float gamma = 1.0f / 2.2f;
2211 const float shade = powf(luminance, gamma);
2212 cairo_set_source_rgba(cr, shade, shade, shade, alpha);
2213}
2214
2215
2216static void draw_exposure_cursor(cairo_t *cr, const double pointerx, const double pointery, const double radius, const float luminance, const float zoom_scale, const int instances, const float alpha)
2217{
2218 // Draw a circle cursor filled with a grey shade corresponding to a luminance value
2219 // or hatches if the value is above the overexposed threshold
2220
2221 const double radius_z = radius / zoom_scale;
2222
2224 cairo_arc(cr, pointerx, pointery, radius_z, 0, 2 * M_PI);
2225 cairo_fill_preserve(cr);
2226 cairo_save(cr);
2227 cairo_clip(cr);
2228
2229 if(log2f(luminance) > 0.0f)
2230 {
2231 // if overexposed, draw hatches
2232 double pointer_coord[2] = { pointerx, pointery };
2233 double span[2] = { radius_z, radius_z };
2234 cairo_draw_hatches(cr, pointer_coord, span, instances, DT_PIXEL_APPLY_DPI(1. / zoom_scale), 0.3);
2235 }
2236 cairo_restore(cr);
2237}
2238
2239
2240static void match_color_to_background(cairo_t *cr, const float exposure, const float alpha)
2241{
2242 float shade = 0.0f;
2243 // TODO: put that as a preference in anselrc
2244 const float contrast = 1.0f;
2245
2246 if(exposure > -2.5f)
2247 shade = (fminf(exposure * contrast, 0.0f) - 2.5f);
2248 else
2249 shade = (fmaxf(exposure / contrast, -5.0f) + 2.5f);
2250
2251 get_shade_from_luminance(cr, exp2f(shade), alpha);
2252}
2253
2254
2255void gui_post_expose(struct dt_iop_module_t *self, cairo_t *cr, int32_t width, int32_t height,
2256 int32_t pointerx, int32_t pointery)
2257{
2258 // Draw the custom exposure cursor over the image preview
2259
2260 dt_develop_t *dev = self->dev;
2262 if(IS_NULL_PTR(g)) return;
2263
2264 // If the darkroom picker owns the center view, keep tone equalizer overlays out of the way.
2265 if(in_mask_editing(self) || dt_iop_color_picker_is_visible(dev)) return;
2266
2267 const int fail = (!g->cursor_valid || !g->interpolation_valid || dev->pipe->processing
2268 || !sanity_check(self) || !self->gui->expanded);
2269 if(fail) return;
2270
2271 if(!g->graph_valid)
2272 if(!_init_drawing(self, self->gui->widget, g)) return;
2273
2274 // Get coordinates. The cursor is stored normalized: sampling resolves it against the luminance
2275 // mask's own size below, while the on-canvas cursor is drawn in the preview-pixel space
2276 // dt_dev_rescale_roi() establishes.
2277 const float norm_x = g->cursor_pos_x;
2278 const float norm_y = g->cursor_pos_y;
2279 const float x_pointer = norm_x * (float)dt_dev_roi_request_preview_width(dev);
2280 const float y_pointer = norm_y * (float)dt_dev_roi_request_preview_height(dev);
2281 dt_pixel_cache_entry_t *preview_entry = NULL;
2282 size_t preview_width = 0;
2283 size_t preview_height = 0;
2284 float factors[PIXEL_CHAN] DT_ALIGNED_ARRAY;
2285 float sigma = 0.0f;
2286
2287 float exposure_in = 0.0f;
2288 float luminance_in = 0.0f;
2289 float correction = 0.0f;
2290 float exposure_out = 0.0f;
2291 float luminance_out = 0.0f;
2292 if(self->enabled)
2293 {
2294 // The drawing pass samples the same retained luminance cacheline as the event
2295 // handlers, so the ref has to be taken before another pipe callback can detach it.
2297 if(g->luminance_valid && !IS_NULL_PTR(g->thumb_preview_entry))
2298 {
2299 preview_entry = g->thumb_preview_entry;
2300 preview_width = g->thumb_preview_buf_width;
2301 preview_height = g->thumb_preview_buf_height;
2303 dt_simd_memcpy(g->factors, factors, PIXEL_CHAN);
2304 sigma = g->sigma;
2305 }
2307 }
2308
2309 if(!IS_NULL_PTR(preview_entry) && preview_width > 0 && preview_height > 0)
2310 {
2312 const float *const preview_buf = (const float *const)dt_pixel_cache_entry_get_data(preview_entry);
2313 if(!IS_NULL_PTR(preview_buf))
2314 {
2315 exposure_in = log2f(get_luminance_at_norm(preview_buf, preview_width, preview_height,
2316 norm_x, norm_y));
2317 luminance_in = exp2f(exposure_in);
2318 correction = log2f(pixel_correction(exposure_in, factors, sigma));
2319 exposure_out = exposure_in + correction;
2320 luminance_out = exp2f(exposure_out);
2321 }
2322 else
2323 {
2324 exposure_in = NAN;
2325 correction = NAN;
2326 }
2328
2329 if(!isnan(exposure_in))
2330 {
2331 g->cursor_exposure = exposure_in;
2332 }
2333 }
2334
2335 if(preview_entry)
2337
2338 if(isnan(correction) || isnan(exposure_in)) return; // something went wrong
2339
2340 // Rescale and shift Cairo drawing coordinates
2341 const float zoom_scale = dt_dev_get_overlay_scale(dev);
2342 dt_dev_rescale_roi(dev, cr, width, height);
2343
2344 // set custom cursor dimensions
2345 const double outer_radius = 16.;
2346 const double inner_radius = outer_radius / 2.0;
2347 const double setting_offset_x = (outer_radius + 4. * g->inner_padding) / zoom_scale;
2348 const double fill_width = DT_PIXEL_APPLY_DPI(4) / zoom_scale;
2349
2350 // setting fill bars
2351 match_color_to_background(cr, exposure_out, 1.0);
2352 cairo_set_line_width(cr, 2.0 * fill_width);
2353 cairo_move_to(cr, x_pointer - setting_offset_x, y_pointer);
2354
2355 if(correction > 0.0f)
2356 cairo_arc(cr, x_pointer, y_pointer, setting_offset_x, M_PI, M_PI + correction * M_PI / 4.0);
2357 else
2358 cairo_arc_negative(cr, x_pointer, y_pointer, setting_offset_x, M_PI, M_PI + correction * M_PI / 4.0);
2359
2360 cairo_stroke(cr);
2361
2362 // setting ground level
2363 cairo_set_line_width(cr, DT_PIXEL_APPLY_DPI(1.5) / zoom_scale);
2364 cairo_move_to(cr, x_pointer + (outer_radius + 2. * g->inner_padding) / zoom_scale, y_pointer);
2365 cairo_line_to(cr, x_pointer + outer_radius / zoom_scale, y_pointer);
2366 cairo_move_to(cr, x_pointer - outer_radius / zoom_scale, y_pointer);
2367 cairo_line_to(cr, x_pointer - setting_offset_x - 4.0 * g->inner_padding / zoom_scale, y_pointer);
2368 cairo_stroke(cr);
2369
2370 // setting cursor cross hair
2371 cairo_set_line_width(cr, DT_PIXEL_APPLY_DPI(1.5) / zoom_scale);
2372 cairo_move_to(cr, x_pointer, y_pointer + setting_offset_x + fill_width);
2373 cairo_line_to(cr, x_pointer, y_pointer + outer_radius / zoom_scale);
2374 cairo_move_to(cr, x_pointer, y_pointer - outer_radius / zoom_scale);
2375 cairo_line_to(cr, x_pointer, y_pointer - setting_offset_x - fill_width);
2376 cairo_stroke(cr);
2377
2378 // draw exposure cursor
2379 draw_exposure_cursor(cr, x_pointer, y_pointer, outer_radius, luminance_in, zoom_scale, 6, .9);
2380 draw_exposure_cursor(cr, x_pointer, y_pointer, inner_radius, luminance_out, zoom_scale, 3, .9);
2381
2382 // Create Pango objects : texts
2383 char text[256];
2384 PangoLayout *layout;
2385 PangoRectangle ink;
2386 PangoFontDescription *desc = pango_font_description_copy_static(dt_bauhaus_get_global()->pango_font_desc);
2387
2388 // Avoid text resizing based on zoom level
2389 const int old_size = pango_font_description_get_size(desc);
2390 pango_font_description_set_size (desc, (int)(old_size / zoom_scale));
2391 layout = pango_cairo_create_layout(cr);
2392 pango_layout_set_font_description(layout, desc);
2394
2395 // Build text object
2396 if(preview_entry && self->enabled)
2397 snprintf(text, sizeof(text), _("%+.1f EV"), exposure_in);
2398 else
2399 snprintf(text, sizeof(text), "? EV");
2400 pango_layout_set_text(layout, text, -1);
2401 pango_layout_get_pixel_extents(layout, &ink, NULL);
2402
2403 // Draw the text plain blackground
2404 get_shade_from_luminance(cr, luminance_out, 0.75);
2405 cairo_rectangle(cr, x_pointer + (outer_radius + 2. * g->inner_padding) / zoom_scale,
2406 y_pointer - ink.y - ink.height / 2.0 - g->inner_padding / zoom_scale,
2407 ink.width + 2.0 * ink.x + 4. * g->inner_padding / zoom_scale,
2408 ink.height + 2.0 * ink.y + 2. * g->inner_padding / zoom_scale);
2409 cairo_fill(cr);
2410
2411 // Display the EV reading
2412 match_color_to_background(cr, exposure_out, 1.0);
2413 cairo_move_to(cr, x_pointer + (outer_radius + 4. * g->inner_padding) / zoom_scale,
2414 y_pointer - ink.y - ink.height / 2.);
2415 pango_cairo_show_layout(cr, layout);
2416
2417 cairo_stroke(cr);
2418
2419 pango_font_description_free(desc);
2420 g_object_unref(layout);
2421
2422 if(preview_entry && self->enabled)
2423 {
2424 // Search for nearest node in graph and highlight it
2425 const float radius_threshold = 0.45f;
2426 g->area_active_node = -1;
2427 if(g->cursor_valid)
2428 for(int i = 0; i < CHANNELS; ++i)
2429 {
2430 const float delta_x = fabsf(g->cursor_exposure - centers_params[i]);
2431 if(delta_x < radius_threshold)
2432 g->area_active_node = i;
2433 }
2434
2435 gtk_widget_queue_draw(GTK_WIDGET(g->area));
2436 }
2437}
2438
2439
2440void gui_focus(struct dt_iop_module_t *self, gboolean in)
2441{
2443 _switch_cursors(self);
2444 if(!in)
2445 {
2446 //lost focus - stop showing mask
2447 const gboolean was_mask = g->mask_display;
2448 g->mask_display = FALSE;
2450 g->pending_preview_hash = DT_PIXELPIPE_CACHE_HASH_INVALID;
2452 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->show_luminance_mask), FALSE);
2453 if(was_mask) dt_dev_pixelpipe_update_history_main(self->dev);
2455 }
2456 else
2457 {
2458 gboolean needs_preview_update = FALSE;
2459
2460 if(self->enabled && self->dev && self->dev->preview_pipe && !self->dev->preview_pipe->processing)
2461 {
2462 // Opening the module can happen after preview processing already finished.
2463 // In that case the preview pipe may stay idle because darkroom can reuse an
2464 // existing backbuffer, so reattach to the existing luminance cacheline here
2465 // instead of waiting for process() to run again. This was tested by opening
2466 // tone equalizer on a fresh darkroom image with no pending recompute.
2467 //
2468 // Hash AND dimensions come from ONE read of the pipe piece: the worker thread replans that
2469 // piece between two reads, and toneequal's ROI genuinely changes size across such a replan
2470 // (`finalscale` enables or disables itself on zoom, which moves this module between the
2471 // full-resolution and the preview-sized half of the pipe). Two reads therefore describe two
2472 // different runs, and pairing one run's hash with the other's dimensions is what let the
2473 // GUI sample a preview-sized cacheline as if it were the full-resolution one.
2474 size_t preview_width = 0;
2475 size_t preview_height = 0;
2476 const uint64_t preview_hash = _current_preview_luminance_hash(self, &preview_width, &preview_height);
2477
2478 if(preview_hash != DT_PIXELPIPE_CACHE_HASH_INVALID)
2479 {
2480 void *preview_buf = NULL;
2481 dt_pixel_cache_entry_t *preview_entry = NULL;
2482
2483 gboolean preview_ready = dt_dev_pixelpipe_cache_ref_entry_by_hash(preview_hash,
2484 &preview_buf, &preview_entry);
2485 if(preview_ready && (IS_NULL_PTR(preview_buf)
2486 || !luminance_entry_fits(preview_entry, preview_width, preview_height)))
2487 {
2488 if(!IS_NULL_PTR(preview_entry))
2490 preview_ready = FALSE;
2491 }
2492
2493 if(preview_ready)
2494 {
2495 dt_pixel_cache_entry_t *old_entry = NULL;
2496 gboolean keep_new_entry = FALSE;
2498 if(g->thumb_preview_entry != preview_entry || g->thumb_preview_hash != preview_hash
2499 || g->thumb_preview_buf_width != preview_width
2500 || g->thumb_preview_buf_height != preview_height || !g->luminance_valid)
2501 {
2502 old_entry = g->thumb_preview_entry;
2503 g->thumb_preview_entry = preview_entry;
2504 g->thumb_preview_hash = preview_hash;
2505 g->pending_preview_hash = DT_PIXELPIPE_CACHE_HASH_INVALID;
2506 g->thumb_preview_buf_width = preview_width;
2507 g->thumb_preview_buf_height = preview_height;
2508 g->luminance_valid = TRUE;
2509 g->histogram_valid = FALSE;
2510 keep_new_entry = TRUE;
2511 }
2513
2514 if(old_entry)
2516 if(!keep_new_entry)
2518 }
2519 else
2520 {
2522 g->pending_preview_hash = preview_hash;
2524 needs_preview_update = TRUE;
2525 }
2526 }
2527 else
2528 needs_preview_update = TRUE;
2529 }
2530
2531 if(needs_preview_update)
2533
2535 _("scroll over image to change tone exposure\n"
2536 "shift+scroll for large steps; "
2537 "ctrl+scroll for small steps"));
2538 }
2539}
2540
2541
2542static inline gboolean _init_drawing(dt_iop_module_t *const restrict self, GtkWidget *widget,
2543 dt_iop_toneequalizer_gui_data_t *const restrict g)
2544{
2545 // Cache the equalizer graph objects to avoid recomputing all the view at each redraw
2546 gtk_widget_get_allocation(widget, &g->allocation);
2547
2548 if(g->cst) cairo_surface_destroy(g->cst);
2549 g->cst = dt_cairo_image_surface_create(CAIRO_FORMAT_ARGB32, g->allocation.width, g->allocation.height);
2550
2551 if(g->cr) cairo_destroy(g->cr);
2552 g->cr = cairo_create(g->cst);
2553
2554 if(g->layout) g_object_unref(g->layout);
2555 g->layout = pango_cairo_create_layout(g->cr);
2556
2557 if(g->desc) pango_font_description_free(g->desc);
2558 g->desc = pango_font_description_copy_static(dt_bauhaus_get_global()->pango_font_desc);
2559
2560 pango_layout_set_font_description(g->layout, g->desc);
2562 g->context = gtk_widget_get_style_context(widget);
2563
2564 char text[256];
2565
2566 // Get the text line height for spacing
2567 snprintf(text, sizeof(text), "X");
2568 pango_layout_set_text(g->layout, text, -1);
2569 pango_layout_get_pixel_extents(g->layout, &g->ink, NULL);
2570 g->line_height = g->ink.height;
2571
2572 // Get the width of a minus sign for legend labels spacing
2573 snprintf(text, sizeof(text), "-");
2574 pango_layout_set_text(g->layout, text, -1);
2575 pango_layout_get_pixel_extents(g->layout, &g->ink, NULL);
2576 g->sign_width = g->ink.width / 2.0;
2577
2578 // Set the sizes, margins and paddings
2579 g->inner_padding = INNER_PADDING;
2580 g->inset = g->inner_padding + dt_bauhaus_get_global()->quad_width;
2581 g->graph_left_space = g->line_height + g->inner_padding;
2582 g->graph_width = g->allocation.width - g->inset - 2.0 * g->line_height; // align the right border on sliders
2583 g->graph_height = g->allocation.height - g->inset - 2.0 * g->line_height; // give room to nodes
2584 g->gradient_left_limit = 0.0;
2585 g->gradient_right_limit = g->graph_width;
2586 g->gradient_top_limit = g->graph_height + 2 * g->inner_padding;
2587 g->gradient_width = g->gradient_right_limit - g->gradient_left_limit;
2588 g->legend_top_limit = -0.5 * g->line_height - 2.0 * g->inner_padding;
2589 g->x_label = g->graph_width + g->sign_width + 3.0 * g->inner_padding;
2590
2591 gtk_render_background(g->context, g->cr, 0, 0, g->allocation.width, g->allocation.height);
2592
2593 // set the graph as the origin of the coordinates
2594 cairo_translate(g->cr, g->line_height + 2 * g->inner_padding, g->line_height + 3 * g->inner_padding);
2595
2596 // display x-axis and y-axis legends (EV)
2597 set_color(g->cr, dt_bauhaus_get_global()->graph_fg);
2598
2599 float value = -8.0f;
2600
2601 for(int k = 0; k < CHANNELS; k++)
2602 {
2603 const float xn = (((float)k) / ((float)(CHANNELS - 1))) * g->graph_width - g->sign_width;
2604 snprintf(text, sizeof(text), "%+.0f", value);
2605 pango_layout_set_text(g->layout, text, -1);
2606 pango_layout_get_pixel_extents(g->layout, &g->ink, NULL);
2607 cairo_move_to(g->cr, xn - 0.5 * g->ink.width - g->ink.x,
2608 g->legend_top_limit - 0.5 * g->ink.height - g->ink.y);
2609 pango_cairo_show_layout(g->cr, g->layout);
2610 cairo_stroke(g->cr);
2611
2612 value += 1.0;
2613 }
2614
2615 value = 2.0f;
2616
2617 for(int k = 0; k < 5; k++)
2618 {
2619 const float yn = (k / 4.0f) * g->graph_height;
2620 snprintf(text, sizeof(text), "%+.0f", value);
2621 pango_layout_set_text(g->layout, text, -1);
2622 pango_layout_get_pixel_extents(g->layout, &g->ink, NULL);
2623 cairo_move_to(g->cr, g->x_label - 0.5 * g->ink.width - g->ink.x,
2624 yn - 0.5 * g->ink.height - g->ink.y);
2625 pango_cairo_show_layout(g->cr, g->layout);
2626 cairo_stroke(g->cr);
2627
2628 value -= 1.0;
2629 }
2630
2632 // Draw the perceptually even gradient
2633 cairo_pattern_t *grad;
2634 grad = cairo_pattern_create_linear(g->gradient_left_limit, 0.0, g->gradient_right_limit, 0.0);
2636 cairo_set_line_width(g->cr, 0.0);
2637 cairo_rectangle(g->cr, g->gradient_left_limit, g->gradient_top_limit, g->gradient_width, g->line_height);
2638 cairo_set_source(g->cr, grad);
2639 cairo_fill(g->cr);
2640 cairo_pattern_destroy(grad);
2641
2643 // Draw the perceptually even gradient
2644 grad = cairo_pattern_create_linear(0.0, g->graph_height, 0.0, 0.0);
2646 cairo_set_line_width(g->cr, 0.0);
2647 cairo_rectangle(g->cr, -g->line_height - 2 * g->inner_padding, 0.0, g->line_height, g->graph_height);
2648 cairo_set_source(g->cr, grad);
2649 cairo_fill(g->cr);
2650
2651 cairo_pattern_destroy(grad);
2652
2653 // Draw frame borders
2654 cairo_set_line_width(g->cr, DT_PIXEL_APPLY_DPI(0.5));
2655 set_color(g->cr, dt_bauhaus_get_global()->graph_border);
2656 cairo_rectangle(g->cr, 0, 0, g->graph_width, g->graph_height);
2657 cairo_stroke_preserve(g->cr);
2658
2659 // end of caching section, this will not be drawn again
2660
2661 g->graph_valid = 1;
2662
2663 return TRUE;
2664}
2665
2666
2667// must be called while holding self->gui_lock
2669{
2670 if(IS_NULL_PTR(g)) return;
2671
2672 if(!g->valid_nodes_x && g->graph_width > 0)
2673 {
2674 for(int i = 0; i < CHANNELS; ++i)
2675 g->nodes_x[i] = (((float)i) / ((float)(CHANNELS - 1))) * g->graph_width;
2676 g->valid_nodes_x = TRUE;
2677 }
2678}
2679
2680
2681// must be called while holding self->gui_lock
2683{
2684 if(IS_NULL_PTR(g)) return;
2685
2686 if(g->user_param_valid && g->graph_height > 0)
2687 {
2688 for(int i = 0; i < CHANNELS; ++i)
2689 g->nodes_y[i] = (0.5 - log2f(g->temp_user_params[i]) / 4.0) * g->graph_height; // assumes factors in [-2 ; 2] EV
2690 g->valid_nodes_y = TRUE;
2691 }
2692}
2693
2694
2695static gboolean area_draw(GtkWidget *widget, cairo_t *cr, gpointer user_data)
2696{
2697 // Draw the widget equalizer view
2698 dt_iop_module_t *self = (dt_iop_module_t *)user_data;
2700 if(IS_NULL_PTR(g)) return FALSE;
2701
2702 // Init or refresh the drawing cache
2703 //if(!g->graph_valid)
2704 if(!_init_drawing(self, widget, g)) return FALSE; // this can be cached and drawn just once, but too lazy to debug a cache invalidation for Cairo objects
2705
2706 // since the widget sizes are not cached and invalidated properly above (yet...)
2707 // force the invalidation of the nodes coordinates to account for possible widget resizing
2708 g->valid_nodes_x = FALSE;
2709 g->valid_nodes_y = FALSE;
2710
2711 // Refresh cached UI elements
2712 update_histogram(self);
2713 update_curve_lut(self);
2714
2715 // Draw graph background
2716 cairo_set_line_width(g->cr, DT_PIXEL_APPLY_DPI(0.5));
2717 cairo_rectangle(g->cr, 0, 0, g->graph_width, g->graph_height);
2718 set_color(g->cr, dt_bauhaus_get_global()->graph_bg);
2719 cairo_fill(g->cr);
2720
2721 // draw grid
2722 cairo_set_line_width(g->cr, DT_PIXEL_APPLY_DPI(0.5));
2723 set_color(g->cr, dt_bauhaus_get_global()->graph_border);
2724 dt_draw_grid(g->cr, 8, 0, 0, g->graph_width, g->graph_height);
2725
2726 // draw ground level
2727 set_color(g->cr, dt_bauhaus_get_global()->graph_fg);
2728 cairo_set_line_width(g->cr, DT_PIXEL_APPLY_DPI(1));
2729 cairo_move_to(g->cr, 0, 0.5 * g->graph_height);
2730 cairo_line_to(g->cr, g->graph_width, 0.5 * g->graph_height);
2731 cairo_stroke(g->cr);
2732
2733 if(g->histogram_valid && self->enabled)
2734 {
2735 // draw the inset histogram
2736 set_color(g->cr, dt_bauhaus_get_global()->inset_histogram);
2737 cairo_set_line_width(g->cr, DT_PIXEL_APPLY_DPI(4.0));
2738 cairo_move_to(g->cr, 0, g->graph_height);
2739
2740 for(int k = 0; k < UI_SAMPLES; k++)
2741 {
2742 // the x range is [-8;+0] EV
2743 const float x_temp = (8.0 * (float)k / (float)(UI_SAMPLES - 1)) - 8.0;
2744 const float y_temp = (float)(g->histogram[k]) / (float)(g->max_histogram) * 0.96;
2745 cairo_line_to(g->cr, (x_temp + 8.0) * g->graph_width / 8.0,
2746 (1.0 - y_temp) * g->graph_height );
2747 }
2748 cairo_line_to(g->cr, g->graph_width, g->graph_height);
2749 cairo_close_path(g->cr);
2750 cairo_fill(g->cr);
2751
2752 if(g->histogram_last_decile > -0.1f)
2753 {
2754 // histogram overflows controls in highlights : display warning
2755 cairo_save(g->cr);
2756 cairo_set_source_rgb(g->cr, 0.75, 0.50, 0.);
2757 dtgtk_cairo_paint_gamut_check(g->cr, g->graph_width - 2.5 * g->line_height, 0.5 * g->line_height,
2758 2.0 * g->line_height, 2.0 * g->line_height, 0, NULL);
2759 cairo_restore(g->cr);
2760 }
2761
2762 if(g->histogram_first_decile < -7.9f)
2763 {
2764 // histogram overflows controls in lowlights : display warning
2765 cairo_save(g->cr);
2766 cairo_set_source_rgb(g->cr, 0.75, 0.50, 0.);
2767 dtgtk_cairo_paint_gamut_check(g->cr, 0.5 * g->line_height, 0.5 * g->line_height,
2768 2.0 * g->line_height, 2.0 * g->line_height, 0, NULL);
2769 cairo_restore(g->cr);
2770 }
2771 }
2772
2773 if(g->lut_valid)
2774 {
2775 // draw the interpolation curve
2776 set_color(g->cr, dt_bauhaus_get_global()->graph_fg);
2777 cairo_move_to(g->cr, 0, g->gui_lut[0] * g->graph_height);
2778 cairo_set_line_width(g->cr, DT_PIXEL_APPLY_DPI(3));
2779
2780 for(int k = 1; k < UI_SAMPLES; k++)
2781 {
2782 // the x range is [-8;+0] EV
2783 const float x_temp = (8.0f * (((float)k) / ((float)(UI_SAMPLES - 1)))) - 8.0f;
2784 const float y_temp = g->gui_lut[k];
2785
2786 cairo_line_to(g->cr, (x_temp + 8.0f) * g->graph_width / 8.0f,
2787 y_temp * g->graph_height );
2788 }
2789 cairo_stroke(g->cr);
2790 }
2791
2792 init_nodes_x(g);
2793 init_nodes_y(g);
2794
2795 if(g->user_param_valid)
2796 {
2797 // draw nodes positions
2798 for(int k = 0; k < CHANNELS; k++)
2799 {
2800 const float xn = g->nodes_x[k];
2801 const float yn = g->nodes_y[k];
2802
2803 // fill bars
2804 cairo_set_line_width(g->cr, DT_PIXEL_APPLY_DPI(6));
2805 set_color(g->cr, dt_bauhaus_get_global()->color_fill);
2806 cairo_move_to(g->cr, xn, 0.5 * g->graph_height);
2807 cairo_line_to(g->cr, xn, yn);
2808 cairo_stroke(g->cr);
2809
2810 // bullets
2811 cairo_set_line_width(g->cr, DT_PIXEL_APPLY_DPI(3));
2812 cairo_arc(g->cr, xn, yn, DT_PIXEL_APPLY_DPI(4), 0, 2. * M_PI);
2813 set_color(g->cr, dt_bauhaus_get_global()->graph_fg);
2814 cairo_stroke_preserve(g->cr);
2815
2816 if(g->area_active_node == k)
2817 set_color(g->cr, dt_bauhaus_get_global()->graph_fg);
2818 else
2819 set_color(g->cr, dt_bauhaus_get_global()->graph_bg);
2820
2821 cairo_fill(g->cr);
2822 }
2823 }
2824
2825 if(self->enabled)
2826 {
2827 if(g->area_cursor_valid)
2828 {
2829 const float radius = g->sigma * g->graph_width / 8.0f / sqrtf(2.0f);
2830 cairo_set_line_width(g->cr, DT_PIXEL_APPLY_DPI(1.5));
2831 const float y =g->gui_lut[(int)CLAMP(((UI_SAMPLES - 1) * g->area_x / g->graph_width), 0, UI_SAMPLES - 1)];
2832 cairo_arc(g->cr, g->area_x, y * g->graph_height, radius, 0, 2. * M_PI);
2833 set_color(g->cr, dt_bauhaus_get_global()->graph_fg);
2834 cairo_stroke(g->cr);
2835 }
2836
2837 if(g->cursor_valid)
2838 {
2839
2840 float x_pos = (g->cursor_exposure + 8.0f) / 8.0f * g->graph_width;
2841
2842 if(x_pos > g->graph_width || x_pos < 0.0f)
2843 {
2844 // exposure at current position is outside [-8; 0] EV :
2845 // bound it in the graph limits and show it in orange
2846 cairo_set_source_rgb(g->cr, 0.75, 0.50, 0.);
2847 cairo_set_line_width(g->cr, DT_PIXEL_APPLY_DPI(3));
2848 x_pos = (x_pos < 0.0f) ? 0.0f : g->graph_width;
2849 }
2850 else
2851 {
2852 set_color(g->cr, dt_bauhaus_get_global()->graph_fg);
2853 cairo_set_line_width(g->cr, DT_PIXEL_APPLY_DPI(1.5));
2854 }
2855
2856 cairo_move_to(g->cr, x_pos, 0.0);
2857 cairo_line_to(g->cr, x_pos, g->graph_height);
2858 cairo_stroke(g->cr);
2859 }
2860 }
2861
2862 // clean and exit
2863 cairo_set_source_surface(cr, g->cst, 0, 0);
2864 cairo_paint(cr);
2865
2866 return TRUE;
2867}
2868
2869static gboolean area_enter_notify(GtkWidget *widget, GdkEventCrossing *event, gpointer user_data)
2870{
2871 dt_iop_module_t *self = (dt_iop_module_t *)user_data;
2872 if(dt_gui_widgets_suppressed()) return 1;
2873 if(!self->enabled) return 0;
2874
2876 g->area_x = (event->x - g->inset);
2877 g->area_y = (event->y - g->inset);
2878 g->area_dragging = FALSE;
2879 g->area_active_node = -1;
2880 g->area_cursor_valid = (g->area_x > 0.0f && g->area_x < g->graph_width && g->area_y > 0.0f && g->area_y < g->graph_height);
2881
2882 gtk_widget_queue_draw(GTK_WIDGET(g->area));
2883 return TRUE;
2884}
2885
2886
2887static gboolean area_leave_notify(GtkWidget *widget, GdkEventCrossing *event, gpointer user_data)
2888{
2889 dt_iop_module_t *self = (dt_iop_module_t *)user_data;
2890 if(dt_gui_widgets_suppressed()) return 1;
2891 if(!self->enabled) return 0;
2892
2895
2896 if(g->area_dragging)
2897 {
2898 // cursor left area : force commit to avoid glitches
2900
2901 dt_dev_add_history_item(self->dev, self, FALSE, TRUE);
2902 }
2903 g->area_x = (event->x - g->inset);
2904 g->area_y = (event->y - g->inset);
2905 g->area_dragging = FALSE;
2906 g->area_active_node = -1;
2907 g->area_cursor_valid = (g->area_x > 0.0f && g->area_x < g->graph_width && g->area_y > 0.0f && g->area_y < g->graph_height);
2908
2909 gtk_widget_queue_draw(GTK_WIDGET(g->area));
2910 return TRUE;
2911}
2912
2913
2914static gboolean area_button_press(GtkWidget *widget, GdkEventButton *event, gpointer user_data)
2915{
2916 dt_iop_module_t *self = (dt_iop_module_t *)user_data;
2917 if(dt_gui_widgets_suppressed()) return 1;
2918
2920
2922
2923 if(event->button == 1 && event->type == GDK_2BUTTON_PRESS)
2924 {
2927
2928 // reset nodes params
2929 p->noise = d->noise;
2930 p->ultra_deep_blacks = d->ultra_deep_blacks;
2931 p->deep_blacks = d->deep_blacks;
2932 p->blacks = d->blacks;
2933 p->shadows = d->shadows;
2934 p->midtones = d->midtones;
2935 p->highlights = d->highlights;
2936 p->whites = d->whites;
2937 p->speculars = d->speculars;
2938
2939 // update UI sliders
2941
2942 // Redraw graph
2943 gtk_widget_queue_draw(self->gui->widget);
2944 dt_dev_add_history_item(self->dev, self, TRUE, TRUE);
2945 return TRUE;
2946 }
2947 else if(event->button == 1)
2948 {
2949 if(self->enabled)
2950 {
2951 g->area_dragging = 1;
2952 gtk_widget_queue_draw(GTK_WIDGET(g->area));
2953 }
2954 else
2955 {
2956 dt_dev_add_history_item(self->dev, self, TRUE, TRUE);
2957 }
2958 return TRUE;
2959 }
2960
2961 // Unlock the colour picker so we can display our own custom cursor
2963
2964 return FALSE;
2965}
2966
2967
2968static gboolean area_motion_notify(GtkWidget *widget, GdkEventMotion *event, gpointer user_data)
2969{
2970 dt_iop_module_t *self = (dt_iop_module_t *)user_data;
2971 if(dt_gui_widgets_suppressed()) return 1;
2972 if(!self->enabled) return 0;
2973
2976
2977 if(g->area_dragging)
2978 {
2979 // vertical distance travelled since button_pressed event
2980 const float offset = (-event->y + g->area_y) / g->graph_height * 4.0f; // graph spans over 4 EV
2981 const float cursor_exposure = g->area_x / g->graph_width * 8.0f - 8.0f;
2982
2983 // Get the desired correction on exposure channels
2984 g->area_dragging = set_new_params_interactive(cursor_exposure, offset, g->sigma * g->sigma / 2.0f, g, p);
2985 }
2986
2987 g->area_x = (event->x - g->graph_left_space);
2988 g->area_y = event->y;
2989 g->area_cursor_valid = (g->area_x > 0.0f && g->area_x < g->graph_width && g->area_y > 0.0f && g->area_y < g->graph_height);
2990 g->area_active_node = -1;
2991
2992 // Search if cursor is close to a node
2993 if(g->valid_nodes_x)
2994 {
2995 const float radius_threshold = fabsf(g->nodes_x[1] - g->nodes_x[0]) * 0.45f;
2996 for(int i = 0; i < CHANNELS; ++i)
2997 {
2998 const float delta_x = fabsf(g->area_x - g->nodes_x[i]);
2999 if(delta_x < radius_threshold)
3000 {
3001 g->area_active_node = i;
3002 g->area_cursor_valid = 1;
3003 }
3004 }
3005 }
3006
3007 gtk_widget_queue_draw(GTK_WIDGET(g->area));
3008 return TRUE;
3009}
3010
3011
3012static gboolean area_button_release(GtkWidget *widget, GdkEventButton *event, gpointer user_data)
3013{
3014 dt_iop_module_t *self = (dt_iop_module_t *)user_data;
3015 if(dt_gui_widgets_suppressed()) return 1;
3016 if(!self->enabled) return 0;
3017
3019
3020 // Give focus to module
3022
3023 if(event->button == 1)
3024 {
3026
3027 if(g->area_dragging)
3028 {
3029 // Update GUI with new params
3031 dt_dev_add_history_item(self->dev, self, FALSE, TRUE);
3032 g->area_dragging= 0;
3033 return TRUE;
3034 }
3035 }
3036 return FALSE;
3037}
3038
3039
3040static gboolean notebook_button_press(GtkWidget *widget, GdkEventButton *event, gpointer user_data)
3041{
3042 dt_iop_module_t *self = (dt_iop_module_t *)user_data;
3043 if(dt_gui_widgets_suppressed()) return 1;
3044
3045 // Give focus to module
3047
3048 // Unlock the colour picker so we can display our own custom cursor
3050
3051 return 0;
3052}
3053
3059static void _develop_ui_pipe_started_callback(gpointer instance, gpointer user_data)
3060{
3061 dt_iop_module_t *self = (dt_iop_module_t *)user_data;
3063 if(IS_NULL_PTR(g)) return;
3064 _switch_cursors(self);
3065
3066 // if module is not active, disable mask preview
3067 if(!self->gui->expanded || !self->enabled)
3068 {
3069 g->mask_display = 0;
3070 }
3071
3073 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->show_luminance_mask), g->mask_display);
3075}
3076
3077
3078static void _develop_history_resync_callback(gpointer instance, gpointer user_data)
3079{
3080 (void)instance;
3081 dt_iop_module_t *self = (dt_iop_module_t *)user_data;
3083 if(IS_NULL_PTR(g) || IS_NULL_PTR(self->dev) || IS_NULL_PTR(self->dev->preview_pipe)) return;
3084
3085 // One read of the pipe piece for both the hash and the dimensions it describes: this callback
3086 // answers DT_SIGNAL_HISTORY_RESYNC, i.e. it runs while the worker is replanning the pipes, and
3087 // a second read would routinely land on a different plan. See gui_focus() for what pairing two
3088 // plans costs.
3089 size_t preview_width = 0;
3090 size_t preview_height = 0;
3091 const uint64_t preview_hash = _current_preview_luminance_hash(self, &preview_width, &preview_height);
3092 if(preview_hash == DT_PIXELPIPE_CACHE_HASH_INVALID)
3093 {
3095 g->pending_preview_hash = DT_PIXELPIPE_CACHE_HASH_INVALID;
3097 _switch_cursors(self);
3098 gtk_widget_queue_draw(GTK_WIDGET(g->area));
3099 return;
3100 }
3101
3102 gboolean already_attached = FALSE;
3104 if(!IS_NULL_PTR(g->thumb_preview_entry) && g->thumb_preview_hash == preview_hash && g->luminance_valid)
3105 {
3106 g->pending_preview_hash = DT_PIXELPIPE_CACHE_HASH_INVALID;
3107 already_attached = TRUE;
3108 }
3110
3111 if(!already_attached)
3112 {
3113 void *preview_buf = NULL;
3114 dt_pixel_cache_entry_t *preview_entry = NULL;
3115 gboolean preview_ready = dt_dev_pixelpipe_cache_ref_entry_by_hash(preview_hash,
3116 &preview_buf, &preview_entry);
3117 if(preview_ready && (IS_NULL_PTR(preview_buf)
3118 || !luminance_entry_fits(preview_entry, preview_width, preview_height)))
3119 {
3120 if(!IS_NULL_PTR(preview_entry))
3122 preview_ready = FALSE;
3123 }
3124
3125 if(preview_ready)
3126 {
3127 dt_pixel_cache_entry_t *old_entry = NULL;
3128 gboolean keep_new_entry = FALSE;
3130 if(g->thumb_preview_entry != preview_entry || g->thumb_preview_hash != preview_hash
3131 || g->thumb_preview_buf_width != preview_width || g->thumb_preview_buf_height != preview_height
3132 || !g->luminance_valid)
3133 {
3134 old_entry = g->thumb_preview_entry;
3135 g->thumb_preview_entry = preview_entry;
3136 g->thumb_preview_hash = preview_hash;
3137 g->pending_preview_hash = DT_PIXELPIPE_CACHE_HASH_INVALID;
3138 g->thumb_preview_buf_width = preview_width;
3139 g->thumb_preview_buf_height = preview_height;
3140 g->luminance_valid = TRUE;
3141 g->histogram_valid = FALSE;
3142 keep_new_entry = TRUE;
3143 }
3144 else
3145 g->pending_preview_hash = DT_PIXELPIPE_CACHE_HASH_INVALID;
3147
3148 if(old_entry)
3150 if(!keep_new_entry)
3152 }
3153 else
3154 {
3156 g->pending_preview_hash = preview_hash;
3158 }
3159 }
3160
3161 _switch_cursors(self);
3162 gtk_widget_queue_draw(GTK_WIDGET(g->area));
3163}
3164
3165static void _develop_cacheline_ready_callback(gpointer instance, const guint64 hash,
3166 const guint64 producer_node_key, gpointer user_data)
3167{
3168 (void)instance;
3169 (void)producer_node_key;
3170 dt_iop_module_t *self = (dt_iop_module_t *)user_data;
3172 if(IS_NULL_PTR(g) || IS_NULL_PTR(self->dev) || IS_NULL_PTR(self->dev->preview_pipe)) return;
3173
3175 const gboolean matched = (g->pending_preview_hash == hash);
3177 if(!matched) return;
3178
3179 size_t preview_width = 0;
3180 size_t preview_height = 0;
3181 const uint64_t preview_hash = _current_preview_luminance_hash(self, &preview_width, &preview_height);
3182 if(preview_hash != hash) return;
3183
3184 void *preview_buf = NULL;
3185 dt_pixel_cache_entry_t *preview_entry = NULL;
3186 const gboolean preview_ready = dt_dev_pixelpipe_cache_ref_entry_by_hash(preview_hash,
3187 &preview_buf, &preview_entry);
3188 if(!preview_ready || IS_NULL_PTR(preview_buf)
3189 || !luminance_entry_fits(preview_entry, preview_width, preview_height))
3190 {
3191 if(!IS_NULL_PTR(preview_entry))
3193 return;
3194 }
3195
3196 dt_pixel_cache_entry_t *old_entry = NULL;
3197 gboolean keep_new_entry = FALSE;
3199 if(g->thumb_preview_entry != preview_entry || g->thumb_preview_hash != preview_hash
3200 || g->thumb_preview_buf_width != preview_width || g->thumb_preview_buf_height != preview_height
3201 || !g->luminance_valid)
3202 {
3203 old_entry = g->thumb_preview_entry;
3204 g->thumb_preview_entry = preview_entry;
3205 g->thumb_preview_hash = preview_hash;
3206 g->thumb_preview_buf_width = preview_width;
3207 g->thumb_preview_buf_height = preview_height;
3208 g->luminance_valid = TRUE;
3209 g->histogram_valid = FALSE;
3210 keep_new_entry = TRUE;
3211 }
3212 g->pending_preview_hash = DT_PIXELPIPE_CACHE_HASH_INVALID;
3214
3215 if(old_entry)
3217 if(!keep_new_entry)
3219
3220 _switch_cursors(self);
3221 gtk_widget_queue_draw(GTK_WIDGET(g->area));
3222}
3223
3224
3225static void _develop_ui_pipe_finished_callback(gpointer instance, gpointer user_data)
3226{
3227 dt_iop_module_t *self = (dt_iop_module_t *)user_data;
3229 if(IS_NULL_PTR(g)) return;
3230 _switch_cursors(self);
3231}
3232
3233
3234void gui_reset(struct dt_iop_module_t *self)
3235{
3237 if(IS_NULL_PTR(g)) return;
3239 dt_bauhaus_widget_set_quad_active(g->exposure_boost, FALSE);
3240 dt_bauhaus_widget_set_quad_active(g->contrast_boost, FALSE);
3241 dt_dev_add_history_item(self->dev, self, TRUE, TRUE);
3242
3243 // Redraw graph
3244 gtk_widget_queue_draw(self->gui->widget);
3245}
3246
3247static gboolean _sample_picker_luminance_mask(const dt_develop_t *const dev, const float *const buffer,
3248 const size_t width, const size_t height,
3249 float *const picked, float *const picked_min, float *const picked_max)
3250{
3251 const dt_colorpicker_sample_t *const sample = dev ? dev->color_picker.primary_sample : NULL;
3252 if(IS_NULL_PTR(buffer) || IS_NULL_PTR(sample) || width < 1 || height < 1 || IS_NULL_PTR(picked) || IS_NULL_PTR(picked_min) || IS_NULL_PTR(picked_max)) return FALSE;
3253
3254 if(sample->size == DT_LIB_COLORPICKER_SIZE_BOX)
3255 {
3256 const size_t box[4] = {
3257 CLAMP((size_t)roundf(sample->box[0] * width), 0, width),
3258 CLAMP((size_t)roundf(sample->box[1] * height), 0, height),
3259 CLAMP((size_t)roundf(sample->box[2] * width), 0, width),
3260 CLAMP((size_t)roundf(sample->box[3] * height), 0, height)
3261 };
3262 const size_t x0 = MIN(box[0], width - 1);
3263 const size_t y0 = MIN(box[1], height - 1);
3264 const size_t x1 = CLAMP(MAX(box[2], x0 + 1), 1, width);
3265 const size_t y1 = CLAMP(MAX(box[3], y0 + 1), 1, height);
3266
3267 float mean = 0.0f;
3268 float minimum = INFINITY;
3269 float maximum = -INFINITY;
3270 size_t count = 0;
3271
3272 // Browse the exact picker box on the preview luminance mask so picker feedback
3273 // reflects the same scalar field tone equalizer actually edits.
3274 for(size_t y = y0; y < y1; ++y)
3275 {
3276 const size_t row = y * width;
3277 for(size_t x = x0; x < x1; ++x)
3278 {
3279 const float value = buffer[row + x];
3280 mean += value;
3281 minimum = fminf(minimum, value);
3282 maximum = fmaxf(maximum, value);
3283 ++count;
3284 }
3285 }
3286
3287 if(count == 0) return FALSE;
3288 *picked = mean / (float)count;
3289 *picked_min = minimum;
3290 *picked_max = maximum;
3291 return isfinite(*picked) && isfinite(*picked_min) && isfinite(*picked_max);
3292 }
3293
3294 const size_t x = CLAMP((size_t)roundf(sample->point[0] * width), 0, width - 1);
3295 const size_t y = CLAMP((size_t)roundf(sample->point[1] * height), 0, height - 1);
3296 const float value = get_luminance_from_buffer(buffer, width, height, x, y);
3297 *picked = value;
3298 *picked_min = value;
3299 *picked_max = value;
3300 return isfinite(value);
3301}
3302
3325{
3328 dt_pixel_cache_entry_t *preview_entry = NULL;
3329 size_t preview_width = 0;
3330 size_t preview_height = 0;
3331
3332 if(IS_NULL_PTR(g) || (picker != g->exposure_boost && picker != g->contrast_boost))
3333 {
3334 dt_print(DT_DEBUG_DEV, "[picker/toneequal] passthrough picker=%p pipe=%p hash=%" PRIu64 "\n",
3335 (void *)picker, (void *)pipe, piece ? piece->global_hash : 0);
3336 _switch_cursors(self);
3337 return;
3338 }
3339
3340 g->area_active_node = -1;
3341
3342 // Picker callbacks can run while the preview pipe publishes a newer luminance
3343 // cacheline, so take the cache ref under the same GUI state lock as other readers.
3345 preview_entry = g->thumb_preview_entry;
3346 preview_width = g->thumb_preview_buf_width;
3347 preview_height = g->thumb_preview_buf_height;
3348 if(!IS_NULL_PTR(preview_entry))
3351
3352 if(IS_NULL_PTR(preview_entry) || preview_width < 1 || preview_height < 1)
3353 {
3354 if(!IS_NULL_PTR(preview_entry))
3356 dt_print(DT_DEBUG_DEV, "[picker/toneequal] no preview mask picker=%p pipe=%p hash=%" PRIu64 "\n",
3357 (void *)picker, (void *)pipe, piece ? piece->global_hash : 0);
3358 _switch_cursors(self);
3359 return;
3360 }
3361
3363 const float *const preview_buf = (const float *const)dt_pixel_cache_entry_get_data(preview_entry);
3364 float picked = NAN;
3365 float picked_min = NAN;
3366 float picked_max = NAN;
3367 const gboolean sampled = _sample_picker_luminance_mask(self->dev, preview_buf, preview_width, preview_height,
3368 &picked, &picked_min, &picked_max);
3371
3372 if(!sampled)
3373 {
3374 dt_print(DT_DEBUG_DEV, "[picker/toneequal] mask sample failed picker=%p pipe=%p hash=%" PRIu64 "\n",
3375 (void *)picker, (void *)pipe, piece ? piece->global_hash : 0);
3376 _switch_cursors(self);
3377 return;
3378 }
3379
3380 g->cursor_valid = isfinite(picked) && picked > 0.0f;
3381 g->cursor_exposure = g->cursor_valid ? log2f(picked) : 0.0f;
3382
3383 if(picker == g->exposure_boost)
3384 {
3385 if(isfinite(picked) && picked > 0.0f)
3386 {
3387 p->exposure_boost = log2f(CONTRAST_FULCRUM / picked);
3389 dt_bauhaus_slider_set(g->exposure_boost, p->exposure_boost);
3392 dt_dev_add_history_item(self->dev, self, TRUE, TRUE);
3394 "[picker/toneequal] exposure picker=%p luminance=%g set=%g pipe=%p hash=%" PRIu64 "\n",
3395 (void *)picker, picked, p->exposure_boost, (void *)pipe, piece ? piece->global_hash : 0);
3396 }
3397 else
3398 {
3400 "[picker/toneequal] exposure picker=%p invalid luminance=%g pipe=%p hash=%" PRIu64 "\n",
3401 (void *)picker, picked, (void *)pipe, piece ? piece->global_hash : 0);
3402 }
3403 }
3404 else
3405 {
3406 const float fd_old = fminf(picked_min, picked_max);
3407 const float ld_old = fmaxf(picked_min, picked_max);
3408
3409 if(isfinite(fd_old) && isfinite(ld_old) && fd_old > 0.0f && ld_old > fd_old)
3410 {
3411 const float s1 = CONTRAST_FULCRUM - exp2f(-7.0f);
3412 const float s2 = exp2f(-1.0f) - CONTRAST_FULCRUM;
3413 const float mix = fd_old * s2 + ld_old * s1;
3414 float contrast = log2f(mix / (CONTRAST_FULCRUM * (ld_old - fd_old)));
3415
3416 // Blur-assisted detail modes need the same positive-contrast correction as
3417 // the legacy auto button because the sampled spread is measured upstream of
3418 // the guided filter blur and would otherwise undershoot in the final mask.
3419 if(p->details == DT_TONEEQ_EIGF && contrast > 0.0f)
3420 {
3421 const float correction = -0.0276f + 0.01823f * p->feathering + (0.7566f - 1.0f) * contrast;
3422 if(p->feathering < 5.0f)
3423 contrast += correction;
3424 else if(p->feathering < 10.0f)
3425 contrast += correction * (2.0f - p->feathering / 5.0f);
3426 }
3427 else if(p->details == DT_TONEEQ_GUIDED && contrast > 0.0f)
3428 {
3429 contrast = 0.0235f + 1.1225f * contrast;
3430 }
3431
3432 p->contrast_boost = contrast;
3434 dt_bauhaus_slider_set(g->contrast_boost, p->contrast_boost);
3437 dt_dev_add_history_item(self->dev, self, TRUE, TRUE);
3439 "[picker/toneequal] contrast picker=%p min=%g max=%g set=%g pipe=%p hash=%" PRIu64 "\n",
3440 (void *)picker, fd_old, ld_old, p->contrast_boost, (void *)pipe,
3441 piece ? piece->global_hash : 0);
3442 }
3443 else
3444 {
3446 "[picker/toneequal] contrast picker=%p invalid min=%g max=%g pipe=%p hash=%" PRIu64 "\n",
3447 (void *)picker, fd_old, ld_old, (void *)pipe, piece ? piece->global_hash : 0);
3448 }
3449 }
3450
3452 gtk_widget_queue_draw(GTK_WIDGET(g->area));
3453 _switch_cursors(self);
3454}
3455
3456
3457void autoset(struct dt_iop_module_t *self, const struct dt_dev_pixelpipe_t *pipe,
3458 const struct dt_dev_pixelpipe_iop_t *piece, const void *i)
3459{
3460 if(piece->dsc_in.channels != 4) return;
3461
3463 const dt_iop_roi_t *const roi_out = &piece->roi_out;
3464 const size_t width = roi_out->width;
3465 const size_t height = roi_out->height;
3466 const size_t num_elem = width * height;
3467 float *luminance = dt_pixelpipe_cache_alloc_align_float(num_elem, pipe);
3468 if(IS_NULL_PTR(luminance)) return;
3469
3470 // Build the same luminance mask scalar field the picker edits, but with neutral
3471 // boost/contrast because autoset can only solve the exposure translation.
3472 luminance_mask((const float *const)i, luminance, width, height, piece->dsc_in.channels, p->method, 1.0f, 0.0f, 1.0f);
3473
3474 float mean = 0.0f;
3475 size_t count = 0;
3476 __OMP_PARALLEL_FOR__(reduction(+:mean, count))
3477 for(size_t k = 0; k < num_elem; ++k)
3478 {
3479 const float value = luminance[k];
3480 if(!isfinite(value) || value <= 0.0f) continue;
3481 mean += value;
3482 count++;
3483 }
3484
3486 if(count == 0) return;
3487
3488 const float picked = mean / (float)count;
3489 p->exposure_boost = log2f(CONTRAST_FULCRUM / picked);
3490}
3491
3492void gui_init(struct dt_iop_module_t *self)
3493{
3495
3496 gui_cache_init(self);
3497
3498 g->notebook = dt_ui_notebook_new();
3499 dt_ui_notebook_set_picker_owner(g->notebook, self);
3500
3501 // Advanced view
3502
3503 self->gui->widget = dt_ui_notebook_page(g->notebook, N_("graph"), NULL);
3504
3505 g->area = GTK_DRAWING_AREA(gtk_drawing_area_new());
3506 gtk_widget_set_hexpand(GTK_WIDGET(g->area), TRUE);
3507 gtk_box_pack_start(GTK_BOX(self->gui->widget),
3508 dt_ui_resizable_drawing_area(GTK_WIDGET(g->area),
3509 "plugins/darkroom/toneequal/graphheight", 280, 120),
3510 FALSE, FALSE, 0);
3511 gtk_widget_add_events(GTK_WIDGET(g->area), GDK_POINTER_MOTION_MASK | dt_widget_scroll_mask()
3512 | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK
3513 | GDK_ENTER_NOTIFY_MASK | GDK_LEAVE_NOTIFY_MASK);
3514 gtk_widget_set_can_focus(GTK_WIDGET(g->area), TRUE);
3515 g_signal_connect(G_OBJECT(g->area), "draw", G_CALLBACK(area_draw), self);
3516 g_signal_connect(G_OBJECT(g->area), "button-press-event", G_CALLBACK(area_button_press), self);
3517 g_signal_connect(G_OBJECT(g->area), "button-release-event", G_CALLBACK(area_button_release), self);
3518 g_signal_connect(G_OBJECT(g->area), "leave-notify-event", G_CALLBACK(area_leave_notify), self);
3519 g_signal_connect(G_OBJECT(g->area), "enter-notify-event", G_CALLBACK(area_enter_notify), self);
3520 g_signal_connect(G_OBJECT(g->area), "motion-notify-event", G_CALLBACK(area_motion_notify), self);
3521 gtk_widget_set_tooltip_text(GTK_WIDGET(g->area), _("double-click to reset the curve"));
3522
3523 g->smoothing = dt_bauhaus_slider_new_with_range(dt_bauhaus_get_global(), DT_GUI_MODULE(self), -2.33f, +1.67f, 0, 0.0f, 2);
3524 dt_bauhaus_slider_set_soft_range(g->smoothing, -1.0f, 1.0f);
3525 dt_bauhaus_widget_set_label(g->smoothing, N_("curve smoothing"));
3526 gtk_widget_set_tooltip_text(g->smoothing, _("positive values will produce more progressive tone transitions\n"
3527 "but the curve might become oscillatory in some settings.\n"
3528 "negative values will avoid oscillations and behave more robustly\n"
3529 "but may produce brutal tone transitions and damage local contrast."));
3530 gtk_box_pack_start(GTK_BOX(self->gui->widget), g->smoothing, FALSE, FALSE, 0);
3531 g_signal_connect(G_OBJECT(g->smoothing), "value-changed", G_CALLBACK(smoothing_callback), self);
3532
3533 g->exposure_boost = dt_color_picker_new(self, DT_COLOR_PICKER_AREA,
3534 dt_bauhaus_slider_from_params(self, "exposure_boost"));
3535 dt_bauhaus_slider_set_soft_range(g->exposure_boost, -4.0, 4.0);
3536 dt_bauhaus_slider_set_format(g->exposure_boost, _(" EV"));
3537 gtk_widget_set_tooltip_text(g->exposure_boost, _("use this to slide the mask average exposure along channels\n"
3538 "for a better control of the exposure correction with the available nodes.\n"
3539 "the color picker will map the sampled tone to -4 EV."));
3540
3541 g->contrast_boost = dt_color_picker_new(self, DT_COLOR_PICKER_AREA,
3542 dt_bauhaus_slider_from_params(self, "contrast_boost"));
3543 dt_bauhaus_slider_set_soft_range(g->contrast_boost, -2.0, 2.0);
3544 dt_bauhaus_slider_set_format(g->contrast_boost, _(" EV"));
3545 gtk_widget_set_tooltip_text(g->contrast_boost, _("use this to counter the averaging effect of the guided filter\n"
3546 "and dilate the mask contrast around -4EV\n"
3547 "this allows to spread the exposure histogram over more channels\n"
3548 "for a better control of the exposure correction.\n"
3549 "the color picker will fit the sampled spread inside the control range."));
3550
3551 // Simple view
3552
3553 self->gui->widget = dt_ui_notebook_page(g->notebook, N_("sliders"), NULL);
3554
3555 g->noise = dt_bauhaus_slider_from_params(self, "noise");
3556 dt_bauhaus_slider_set_format(g->noise, _(" EV"));
3557
3558 g->ultra_deep_blacks = dt_bauhaus_slider_from_params(self, "ultra_deep_blacks");
3559 dt_bauhaus_slider_set_format(g->ultra_deep_blacks, _(" EV"));
3560
3561 g->deep_blacks = dt_bauhaus_slider_from_params(self, "deep_blacks");
3562 dt_bauhaus_slider_set_format(g->deep_blacks, _(" EV"));
3563
3564 g->blacks = dt_bauhaus_slider_from_params(self, "blacks");
3565 dt_bauhaus_slider_set_format(g->blacks, _(" EV"));
3566
3567 g->shadows = dt_bauhaus_slider_from_params(self, "shadows");
3568 dt_bauhaus_slider_set_format(g->shadows, _(" EV"));
3569
3570 g->midtones = dt_bauhaus_slider_from_params(self, "midtones");
3571 dt_bauhaus_slider_set_format(g->midtones, _(" EV"));
3572
3573 g->highlights = dt_bauhaus_slider_from_params(self, "highlights");
3574 dt_bauhaus_slider_set_format(g->highlights, _(" EV"));
3575
3576 g->whites = dt_bauhaus_slider_from_params(self, "whites");
3577 dt_bauhaus_slider_set_format(g->whites, _(" EV"));
3578
3579 g->speculars = dt_bauhaus_slider_from_params(self, "speculars");
3580 dt_bauhaus_slider_set_format(g->speculars, _(" EV"));
3581
3582 dt_bauhaus_widget_set_label(g->noise, N_("-8 EV"));
3583 dt_bauhaus_widget_set_label(g->ultra_deep_blacks, N_("-7 EV"));
3584 dt_bauhaus_widget_set_label(g->deep_blacks, N_("-6 EV"));
3585 dt_bauhaus_widget_set_label(g->blacks, N_("-5 EV"));
3586 dt_bauhaus_widget_set_label(g->shadows, N_("-4 EV"));
3587 dt_bauhaus_widget_set_label(g->midtones, N_("-3 EV"));
3588 dt_bauhaus_widget_set_label(g->highlights, N_("-2 EV"));
3589 dt_bauhaus_widget_set_label(g->whites, N_("-1 EV"));
3590 dt_bauhaus_widget_set_label(g->speculars, N_("+0 EV"));
3591
3592 // Masking options
3593
3594 self->gui->widget = dt_ui_notebook_page(g->notebook, N_("masking"), NULL);
3595
3596 g->method = dt_bauhaus_combobox_from_params(self, "method");
3598 gtk_widget_set_tooltip_text(g->method, _("preview the mask and chose the estimator that gives you the\n"
3599 "higher contrast between areas to dodge and areas to burn"));
3600
3601 g->details = dt_bauhaus_combobox_from_params(self, N_("details"));
3602 dt_bauhaus_widget_set_label(g->details, N_("preserve details"));
3603 gtk_widget_set_tooltip_text(g->details, _("'no' affects global and local contrast (safe if you only add contrast)\n"
3604 "'guided filter' only affects global contrast and tries to preserve local contrast\n"
3605 "'averaged guided filter' is a geometric mean of 'no' and 'guided filter' methods\n"
3606 "'eigf' (exposure-independent guided filter) is a guided filter that is exposure-independent, it smooths shadows and highlights the same way (contrary to guided filter which smooths less the highlights)\n"
3607 "'averaged eigf' is a geometric mean of 'no' and 'exposure-independent guided filter' methods"));
3608
3609 g->iterations = dt_bauhaus_slider_from_params(self, "iterations");
3610 dt_bauhaus_slider_set_soft_max(g->iterations, 5);
3611 gtk_widget_set_tooltip_text(g->iterations, _("number of passes of guided filter to apply\n"
3612 "helps diffusing the edges of the filter at the expense of speed"));
3613
3614 g->blending = dt_bauhaus_slider_from_params(self, "blending");
3615 dt_bauhaus_slider_set_soft_range(g->blending, 1.0, 45.0);
3616 dt_bauhaus_slider_set_format(g->blending, "%");
3617 gtk_widget_set_tooltip_text(g->blending, _("diameter of the blur in percent of the largest image size\n"
3618 "warning: big values of this parameter can make the darkroom\n"
3619 "preview much slower if denoise profiled is used."));
3620
3621 g->feathering = dt_bauhaus_slider_from_params(self, "feathering");
3622 dt_bauhaus_slider_set_soft_range(g->feathering, 0.1, 50.0);
3623 gtk_widget_set_tooltip_text(g->feathering, _("precision of the feathering:\n"
3624 "higher values force the mask to follow edges more closely\n"
3625 "but may void the effect of the smoothing\n"
3626 "lower values give smoother gradients and better smoothing\n"
3627 "but may lead to inaccurate edges taping and halos"));
3628
3629 g->quantization = dt_bauhaus_slider_from_params(self, "quantization");
3630 dt_bauhaus_slider_set_format(g->quantization, _(" EV"));
3631 gtk_widget_set_tooltip_text(g->quantization, _("0 disables the quantization.\n"
3632 "higher values posterize the luminance mask to help the guiding\n"
3633 "produce piece-wise smooth areas when using high feathering values"));
3634
3635 // start building top level widget
3636 self->gui->widget = gtk_box_new(GTK_ORIENTATION_VERTICAL, DT_GUI_BOX_SPACING);
3637
3638 const int active_page = dt_conf_get_int("plugins/darkroom/toneequal/gui_page");
3639 gtk_widget_show(gtk_notebook_get_nth_page(g->notebook, active_page));
3640 gtk_notebook_set_current_page(g->notebook, active_page);
3641
3642 g_signal_connect(G_OBJECT(g->notebook), "button-press-event", G_CALLBACK(notebook_button_press), self);
3643 gtk_box_pack_start(GTK_BOX(self->gui->widget), GTK_WIDGET(g->notebook), FALSE, FALSE, 0);
3644
3645 GtkWidget *hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, DT_GUI_BOX_SPACING);
3646 gtk_box_pack_start(GTK_BOX(hbox), dt_ui_label_new(_("display exposure mask")), TRUE, TRUE, 0);
3647 g->show_luminance_mask = dt_iop_togglebutton_new(self, NULL, N_("display exposure mask"), NULL, G_CALLBACK(show_luminance_mask_callback),
3648 FALSE, 0, 0, dtgtk_cairo_paint_showmask, hbox);
3649
3651 dt_gui_add_class(g->show_luminance_mask, "dt_bauhaus_alignment");
3652 gtk_box_pack_start(GTK_BOX(self->gui->widget), hbox, FALSE, FALSE, 0);
3653
3654 // Force UI redraws when pipe starts/finishes computing and switch cursors
3656 G_CALLBACK(_develop_history_resync_callback), self);
3658 G_CALLBACK(_develop_cacheline_ready_callback), self);
3660 G_CALLBACK(_develop_ui_pipe_finished_callback), self);
3661
3663 G_CALLBACK(_develop_ui_pipe_started_callback), self);
3664}
3665
3666
3668{
3671
3672 dt_conf_set_int("plugins/darkroom/toneequal/gui_page", gtk_notebook_get_current_page (g->notebook));
3673
3678
3679 dt_pixel_cache_entry_t *preview_entry = NULL;
3681 preview_entry = g->thumb_preview_entry;
3682 g->thumb_preview_entry = NULL;
3683 g->thumb_preview_hash = DT_PIXELPIPE_CACHE_HASH_INVALID;
3684 g->pending_preview_hash = DT_PIXELPIPE_CACHE_HASH_INVALID;
3685 g->thumb_preview_buf_width = 0;
3686 g->thumb_preview_buf_height = 0;
3687 g->luminance_valid = FALSE;
3689 if(!IS_NULL_PTR(preview_entry))
3691 if(g->desc) pango_font_description_free(g->desc);
3692 if(g->layout) g_object_unref(g->layout);
3693 if(g->cr) cairo_destroy(g->cr);
3694 if(g->cst) cairo_surface_destroy(g->cst);
3695
3697}
3698
3699// clang-format off
3700// modelines: These editor modelines have been set for all relevant files by tools/update_modelines.py
3701// vim: shiftwidth=2 expandtab tabstop=2 cindent
3702// kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified;
3703// clang-format on
Handle default and user-set shortcuts (accelerators)
#define DT_PRIMARY_MASK
void dt_gui_set_pango_resolution(PangoLayout *layout)
#define TRUE
Definition ashift_lsd.c:162
#define FALSE
Definition ashift_lsd.c:158
int position()
void dt_bauhaus_slider_set_soft_range(GtkWidget *widget, float soft_min, float soft_max)
Definition bauhaus.c:1498
float dt_bauhaus_slider_get(GtkWidget *widget)
Definition bauhaus.c:3280
void dt_bauhaus_slider_set_soft_max(GtkWidget *widget, float val)
Definition bauhaus.c:1474
void dt_bauhaus_widget_set_quad_active(GtkWidget *widget, int active)
Definition bauhaus.c:1578
void dt_bauhaus_slider_set(GtkWidget *widget, float pos)
Definition bauhaus.c:3331
void dt_bauhaus_widget_set_label(GtkWidget *widget, const char *label)
Definition bauhaus.c:1504
GtkWidget * dt_bauhaus_slider_new_with_range(dt_bauhaus_t *bh, dt_gui_module_t *self, float min, float max, float step, float defval, int digits)
Definition bauhaus.c:1632
void dt_bauhaus_combobox_remove_at(GtkWidget *widget, int pos)
Definition bauhaus.c:1909
void dt_bauhaus_slider_set_format(GtkWidget *widget, const char *format)
Definition bauhaus.c:3407
#define INNER_PADDING
Definition bauhaus.h:81
typedef void((*dt_cache_allocate_t)(void *userdata, dt_cache_entry_t *entry))
static int pseudo_solve(float *const restrict A, float *const restrict y, const size_t m, const size_t n, const int checks)
Definition choleski.h:396
static const float scaling
return vector dt_simd_set1(valid ?(scaling+NORM_MIN) :NORM_MIN)
dt_collection_t * dt_collection_get_global(void)
Definition collection.c:138
void dt_collection_hint_message(const dt_collection_t *collection)
Definition collection.c:912
@ IOP_CS_RGB
void dt_iop_color_picker_reset(dt_iop_module_t *module, gboolean keep)
GtkWidget * dt_color_picker_new(dt_iop_module_t *module, dt_iop_color_picker_kind_t kind, GtkWidget *w)
gboolean dt_iop_color_picker_is_visible(const dt_develop_t *dev)
@ DT_COLOR_PICKER_AREA
@ DT_LIB_COLORPICKER_SIZE_BOX
Definition colorpicker.h:39
static const float x
const float *const lut
#define A(y, x)
struct _GtkWidget GtkWidget
GtkWidget, opaque, spelled exactly as GTK spells it.
Definition colorspaces.h:98
const dt_colormatrix_t dt_aligned_pixel_t out
static const int row
const float delta
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_control_log(const char *msg,...)
Definition control.c:824
void dt_control_queue_redraw_center()
Request a redraw of the centre view.
Definition control.c:924
void dt_control_hinter_message(const struct dt_control_t *s, const char *message)
Definition control.c:981
void dt_control_queue_cursor_by_name(const char *curs_str)
Queue a GTK named cursor for the next cursor commit.
Definition control.c:407
#define dt_control_set_cursor_visible(visible)
Definition control.h:153
struct dt_control_t * dt_control_get_global(void)
Definition darktable.c:651
struct dt_bauhaus_t * dt_bauhaus_get_global(void)
Definition darktable.c:646
#define dt_dev_add_history_item(dev, module, enable, redraw)
void dt_iop_params_t
Definition dev_history.h:43
#define dt_dev_pixelpipe_update_history_main(dev)
#define dt_dev_pixelpipe_update_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)
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_dev_pixelpipe_iop_t * dt_dev_distort_get_iop_pipe(struct dt_dev_pixelpipe_t *pipe, struct dt_iop_module_t *module)
Definition develop.c:1833
float dt_dev_get_overlay_scale(dt_develop_t *dev)
Get the overlay scale factor in GUI logical coordinates.
Definition develop.c:1959
gboolean dt_dev_rescale_roi(dt_develop_t *dev, cairo_t *cr, int32_t width, int32_t height)
Scale the ROI to fit within given width/height, centered.
Definition develop.c:2071
gboolean dt_dev_pixelpipe_has_preview_output(const dt_develop_t *dev, const dt_dev_pixelpipe_t *pipe, const dt_iop_roi_t *roi)
Definition develop.c:402
void dt_dev_coordinates_widget_to_image_norm(dt_develop_t *dev, float *points, size_t num_points)
Coordinate conversion helpers between widget, normalized image, and absolute image spaces.
Definition develop.c:1144
@ DT_DEV_PIXELPIPE_DISPLAY_MASK
Definition develop.h:123
@ DT_DEV_PIXELPIPE_DISPLAY_PASSTHRU
Definition develop.h:141
@ DT_DEV_PIXELPIPE_DISPLAY_NONE
Definition develop.h:122
gboolean enabled
–doc was passed on the command line
static void set_color(cairo_t *cr, GdkRGBA color)
Definition draw.h:125
static void dt_draw_grid(cairo_t *cr, const int num, const int left, const int top, const int right, const int bottom)
Definition draw.h:158
static void dt_cairo_perceptual_gradient(cairo_pattern_t *grad, double alpha)
Definition draw.h:506
static __DT_CLONE_TARGETS__ int fast_eigf_surface_blur(float *const restrict image, const size_t width, const size_t height, const float sigma, float feathering, const int iterations, const dt_iop_guided_filter_blending_t filter, const float scale, const float quantization, const float quantize_min, const float quantize_max)
Definition eigf.h:262
static __DT_CLONE_TARGETS__ int fast_surface_blur(float *const restrict image, const size_t width, const size_t height, const int radius, float feathering, const int iterations, const dt_iop_guided_filter_blending_t filter, const float scale, const float quantization, const float quantize_min, const float quantize_max)
@ DT_GF_BLENDING_LINEAR
@ DT_GF_BLENDING_GEOMEAN
static float fast_clamp(const float value, const float bottom, const float top)
@ TYPE_FLOAT
Definition format.h:56
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)
static uint64_t dt_hash(uint64_t hash, const char *str, size_t size)
Definition hash.h:55
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
const char ** dt_iop_set_description(dt_iop_module_t *module, const char *main_text, const char *purpose, const char *input, const char *process, const char *output)
Definition imageop.c:1893
void dt_iop_set_cache_bypass(dt_iop_module_t *module, gboolean state)
Definition imageop.c:1669
@ DT_REQUEST_COLORPICK_OFF
Definition imageop.h:215
void dt_iop_gui_leave_critical_section(dt_iop_module_t *const module)
Release what dt_iop_gui_enter_critical_section() took. Also a no-op headless.
void dt_iop_request_focus(dt_iop_module_t *module)
Move darkroom focus to module, or clear it with NULL.
@ IOP_FLAGS_INCLUDE_IN_STYLES
Definition imageop.h:185
@ IOP_FLAGS_SUPPORTS_BLENDING
Definition imageop.h:186
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_TONES
Definition imageop.h:156
GtkWidget * dt_iop_togglebutton_new(dt_iop_module_t *self, const char *section, const gchar *label, const gchar *ctrl_label, GCallback callback, gboolean local, guint accel_key, GdkModifierType mods, DTGTKCairoPaintIconFunc paint, GtkWidget *box)
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
int dt_ioppr_get_iop_order(GList *iop_order_list, const char *op_name, const int multi_priority)
Return the iop_order for a given operation/instance pair.
Definition iop_order.c:913
GtkWidget * dt_ui_label_new(const gchar *str)
Definition label.c:125
static float mix(const float a, const float b, const float t)
Definition liquify.c:751
@ 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 const size_t const float exposure_boost
dt_iop_luminance_mask_method_t
@ DT_TONEEQ_NORM_2
@ DT_TONEEQ_LAST
@ DT_TONEEQ_NORM_POWER
float *const restrict luminance
float *const restrict const size_t const size_t const float const float const float contrast_boost
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
static const int max_size
Definition map.c:132
dt_masks_form_t * dt_masks_get_visible_form(const dt_develop_t *dev)
Return the currently visible form used by the masks GUI.
Definition masks_gui.c:1408
The interactive half of the masks subsystem: the editing state (dt_masks_form_gui_t),...
#define M_PI
Definition math.h:47
float DT_ALIGNED_ARRAY dt_colormatrix_t[4][4]
Definition matrices.h:34
#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 void * dt_check_sse_aligned(void *pointer)
Runtime-checked counterpart to DT_IS_ALIGNED().
Definition mem_alloc.h:251
#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
#define __OMP_SIMD__(...)
Definition openmp.h:99
#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 __OMP_FOR_SIMD__(...)
Definition openmp.h:97
#define __OMP_PARALLEL_FOR_SIMD__(...)
Definition openmp.h:96
@ DT_DEV_PIXELPIPE_FULL
Definition pixelpipe.h:43
void dt_dev_pixelpipe_cache_wrlock_entry(gboolean lock, dt_pixel_cache_entry_t *cache_entry)
Lock or release the write lock on the entry.
void * dt_pixel_cache_entry_get_data(dt_pixel_cache_entry_t *entry)
void dt_dev_pixelpipe_cache_ref_count_entry(gboolean lock, dt_pixel_cache_entry_t *cache_entry)
Increase/Decrease the reference count on the cache line as to prevent LRU item removal....
void dt_dev_pixelpipe_cache_rdlock_entry(gboolean lock, dt_pixel_cache_entry_t *cache_entry)
Lock or release the read lock on the entry.
int dt_dev_pixelpipe_cache_get(const uint64_t hash, const size_t size, const char *name, const int id, const gboolean alloc, void **data, dt_pixel_cache_entry_t **entry)
Get a cache line from the cache.
int dt_dev_pixelpipe_cache_remove(const gboolean force, dt_pixel_cache_entry_t *cache_entry)
Arbitrarily remove the cache entry matching hash. Entries having a reference count > 0 (inter-thread ...
size_t dt_pixel_cache_entry_get_size(dt_pixel_cache_entry_t *entry)
Peek the size (in bytes) reserved for the host buffer of a cache entry.
gboolean dt_dev_pixelpipe_cache_ref_entry_by_hash(const uint64_t hash, void **data, dt_pixel_cache_entry_t **entry)
Resolve and retain an existing cache entry by hash.
Pixelpipe cache for storing intermediate results in the pixelpipe.
#define DT_PIXELPIPE_CACHE_HASH_INVALID
#define dt_pixelpipe_cache_free_align(mem)
#define dt_pixelpipe_cache_alloc_align_float(pixels, pipe)
static cairo_surface_t * dt_cairo_image_surface_create(cairo_format_t format, int width, int height)
GtkWidget * dt_ui_resizable_drawing_area(GtkWidget *area, char *config_str, int default_height, int min_height)
Make a self-drawing widget (typically a GtkDrawingArea graph or scope) vertically resizable.
#define DT_DEBUG_CONTROL_SIGNAL_DISCONNECT(ctlsig, cb, user_data)
Definition signal.h:407
struct dt_control_signal_t * dt_control_signal_get_global(void)
Definition darktable.c:616
@ DT_SIGNAL_DEVELOP_HISTORY_CHANGE
This signal is raised when develop history is changed no param, no returned value.
Definition signal.h:207
@ DT_SIGNAL_HISTORY_RESYNC
This signal is raised once darkroom history has been resynchronized into all live pipelines....
Definition signal.h:212
@ DT_SIGNAL_CACHELINE_READY
This signal is raised when one cacheline write lock is released. 1 : uint64_t cacheline hash no retur...
Definition signal.h:188
@ DT_SIGNAL_DEVELOP_UI_PIPE_FINISHED
This signal is raised when pipe is finished and the gui is attached no param, no returned value.
Definition signal.h:182
#define DT_DEBUG_CONTROL_SIGNAL_CONNECT(ctlsig, signal, cb, user_data)
Definition signal.h:396
#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
static const dt_aligned_pixel_simd_t value
Definition simd.h:144
const float uint32_t state[4]
const float sigma
const float noise
unsigned __int64 uint64_t
Definition strptime.c:75
float quad_width
Definition bauhaus.h:273
dt_lib_colorpicker_size_t size
Definition colorpicker.h:64
dt_boundingbox_t box
Definition colorpicker.h:63
dt_iop_buffer_dsc_t dsc_in
struct dt_iop_module_t *void * data
int32_t gui_attached
Definition develop.h:167
GList * iop_order_list
Definition develop.h:275
struct dt_colorpicker_sample_t * primary_sample
Definition develop.h:374
struct dt_develop_t::@13 color_picker
Authoritative darkroom color-picker state.
struct dt_dev_pixelpipe_t * preview_pipe
Definition develop.h:214
struct dt_masks_form_gui_t * form_gui
Definition develop.h:310
struct dt_dev_pixelpipe_t * pipe
Definition develop.h:214
unsigned int channels
Definition format.h:83
dt_iop_buffer_type_t datatype
Definition format.h:85
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
gboolean enabled
Definition imageop.h:313
int request_mask_display
Definition imageop.h:276
dt_iop_params_t * params
Definition imageop.h:333
Region of interest passed through the pixelpipe.
Definition format.h:49
double scale
Definition format.h:51
int width
Definition format.h:50
int height
Definition format.h:50
float factors[8] DT_ALIGNED_ARRAY
Definition toneequal.c:221
dt_iop_toneequalizer_filter_t details
Definition toneequal.c:228
float correction_lut[8 *10000+1] DT_ALIGNED_ARRAY
Definition toneequal.c:222
dt_iop_luminance_mask_method_t method
Definition toneequal.c:227
int histogram[256] DT_ALIGNED_ARRAY
Definition toneequal.c:244
dt_pixel_cache_entry_t * thumb_preview_entry
Definition toneequal.c:283
float interpolation_matrix[9 *8] DT_ALIGNED_ARRAY
Definition toneequal.c:243
float factors[8] DT_ALIGNED_ARRAY
Definition toneequal.c:241
float temp_user_params[9] DT_ALIGNED_ARRAY
Definition toneequal.c:245
float gui_lut[256] DT_ALIGNED_ARRAY
Definition toneequal.c:242
PangoFontDescription * desc
Definition toneequal.c:314
dt_iop_toneequalizer_filter_t details
Definition toneequal.c:213
dt_iop_luminance_mask_method_t method
Definition toneequal.c:214
#define __DT_CLONE_TARGETS__
typedef double((*spd)(unsigned long int wavelength, double TempK))
#define MIN(a, b)
Definition thinplate.c:32
#define MAX(a, b)
Definition thinplate.c:29
void dtgtk_togglebutton_set_paint(GtkDarktableToggleButton *button, DTGTKCairoPaintIconFunc paint, gint paintflags, void *paintdata)
#define DTGTK_TOGGLEBUTTON(obj)
static void get_shade_from_luminance(cairo_t *cr, const float luminance, const float alpha)
Definition toneequal.c:2207
static void draw_exposure_cursor(cairo_t *cr, const double pointerx, const double pointery, const double radius, const float luminance, const float zoom_scale, const int instances, const float alpha)
Definition toneequal.c:2216
static void smoothing_callback(GtkWidget *slider, gpointer user_data)
Definition toneequal.c:1808
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)
Definition toneequal.c:1642
static gboolean area_draw(GtkWidget *widget, cairo_t *cr, gpointer user_data)
Definition toneequal.c:2695
static gboolean area_button_press(GtkWidget *widget, GdkEventButton *event, gpointer user_data)
Definition toneequal.c:2914
static int compute_luminance_mask(const float *const restrict in, float *const restrict luminance, const size_t width, const size_t height, const size_t ch, const dt_iop_toneequalizer_data_t *const d)
Definition toneequal.c:870
const char ** description(struct dt_iop_module_t *self)
Definition toneequal.c:355
int default_group()
Definition toneequal.c:364
void cairo_draw_hatches(cairo_t *cr, double center[2], double span[2], int instances, double line_width, double shade)
Definition toneequal.c:2184
static gboolean area_leave_notify(GtkWidget *widget, GdkEventCrossing *event, gpointer user_data)
Definition toneequal.c:2887
static float pixel_correction(const float exposure, const float *const restrict factors, const float sigma)
Definition toneequal.c:853
int scrolled(struct dt_iop_module_t *self, double x, double y, int up, uint32_t state)
Definition toneequal.c:2081
#define CHANNELS
Definition toneequal.c:168
static __DT_CLONE_TARGETS__ void compute_log_histogram_and_stats(const float *const restrict luminance, int histogram[256], const size_t num_elem, int *max_histogram, float *first_decile, float *last_decile)
Definition toneequal.c:1416
static void get_channels_gains(float factors[9], const dt_iop_toneequalizer_params_t *p)
Definition toneequal.c:1254
static void _develop_history_resync_callback(gpointer instance, gpointer user_data)
Definition toneequal.c:3078
static void match_color_to_background(cairo_t *cr, const float exposure, const float alpha)
Definition toneequal.c:2240
#define TEMP_SAMPLES
static void build_interpolation_matrix(float A[9 *8], const float sigma)
Definition toneequal.c:1401
#define PIXEL_CHAN
Definition toneequal.c:169
static gboolean in_mask_editing(dt_iop_module_t *self)
Definition toneequal.c:593
void show_guiding_controls(struct dt_iop_module_t *self)
Definition toneequal.c:1714
const char * aliases()
Definition toneequal.c:349
static void init_nodes_y(dt_iop_toneequalizer_gui_data_t *g)
Definition toneequal.c:2682
static gboolean update_curve_lut(struct dt_iop_module_t *self)
Definition toneequal.c:1576
static void _switch_cursors(struct dt_iop_module_t *self)
Definition toneequal.c:1864
static float get_luminance_at_norm(const float *const buffer, const size_t width, const size_t height, const float norm_x, const float norm_y)
Sample the luminance mask at a NORMALIZED image position.
Definition toneequal.c:721
void gui_focus(struct dt_iop_module_t *self, gboolean in)
Definition toneequal.c:2440
void init_pipe(struct dt_iop_module_t *self, dt_dev_pixelpipe_t *pipe, dt_dev_pixelpipe_iop_t *piece)
Definition toneequal.c:1701
static gboolean luminance_entry_fits(dt_pixel_cache_entry_t *entry, const size_t width, const size_t height)
Tell whether a cache entry can hold a width x height luminance mask.
Definition toneequal.c:745
static const float centers_ops[8] DT_ALIGNED_ARRAY
Definition toneequal.c:173
static __DT_CLONE_TARGETS__ void compute_lut_correction(struct dt_iop_toneequalizer_gui_data_t *g, const float offset, const float scaling)
Definition toneequal.c:1554
static int set_new_params_interactive(const float control_exposure, const float exposure_offset, const float blending_sigma, dt_iop_toneequalizer_gui_data_t *g, dt_iop_toneequalizer_params_t *p)
Definition toneequal.c:2028
const char * name()
Definition toneequal.c:344
void gui_reset(struct dt_iop_module_t *self)
Definition toneequal.c:3234
#define CONTRAST_FULCRUM
Definition toneequal.c:160
void gui_update(struct dt_iop_module_t *self)
Refresh GUI controls from current params and configuration.
Definition toneequal.c:1772
static void compress_shadows_highlight_preset_set_exposure_params(dt_iop_toneequalizer_params_t *p, const float step)
Definition toneequal.c:435
void gui_init(struct dt_iop_module_t *self)
Definition toneequal.c:3492
#define UI_SAMPLES
Definition toneequal.c:159
static void dilate_shadows_highlight_preset_set_exposure_params(dt_iop_toneequalizer_params_t *p, const float step)
Definition toneequal.c:452
void gui_changed(dt_iop_module_t *self, GtkWidget *w, void *previous)
Definition toneequal.c:1785
static void init_nodes_x(dt_iop_toneequalizer_gui_data_t *g)
Definition toneequal.c:2668
static int compute_channels_gains(const float in[9], float out[9])
Definition toneequal.c:1307
static void compute_correction_lut(float *restrict lut, const float sigma, const float *const restrict factors)
Definition toneequal.c:1236
static const dt_colormatrix_t gauss_kernel
Definition toneequal.c:661
void cleanup_global(dt_iop_module_so_t *module)
Definition toneequal.c:1636
static void gui_cache_init(struct dt_iop_module_t *self)
Definition toneequal.c:1342
static void update_histogram(struct dt_iop_module_t *const self)
Definition toneequal.c:1486
static uint64_t _current_preview_luminance_hash(dt_iop_module_t *self, size_t *width, size_t *height)
Definition toneequal.c:1383
static void _develop_ui_pipe_started_callback(gpointer instance, gpointer user_data)
Definition toneequal.c:3059
static gboolean area_enter_notify(GtkWidget *widget, GdkEventCrossing *event, gpointer user_data)
Definition toneequal.c:2869
void update_exposure_sliders(dt_iop_toneequalizer_gui_data_t *g, dt_iop_toneequalizer_params_t *p)
Definition toneequal.c:1756
static gboolean area_button_release(GtkWidget *widget, GdkEventButton *event, gpointer user_data)
Definition toneequal.c:3012
int default_colorspace(dt_iop_module_t *self, dt_dev_pixelpipe_t *pipe, const dt_dev_pixelpipe_iop_t *piece)
Definition toneequal.c:374
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)
Definition toneequal.c:379
static gboolean area_motion_notify(GtkWidget *widget, GdkEventMotion *event, gpointer user_data)
Definition toneequal.c:2968
int flags()
Definition toneequal.c:369
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)
Definition toneequal.c:2255
static void _develop_cacheline_ready_callback(gpointer instance, const guint64 hash, const guint64 producer_node_key, gpointer user_data)
Definition toneequal.c:3165
void gui_cleanup(struct dt_iop_module_t *self)
Definition toneequal.c:3667
dt_iop_toneequalizer_filter_t
Definition toneequal.c:187
@ DT_TONEEQ_AVG_GUIDED
Definition toneequal.c:189
@ DT_TONEEQ_NONE
Definition toneequal.c:188
@ DT_TONEEQ_EIGF
Definition toneequal.c:192
@ DT_TONEEQ_GUIDED
Definition toneequal.c:190
@ DT_TONEEQ_AVG_EIGF
Definition toneequal.c:191
void init_presets(dt_iop_module_so_t *self)
Definition toneequal.c:467
int mouse_leave(struct dt_iop_module_t *self)
Definition toneequal.c:2010
static gboolean notebook_button_press(GtkWidget *widget, GdkEventButton *event, gpointer user_data)
Definition toneequal.c:3040
static void get_channels_factors(float factors[9], const dt_iop_toneequalizer_params_t *p)
Definition toneequal.c:1271
static void _develop_ui_pipe_finished_callback(gpointer instance, gpointer user_data)
Definition toneequal.c:3225
static __DT_CLONE_TARGETS__ void display_luminance_mask(const float *const restrict in, const float *const restrict luminance, float *const restrict out, const dt_iop_roi_t *const roi_in, const dt_iop_roi_t *const roi_out, const dt_dev_pixelpipe_t *pipe, const size_t ch)
Definition toneequal.c:945
static gboolean _init_drawing(dt_iop_module_t *const restrict self, GtkWidget *widget, dt_iop_toneequalizer_gui_data_t *const restrict g)
Definition toneequal.c:2542
void cleanup_pipe(struct dt_iop_module_t *self, dt_dev_pixelpipe_t *pipe, dt_dev_pixelpipe_iop_t *piece)
Definition toneequal.c:1708
static float get_luminance_from_buffer(const float *const buffer, const size_t width, const size_t height, const size_t x, const size_t y)
Definition toneequal.c:666
void init_global(dt_iop_module_so_t *module)
Definition toneequal.c:1627
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)
Definition toneequal.c:3457
static int commit_channels_gains(const float factors[9], dt_iop_toneequalizer_params_t *p)
Definition toneequal.c:1321
int mouse_moved(struct dt_iop_module_t *self, double x, double y, double pressure, int which)
Definition toneequal.c:1921
static int compute_channels_factors(const float factors[8], float out[9], const float sigma)
Definition toneequal.c:1285
void color_picker_apply(dt_iop_module_t *self, GtkWidget *picker, dt_dev_pixelpipe_t *pipe, dt_dev_pixelpipe_iop_t *piece)
Update tone equalizer sliders from one picker sample.
Definition toneequal.c:3323
static gboolean _sample_picker_luminance_mask(const dt_develop_t *const dev, const float *const buffer, const size_t width, const size_t height, float *const picked, float *const picked_min, float *const picked_max)
Definition toneequal.c:3247
static void show_luminance_mask_callback(GtkWidget *togglebutton, GdkEventButton *event, dt_iop_module_t *self)
Definition toneequal.c:1833
void modify_roi_in(struct dt_iop_module_t *self, const struct dt_dev_pixelpipe_t *pipe, struct dt_dev_pixelpipe_iop_t *piece, const dt_iop_roi_t *roi_out, dt_iop_roi_t *roi_in)
Definition toneequal.c:1179
int legacy_params(dt_iop_module_t *self, const void *const old_params, const int old_version, void *new_params, const int new_version)
Definition toneequal.c:387
#define LUT_RESOLUTION
Definition toneequal.c:170
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)
Definition toneequal.c:1170
static void invalidate_luminance_cache(dt_iop_module_t *const self)
Definition toneequal.c:599
static __DT_CLONE_TARGETS__ void apply_toneequalizer(const float *const restrict in, const float *const restrict luminance, float *const restrict out, const dt_iop_roi_t *const roi_in, const dt_iop_roi_t *const roi_out, const size_t ch, const dt_iop_toneequalizer_data_t *const d)
Definition toneequal.c:818
GdkEventMask dt_widget_scroll_mask(void)
gboolean dt_gui_widgets_suppressed(void)
static gboolean dt_modifier_is(GdkModifierType state, const GdkModifierType desired_modifier_mask)
#define dt_gui_freeze_begin()
#define dt_gui_freeze_end()
#define DT_GUI_BOX_SPACING
#define DT_PIXEL_APPLY_DPI(value)
void dt_gui_add_class(GtkWidget *widget, const gchar *class_name)
void dtgtk_cairo_paint_gamut_check(cairo_t *cr, gint x, gint y, gint w, gint h, gint flags, void *data)
void dtgtk_cairo_paint_showmask(cairo_t *cr, gint x, gint y, gint w, gint h, gint flags, void *data)