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
378
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,
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 // Created without a buffer: flagged while still held, so the release removes it rather
1078 // than leave a line a later get() would allocate and hand back unwritten.
1079 if(created_luminance_entry)
1080 {
1083 }
1085 }
1086 return 1;
1087 }
1088
1089 if(created_luminance_entry)
1090 {
1092 {
1093 // Flagged while still held and before the write lock goes, so the release removes it.
1097 return 1;
1098 }
1099
1101 }
1102 }
1103 else
1104 {
1105 // Export/thumbnail pipes don't need persistent GUI sampling, so a local temp buffer is enough.
1107 if(IS_NULL_PTR(luminance)) return 1;
1108
1110 {
1112 return 1;
1113 }
1114 }
1115
1116 // Display output
1117 if(self->dev->gui_attached && pipe->type == DT_DEV_PIXELPIPE_FULL)
1118 {
1119 if(!IS_NULL_PTR(g) && g->mask_display)
1120 {
1121 display_luminance_mask(in, luminance, out, roi_in, roi_out, pipe, ch);
1123 ((dt_dev_pixelpipe_t *)pipe)->bypass_blendif = 1;
1124 }
1125 else
1126 apply_toneequalizer(in, luminance, out, roi_in, roi_out, ch, d);
1127 }
1128 else
1129 {
1130 apply_toneequalizer(in, luminance, out, roi_in, roi_out, ch, d);
1131 }
1132
1133 if(preview_output && self->dev->gui_attached && !IS_NULL_PTR(g)
1134 && luminance_entry_fits(luminance_entry, width, height))
1135 {
1136 dt_pixel_cache_entry_t *old_entry = NULL;
1137 gboolean keep_process_ref = FALSE;
1138
1139 // Transfer one cache ref from this process run to the GUI if this run produced
1140 // the preview-sized image darkroom is sampling from. Keeping this explicit avoids
1141 // guessing whether FULL/PREVIEW pipe type owns the GUI state when both pipes can
1142 // share the same output size.
1144 if(g->thumb_preview_entry != luminance_entry || g->thumb_preview_hash != luminance_hash)
1145 {
1146 old_entry = g->thumb_preview_entry;
1147 g->thumb_preview_entry = luminance_entry;
1148 g->thumb_preview_hash = luminance_hash;
1149 g->pending_preview_hash = DT_PIXELPIPE_CACHE_HASH_INVALID;
1150 g->thumb_preview_buf_width = width;
1151 g->thumb_preview_buf_height = height;
1152 g->luminance_valid = TRUE;
1153 g->histogram_valid = FALSE;
1154 keep_process_ref = TRUE;
1155 }
1157
1158 if(old_entry)
1160
1161 if(!keep_process_ref)
1163 }
1164 else if(luminance_entry)
1165 {
1167 }
1168 else
1169 {
1171 }
1172
1173 return 0;
1174}
1175
1177 const void *const restrict ivoid, void *const restrict ovoid)
1178{
1179 const dt_iop_roi_t *const roi_in = &piece->roi_in;
1180 const dt_iop_roi_t *const roi_out = &piece->roi_out;
1181 return toneeq_process(self, pipe, piece, ivoid, ovoid, roi_in, roi_out);
1182}
1183
1184
1185void modify_roi_in(struct dt_iop_module_t *self, const struct dt_dev_pixelpipe_t *pipe,
1187 const dt_iop_roi_t *roi_out, dt_iop_roi_t *roi_in)
1188{
1189 // Pad the zoomed-in view to avoid weird stuff with local averages at the borders of
1190 // the preview
1191
1193
1194 // Get the scaled window radius for the box average
1195 const int max_size = (piece->iwidth > piece->iheight) ? piece->iwidth : piece->iheight;
1196 const float diameter = d->blending * max_size * roi_in->scale;
1197 const int radius = (int)((diameter - 1.0f) / ( 2.0f));
1198 d->radius = radius;
1199
1200 /*
1201 // Enlarge the preview roi with padding if needed
1202 if(self->dev->gui_attached && sanity_check(self))
1203 {
1204 int roiy = fmaxf(roi_in->y - radius, 0.0f);
1205 int roix = fmaxf(roi_in->x - radius, 0.0f);
1206 int roir = fminf(roix + roi_in->width + 2 * radius, piece->buf_in.width * roi_in->scale);
1207 int roib = fminf(roiy + roi_in->height + 2 * radius, piece->buf_in.height * roi_in->scale);
1208
1209 // Set the values and check
1210 roi_in->x = roix;
1211 roi_in->y = roiy;
1212 roi_in->width = roir - roi_in->x;
1213 roi_in->height = roib - roi_in->y;
1214 }
1215 */
1216}
1217
1218
1219/***
1220 * Setters and Getters for parameters
1221 *
1222 * Remember the user params split the [-8; 0] EV range in 9 channels and define a set of (x, y)
1223 * coordinates, where x are the exposure channels (evenly-spaced by 1 EV in [-8; 0] EV)
1224 * and y are the desired exposure compensation for each channel.
1225 *
1226 * This (x, y) set is interpolated by radial-basis function using a series of 8 gaussians.
1227 * Losing 1 degree of freedom makes it an approximation rather than an interpolation but
1228 * helps reducing a bit the oscillations and fills a full AVX vector.
1229 *
1230 * The coefficients/factors used in the interpolation/approximation are linear, but keep in
1231 * mind that users params are expressed as log2 gains, so we always need to do the log2/exp2
1232 * flip/flop between both.
1233 *
1234 * User params of exposure compensation are expected between [-2 ; +2] EV for practical UI reasons
1235 * and probably numerical stability reasons, but there is no theoretical obstacle to enlarge
1236 * this range. The main reason for not allowing it is tone equalizer is mostly intended
1237 * to do local changes, and these don't look so well if you are too harsh on the changes.
1238 * For heavier tonemapping, it should be used in combination with a tone curve or filmic.
1239 *
1240 ***/
1241
1242static void compute_correction_lut(float* restrict lut, const float sigma, const float *const restrict factors)
1243{
1244 const float gauss_denom = gaussian_denom(sigma);
1245 const int min_ev = -8;
1246 assert(PIXEL_CHAN == -min_ev);
1247 for(int j = 0; j <= LUT_RESOLUTION * PIXEL_CHAN; j++)
1248 {
1249 // build the correction for each pixel
1250 // as the sum of the contribution of each luminance channelcorrection
1251 float exposure = (float)j / (float)LUT_RESOLUTION + min_ev;
1252 float result = 0.0f;
1253 for(int i = 0; i < PIXEL_CHAN; ++i)
1254 result += gaussian_func(exposure - centers_ops[i], gauss_denom) * factors[i];
1255 // the user-set correction is expected in [-2;+2] EV, so is the interpolated one
1256 lut[j] = fast_clamp(result, 0.25f, 4.0f);
1257 }
1258}
1259
1261{
1262 assert(CHANNELS == 9);
1263
1264 // Get user-set channels gains in EV (log2)
1265 factors[0] = p->noise; // -8 EV
1266 factors[1] = p->ultra_deep_blacks; // -7 EV
1267 factors[2] = p->deep_blacks; // -6 EV
1268 factors[3] = p->blacks; // -5 EV
1269 factors[4] = p->shadows; // -4 EV
1270 factors[5] = p->midtones; // -3 EV
1271 factors[6] = p->highlights; // -2 EV
1272 factors[7] = p->whites; // -1 EV
1273 factors[8] = p->speculars; // +0 EV
1274}
1275
1276
1278{
1279 assert(CHANNELS == 9);
1280
1281 // Get user-set channels gains in EV (log2)
1282 get_channels_gains(factors, p);
1283
1284 // Convert from EV offsets to linear factors
1285 __OMP_SIMD__(aligned(factors:64))
1286 for(int c = 0; c < CHANNELS; ++c)
1287 factors[c] = exp2f(factors[c]);
1288}
1289
1290
1291static int compute_channels_factors(const float factors[PIXEL_CHAN], float out[CHANNELS], const float sigma)
1292{
1293 // Input factors are the weights for the radial-basis curve approximation of user params
1294 // Output factors are the gains of the user parameters channels
1295 // aka the y coordinates of the approximation for x = { CHANNELS }
1296 assert(PIXEL_CHAN == 8);
1297
1298 int valid = 1;
1299 __OMP_PARALLEL_FOR_SIMD__(aligned(factors, out, centers_params:64) shared(valid) firstprivate(centers_params))
1300 for(int i = 0; i < CHANNELS; ++i)
1301 {
1302 // Compute the new channels factors
1303 out[i] = pixel_correction(centers_params[i], factors, sigma);
1304
1305 // check they are in [-2, 2] EV and not NAN
1306 if(isnan(out[i]) || out[i] < 0.25f || out[i] > 4.0f) valid = 0;
1307 }
1308
1309 return valid;
1310}
1311
1312
1313static int compute_channels_gains(const float in[CHANNELS], float out[CHANNELS])
1314{
1315 // Helper function to compute the new channels gains (log) from the factors (linear)
1316 assert(PIXEL_CHAN == 8);
1317
1318 const int valid = 1;
1319
1320 for(int i = 0; i < CHANNELS; ++i)
1321 out[i] = log2f(in[i]);
1322
1323 return valid;
1324}
1325
1326
1328{
1329 p->noise = factors[0];
1330 p->ultra_deep_blacks = factors[1];
1331 p->deep_blacks = factors[2];
1332 p->blacks = factors[3];
1333 p->shadows = factors[4];
1334 p->midtones = factors[5];
1335 p->highlights = factors[6];
1336 p->whites = factors[7];
1337 p->speculars = factors[8];
1338
1339 return 1;
1340}
1341
1342
1343/***
1344 * Cache invalidation and initializatiom
1345 ***/
1346
1347
1348static void gui_cache_init(struct dt_iop_module_t *self)
1349{
1351 if(IS_NULL_PTR(g)) return;
1352
1354 g->thumb_preview_hash = DT_PIXELPIPE_CACHE_HASH_INVALID;
1355 g->pending_preview_hash = DT_PIXELPIPE_CACHE_HASH_INVALID;
1356 g->max_histogram = 1;
1357 g->scale = 1.0f;
1358 g->sigma = sqrtf(2.0f);
1359 g->mask_display = FALSE;
1360
1361 g->interpolation_valid = FALSE; // TRUE if the interpolation_matrix is ready
1362 g->luminance_valid = FALSE; // TRUE if the luminance cache is ready
1363 g->histogram_valid = FALSE; // TRUE if the histogram cache and stats are ready
1364 g->lut_valid = FALSE; // TRUE if the gui_lut is ready
1365 g->graph_valid = FALSE; // TRUE if the UI graph view is ready
1366 g->user_param_valid = FALSE; // TRUE if users params set in interactive view are in bounds
1367 g->factors_valid = TRUE; // TRUE if radial-basis coeffs are ready
1368
1369 g->valid_nodes_x = FALSE; // TRUE if x coordinates of graph nodes have been inited
1370 g->valid_nodes_y = FALSE; // TRUE if y coordinates of graph nodes have been inited
1371 g->area_cursor_valid = FALSE; // TRUE if mouse cursor is over the graph area
1372 g->area_dragging = FALSE; // TRUE if left-button has been pushed but not released and cursor motion is recorded
1373 g->cursor_valid = FALSE; // TRUE if mouse cursor is over the preview image
1374
1375 g->thumb_preview_entry = NULL;
1376 g->thumb_preview_buf_width = 0;
1377 g->thumb_preview_buf_height = 0;
1378
1379 g->desc = NULL;
1380 g->layout = NULL;
1381 g->cr = NULL;
1382 g->cst = NULL;
1383 g->context = NULL;
1384
1385 g->pipe_order = 0;
1387}
1388
1390{
1391 if(width) *width = 0;
1392 if(height) *height = 0;
1394
1396 if(IS_NULL_PTR(piece) || !piece->enabled || piece->roi_in.width <= 0 || piece->roi_in.height <= 0)
1398
1399 if(width) *width = piece->roi_in.width;
1400 if(height) *height = piece->roi_in.height;
1401
1402 static const char cache_tag[] = "toneequal:luminance";
1403 return dt_hash(piece->global_hash, cache_tag, sizeof(cache_tag));
1404}
1405
1406
1408 const float sigma)
1409{
1410 // Build the symmetrical definite positive part of the augmented matrix
1411 // of the radial-basis interpolation weights
1412
1413 const float gauss_denom = gaussian_denom(sigma);
1414 __OMP_SIMD__(aligned(A, centers_ops, centers_params:64) collapse(2))
1415 for(int i = 0; i < CHANNELS; ++i)
1416 for(int j = 0; j < PIXEL_CHAN; ++j)
1417 A[i * PIXEL_CHAN + j] = gaussian_func(centers_params[i] - centers_ops[j], gauss_denom);
1418}
1419
1420
1422static inline void compute_log_histogram_and_stats(const float *const restrict luminance,
1423 int histogram[UI_SAMPLES],
1424 const size_t num_elem,
1425 int *max_histogram,
1426 float *first_decile, float *last_decile)
1427{
1428 // (Re)init the histogram
1429 memset(histogram, 0, sizeof(int) * UI_SAMPLES);
1430
1431 // we first calculate an extended histogram for better accuracy
1432 #define TEMP_SAMPLES 2 * UI_SAMPLES
1433 int temp_hist[TEMP_SAMPLES];
1434 memset(temp_hist, 0, sizeof(int) * TEMP_SAMPLES);
1435
1436 // Split exposure in bins
1437 __OMP_PARALLEL_FOR__(reduction(+:temp_hist[:TEMP_SAMPLES]))
1438 for(size_t k = 0; k < num_elem; k++)
1439 {
1440 // extended histogram bins between [-10; +6] EV remapped between [0 ; 2 * UI_SAMPLES]
1441 const int index = CLAMP((int)(((log2f(luminance[k]) + 10.0f) / 16.0f) * (float)TEMP_SAMPLES), 0, TEMP_SAMPLES - 1);
1442 temp_hist[index] += 1;
1443 }
1444
1445 const int first = (int)((float)num_elem * 0.05f);
1446 const int last = (int)((float)num_elem * (1.0f - 0.95f));
1447 int population = 0;
1448 int first_pos = 0;
1449 int last_pos = 0;
1450
1451 // scout the extended histogram bins looking for deciles
1452 // these would not be accurate with the regular histogram
1453 for(int k = 0; k < TEMP_SAMPLES; ++k)
1454 {
1455 const size_t prev_population = population;
1456 population += temp_hist[k];
1457 if(prev_population < first && first <= population)
1458 {
1459 first_pos = k;
1460 break;
1461 }
1462 }
1463 population = 0;
1464 for(int k = TEMP_SAMPLES - 1; k >= 0; --k)
1465 {
1466 const size_t prev_population = population;
1467 population += temp_hist[k];
1468 if(prev_population < last && last <= population)
1469 {
1470 last_pos = k;
1471 break;
1472 }
1473 }
1474
1475 // Convert decile positions to exposures
1476 *first_decile = 16.0 * (float)first_pos / (float)(TEMP_SAMPLES - 1) - 10.0;
1477 *last_decile = 16.0 * (float)last_pos / (float)(TEMP_SAMPLES - 1) - 10.0;
1478
1479 // remap the extended histogram into the normal one
1480 // bins between [-8; 0] EV remapped between [0 ; UI_SAMPLES]
1481 for(size_t k = 0; k < TEMP_SAMPLES; ++k)
1482 {
1483 float EV = 16.0 * (float)k / (float)(TEMP_SAMPLES - 1) - 10.0;
1484 const int i = CLAMP((int)(((EV + 8.0f) / 8.0f) * (float)UI_SAMPLES), 0, UI_SAMPLES - 1);
1485 histogram[i] += temp_hist[k];
1486
1487 // store the max numbers of elements in bins for later normalization
1488 *max_histogram = histogram[i] > *max_histogram ? histogram[i] : *max_histogram;
1489 }
1490}
1491
1492static inline void update_histogram(struct dt_iop_module_t *const self)
1493{
1495 if(IS_NULL_PTR(g)) return;
1496
1497 dt_pixel_cache_entry_t *preview_entry = NULL;
1498 size_t width = 0;
1499 size_t height = 0;
1501 gboolean needs_histogram = FALSE;
1502
1503 // Readers take a temporary cache ref while copying the GUI-visible entry pointer,
1504 // then read-lock the cacheline only around the actual sampling. This keeps both
1505 // ownership transfer and lock lifetime visible at the call site.
1507 if(!g->histogram_valid && g->luminance_valid && g->thumb_preview_entry)
1508 {
1509 preview_entry = g->thumb_preview_entry;
1510 width = g->thumb_preview_buf_width;
1511 height = g->thumb_preview_buf_height;
1512 preview_hash = g->thumb_preview_hash;
1514 needs_histogram = TRUE;
1515 }
1517
1518 if(!needs_histogram || width == 0 || height == 0)
1519 {
1520 if(!IS_NULL_PTR(preview_entry))
1522 return;
1523 }
1524
1525 int histogram[UI_SAMPLES];
1526 int max_histogram = 1;
1527 float first_decile = 0.0f;
1528 float last_decile = 0.0f;
1529
1531 const float *const preview_buf = (const float *const)dt_pixel_cache_entry_get_data(preview_entry);
1532 if(preview_buf)
1533 compute_log_histogram_and_stats(preview_buf, histogram, width * height, &max_histogram, &first_decile,
1534 &last_decile);
1536
1537 if(IS_NULL_PTR(preview_buf))
1538 {
1540 return;
1541 }
1542
1544 if(g->thumb_preview_entry == preview_entry && g->thumb_preview_hash == preview_hash && !g->histogram_valid)
1545 {
1546 memcpy(g->histogram, histogram, sizeof(histogram));
1547 g->max_histogram = max_histogram;
1548 g->histogram_first_decile = first_decile;
1549 g->histogram_last_decile = last_decile;
1550 g->histogram_average = (first_decile + last_decile) / 2.0f;
1551 g->histogram_valid = TRUE;
1552 }
1554
1556}
1557
1558
1561 const float offset,
1562 const float scaling)
1563{
1564 // Compute the LUT of the exposure corrections in EV,
1565 // offset and scale it for display in GUI widget graph
1566
1567 float *const restrict LUT = g->gui_lut;
1568 const float *const restrict factors = g->factors;
1569 const float sigma = g->sigma;
1570 __OMP_FOR_SIMD__(aligned(LUT, factors:64))
1571 for(int k = 0; k < UI_SAMPLES; k++)
1572 {
1573 // build the inset graph curve LUT
1574 // the x range is [-14;+2] EV
1575 const float x = (8.0f * (((float)k) / ((float)(UI_SAMPLES - 1)))) - 8.0f;
1576 LUT[k] = offset - log2f(pixel_correction(x, factors, sigma)) / scaling;
1577 }
1578}
1579
1580
1581
1582static inline gboolean update_curve_lut(struct dt_iop_module_t *self)
1583{
1586
1587 if(IS_NULL_PTR(g)) return FALSE;
1588
1589 gboolean valid = TRUE;
1590
1591 if(!g->interpolation_valid)
1592 {
1593 build_interpolation_matrix(g->interpolation_matrix, g->sigma);
1594 g->interpolation_valid = TRUE;
1595 g->factors_valid = FALSE;
1596 }
1597
1598 if(!g->user_param_valid)
1599 {
1600 float factors[CHANNELS] DT_ALIGNED_ARRAY;
1601 get_channels_factors(factors, p);
1602 dt_simd_memcpy(factors, g->temp_user_params, CHANNELS);
1603 g->user_param_valid = TRUE;
1604 g->factors_valid = FALSE;
1605 }
1606
1607 if(!g->factors_valid && g->user_param_valid)
1608 {
1609 float factors[CHANNELS] DT_ALIGNED_ARRAY;
1610 dt_simd_memcpy(g->temp_user_params, factors, CHANNELS);
1611 if(pseudo_solve(g->interpolation_matrix, factors, CHANNELS, PIXEL_CHAN, 1) != 0)
1612 {
1613 valid = FALSE;
1614 }
1615 else
1616 {
1617 dt_simd_memcpy(factors, g->factors, PIXEL_CHAN);
1618 g->factors_valid = TRUE;
1619 g->lut_valid = FALSE;
1620 }
1621 }
1622
1623 if(!g->lut_valid && g->factors_valid)
1624 {
1625 compute_lut_correction(g, 0.5f, 4.0f);
1626 g->lut_valid = TRUE;
1627 }
1628
1629 return valid;
1630}
1631
1632
1634{
1637
1638 module->data = gd;
1639}
1640
1641
1643{
1644 dt_free(module->data);
1645}
1646
1647
1650{
1654
1655 // Trivial params passing
1656 d->method = p->method;
1657 d->details = p->details;
1658 d->iterations = p->iterations;
1659 d->smoothing = p->smoothing;
1660 d->quantization = p->quantization;
1661
1662 // UI blending param is set in % of the largest image dimension
1663 d->blending = p->blending / 100.0f;
1664
1665 // UI guided filter feathering param increases the edges taping
1666 // but the actual regularization params applied in guided filter behaves the other way
1667 d->feathering = 1.f / (p->feathering);
1668
1669 // UI params are in log2 offsets (EV) : convert to linear factors
1670 d->contrast_boost = exp2f(p->contrast_boost);
1671 d->exposure_boost = exp2f(p->exposure_boost);
1672
1673 /*
1674 * Perform a radial-based interpolation using a series gaussian functions
1675 */
1676 // FIXME: trying to spare some CPU cycles by mixing GUI params update
1677 // with pipeline code is not worth the thread-safety issues (solved only by deadlocks).
1678 // Move that to GUI code.
1679 if(self->dev->gui_attached && !IS_NULL_PTR(g))
1680 {
1681 if(g->sigma != p->smoothing) g->interpolation_valid = FALSE;
1682 g->sigma = p->smoothing;
1683 g->user_param_valid = FALSE; // force updating channels factors
1684
1685 update_curve_lut(self);
1686 dt_simd_memcpy(g->factors, d->factors, PIXEL_CHAN);
1687 }
1688 else
1689 {
1690 // No cache : Build / Solve interpolation matrix
1691 float factors[CHANNELS] DT_ALIGNED_ARRAY;
1692 get_channels_factors(factors, p);
1693
1695 build_interpolation_matrix(A, p->smoothing);
1696 if(pseudo_solve(A, factors, CHANNELS, PIXEL_CHAN, 0) != 0) return;
1697
1698 dt_simd_memcpy(factors, d->factors, PIXEL_CHAN);
1699 }
1700
1701 // compute the correction LUT here to spare some time in process
1702 // when computing several times toneequalizer with same parameters
1703 compute_correction_lut(d->correction_lut, d->smoothing, d->factors);
1704}
1705
1706
1712
1713
1715{
1716 dt_free_align(piece->data);
1717 piece->data = NULL;
1718}
1719
1721{
1722 dt_iop_module_t *module = (dt_iop_module_t *)self;
1724 const dt_iop_toneequalizer_params_t *p = (const dt_iop_toneequalizer_params_t *)module->params;
1725
1726 switch(p->details)
1727 {
1728 case(DT_TONEEQ_NONE):
1729 {
1730 gtk_widget_set_visible(g->blending, FALSE);
1731 gtk_widget_set_visible(g->feathering, FALSE);
1732 gtk_widget_set_visible(g->iterations, FALSE);
1733 gtk_widget_set_visible(g->contrast_boost, FALSE);
1734 gtk_widget_set_visible(g->quantization, FALSE);
1735 break;
1736 }
1737
1739 case(DT_TONEEQ_AVG_EIGF):
1740 {
1741 gtk_widget_set_visible(g->blending, TRUE);
1742 gtk_widget_set_visible(g->feathering, TRUE);
1743 gtk_widget_set_visible(g->iterations, TRUE);
1744 gtk_widget_set_visible(g->contrast_boost, FALSE);
1745 gtk_widget_set_visible(g->quantization, TRUE);
1746 break;
1747 }
1748
1749 case(DT_TONEEQ_GUIDED):
1750 case(DT_TONEEQ_EIGF):
1751 {
1752 gtk_widget_set_visible(g->blending, TRUE);
1753 gtk_widget_set_visible(g->feathering, TRUE);
1754 gtk_widget_set_visible(g->iterations, TRUE);
1755 gtk_widget_set_visible(g->contrast_boost, TRUE);
1756 gtk_widget_set_visible(g->quantization, TRUE);
1757 break;
1758 }
1759 }
1760}
1761
1763{
1765 dt_bauhaus_slider_set(g->noise, p->noise);
1766 dt_bauhaus_slider_set(g->ultra_deep_blacks, p->ultra_deep_blacks);
1767 dt_bauhaus_slider_set(g->deep_blacks, p->deep_blacks);
1768 dt_bauhaus_slider_set(g->blacks, p->blacks);
1769 dt_bauhaus_slider_set(g->shadows, p->shadows);
1770 dt_bauhaus_slider_set(g->midtones, p->midtones);
1771 dt_bauhaus_slider_set(g->highlights, p->highlights);
1772 dt_bauhaus_slider_set(g->whites, p->whites);
1773 dt_bauhaus_slider_set(g->speculars, p->speculars);
1775}
1776
1777
1778void gui_update(struct dt_iop_module_t *self)
1779{
1782
1783 dt_bauhaus_slider_set(g->smoothing, logf(p->smoothing) / logf(sqrtf(2.0f)) - 1.0f);
1784
1787
1788 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->show_luminance_mask), g->mask_display);
1789}
1790
1791void gui_changed(dt_iop_module_t *self, GtkWidget *w, void *previous)
1792{
1794 if(w == g->method ||
1795 w == g->blending ||
1796 w == g->feathering ||
1797 w == g->iterations ||
1798 w == g->quantization)
1799 {
1801 }
1802 else if (w == g->details)
1803 {
1806 }
1807 else if (w == g->contrast_boost || w == g->exposure_boost)
1808 {
1811 }
1812}
1813
1814static void smoothing_callback(GtkWidget *slider, gpointer user_data)
1815{
1816 dt_iop_module_t *self = (dt_iop_module_t *)user_data;
1817 if(dt_gui_widgets_suppressed()) return;
1820
1821 p->smoothing= powf(sqrtf(2.0f), 1.0f + dt_bauhaus_slider_get(slider));
1822
1823 float factors[CHANNELS] DT_ALIGNED_ARRAY;
1824 get_channels_factors(factors, p);
1825
1826 // Solve the interpolation by least-squares to check the validity of the smoothing param
1827 const int valid = update_curve_lut(self);
1828 if(!valid) dt_control_log(_("the interpolation is unstable, decrease the curve smoothing"));
1829
1830 // Redraw graph before launching computation
1831 update_curve_lut(self);
1832 gtk_widget_queue_draw(GTK_WIDGET(g->area));
1833 dt_dev_add_history_item(self->dev, self, TRUE, TRUE);
1834
1835 // Unlock the colour picker so we can display our own custom cursor
1837}
1838
1839static void show_luminance_mask_callback(GtkWidget *togglebutton, GdkEventButton *event, dt_iop_module_t *self)
1840{
1841 if(dt_gui_widgets_suppressed()) return;
1843
1844 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(self->gui->off), TRUE);
1845
1847
1848 // if blend module is displaying mask do not display it here
1851
1852 g->mask_display = !g->mask_display;
1853
1854 if(g->mask_display)
1856
1857 dt_iop_set_cache_bypass(self, g->mask_display);
1858 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->show_luminance_mask), g->mask_display);
1860
1861 // Unlock the colour picker so we can display our own custom cursor
1863}
1864
1865
1866/***
1867 * GUI Interactivity
1868 **/
1869
1870static void _switch_cursors(struct dt_iop_module_t *self)
1871{
1873 if(IS_NULL_PTR(g) || !self->dev->gui_attached) return;
1874
1875 if(!self->gui->expanded)
1876 {
1877 // If the module lost focus, do nothing and let the app decide.
1878 return;
1879 }
1880 else if(!self->enabled)
1881 {
1882 // A disabled module does not own the cursor state: leave whatever the view
1883 // or another tool has currently selected, and most importantly do not hide it.
1884 return;
1885 }
1886 else if(!sanity_check(self) || in_mask_editing(self) || dt_iop_color_picker_is_visible(self->dev))
1887 {
1888 // if we are editing masks or using colour-pickers, do not display our own custom cursor,
1889 // but do not force a specific shape either: the mouse can still be over the image here,
1890 // and darkroom's own default cursor logic (dot/crosshair/left_ptr, picked per position in
1891 // _darkroom_set_default_cursor) already queued the right one before this module's
1892 // mouse_moved ran. Un-hide it (it may have been hidden by the branch below) and leave it.
1894 return;
1895 }
1896 else if((self->dev->pipe->processing || self->dev->preview_pipe->processing) && g->cursor_valid)
1897 {
1898 // if pipe is busy or dirty but cursor is on preview,
1899 // display waiting cursor while pipe reprocesses
1902
1904 }
1905 else if(self->enabled && g->cursor_valid && !self->dev->pipe->processing)
1906 {
1907 // if pipe is clean and idle and cursor is on preview,
1908 // hide GTK cursor because we display our custom one
1911 _("scroll over image to change tone exposure\n"
1912 "shift+scroll for large steps; "
1913 "ctrl+scroll for small steps"));
1914
1916 }
1917 else
1918 {
1919 // Cursor is out of the preview (off the image entirely): same reasoning as above, let
1920 // darkroom's own default cursor stand instead of forcing a specific shape.
1923 }
1924}
1925
1926
1927int mouse_moved(struct dt_iop_module_t *self, double x, double y, double pressure, int which)
1928{
1929 // Whenever the mouse moves over the picture preview, store its coordinates in the GUI struct
1930 // for later use. This works only if dev->preview_pipe perfectly overlaps with the UI preview
1931 // meaning all distortions, cropping, rotations etc. are applied before this module in the pipe.
1932
1933 dt_develop_t *dev = self->dev;
1935
1936 const int fail = !sanity_check(self);
1937 if(fail) return 0;
1938
1940 {
1941 g->cursor_valid = FALSE;
1942 g->area_active_node = -1;
1943 _switch_cursors(self);
1944 gtk_widget_queue_draw(GTK_WIDGET(g->area));
1945 return 0;
1946 }
1947
1948 if(IS_NULL_PTR(g)) return 0;
1949
1950 // Stop at the normalized image coordinates: they describe the position on the picture itself,
1951 // which is the only space both the preview-sized and the full-resolution luminance mask share.
1952 float pzxpy[2] = { (float)x, (float)y };
1954
1955 const float x_pointer = pzxpy[0];
1956 const float y_pointer = pzxpy[1];
1957
1958 // Cursor is valid if it's inside the picture frame
1959 if(x_pointer >= 0.f && x_pointer < 1.f && y_pointer >= 0.f && y_pointer < 1.f)
1960 {
1961 g->cursor_valid = TRUE;
1962 g->cursor_pos_x = x_pointer;
1963 g->cursor_pos_y = y_pointer;
1964 }
1965 else
1966 {
1967 g->cursor_valid = FALSE;
1968 g->cursor_pos_x = 0.f;
1969 g->cursor_pos_y = 0.f;
1970 }
1971
1972 // Store the current preview exposure too, to spare recomputing it in the UI callbacks.
1973 if(g->cursor_valid && !dev->pipe->processing)
1974 {
1975 dt_pixel_cache_entry_t *preview_entry = NULL;
1976 size_t preview_width = 0;
1977 size_t preview_height = 0;
1978
1979 // Keep the GUI cache entry alive before releasing the GUI state lock.
1980 // Pipe workers can replace the retained entry while mouse motion keeps sampling it.
1982 if(g->luminance_valid && !IS_NULL_PTR(g->thumb_preview_entry))
1983 {
1984 preview_entry = g->thumb_preview_entry;
1985 preview_width = g->thumb_preview_buf_width;
1986 preview_height = g->thumb_preview_buf_height;
1988 }
1990
1991 if(!IS_NULL_PTR(preview_entry) && preview_width > 0 && preview_height > 0)
1992 {
1994 const float *const preview_buf = (const float *const)dt_pixel_cache_entry_get_data(preview_entry);
1995 const float cursor_exposure
1996 = log2f(get_luminance_at_norm(preview_buf, preview_width, preview_height,
1997 x_pointer, y_pointer));
1999
2000 if(!isnan(cursor_exposure))
2001 {
2002 g->cursor_exposure = cursor_exposure;
2003 }
2004
2005 }
2006
2007 if(preview_entry)
2009 }
2010
2011 _switch_cursors(self);
2012 return 1;
2013}
2014
2015
2017{
2019
2020 if(IS_NULL_PTR(g)) return 0;
2021
2022 g->cursor_valid = FALSE;
2023 g->area_active_node = -1;
2024
2025 // display default cursor
2028 gtk_widget_queue_draw(GTK_WIDGET(g->area));
2029
2030 return 1;
2031}
2032
2033
2034static inline int set_new_params_interactive(const float control_exposure, const float exposure_offset, const float blending_sigma,
2036{
2037 // Apply an exposure offset optimized smoothly over all the exposure channels,
2038 // taking user instruction to apply exposure_offset EV at control_exposure EV,
2039 // and commit the new params is the solution is valid.
2040
2041 // Raise the user params accordingly to control correction and distance from cursor exposure
2042 // to blend smoothly the desired correction
2043 const float std = gaussian_denom(blending_sigma);
2044 if(g->user_param_valid)
2045 {
2046 for(int i = 0; i < CHANNELS; ++i)
2047 g->temp_user_params[i] *= exp2f(gaussian_func(centers_params[i] - control_exposure, std) * exposure_offset);
2048 }
2049
2050 // Get the new weights for the radial-basis approximation
2051 float factors[CHANNELS] DT_ALIGNED_ARRAY;
2052 dt_simd_memcpy(g->temp_user_params, factors, CHANNELS);
2053 if(g->user_param_valid)
2054 g->user_param_valid = (pseudo_solve(g->interpolation_matrix, factors, CHANNELS, PIXEL_CHAN, 1) == 0);
2055 if(!g->user_param_valid) dt_control_log(_("the interpolation is unstable, decrease the curve smoothing"));
2056
2057 // Compute new user params for channels and store them locally
2058 if(g->user_param_valid)
2059 g->user_param_valid = compute_channels_factors(factors, g->temp_user_params, g->sigma);
2060 if(!g->user_param_valid) dt_control_log(_("some parameters are out-of-bounds"));
2061
2062 const int commit = g->user_param_valid;
2063
2064 if(commit)
2065 {
2066 // Accept the solution
2067 dt_simd_memcpy(factors, g->factors, PIXEL_CHAN);
2068 g->lut_valid = 0;
2069
2070 // Convert the linear temp parameters to log gains and commit
2071 float gains[CHANNELS] DT_ALIGNED_ARRAY;
2072 compute_channels_gains(g->temp_user_params, gains);
2073 commit_channels_gains(gains, p);
2074 }
2075 else
2076 {
2077 // Reset the GUI copy of user params
2078 get_channels_factors(factors, p);
2079 dt_simd_memcpy(factors, g->temp_user_params, CHANNELS);
2080 g->user_param_valid = 1;
2081 }
2082
2083 return commit;
2084}
2085
2086
2087int scrolled(struct dt_iop_module_t *self, double x, double y, int up, uint32_t state)
2088{
2089 dt_develop_t *dev = self->dev;
2092
2093 if(!sanity_check(self)) return 0;
2094 if(dt_gui_widgets_suppressed()) return 1;
2095 if(IS_NULL_PTR(g)) return 0;
2096 if(!self->gui->expanded) return 0;
2097 if(in_mask_editing(self) || dt_iop_color_picker_is_visible(dev)) return 0;
2098
2099 // turn-on the module if off
2100 if(!self->enabled)
2101 if(self->gui->off) gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(self->gui->off), 1);
2102
2103 // if GUI buffers not ready, exit but still handle the cursor
2104 const int fail = (!g->cursor_valid || !g->interpolation_valid || !g->user_param_valid || dev->pipe->processing || !self->gui->expanded);
2105 if(fail) return 1;
2106
2107 // Re-read the exposure in case the preview changed after the mouse moved.
2108 dt_pixel_cache_entry_t *preview_entry = NULL;
2109 size_t preview_width = 0;
2110 size_t preview_height = 0;
2111 float cursor_x = 0.f;
2112 float cursor_y = 0.f;
2113
2114 // Copy the cursor sample source while holding the GUI state lock, then keep the
2115 // cacheline alive with an explicit ref until the sampling read lock is released.
2117 if(g->luminance_valid && !IS_NULL_PTR(g->thumb_preview_entry))
2118 {
2119 preview_entry = g->thumb_preview_entry;
2120 preview_width = g->thumb_preview_buf_width;
2121 preview_height = g->thumb_preview_buf_height;
2122 cursor_x = g->cursor_pos_x;
2123 cursor_y = g->cursor_pos_y;
2125 }
2127
2128 if(IS_NULL_PTR(preview_entry) || preview_width == 0 || preview_height == 0)
2129 {
2130 if(!IS_NULL_PTR(preview_entry))
2132 return 1;
2133 }
2134
2135 if(!IS_NULL_PTR(preview_entry) && preview_width > 0 && preview_height > 0)
2136 {
2138 const float *const preview_buf = (const float *const)dt_pixel_cache_entry_get_data(preview_entry);
2139 const float cursor_exposure
2140 = log2f(get_luminance_at_norm(preview_buf, preview_width, preview_height,
2141 cursor_x, cursor_y));
2143
2144 if(!isnan(cursor_exposure))
2145 {
2146 g->cursor_exposure = cursor_exposure;
2147 }
2148 }
2149
2150 if(!IS_NULL_PTR(preview_entry))
2152
2153 // Set the correction from mouse scroll input
2154 const float increment = (up) ? +1.0f : -1.0f;
2155
2156 float step;
2157 if(dt_modifier_is(state, GDK_SHIFT_MASK))
2158 step = 1.0f; // coarse
2160 step = 0.1f; // fine
2161 else
2162 step = 0.25f; // standard
2163
2164 const float offset = step * ((float)increment);
2165
2166 // Get the desired correction on exposure channels
2167 const int commit = set_new_params_interactive(g->cursor_exposure, offset, g->sigma * g->sigma / 2.0f, g, p);
2168
2169 gtk_widget_queue_draw(GTK_WIDGET(g->area));
2170
2171 if(commit)
2172 {
2173 // Update GUI with new params
2175
2176 dt_dev_add_history_item(self->dev, self, FALSE, TRUE);
2177 }
2178
2179 return 1;
2180}
2181
2182/***
2183 * GTK/Cairo drawings and custom widgets
2184 **/
2185
2186static inline gboolean _init_drawing(dt_iop_module_t *const restrict self, GtkWidget *widget,
2187 dt_iop_toneequalizer_gui_data_t *const restrict g);
2188
2189
2190void cairo_draw_hatches(cairo_t *cr, double center[2], double span[2], int instances, double line_width, double shade)
2191{
2192 // center is the (x, y) coordinates of the region to draw
2193 // span is the distance of the region's bounds to the center, over (x, y) axes
2194
2195 // Get the coordinates of the corners of the bounding box of the region
2196 double C0[2] = { center[0] - span[0], center[1] - span[1] };
2197 double C2[2] = { center[0] + span[0], center[1] + span[1] };
2198
2199 double delta[2] = { 2.0 * span[0] / (double)instances,
2200 2.0 * span[1] / (double)instances };
2201
2202 cairo_set_line_width(cr, line_width);
2203 cairo_set_source_rgb(cr, shade, shade, shade);
2204
2205 for(int i = -instances / 2 - 1; i <= instances / 2 + 1; i++)
2206 {
2207 cairo_move_to(cr, C0[0] + (double)i * delta[0], C0[1]);
2208 cairo_line_to(cr, C2[0] + (double)i * delta[0], C2[1]);
2209 cairo_stroke(cr);
2210 }
2211}
2212
2213static void get_shade_from_luminance(cairo_t *cr, const float luminance, const float alpha)
2214{
2215 // TODO: fetch screen gamma from ICC display profile
2216 const float gamma = 1.0f / 2.2f;
2217 const float shade = powf(luminance, gamma);
2218 cairo_set_source_rgba(cr, shade, shade, shade, alpha);
2219}
2220
2221
2222static 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)
2223{
2224 // Draw a circle cursor filled with a grey shade corresponding to a luminance value
2225 // or hatches if the value is above the overexposed threshold
2226
2227 const double radius_z = radius / zoom_scale;
2228
2230 cairo_arc(cr, pointerx, pointery, radius_z, 0, 2 * M_PI);
2231 cairo_fill_preserve(cr);
2232 cairo_save(cr);
2233 cairo_clip(cr);
2234
2235 if(log2f(luminance) > 0.0f)
2236 {
2237 // if overexposed, draw hatches
2238 double pointer_coord[2] = { pointerx, pointery };
2239 double span[2] = { radius_z, radius_z };
2240 cairo_draw_hatches(cr, pointer_coord, span, instances, DT_PIXEL_APPLY_DPI(1. / zoom_scale), 0.3);
2241 }
2242 cairo_restore(cr);
2243}
2244
2245
2246static void match_color_to_background(cairo_t *cr, const float exposure, const float alpha)
2247{
2248 float shade = 0.0f;
2249 // TODO: put that as a preference in anselrc
2250 const float contrast = 1.0f;
2251
2252 if(exposure > -2.5f)
2253 shade = (fminf(exposure * contrast, 0.0f) - 2.5f);
2254 else
2255 shade = (fmaxf(exposure / contrast, -5.0f) + 2.5f);
2256
2257 get_shade_from_luminance(cr, exp2f(shade), alpha);
2258}
2259
2260
2261void gui_post_expose(struct dt_iop_module_t *self, cairo_t *cr, int32_t width, int32_t height,
2262 int32_t pointerx, int32_t pointery)
2263{
2264 // Draw the custom exposure cursor over the image preview
2265
2266 dt_develop_t *dev = self->dev;
2268 if(IS_NULL_PTR(g)) return;
2269
2270 // If the darkroom picker owns the center view, keep tone equalizer overlays out of the way.
2271 if(in_mask_editing(self) || dt_iop_color_picker_is_visible(dev)) return;
2272
2273 const int fail = (!g->cursor_valid || !g->interpolation_valid || dev->pipe->processing
2274 || !sanity_check(self) || !self->gui->expanded);
2275 if(fail) return;
2276
2277 if(!g->graph_valid)
2278 if(!_init_drawing(self, self->gui->widget, g)) return;
2279
2280 // Get coordinates. The cursor is stored normalized: sampling resolves it against the luminance
2281 // mask's own size below, while the on-canvas cursor is drawn in the preview-pixel space
2282 // dt_dev_rescale_roi() establishes.
2283 const float norm_x = g->cursor_pos_x;
2284 const float norm_y = g->cursor_pos_y;
2285 const float x_pointer = norm_x * (float)dt_dev_roi_request_preview_width(dev);
2286 const float y_pointer = norm_y * (float)dt_dev_roi_request_preview_height(dev);
2287 dt_pixel_cache_entry_t *preview_entry = NULL;
2288 size_t preview_width = 0;
2289 size_t preview_height = 0;
2290 float factors[PIXEL_CHAN] DT_ALIGNED_ARRAY;
2291 float sigma = 0.0f;
2292
2293 float exposure_in = 0.0f;
2294 float luminance_in = 0.0f;
2295 float correction = 0.0f;
2296 float exposure_out = 0.0f;
2297 float luminance_out = 0.0f;
2298 if(self->enabled)
2299 {
2300 // The drawing pass samples the same retained luminance cacheline as the event
2301 // handlers, so the ref has to be taken before another pipe callback can detach it.
2303 if(g->luminance_valid && !IS_NULL_PTR(g->thumb_preview_entry))
2304 {
2305 preview_entry = g->thumb_preview_entry;
2306 preview_width = g->thumb_preview_buf_width;
2307 preview_height = g->thumb_preview_buf_height;
2309 dt_simd_memcpy(g->factors, factors, PIXEL_CHAN);
2310 sigma = g->sigma;
2311 }
2313 }
2314
2315 if(!IS_NULL_PTR(preview_entry) && preview_width > 0 && preview_height > 0)
2316 {
2318 const float *const preview_buf = (const float *const)dt_pixel_cache_entry_get_data(preview_entry);
2319 if(!IS_NULL_PTR(preview_buf))
2320 {
2321 exposure_in = log2f(get_luminance_at_norm(preview_buf, preview_width, preview_height,
2322 norm_x, norm_y));
2323 luminance_in = exp2f(exposure_in);
2324 correction = log2f(pixel_correction(exposure_in, factors, sigma));
2325 exposure_out = exposure_in + correction;
2326 luminance_out = exp2f(exposure_out);
2327 }
2328 else
2329 {
2330 exposure_in = NAN;
2331 correction = NAN;
2332 }
2334
2335 if(!isnan(exposure_in))
2336 {
2337 g->cursor_exposure = exposure_in;
2338 }
2339 }
2340
2341 if(preview_entry)
2343
2344 if(isnan(correction) || isnan(exposure_in)) return; // something went wrong
2345
2346 // Rescale and shift Cairo drawing coordinates
2347 const float zoom_scale = dt_dev_get_overlay_scale(dev);
2348 dt_dev_rescale_roi(dev, cr, width, height);
2349
2350 // set custom cursor dimensions
2351 const double outer_radius = 16.;
2352 const double inner_radius = outer_radius / 2.0;
2353 const double setting_offset_x = (outer_radius + 4. * g->inner_padding) / zoom_scale;
2354 const double fill_width = DT_PIXEL_APPLY_DPI(4) / zoom_scale;
2355
2356 // setting fill bars
2357 match_color_to_background(cr, exposure_out, 1.0);
2358 cairo_set_line_width(cr, 2.0 * fill_width);
2359 cairo_move_to(cr, x_pointer - setting_offset_x, y_pointer);
2360
2361 if(correction > 0.0f)
2362 cairo_arc(cr, x_pointer, y_pointer, setting_offset_x, M_PI, M_PI + correction * M_PI / 4.0);
2363 else
2364 cairo_arc_negative(cr, x_pointer, y_pointer, setting_offset_x, M_PI, M_PI + correction * M_PI / 4.0);
2365
2366 cairo_stroke(cr);
2367
2368 // setting ground level
2369 cairo_set_line_width(cr, DT_PIXEL_APPLY_DPI(1.5) / zoom_scale);
2370 cairo_move_to(cr, x_pointer + (outer_radius + 2. * g->inner_padding) / zoom_scale, y_pointer);
2371 cairo_line_to(cr, x_pointer + outer_radius / zoom_scale, y_pointer);
2372 cairo_move_to(cr, x_pointer - outer_radius / zoom_scale, y_pointer);
2373 cairo_line_to(cr, x_pointer - setting_offset_x - 4.0 * g->inner_padding / zoom_scale, y_pointer);
2374 cairo_stroke(cr);
2375
2376 // setting cursor cross hair
2377 cairo_set_line_width(cr, DT_PIXEL_APPLY_DPI(1.5) / zoom_scale);
2378 cairo_move_to(cr, x_pointer, y_pointer + setting_offset_x + fill_width);
2379 cairo_line_to(cr, x_pointer, y_pointer + outer_radius / zoom_scale);
2380 cairo_move_to(cr, x_pointer, y_pointer - outer_radius / zoom_scale);
2381 cairo_line_to(cr, x_pointer, y_pointer - setting_offset_x - fill_width);
2382 cairo_stroke(cr);
2383
2384 // draw exposure cursor
2385 draw_exposure_cursor(cr, x_pointer, y_pointer, outer_radius, luminance_in, zoom_scale, 6, .9);
2386 draw_exposure_cursor(cr, x_pointer, y_pointer, inner_radius, luminance_out, zoom_scale, 3, .9);
2387
2388 // Create Pango objects : texts
2389 char text[256];
2390 PangoLayout *layout;
2391 PangoRectangle ink;
2392 PangoFontDescription *desc = pango_font_description_copy_static(dt_bauhaus_get_global()->pango_font_desc);
2393
2394 // Avoid text resizing based on zoom level
2395 const int old_size = pango_font_description_get_size(desc);
2396 pango_font_description_set_size (desc, (int)(old_size / zoom_scale));
2397 layout = pango_cairo_create_layout(cr);
2398 pango_layout_set_font_description(layout, desc);
2400
2401 // Build text object
2402 if(preview_entry && self->enabled)
2403 snprintf(text, sizeof(text), _("%+.1f EV"), exposure_in);
2404 else
2405 snprintf(text, sizeof(text), "? EV");
2406 pango_layout_set_text(layout, text, -1);
2407 pango_layout_get_pixel_extents(layout, &ink, NULL);
2408
2409 // Draw the text plain blackground
2410 get_shade_from_luminance(cr, luminance_out, 0.75);
2411 cairo_rectangle(cr, x_pointer + (outer_radius + 2. * g->inner_padding) / zoom_scale,
2412 y_pointer - ink.y - ink.height / 2.0 - g->inner_padding / zoom_scale,
2413 ink.width + 2.0 * ink.x + 4. * g->inner_padding / zoom_scale,
2414 ink.height + 2.0 * ink.y + 2. * g->inner_padding / zoom_scale);
2415 cairo_fill(cr);
2416
2417 // Display the EV reading
2418 match_color_to_background(cr, exposure_out, 1.0);
2419 cairo_move_to(cr, x_pointer + (outer_radius + 4. * g->inner_padding) / zoom_scale,
2420 y_pointer - ink.y - ink.height / 2.);
2421 pango_cairo_show_layout(cr, layout);
2422
2423 cairo_stroke(cr);
2424
2425 pango_font_description_free(desc);
2426 g_object_unref(layout);
2427
2428 if(preview_entry && self->enabled)
2429 {
2430 // Search for nearest node in graph and highlight it
2431 const float radius_threshold = 0.45f;
2432 g->area_active_node = -1;
2433 if(g->cursor_valid)
2434 for(int i = 0; i < CHANNELS; ++i)
2435 {
2436 const float delta_x = fabsf(g->cursor_exposure - centers_params[i]);
2437 if(delta_x < radius_threshold)
2438 g->area_active_node = i;
2439 }
2440
2441 gtk_widget_queue_draw(GTK_WIDGET(g->area));
2442 }
2443}
2444
2445
2446void gui_focus(struct dt_iop_module_t *self, gboolean in)
2447{
2449 _switch_cursors(self);
2450 if(!in)
2451 {
2452 //lost focus - stop showing mask
2453 const gboolean was_mask = g->mask_display;
2454 g->mask_display = FALSE;
2456 g->pending_preview_hash = DT_PIXELPIPE_CACHE_HASH_INVALID;
2458 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->show_luminance_mask), FALSE);
2459 if(was_mask) dt_dev_pixelpipe_update_history_main(self->dev);
2461 }
2462 else
2463 {
2464 gboolean needs_preview_update = FALSE;
2465
2466 if(self->enabled && self->dev && self->dev->preview_pipe && !self->dev->preview_pipe->processing)
2467 {
2468 // Opening the module can happen after preview processing already finished.
2469 // In that case the preview pipe may stay idle because darkroom can reuse an
2470 // existing backbuffer, so reattach to the existing luminance cacheline here
2471 // instead of waiting for process() to run again. This was tested by opening
2472 // tone equalizer on a fresh darkroom image with no pending recompute.
2473 //
2474 // Hash AND dimensions come from ONE read of the pipe piece: the worker thread replans that
2475 // piece between two reads, and toneequal's ROI genuinely changes size across such a replan
2476 // (`finalscale` enables or disables itself on zoom, which moves this module between the
2477 // full-resolution and the preview-sized half of the pipe). Two reads therefore describe two
2478 // different runs, and pairing one run's hash with the other's dimensions is what let the
2479 // GUI sample a preview-sized cacheline as if it were the full-resolution one.
2480 size_t preview_width = 0;
2481 size_t preview_height = 0;
2482 const uint64_t preview_hash = _current_preview_luminance_hash(self, &preview_width, &preview_height);
2483
2484 if(preview_hash != DT_PIXELPIPE_CACHE_HASH_INVALID)
2485 {
2486 void *preview_buf = NULL;
2487 dt_pixel_cache_entry_t *preview_entry = NULL;
2488
2489 gboolean preview_ready = dt_dev_pixelpipe_cache_ref_entry_by_hash(preview_hash,
2490 &preview_buf, &preview_entry);
2491 if(preview_ready && (IS_NULL_PTR(preview_buf)
2492 || !luminance_entry_fits(preview_entry, preview_width, preview_height)))
2493 {
2494 if(!IS_NULL_PTR(preview_entry))
2496 preview_ready = FALSE;
2497 }
2498
2499 if(preview_ready)
2500 {
2501 dt_pixel_cache_entry_t *old_entry = NULL;
2502 gboolean keep_new_entry = FALSE;
2504 if(g->thumb_preview_entry != preview_entry || g->thumb_preview_hash != preview_hash
2505 || g->thumb_preview_buf_width != preview_width
2506 || g->thumb_preview_buf_height != preview_height || !g->luminance_valid)
2507 {
2508 old_entry = g->thumb_preview_entry;
2509 g->thumb_preview_entry = preview_entry;
2510 g->thumb_preview_hash = preview_hash;
2511 g->pending_preview_hash = DT_PIXELPIPE_CACHE_HASH_INVALID;
2512 g->thumb_preview_buf_width = preview_width;
2513 g->thumb_preview_buf_height = preview_height;
2514 g->luminance_valid = TRUE;
2515 g->histogram_valid = FALSE;
2516 keep_new_entry = TRUE;
2517 }
2519
2520 if(old_entry)
2522 if(!keep_new_entry)
2524 }
2525 else
2526 {
2528 g->pending_preview_hash = preview_hash;
2530 needs_preview_update = TRUE;
2531 }
2532 }
2533 else
2534 needs_preview_update = TRUE;
2535 }
2536
2537 if(needs_preview_update)
2539
2541 _("scroll over image to change tone exposure\n"
2542 "shift+scroll for large steps; "
2543 "ctrl+scroll for small steps"));
2544 }
2545}
2546
2547
2548static inline gboolean _init_drawing(dt_iop_module_t *const restrict self, GtkWidget *widget,
2549 dt_iop_toneequalizer_gui_data_t *const restrict g)
2550{
2551 // Cache the equalizer graph objects to avoid recomputing all the view at each redraw
2552 gtk_widget_get_allocation(widget, &g->allocation);
2553
2554 if(g->cst) cairo_surface_destroy(g->cst);
2555 g->cst = dt_cairo_image_surface_create(CAIRO_FORMAT_ARGB32, g->allocation.width, g->allocation.height);
2556
2557 if(g->cr) cairo_destroy(g->cr);
2558 g->cr = cairo_create(g->cst);
2559
2560 if(g->layout) g_object_unref(g->layout);
2561 g->layout = pango_cairo_create_layout(g->cr);
2562
2563 if(g->desc) pango_font_description_free(g->desc);
2564 g->desc = pango_font_description_copy_static(dt_bauhaus_get_global()->pango_font_desc);
2565
2566 pango_layout_set_font_description(g->layout, g->desc);
2568 g->context = gtk_widget_get_style_context(widget);
2569
2570 char text[256];
2571
2572 // Get the text line height for spacing
2573 snprintf(text, sizeof(text), "X");
2574 pango_layout_set_text(g->layout, text, -1);
2575 pango_layout_get_pixel_extents(g->layout, &g->ink, NULL);
2576 g->line_height = g->ink.height;
2577
2578 // Get the width of a minus sign for legend labels spacing
2579 snprintf(text, sizeof(text), "-");
2580 pango_layout_set_text(g->layout, text, -1);
2581 pango_layout_get_pixel_extents(g->layout, &g->ink, NULL);
2582 g->sign_width = g->ink.width / 2.0;
2583
2584 // Set the sizes, margins and paddings
2585 g->inner_padding = INNER_PADDING;
2586 g->inset = g->inner_padding + dt_bauhaus_get_global()->quad_width;
2587 g->graph_left_space = g->line_height + g->inner_padding;
2588 g->graph_width = g->allocation.width - g->inset - 2.0 * g->line_height; // align the right border on sliders
2589 g->graph_height = g->allocation.height - g->inset - 2.0 * g->line_height; // give room to nodes
2590 g->gradient_left_limit = 0.0;
2591 g->gradient_right_limit = g->graph_width;
2592 g->gradient_top_limit = g->graph_height + 2 * g->inner_padding;
2593 g->gradient_width = g->gradient_right_limit - g->gradient_left_limit;
2594 g->legend_top_limit = -0.5 * g->line_height - 2.0 * g->inner_padding;
2595 g->x_label = g->graph_width + g->sign_width + 3.0 * g->inner_padding;
2596
2597 gtk_render_background(g->context, g->cr, 0, 0, g->allocation.width, g->allocation.height);
2598
2599 // set the graph as the origin of the coordinates
2600 cairo_translate(g->cr, g->line_height + 2 * g->inner_padding, g->line_height + 3 * g->inner_padding);
2601
2602 // display x-axis and y-axis legends (EV)
2603 set_color(g->cr, dt_bauhaus_get_global()->graph_fg);
2604
2605 float value = -8.0f;
2606
2607 for(int k = 0; k < CHANNELS; k++)
2608 {
2609 const float xn = (((float)k) / ((float)(CHANNELS - 1))) * g->graph_width - g->sign_width;
2610 snprintf(text, sizeof(text), "%+.0f", value);
2611 pango_layout_set_text(g->layout, text, -1);
2612 pango_layout_get_pixel_extents(g->layout, &g->ink, NULL);
2613 cairo_move_to(g->cr, xn - 0.5 * g->ink.width - g->ink.x,
2614 g->legend_top_limit - 0.5 * g->ink.height - g->ink.y);
2615 pango_cairo_show_layout(g->cr, g->layout);
2616 cairo_stroke(g->cr);
2617
2618 value += 1.0;
2619 }
2620
2621 value = 2.0f;
2622
2623 for(int k = 0; k < 5; k++)
2624 {
2625 const float yn = (k / 4.0f) * g->graph_height;
2626 snprintf(text, sizeof(text), "%+.0f", value);
2627 pango_layout_set_text(g->layout, text, -1);
2628 pango_layout_get_pixel_extents(g->layout, &g->ink, NULL);
2629 cairo_move_to(g->cr, g->x_label - 0.5 * g->ink.width - g->ink.x,
2630 yn - 0.5 * g->ink.height - g->ink.y);
2631 pango_cairo_show_layout(g->cr, g->layout);
2632 cairo_stroke(g->cr);
2633
2634 value -= 1.0;
2635 }
2636
2638 // Draw the perceptually even gradient
2639 cairo_pattern_t *grad;
2640 grad = cairo_pattern_create_linear(g->gradient_left_limit, 0.0, g->gradient_right_limit, 0.0);
2642 cairo_set_line_width(g->cr, 0.0);
2643 cairo_rectangle(g->cr, g->gradient_left_limit, g->gradient_top_limit, g->gradient_width, g->line_height);
2644 cairo_set_source(g->cr, grad);
2645 cairo_fill(g->cr);
2646 cairo_pattern_destroy(grad);
2647
2649 // Draw the perceptually even gradient
2650 grad = cairo_pattern_create_linear(0.0, g->graph_height, 0.0, 0.0);
2652 cairo_set_line_width(g->cr, 0.0);
2653 cairo_rectangle(g->cr, -g->line_height - 2 * g->inner_padding, 0.0, g->line_height, g->graph_height);
2654 cairo_set_source(g->cr, grad);
2655 cairo_fill(g->cr);
2656
2657 cairo_pattern_destroy(grad);
2658
2659 // Draw frame borders
2660 cairo_set_line_width(g->cr, DT_PIXEL_APPLY_DPI(0.5));
2661 set_color(g->cr, dt_bauhaus_get_global()->graph_border);
2662 cairo_rectangle(g->cr, 0, 0, g->graph_width, g->graph_height);
2663 cairo_stroke_preserve(g->cr);
2664
2665 // end of caching section, this will not be drawn again
2666
2667 g->graph_valid = 1;
2668
2669 return TRUE;
2670}
2671
2672
2673// must be called while holding self->gui_lock
2675{
2676 if(IS_NULL_PTR(g)) return;
2677
2678 if(!g->valid_nodes_x && g->graph_width > 0)
2679 {
2680 for(int i = 0; i < CHANNELS; ++i)
2681 g->nodes_x[i] = (((float)i) / ((float)(CHANNELS - 1))) * g->graph_width;
2682 g->valid_nodes_x = TRUE;
2683 }
2684}
2685
2686
2687// must be called while holding self->gui_lock
2689{
2690 if(IS_NULL_PTR(g)) return;
2691
2692 if(g->user_param_valid && g->graph_height > 0)
2693 {
2694 for(int i = 0; i < CHANNELS; ++i)
2695 g->nodes_y[i] = (0.5 - log2f(g->temp_user_params[i]) / 4.0) * g->graph_height; // assumes factors in [-2 ; 2] EV
2696 g->valid_nodes_y = TRUE;
2697 }
2698}
2699
2700
2701static gboolean area_draw(GtkWidget *widget, cairo_t *cr, gpointer user_data)
2702{
2703 // Draw the widget equalizer view
2704 dt_iop_module_t *self = (dt_iop_module_t *)user_data;
2706 if(IS_NULL_PTR(g)) return FALSE;
2707
2708 // Init or refresh the drawing cache
2709 //if(!g->graph_valid)
2710 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
2711
2712 // since the widget sizes are not cached and invalidated properly above (yet...)
2713 // force the invalidation of the nodes coordinates to account for possible widget resizing
2714 g->valid_nodes_x = FALSE;
2715 g->valid_nodes_y = FALSE;
2716
2717 // Refresh cached UI elements
2718 update_histogram(self);
2719 update_curve_lut(self);
2720
2721 // Draw graph background
2722 cairo_set_line_width(g->cr, DT_PIXEL_APPLY_DPI(0.5));
2723 cairo_rectangle(g->cr, 0, 0, g->graph_width, g->graph_height);
2724 set_color(g->cr, dt_bauhaus_get_global()->graph_bg);
2725 cairo_fill(g->cr);
2726
2727 // draw grid
2728 cairo_set_line_width(g->cr, DT_PIXEL_APPLY_DPI(0.5));
2729 set_color(g->cr, dt_bauhaus_get_global()->graph_border);
2730 dt_draw_grid(g->cr, 8, 0, 0, g->graph_width, g->graph_height);
2731
2732 // draw ground level
2733 set_color(g->cr, dt_bauhaus_get_global()->graph_fg);
2734 cairo_set_line_width(g->cr, DT_PIXEL_APPLY_DPI(1));
2735 cairo_move_to(g->cr, 0, 0.5 * g->graph_height);
2736 cairo_line_to(g->cr, g->graph_width, 0.5 * g->graph_height);
2737 cairo_stroke(g->cr);
2738
2739 if(g->histogram_valid && self->enabled)
2740 {
2741 // draw the inset histogram
2742 set_color(g->cr, dt_bauhaus_get_global()->inset_histogram);
2743 cairo_set_line_width(g->cr, DT_PIXEL_APPLY_DPI(4.0));
2744 cairo_move_to(g->cr, 0, g->graph_height);
2745
2746 for(int k = 0; k < UI_SAMPLES; k++)
2747 {
2748 // the x range is [-8;+0] EV
2749 const float x_temp = (8.0 * (float)k / (float)(UI_SAMPLES - 1)) - 8.0;
2750 const float y_temp = (float)(g->histogram[k]) / (float)(g->max_histogram) * 0.96;
2751 cairo_line_to(g->cr, (x_temp + 8.0) * g->graph_width / 8.0,
2752 (1.0 - y_temp) * g->graph_height );
2753 }
2754 cairo_line_to(g->cr, g->graph_width, g->graph_height);
2755 cairo_close_path(g->cr);
2756 cairo_fill(g->cr);
2757
2758 if(g->histogram_last_decile > -0.1f)
2759 {
2760 // histogram overflows controls in highlights : display warning
2761 cairo_save(g->cr);
2762 cairo_set_source_rgb(g->cr, 0.75, 0.50, 0.);
2763 dtgtk_cairo_paint_gamut_check(g->cr, g->graph_width - 2.5 * g->line_height, 0.5 * g->line_height,
2764 2.0 * g->line_height, 2.0 * g->line_height, 0, NULL);
2765 cairo_restore(g->cr);
2766 }
2767
2768 if(g->histogram_first_decile < -7.9f)
2769 {
2770 // histogram overflows controls in lowlights : display warning
2771 cairo_save(g->cr);
2772 cairo_set_source_rgb(g->cr, 0.75, 0.50, 0.);
2773 dtgtk_cairo_paint_gamut_check(g->cr, 0.5 * g->line_height, 0.5 * g->line_height,
2774 2.0 * g->line_height, 2.0 * g->line_height, 0, NULL);
2775 cairo_restore(g->cr);
2776 }
2777 }
2778
2779 if(g->lut_valid)
2780 {
2781 // draw the interpolation curve
2782 set_color(g->cr, dt_bauhaus_get_global()->graph_fg);
2783 cairo_move_to(g->cr, 0, g->gui_lut[0] * g->graph_height);
2784 cairo_set_line_width(g->cr, DT_PIXEL_APPLY_DPI(3));
2785
2786 for(int k = 1; k < UI_SAMPLES; k++)
2787 {
2788 // the x range is [-8;+0] EV
2789 const float x_temp = (8.0f * (((float)k) / ((float)(UI_SAMPLES - 1)))) - 8.0f;
2790 const float y_temp = g->gui_lut[k];
2791
2792 cairo_line_to(g->cr, (x_temp + 8.0f) * g->graph_width / 8.0f,
2793 y_temp * g->graph_height );
2794 }
2795 cairo_stroke(g->cr);
2796 }
2797
2798 init_nodes_x(g);
2799 init_nodes_y(g);
2800
2801 if(g->user_param_valid)
2802 {
2803 // draw nodes positions
2804 for(int k = 0; k < CHANNELS; k++)
2805 {
2806 const float xn = g->nodes_x[k];
2807 const float yn = g->nodes_y[k];
2808
2809 // fill bars
2810 cairo_set_line_width(g->cr, DT_PIXEL_APPLY_DPI(6));
2811 set_color(g->cr, dt_bauhaus_get_global()->color_fill);
2812 cairo_move_to(g->cr, xn, 0.5 * g->graph_height);
2813 cairo_line_to(g->cr, xn, yn);
2814 cairo_stroke(g->cr);
2815
2816 // bullets
2817 cairo_set_line_width(g->cr, DT_PIXEL_APPLY_DPI(3));
2818 cairo_arc(g->cr, xn, yn, DT_PIXEL_APPLY_DPI(4), 0, 2. * M_PI);
2819 set_color(g->cr, dt_bauhaus_get_global()->graph_fg);
2820 cairo_stroke_preserve(g->cr);
2821
2822 if(g->area_active_node == k)
2823 set_color(g->cr, dt_bauhaus_get_global()->graph_fg);
2824 else
2825 set_color(g->cr, dt_bauhaus_get_global()->graph_bg);
2826
2827 cairo_fill(g->cr);
2828 }
2829 }
2830
2831 if(self->enabled)
2832 {
2833 if(g->area_cursor_valid)
2834 {
2835 const float radius = g->sigma * g->graph_width / 8.0f / sqrtf(2.0f);
2836 cairo_set_line_width(g->cr, DT_PIXEL_APPLY_DPI(1.5));
2837 const float y =g->gui_lut[(int)CLAMP(((UI_SAMPLES - 1) * g->area_x / g->graph_width), 0, UI_SAMPLES - 1)];
2838 cairo_arc(g->cr, g->area_x, y * g->graph_height, radius, 0, 2. * M_PI);
2839 set_color(g->cr, dt_bauhaus_get_global()->graph_fg);
2840 cairo_stroke(g->cr);
2841 }
2842
2843 if(g->cursor_valid)
2844 {
2845
2846 float x_pos = (g->cursor_exposure + 8.0f) / 8.0f * g->graph_width;
2847
2848 if(x_pos > g->graph_width || x_pos < 0.0f)
2849 {
2850 // exposure at current position is outside [-8; 0] EV :
2851 // bound it in the graph limits and show it in orange
2852 cairo_set_source_rgb(g->cr, 0.75, 0.50, 0.);
2853 cairo_set_line_width(g->cr, DT_PIXEL_APPLY_DPI(3));
2854 x_pos = (x_pos < 0.0f) ? 0.0f : g->graph_width;
2855 }
2856 else
2857 {
2858 set_color(g->cr, dt_bauhaus_get_global()->graph_fg);
2859 cairo_set_line_width(g->cr, DT_PIXEL_APPLY_DPI(1.5));
2860 }
2861
2862 cairo_move_to(g->cr, x_pos, 0.0);
2863 cairo_line_to(g->cr, x_pos, g->graph_height);
2864 cairo_stroke(g->cr);
2865 }
2866 }
2867
2868 // clean and exit
2869 cairo_set_source_surface(cr, g->cst, 0, 0);
2870 cairo_paint(cr);
2871
2872 return TRUE;
2873}
2874
2875static gboolean area_enter_notify(GtkWidget *widget, GdkEventCrossing *event, gpointer user_data)
2876{
2877 dt_iop_module_t *self = (dt_iop_module_t *)user_data;
2878 if(dt_gui_widgets_suppressed()) return 1;
2879 if(!self->enabled) return 0;
2880
2882 g->area_x = (event->x - g->inset);
2883 g->area_y = (event->y - g->inset);
2884 g->area_dragging = FALSE;
2885 g->area_active_node = -1;
2886 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);
2887
2888 gtk_widget_queue_draw(GTK_WIDGET(g->area));
2889 return TRUE;
2890}
2891
2892
2893static gboolean area_leave_notify(GtkWidget *widget, GdkEventCrossing *event, gpointer user_data)
2894{
2895 dt_iop_module_t *self = (dt_iop_module_t *)user_data;
2896 if(dt_gui_widgets_suppressed()) return 1;
2897 if(!self->enabled) return 0;
2898
2901
2902 if(g->area_dragging)
2903 {
2904 // cursor left area : force commit to avoid glitches
2906
2907 dt_dev_add_history_item(self->dev, self, FALSE, TRUE);
2908 }
2909 g->area_x = (event->x - g->inset);
2910 g->area_y = (event->y - g->inset);
2911 g->area_dragging = FALSE;
2912 g->area_active_node = -1;
2913 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);
2914
2915 gtk_widget_queue_draw(GTK_WIDGET(g->area));
2916 return TRUE;
2917}
2918
2919
2920static gboolean area_button_press(GtkWidget *widget, GdkEventButton *event, gpointer user_data)
2921{
2922 dt_iop_module_t *self = (dt_iop_module_t *)user_data;
2923 if(dt_gui_widgets_suppressed()) return 1;
2924
2926
2928
2929 if(event->button == 1 && event->type == GDK_2BUTTON_PRESS)
2930 {
2933
2934 // reset nodes params
2935 p->noise = d->noise;
2936 p->ultra_deep_blacks = d->ultra_deep_blacks;
2937 p->deep_blacks = d->deep_blacks;
2938 p->blacks = d->blacks;
2939 p->shadows = d->shadows;
2940 p->midtones = d->midtones;
2941 p->highlights = d->highlights;
2942 p->whites = d->whites;
2943 p->speculars = d->speculars;
2944
2945 // update UI sliders
2947
2948 // Redraw graph
2949 gtk_widget_queue_draw(self->gui->widget);
2950 dt_dev_add_history_item(self->dev, self, TRUE, TRUE);
2951 return TRUE;
2952 }
2953 else if(event->button == 1)
2954 {
2955 if(self->enabled)
2956 {
2957 g->area_dragging = 1;
2958 gtk_widget_queue_draw(GTK_WIDGET(g->area));
2959 }
2960 else
2961 {
2962 dt_dev_add_history_item(self->dev, self, TRUE, TRUE);
2963 }
2964 return TRUE;
2965 }
2966
2967 // Unlock the colour picker so we can display our own custom cursor
2969
2970 return FALSE;
2971}
2972
2973
2974static gboolean area_motion_notify(GtkWidget *widget, GdkEventMotion *event, gpointer user_data)
2975{
2976 dt_iop_module_t *self = (dt_iop_module_t *)user_data;
2977 if(dt_gui_widgets_suppressed()) return 1;
2978 if(!self->enabled) return 0;
2979
2982
2983 if(g->area_dragging)
2984 {
2985 // vertical distance travelled since button_pressed event
2986 const float offset = (-event->y + g->area_y) / g->graph_height * 4.0f; // graph spans over 4 EV
2987 const float cursor_exposure = g->area_x / g->graph_width * 8.0f - 8.0f;
2988
2989 // Get the desired correction on exposure channels
2990 g->area_dragging = set_new_params_interactive(cursor_exposure, offset, g->sigma * g->sigma / 2.0f, g, p);
2991 }
2992
2993 g->area_x = (event->x - g->graph_left_space);
2994 g->area_y = event->y;
2995 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);
2996 g->area_active_node = -1;
2997
2998 // Search if cursor is close to a node
2999 if(g->valid_nodes_x)
3000 {
3001 const float radius_threshold = fabsf(g->nodes_x[1] - g->nodes_x[0]) * 0.45f;
3002 for(int i = 0; i < CHANNELS; ++i)
3003 {
3004 const float delta_x = fabsf(g->area_x - g->nodes_x[i]);
3005 if(delta_x < radius_threshold)
3006 {
3007 g->area_active_node = i;
3008 g->area_cursor_valid = 1;
3009 }
3010 }
3011 }
3012
3013 gtk_widget_queue_draw(GTK_WIDGET(g->area));
3014 return TRUE;
3015}
3016
3017
3018static gboolean area_button_release(GtkWidget *widget, GdkEventButton *event, gpointer user_data)
3019{
3020 dt_iop_module_t *self = (dt_iop_module_t *)user_data;
3021 if(dt_gui_widgets_suppressed()) return 1;
3022 if(!self->enabled) return 0;
3023
3025
3026 // Give focus to module
3028
3029 if(event->button == 1)
3030 {
3032
3033 if(g->area_dragging)
3034 {
3035 // Update GUI with new params
3037 dt_dev_add_history_item(self->dev, self, FALSE, TRUE);
3038 g->area_dragging= 0;
3039 return TRUE;
3040 }
3041 }
3042 return FALSE;
3043}
3044
3045
3046static gboolean notebook_button_press(GtkWidget *widget, GdkEventButton *event, gpointer user_data)
3047{
3048 dt_iop_module_t *self = (dt_iop_module_t *)user_data;
3049 if(dt_gui_widgets_suppressed()) return 1;
3050
3051 // Give focus to module
3053
3054 // Unlock the colour picker so we can display our own custom cursor
3056
3057 return 0;
3058}
3059
3065static void _develop_ui_pipe_started_callback(gpointer instance, gpointer user_data)
3066{
3067 dt_iop_module_t *self = (dt_iop_module_t *)user_data;
3069 if(IS_NULL_PTR(g)) return;
3070 _switch_cursors(self);
3071
3072 // if module is not active, disable mask preview
3073 if(!self->gui->expanded || !self->enabled)
3074 {
3075 g->mask_display = 0;
3076 }
3077
3079 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->show_luminance_mask), g->mask_display);
3081}
3082
3083
3084static void _develop_history_resync_callback(gpointer instance, gpointer user_data)
3085{
3086 (void)instance;
3087 dt_iop_module_t *self = (dt_iop_module_t *)user_data;
3089 if(IS_NULL_PTR(g) || IS_NULL_PTR(self->dev) || IS_NULL_PTR(self->dev->preview_pipe)) return;
3090
3091 // One read of the pipe piece for both the hash and the dimensions it describes: this callback
3092 // answers DT_SIGNAL_HISTORY_RESYNC, i.e. it runs while the worker is replanning the pipes, and
3093 // a second read would routinely land on a different plan. See gui_focus() for what pairing two
3094 // plans costs.
3095 size_t preview_width = 0;
3096 size_t preview_height = 0;
3097 const uint64_t preview_hash = _current_preview_luminance_hash(self, &preview_width, &preview_height);
3098 if(preview_hash == DT_PIXELPIPE_CACHE_HASH_INVALID)
3099 {
3101 g->pending_preview_hash = DT_PIXELPIPE_CACHE_HASH_INVALID;
3103 _switch_cursors(self);
3104 gtk_widget_queue_draw(GTK_WIDGET(g->area));
3105 return;
3106 }
3107
3108 gboolean already_attached = FALSE;
3110 if(!IS_NULL_PTR(g->thumb_preview_entry) && g->thumb_preview_hash == preview_hash && g->luminance_valid)
3111 {
3112 g->pending_preview_hash = DT_PIXELPIPE_CACHE_HASH_INVALID;
3113 already_attached = TRUE;
3114 }
3116
3117 if(!already_attached)
3118 {
3119 void *preview_buf = NULL;
3120 dt_pixel_cache_entry_t *preview_entry = NULL;
3121 gboolean preview_ready = dt_dev_pixelpipe_cache_ref_entry_by_hash(preview_hash,
3122 &preview_buf, &preview_entry);
3123 if(preview_ready && (IS_NULL_PTR(preview_buf)
3124 || !luminance_entry_fits(preview_entry, preview_width, preview_height)))
3125 {
3126 if(!IS_NULL_PTR(preview_entry))
3128 preview_ready = FALSE;
3129 }
3130
3131 if(preview_ready)
3132 {
3133 dt_pixel_cache_entry_t *old_entry = NULL;
3134 gboolean keep_new_entry = FALSE;
3136 if(g->thumb_preview_entry != preview_entry || g->thumb_preview_hash != preview_hash
3137 || g->thumb_preview_buf_width != preview_width || g->thumb_preview_buf_height != preview_height
3138 || !g->luminance_valid)
3139 {
3140 old_entry = g->thumb_preview_entry;
3141 g->thumb_preview_entry = preview_entry;
3142 g->thumb_preview_hash = preview_hash;
3143 g->pending_preview_hash = DT_PIXELPIPE_CACHE_HASH_INVALID;
3144 g->thumb_preview_buf_width = preview_width;
3145 g->thumb_preview_buf_height = preview_height;
3146 g->luminance_valid = TRUE;
3147 g->histogram_valid = FALSE;
3148 keep_new_entry = TRUE;
3149 }
3150 else
3151 g->pending_preview_hash = DT_PIXELPIPE_CACHE_HASH_INVALID;
3153
3154 if(old_entry)
3156 if(!keep_new_entry)
3158 }
3159 else
3160 {
3162 g->pending_preview_hash = preview_hash;
3164 }
3165 }
3166
3167 _switch_cursors(self);
3168 gtk_widget_queue_draw(GTK_WIDGET(g->area));
3169}
3170
3171static void _develop_cacheline_ready_callback(gpointer instance, const guint64 hash,
3172 const guint64 producer_node_key, gpointer user_data)
3173{
3174 (void)instance;
3175 (void)producer_node_key;
3176 dt_iop_module_t *self = (dt_iop_module_t *)user_data;
3178 if(IS_NULL_PTR(g) || IS_NULL_PTR(self->dev) || IS_NULL_PTR(self->dev->preview_pipe)) return;
3179
3181 const gboolean matched = (g->pending_preview_hash == hash);
3183 if(!matched) return;
3184
3185 size_t preview_width = 0;
3186 size_t preview_height = 0;
3187 const uint64_t preview_hash = _current_preview_luminance_hash(self, &preview_width, &preview_height);
3188 if(preview_hash != hash) return;
3189
3190 void *preview_buf = NULL;
3191 dt_pixel_cache_entry_t *preview_entry = NULL;
3192 const gboolean preview_ready = dt_dev_pixelpipe_cache_ref_entry_by_hash(preview_hash,
3193 &preview_buf, &preview_entry);
3194 if(!preview_ready || IS_NULL_PTR(preview_buf)
3195 || !luminance_entry_fits(preview_entry, preview_width, preview_height))
3196 {
3197 if(!IS_NULL_PTR(preview_entry))
3199 return;
3200 }
3201
3202 dt_pixel_cache_entry_t *old_entry = NULL;
3203 gboolean keep_new_entry = FALSE;
3205 if(g->thumb_preview_entry != preview_entry || g->thumb_preview_hash != preview_hash
3206 || g->thumb_preview_buf_width != preview_width || g->thumb_preview_buf_height != preview_height
3207 || !g->luminance_valid)
3208 {
3209 old_entry = g->thumb_preview_entry;
3210 g->thumb_preview_entry = preview_entry;
3211 g->thumb_preview_hash = preview_hash;
3212 g->thumb_preview_buf_width = preview_width;
3213 g->thumb_preview_buf_height = preview_height;
3214 g->luminance_valid = TRUE;
3215 g->histogram_valid = FALSE;
3216 keep_new_entry = TRUE;
3217 }
3218 g->pending_preview_hash = DT_PIXELPIPE_CACHE_HASH_INVALID;
3220
3221 if(old_entry)
3223 if(!keep_new_entry)
3225
3226 _switch_cursors(self);
3227 gtk_widget_queue_draw(GTK_WIDGET(g->area));
3228}
3229
3230
3231static void _develop_ui_pipe_finished_callback(gpointer instance, gpointer user_data)
3232{
3233 dt_iop_module_t *self = (dt_iop_module_t *)user_data;
3235 if(IS_NULL_PTR(g)) return;
3236 _switch_cursors(self);
3237}
3238
3239
3240void gui_reset(struct dt_iop_module_t *self)
3241{
3243 if(IS_NULL_PTR(g)) return;
3245 dt_bauhaus_widget_set_quad_active(g->exposure_boost, FALSE);
3246 dt_bauhaus_widget_set_quad_active(g->contrast_boost, FALSE);
3247 dt_dev_add_history_item(self->dev, self, TRUE, TRUE);
3248
3249 // Redraw graph
3250 gtk_widget_queue_draw(self->gui->widget);
3251}
3252
3253static gboolean _sample_picker_luminance_mask(const dt_develop_t *const dev, const float *const buffer,
3254 const size_t width, const size_t height,
3255 float *const picked, float *const picked_min, float *const picked_max)
3256{
3257 const dt_colorpicker_sample_t *const sample = dev ? dev->color_picker.primary_sample : NULL;
3258 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;
3259
3260 if(sample->size == DT_LIB_COLORPICKER_SIZE_BOX)
3261 {
3262 const size_t box[4] = {
3263 CLAMP((size_t)roundf(sample->box[0] * width), 0, width),
3264 CLAMP((size_t)roundf(sample->box[1] * height), 0, height),
3265 CLAMP((size_t)roundf(sample->box[2] * width), 0, width),
3266 CLAMP((size_t)roundf(sample->box[3] * height), 0, height)
3267 };
3268 const size_t x0 = MIN(box[0], width - 1);
3269 const size_t y0 = MIN(box[1], height - 1);
3270 const size_t x1 = CLAMP(MAX(box[2], x0 + 1), 1, width);
3271 const size_t y1 = CLAMP(MAX(box[3], y0 + 1), 1, height);
3272
3273 float mean = 0.0f;
3274 float minimum = INFINITY;
3275 float maximum = -INFINITY;
3276 size_t count = 0;
3277
3278 // Browse the exact picker box on the preview luminance mask so picker feedback
3279 // reflects the same scalar field tone equalizer actually edits.
3280 for(size_t y = y0; y < y1; ++y)
3281 {
3282 const size_t row = y * width;
3283 for(size_t x = x0; x < x1; ++x)
3284 {
3285 const float value = buffer[row + x];
3286 mean += value;
3287 minimum = fminf(minimum, value);
3288 maximum = fmaxf(maximum, value);
3289 ++count;
3290 }
3291 }
3292
3293 if(count == 0) return FALSE;
3294 *picked = mean / (float)count;
3295 *picked_min = minimum;
3296 *picked_max = maximum;
3297 return isfinite(*picked) && isfinite(*picked_min) && isfinite(*picked_max);
3298 }
3299
3300 const size_t x = CLAMP((size_t)roundf(sample->point[0] * width), 0, width - 1);
3301 const size_t y = CLAMP((size_t)roundf(sample->point[1] * height), 0, height - 1);
3302 const float value = get_luminance_from_buffer(buffer, width, height, x, y);
3303 *picked = value;
3304 *picked_min = value;
3305 *picked_max = value;
3306 return isfinite(value);
3307}
3308
3331{
3334 dt_pixel_cache_entry_t *preview_entry = NULL;
3335 size_t preview_width = 0;
3336 size_t preview_height = 0;
3337
3338 if(IS_NULL_PTR(g) || (picker != g->exposure_boost && picker != g->contrast_boost))
3339 {
3340 dt_print(DT_DEBUG_DEV, "[picker/toneequal] passthrough picker=%p pipe=%p hash=%" PRIu64 "\n",
3341 (void *)picker, (void *)pipe, piece ? piece->global_hash : 0);
3342 _switch_cursors(self);
3343 return;
3344 }
3345
3346 g->area_active_node = -1;
3347
3348 // Picker callbacks can run while the preview pipe publishes a newer luminance
3349 // cacheline, so take the cache ref under the same GUI state lock as other readers.
3351 preview_entry = g->thumb_preview_entry;
3352 preview_width = g->thumb_preview_buf_width;
3353 preview_height = g->thumb_preview_buf_height;
3354 if(!IS_NULL_PTR(preview_entry))
3357
3358 if(IS_NULL_PTR(preview_entry) || preview_width < 1 || preview_height < 1)
3359 {
3360 if(!IS_NULL_PTR(preview_entry))
3362 dt_print(DT_DEBUG_DEV, "[picker/toneequal] no preview mask picker=%p pipe=%p hash=%" PRIu64 "\n",
3363 (void *)picker, (void *)pipe, piece ? piece->global_hash : 0);
3364 _switch_cursors(self);
3365 return;
3366 }
3367
3369 const float *const preview_buf = (const float *const)dt_pixel_cache_entry_get_data(preview_entry);
3370 float picked = NAN;
3371 float picked_min = NAN;
3372 float picked_max = NAN;
3373 const gboolean sampled = _sample_picker_luminance_mask(self->dev, preview_buf, preview_width, preview_height,
3374 &picked, &picked_min, &picked_max);
3377
3378 if(!sampled)
3379 {
3380 dt_print(DT_DEBUG_DEV, "[picker/toneequal] mask sample failed picker=%p pipe=%p hash=%" PRIu64 "\n",
3381 (void *)picker, (void *)pipe, piece ? piece->global_hash : 0);
3382 _switch_cursors(self);
3383 return;
3384 }
3385
3386 g->cursor_valid = isfinite(picked) && picked > 0.0f;
3387 g->cursor_exposure = g->cursor_valid ? log2f(picked) : 0.0f;
3388
3389 if(picker == g->exposure_boost)
3390 {
3391 if(isfinite(picked) && picked > 0.0f)
3392 {
3393 p->exposure_boost = log2f(CONTRAST_FULCRUM / picked);
3395 dt_bauhaus_slider_set(g->exposure_boost, p->exposure_boost);
3398 dt_dev_add_history_item(self->dev, self, TRUE, TRUE);
3400 "[picker/toneequal] exposure picker=%p luminance=%g set=%g pipe=%p hash=%" PRIu64 "\n",
3401 (void *)picker, picked, p->exposure_boost, (void *)pipe, piece ? piece->global_hash : 0);
3402 }
3403 else
3404 {
3406 "[picker/toneequal] exposure picker=%p invalid luminance=%g pipe=%p hash=%" PRIu64 "\n",
3407 (void *)picker, picked, (void *)pipe, piece ? piece->global_hash : 0);
3408 }
3409 }
3410 else
3411 {
3412 const float fd_old = fminf(picked_min, picked_max);
3413 const float ld_old = fmaxf(picked_min, picked_max);
3414
3415 if(isfinite(fd_old) && isfinite(ld_old) && fd_old > 0.0f && ld_old > fd_old)
3416 {
3417 const float s1 = CONTRAST_FULCRUM - exp2f(-7.0f);
3418 const float s2 = exp2f(-1.0f) - CONTRAST_FULCRUM;
3419 const float mix = fd_old * s2 + ld_old * s1;
3420 float contrast = log2f(mix / (CONTRAST_FULCRUM * (ld_old - fd_old)));
3421
3422 // Blur-assisted detail modes need the same positive-contrast correction as
3423 // the legacy auto button because the sampled spread is measured upstream of
3424 // the guided filter blur and would otherwise undershoot in the final mask.
3425 if(p->details == DT_TONEEQ_EIGF && contrast > 0.0f)
3426 {
3427 const float correction = -0.0276f + 0.01823f * p->feathering + (0.7566f - 1.0f) * contrast;
3428 if(p->feathering < 5.0f)
3429 contrast += correction;
3430 else if(p->feathering < 10.0f)
3431 contrast += correction * (2.0f - p->feathering / 5.0f);
3432 }
3433 else if(p->details == DT_TONEEQ_GUIDED && contrast > 0.0f)
3434 {
3435 contrast = 0.0235f + 1.1225f * contrast;
3436 }
3437
3438 p->contrast_boost = contrast;
3440 dt_bauhaus_slider_set(g->contrast_boost, p->contrast_boost);
3443 dt_dev_add_history_item(self->dev, self, TRUE, TRUE);
3445 "[picker/toneequal] contrast picker=%p min=%g max=%g set=%g pipe=%p hash=%" PRIu64 "\n",
3446 (void *)picker, fd_old, ld_old, p->contrast_boost, (void *)pipe,
3447 piece ? piece->global_hash : 0);
3448 }
3449 else
3450 {
3452 "[picker/toneequal] contrast picker=%p invalid min=%g max=%g pipe=%p hash=%" PRIu64 "\n",
3453 (void *)picker, fd_old, ld_old, (void *)pipe, piece ? piece->global_hash : 0);
3454 }
3455 }
3456
3458 gtk_widget_queue_draw(GTK_WIDGET(g->area));
3459 _switch_cursors(self);
3460}
3461
3462
3463void autoset(struct dt_iop_module_t *self, const struct dt_dev_pixelpipe_t *pipe,
3464 const struct dt_dev_pixelpipe_iop_t *piece, const void *i)
3465{
3466 if(piece->dsc_in.channels != 4) return;
3467
3469 const dt_iop_roi_t *const roi_out = &piece->roi_out;
3470 const size_t width = roi_out->width;
3471 const size_t height = roi_out->height;
3472 const size_t num_elem = width * height;
3474 if(IS_NULL_PTR(luminance)) return;
3475
3476 // Build the same luminance mask scalar field the picker edits, but with neutral
3477 // boost/contrast because autoset can only solve the exposure translation.
3478 luminance_mask((const float *const)i, luminance, width, height, piece->dsc_in.channels, p->method, 1.0f, 0.0f, 1.0f);
3479
3480 float mean = 0.0f;
3481 size_t count = 0;
3482 __OMP_PARALLEL_FOR__(reduction(+:mean, count))
3483 for(size_t k = 0; k < num_elem; ++k)
3484 {
3485 const float value = luminance[k];
3486 if(!isfinite(value) || value <= 0.0f) continue;
3487 mean += value;
3488 count++;
3489 }
3490
3492 if(count == 0) return;
3493
3494 const float picked = mean / (float)count;
3495 p->exposure_boost = log2f(CONTRAST_FULCRUM / picked);
3496}
3497
3498void gui_init(struct dt_iop_module_t *self)
3499{
3501
3502 gui_cache_init(self);
3503
3504 g->notebook = dt_ui_notebook_new();
3505 dt_ui_notebook_set_picker_owner(g->notebook, self);
3506
3507 // Advanced view
3508
3509 self->gui->widget = dt_ui_notebook_page(g->notebook, N_("graph"), NULL);
3510
3511 g->area = GTK_DRAWING_AREA(gtk_drawing_area_new());
3512 gtk_widget_set_hexpand(GTK_WIDGET(g->area), TRUE);
3513 gtk_box_pack_start(GTK_BOX(self->gui->widget),
3514 dt_ui_resizable_drawing_area(GTK_WIDGET(g->area),
3515 "plugins/darkroom/toneequal/graphheight", 280, 120),
3516 FALSE, FALSE, 0);
3517 gtk_widget_add_events(GTK_WIDGET(g->area), GDK_POINTER_MOTION_MASK | dt_widget_scroll_mask()
3518 | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK
3519 | GDK_ENTER_NOTIFY_MASK | GDK_LEAVE_NOTIFY_MASK);
3520 gtk_widget_set_can_focus(GTK_WIDGET(g->area), TRUE);
3521 g_signal_connect(G_OBJECT(g->area), "draw", G_CALLBACK(area_draw), self);
3522 g_signal_connect(G_OBJECT(g->area), "button-press-event", G_CALLBACK(area_button_press), self);
3523 g_signal_connect(G_OBJECT(g->area), "button-release-event", G_CALLBACK(area_button_release), self);
3524 g_signal_connect(G_OBJECT(g->area), "leave-notify-event", G_CALLBACK(area_leave_notify), self);
3525 g_signal_connect(G_OBJECT(g->area), "enter-notify-event", G_CALLBACK(area_enter_notify), self);
3526 g_signal_connect(G_OBJECT(g->area), "motion-notify-event", G_CALLBACK(area_motion_notify), self);
3527 gtk_widget_set_tooltip_text(GTK_WIDGET(g->area), _("double-click to reset the curve"));
3528
3529 g->smoothing = dt_bauhaus_slider_new_with_range(dt_bauhaus_get_global(), DT_GUI_MODULE(self), -2.33f, +1.67f, 0, 0.0f, 2);
3530 dt_bauhaus_slider_set_soft_range(g->smoothing, -1.0f, 1.0f);
3531 dt_bauhaus_widget_set_label(g->smoothing, N_("curve smoothing"));
3532 gtk_widget_set_tooltip_text(g->smoothing, _("positive values will produce more progressive tone transitions\n"
3533 "but the curve might become oscillatory in some settings.\n"
3534 "negative values will avoid oscillations and behave more robustly\n"
3535 "but may produce brutal tone transitions and damage local contrast."));
3536 gtk_box_pack_start(GTK_BOX(self->gui->widget), g->smoothing, FALSE, FALSE, 0);
3537 g_signal_connect(G_OBJECT(g->smoothing), "value-changed", G_CALLBACK(smoothing_callback), self);
3538
3539 g->exposure_boost = dt_color_picker_new(self, DT_COLOR_PICKER_AREA,
3540 dt_bauhaus_slider_from_params(self, "exposure_boost"));
3541 dt_bauhaus_slider_set_soft_range(g->exposure_boost, -4.0, 4.0);
3542 dt_bauhaus_slider_set_format(g->exposure_boost, _(" EV"));
3543 gtk_widget_set_tooltip_text(g->exposure_boost, _("use this to slide the mask average exposure along channels\n"
3544 "for a better control of the exposure correction with the available nodes.\n"
3545 "the color picker will map the sampled tone to -4 EV."));
3546
3547 g->contrast_boost = dt_color_picker_new(self, DT_COLOR_PICKER_AREA,
3548 dt_bauhaus_slider_from_params(self, "contrast_boost"));
3549 dt_bauhaus_slider_set_soft_range(g->contrast_boost, -2.0, 2.0);
3550 dt_bauhaus_slider_set_format(g->contrast_boost, _(" EV"));
3551 gtk_widget_set_tooltip_text(g->contrast_boost, _("use this to counter the averaging effect of the guided filter\n"
3552 "and dilate the mask contrast around -4EV\n"
3553 "this allows to spread the exposure histogram over more channels\n"
3554 "for a better control of the exposure correction.\n"
3555 "the color picker will fit the sampled spread inside the control range."));
3556
3557 // Simple view
3558
3559 self->gui->widget = dt_ui_notebook_page(g->notebook, N_("sliders"), NULL);
3560
3561 g->noise = dt_bauhaus_slider_from_params(self, "noise");
3562 dt_bauhaus_slider_set_format(g->noise, _(" EV"));
3563
3564 g->ultra_deep_blacks = dt_bauhaus_slider_from_params(self, "ultra_deep_blacks");
3565 dt_bauhaus_slider_set_format(g->ultra_deep_blacks, _(" EV"));
3566
3567 g->deep_blacks = dt_bauhaus_slider_from_params(self, "deep_blacks");
3568 dt_bauhaus_slider_set_format(g->deep_blacks, _(" EV"));
3569
3570 g->blacks = dt_bauhaus_slider_from_params(self, "blacks");
3571 dt_bauhaus_slider_set_format(g->blacks, _(" EV"));
3572
3573 g->shadows = dt_bauhaus_slider_from_params(self, "shadows");
3574 dt_bauhaus_slider_set_format(g->shadows, _(" EV"));
3575
3576 g->midtones = dt_bauhaus_slider_from_params(self, "midtones");
3577 dt_bauhaus_slider_set_format(g->midtones, _(" EV"));
3578
3579 g->highlights = dt_bauhaus_slider_from_params(self, "highlights");
3580 dt_bauhaus_slider_set_format(g->highlights, _(" EV"));
3581
3582 g->whites = dt_bauhaus_slider_from_params(self, "whites");
3583 dt_bauhaus_slider_set_format(g->whites, _(" EV"));
3584
3585 g->speculars = dt_bauhaus_slider_from_params(self, "speculars");
3586 dt_bauhaus_slider_set_format(g->speculars, _(" EV"));
3587
3588 dt_bauhaus_widget_set_label(g->noise, N_("-8 EV"));
3589 dt_bauhaus_widget_set_label(g->ultra_deep_blacks, N_("-7 EV"));
3590 dt_bauhaus_widget_set_label(g->deep_blacks, N_("-6 EV"));
3591 dt_bauhaus_widget_set_label(g->blacks, N_("-5 EV"));
3592 dt_bauhaus_widget_set_label(g->shadows, N_("-4 EV"));
3593 dt_bauhaus_widget_set_label(g->midtones, N_("-3 EV"));
3594 dt_bauhaus_widget_set_label(g->highlights, N_("-2 EV"));
3595 dt_bauhaus_widget_set_label(g->whites, N_("-1 EV"));
3596 dt_bauhaus_widget_set_label(g->speculars, N_("+0 EV"));
3597
3598 // Masking options
3599
3600 self->gui->widget = dt_ui_notebook_page(g->notebook, N_("masking"), NULL);
3601
3602 g->method = dt_bauhaus_combobox_from_params(self, "method");
3604 gtk_widget_set_tooltip_text(g->method, _("preview the mask and chose the estimator that gives you the\n"
3605 "higher contrast between areas to dodge and areas to burn"));
3606
3607 g->details = dt_bauhaus_combobox_from_params(self, N_("details"));
3608 dt_bauhaus_widget_set_label(g->details, N_("preserve details"));
3609 gtk_widget_set_tooltip_text(g->details, _("'no' affects global and local contrast (safe if you only add contrast)\n"
3610 "'guided filter' only affects global contrast and tries to preserve local contrast\n"
3611 "'averaged guided filter' is a geometric mean of 'no' and 'guided filter' methods\n"
3612 "'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"
3613 "'averaged eigf' is a geometric mean of 'no' and 'exposure-independent guided filter' methods"));
3614
3615 g->iterations = dt_bauhaus_slider_from_params(self, "iterations");
3616 dt_bauhaus_slider_set_soft_max(g->iterations, 5);
3617 gtk_widget_set_tooltip_text(g->iterations, _("number of passes of guided filter to apply\n"
3618 "helps diffusing the edges of the filter at the expense of speed"));
3619
3620 g->blending = dt_bauhaus_slider_from_params(self, "blending");
3621 dt_bauhaus_slider_set_soft_range(g->blending, 1.0, 45.0);
3622 dt_bauhaus_slider_set_format(g->blending, "%");
3623 gtk_widget_set_tooltip_text(g->blending, _("diameter of the blur in percent of the largest image size\n"
3624 "warning: big values of this parameter can make the darkroom\n"
3625 "preview much slower if denoise profiled is used."));
3626
3627 g->feathering = dt_bauhaus_slider_from_params(self, "feathering");
3628 dt_bauhaus_slider_set_soft_range(g->feathering, 0.1, 50.0);
3629 gtk_widget_set_tooltip_text(g->feathering, _("precision of the feathering:\n"
3630 "higher values force the mask to follow edges more closely\n"
3631 "but may void the effect of the smoothing\n"
3632 "lower values give smoother gradients and better smoothing\n"
3633 "but may lead to inaccurate edges taping and halos"));
3634
3635 g->quantization = dt_bauhaus_slider_from_params(self, "quantization");
3636 dt_bauhaus_slider_set_format(g->quantization, _(" EV"));
3637 gtk_widget_set_tooltip_text(g->quantization, _("0 disables the quantization.\n"
3638 "higher values posterize the luminance mask to help the guiding\n"
3639 "produce piece-wise smooth areas when using high feathering values"));
3640
3641 // start building top level widget
3642 self->gui->widget = gtk_box_new(GTK_ORIENTATION_VERTICAL, DT_GUI_BOX_SPACING);
3643
3644 const int active_page = dt_conf_get_int("plugins/darkroom/toneequal/gui_page");
3645 gtk_widget_show(gtk_notebook_get_nth_page(g->notebook, active_page));
3646 gtk_notebook_set_current_page(g->notebook, active_page);
3647
3648 g_signal_connect(G_OBJECT(g->notebook), "button-press-event", G_CALLBACK(notebook_button_press), self);
3649 gtk_box_pack_start(GTK_BOX(self->gui->widget), GTK_WIDGET(g->notebook), FALSE, FALSE, 0);
3650
3651 GtkWidget *hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, DT_GUI_BOX_SPACING);
3652 gtk_box_pack_start(GTK_BOX(hbox), dt_ui_label_new(_("display exposure mask")), TRUE, TRUE, 0);
3653 g->show_luminance_mask = dt_iop_togglebutton_new(self, NULL, N_("display exposure mask"), NULL, G_CALLBACK(show_luminance_mask_callback),
3654 FALSE, 0, 0, dtgtk_cairo_paint_showmask, hbox);
3655
3657 dt_gui_add_class(g->show_luminance_mask, "dt_bauhaus_alignment");
3658 gtk_box_pack_start(GTK_BOX(self->gui->widget), hbox, FALSE, FALSE, 0);
3659
3660 // Force UI redraws when pipe starts/finishes computing and switch cursors
3662 G_CALLBACK(_develop_history_resync_callback), self);
3664 G_CALLBACK(_develop_cacheline_ready_callback), self);
3666 G_CALLBACK(_develop_ui_pipe_finished_callback), self);
3667
3669 G_CALLBACK(_develop_ui_pipe_started_callback), self);
3670}
3671
3672
3674{
3677
3678 dt_conf_set_int("plugins/darkroom/toneequal/gui_page", gtk_notebook_get_current_page (g->notebook));
3679
3684
3685 dt_pixel_cache_entry_t *preview_entry = NULL;
3687 preview_entry = g->thumb_preview_entry;
3688 g->thumb_preview_entry = NULL;
3689 g->thumb_preview_hash = DT_PIXELPIPE_CACHE_HASH_INVALID;
3690 g->pending_preview_hash = DT_PIXELPIPE_CACHE_HASH_INVALID;
3691 g->thumb_preview_buf_width = 0;
3692 g->thumb_preview_buf_height = 0;
3693 g->luminance_valid = FALSE;
3695 if(!IS_NULL_PTR(preview_entry))
3697 if(g->desc) pango_font_description_free(g->desc);
3698 if(g->layout) g_object_unref(g->layout);
3699 if(g->cr) cairo_destroy(g->cr);
3700 if(g->cst) cairo_surface_destroy(g->cst);
3701
3703}
3704
3705// clang-format off
3706// modelines: These editor modelines have been set for all relevant files by tools/update_modelines.py
3707// vim: shiftwidth=2 expandtab tabstop=2 cindent
3708// kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified;
3709// 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:3283
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:1581
void dt_bauhaus_slider_set(GtkWidget *widget, float pos)
Definition bauhaus.c:3334
void dt_bauhaus_widget_set_label(GtkWidget *widget, const char *label)
Definition bauhaus.c:1504
GtkWidget * dt_bauhaus_slider_new_with_range(dt_bauhaus_t *bh, dt_gui_module_t *self, float min, float max, float step, float defval, int digits)
Definition bauhaus.c:1635
void dt_bauhaus_combobox_remove_at(GtkWidget *widget, int pos)
Definition bauhaus.c:1912
void dt_bauhaus_slider_set_format(GtkWidget *widget, const char *format)
Definition bauhaus.c:3410
#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:839
void dt_control_queue_redraw_center()
Request a redraw of the centre view.
Definition control.c:970
void dt_control_hinter_message(const struct dt_control_t *s, const char *message)
Definition control.c:1027
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:654
struct dt_bauhaus_t * dt_bauhaus_get_global(void)
Definition darktable.c:649
#define dt_dev_add_history_item(dev, module, enable, redraw)
void dt_iop_params_t
Definition dev_history.h:43
#define dt_dev_pixelpipe_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:1900
float dt_dev_get_overlay_scale(dt_develop_t *dev)
Get the overlay scale factor in GUI logical coordinates.
Definition develop.c:2026
gboolean dt_dev_rescale_roi(dt_develop_t *dev, cairo_t *cr, int32_t width, int32_t height)
Scale the ROI to fit within given width/height, centered.
Definition develop.c:2138
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:405
void dt_dev_coordinates_widget_to_image_norm(dt_develop_t *dev, float *points, size_t num_points)
Coordinate conversion helpers between widget, normalized image, and absolute image spaces.
Definition develop.c:1211
@ 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:1909
void dt_iop_set_cache_bypass(dt_iop_module_t *module, gboolean state)
Definition imageop.c:1685
@ DT_REQUEST_COLORPICK_OFF
Definition imageop.h:215
void dt_iop_gui_leave_critical_section(dt_iop_module_t *const module)
Release what dt_iop_gui_enter_critical_section() took. Also a no-op headless.
void dt_iop_request_focus(dt_iop_module_t *module)
Move darkroom focus to module, or clear it with NULL.
@ 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 const dt_dev_pixelpipe_t * pipe
Definition layers.h:49
static const dt_dev_pixelpipe_t const dt_dev_pixelpipe_iop_t * piece
Definition layers.h:50
hash
Definition layers.h:90
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 o
#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)
Peek the host pixels a cache entry holds for its current hash, without allocating.
void dt_dev_pixelpipe_cache_ref_count_entry(gboolean lock, dt_pixel_cache_entry_t *cache_entry)
Take or release one reference on a cache line. A referenced line is never evicted,...
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.
size_t dt_pixel_cache_entry_get_size(const dt_pixel_cache_entry_t *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.
void dt_dev_pixelpipe_cache_flag_auto_destroy(dt_pixel_cache_entry_t *cache_entry)
Flag a cache line as disposable (auto_destroy), for short-lived lines nothing will need again: the re...
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:411
struct dt_control_signal_t * dt_control_signal_get_global(void)
Definition darktable.c:619
@ DT_SIGNAL_DEVELOP_HISTORY_CHANGE
This signal is raised when develop history is changed no param, no returned value.
Definition signal.h:208
@ DT_SIGNAL_HISTORY_RESYNC
This signal is raised once darkroom history has been resynchronized into all live pipelines....
Definition signal.h:213
@ 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:189
@ DT_SIGNAL_DEVELOP_UI_PIPE_FINISHED
This signal is raised when pipe is finished and the gui is attached no param, no returned value.
Definition signal.h:183
#define DT_DEBUG_CONTROL_SIGNAL_CONNECT(ctlsig, signal, cb, user_data)
Definition signal.h:400
#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_dev_pixelpipe_type_t type
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:2213
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:2222
static void smoothing_callback(GtkWidget *slider, gpointer user_data)
Definition toneequal.c:1814
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:1648
static gboolean area_draw(GtkWidget *widget, cairo_t *cr, gpointer user_data)
Definition toneequal.c:2701
static gboolean area_button_press(GtkWidget *widget, GdkEventButton *event, gpointer user_data)
Definition toneequal.c:2920
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:2190
static gboolean area_leave_notify(GtkWidget *widget, GdkEventCrossing *event, gpointer user_data)
Definition toneequal.c:2893
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:2087
#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:1422
static void get_channels_gains(float factors[9], const dt_iop_toneequalizer_params_t *p)
Definition toneequal.c:1260
static void _develop_history_resync_callback(gpointer instance, gpointer user_data)
Definition toneequal.c:3084
static void match_color_to_background(cairo_t *cr, const float exposure, const float alpha)
Definition toneequal.c:2246
#define TEMP_SAMPLES
static void build_interpolation_matrix(float A[9 *8], const float sigma)
Definition toneequal.c:1407
#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:1720
const char * aliases()
Definition toneequal.c:349
static void init_nodes_y(dt_iop_toneequalizer_gui_data_t *g)
Definition toneequal.c:2688
static gboolean update_curve_lut(struct dt_iop_module_t *self)
Definition toneequal.c:1582
static void _switch_cursors(struct dt_iop_module_t *self)
Definition toneequal.c:1870
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:2446
void init_pipe(struct dt_iop_module_t *self, dt_dev_pixelpipe_t *pipe, dt_dev_pixelpipe_iop_t *piece)
Definition toneequal.c:1707
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:1560
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:2034
const char * name()
Definition toneequal.c:344
void gui_reset(struct dt_iop_module_t *self)
Definition toneequal.c:3240
#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:1778
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:3498
#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:1791
static void init_nodes_x(dt_iop_toneequalizer_gui_data_t *g)
Definition toneequal.c:2674
static int compute_channels_gains(const float in[9], float out[9])
Definition toneequal.c:1313
static void compute_correction_lut(float *restrict lut, const float sigma, const float *const restrict factors)
Definition toneequal.c:1242
static const dt_colormatrix_t gauss_kernel
Definition toneequal.c:661
void cleanup_global(dt_iop_module_so_t *module)
Definition toneequal.c:1642
static void gui_cache_init(struct dt_iop_module_t *self)
Definition toneequal.c:1348
static void update_histogram(struct dt_iop_module_t *const self)
Definition toneequal.c:1492
static uint64_t _current_preview_luminance_hash(dt_iop_module_t *self, size_t *width, size_t *height)
Definition toneequal.c:1389
static void _develop_ui_pipe_started_callback(gpointer instance, gpointer user_data)
Definition toneequal.c:3065
static gboolean area_enter_notify(GtkWidget *widget, GdkEventCrossing *event, gpointer user_data)
Definition toneequal.c:2875
void update_exposure_sliders(dt_iop_toneequalizer_gui_data_t *g, dt_iop_toneequalizer_params_t *p)
Definition toneequal.c:1762
static gboolean area_button_release(GtkWidget *widget, GdkEventButton *event, gpointer user_data)
Definition toneequal.c:3018
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:2974
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:2261
static void _develop_cacheline_ready_callback(gpointer instance, const guint64 hash, const guint64 producer_node_key, gpointer user_data)
Definition toneequal.c:3171
void gui_cleanup(struct dt_iop_module_t *self)
Definition toneequal.c:3673
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:2016
static gboolean notebook_button_press(GtkWidget *widget, GdkEventButton *event, gpointer user_data)
Definition toneequal.c:3046
static void get_channels_factors(float factors[9], const dt_iop_toneequalizer_params_t *p)
Definition toneequal.c:1277
static void _develop_ui_pipe_finished_callback(gpointer instance, gpointer user_data)
Definition toneequal.c:3231
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:2548
void cleanup_pipe(struct dt_iop_module_t *self, dt_dev_pixelpipe_t *pipe, dt_dev_pixelpipe_iop_t *piece)
Definition toneequal.c:1714
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:1633
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:3463
static int commit_channels_gains(const float factors[9], dt_iop_toneequalizer_params_t *p)
Definition toneequal.c:1327
int mouse_moved(struct dt_iop_module_t *self, double x, double y, double pressure, int which)
Definition toneequal.c:1927
static int compute_channels_factors(const float factors[8], float out[9], const float sigma)
Definition toneequal.c:1291
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:3329
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:3253
static void show_luminance_mask_callback(GtkWidget *togglebutton, GdkEventButton *event, dt_iop_module_t *self)
Definition toneequal.c:1839
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:1185
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:1176
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)