1 /*
2     This file is part of darktable,
3     Copyright (C) 2018-2021 darktable developers.
4 
5     darktable is free software: you can redistribute it and/or modify
6     it under the terms of the GNU General Public License as published by
7     the Free Software Foundation, either version 3 of the License, or
8     (at your option) any later version.
9 
10     darktable is distributed in the hope that it will be useful,
11     but WITHOUT ANY WARRANTY; without even the implied warranty of
12     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13     GNU General Public License for more details.
14 
15     You should have received a copy of the GNU General Public License
16     along with darktable.  If not, see <http://www.gnu.org/licenses/>.
17 */
18 
19 /*** DOCUMENTATION
20  *
21  * This module aims at relighting the scene by performing an exposure compensation
22  * selectively on specified exposures octaves, the same way HiFi audio equalizers allow to set
23  * a gain for each octave.
24  *
25  * It is intended to work in scene-linear camera RGB, to behave as if light was physically added
26  * or removed from the scene. As such, it should be put before input profile in the pipe, but preferably
27  * after exposure. It also need to be placed after the rotation, perspective and cropping modules
28  * for the interactive editing to work properly (so the image buffer overlap perfectly with the
29  * image preview).
30  *
31  * Because it works before camera RGB -> XYZ conversion, the exposure cannot be computed from
32  * any human-based perceptual colour model (Y channel), hence why several RGB norms are provided as estimators of
33  * the pixel energy to compute a luminance map. None of them is perfect, and I'm still
34  * looking forward to a real spectral energy estimator. The best physically-accurate norm should be the euclidean
35  * norm, but the best looking is often the power norm, which has no theoretical background.
36  * The geometric mean also display interesting properties as it interprets saturated colours
37  * as low-lights, allowing to lighten and desaturate them in a realistic way.
38  *
39  * The exposure correction is computed as a series of each octave's gain weighted by the
40  * gaussian of the radial distance between the current pixel exposure and each octave's center.
41  * This allows for a smooth and continuous infinite-order interpolation, preserving exposure gradients
42  * as best as possible. The radius of the kernel is user-defined and can be tweaked to get
43  * a smoother interpolation (possibly generating oscillations), or a more monotonous one
44  * (possibly less smooth). The actual factors of the gaussian series are computed by
45  * solving the linear system taking the user-input parameters as target exposures compensations.
46  *
47  * Notice that every pixel operation is performed in linear space. The exposures in log2 (EV)
48  * are only used for user-input parameters and for the gaussian weights of the radial distance
49  * between pixel exposure and octave's centers.
50  *
51  * The details preservation modes make use of a fast guided filter optimized to perform
52  * an edge-aware surface blur on the luminance mask, in the same spirit as the bilateral
53  * filter, but without its classic issues of gradient reversal around sharp edges. This
54  * surface blur will allow to perform piece-wise smooth exposure compensation, so local contrast
55  * will be preserved inside contiguous regions. Various mask refinements are provided to help
56  * the edge-taping of the filter (feathering parameter) while keeping smooth contiguous region
57  * (quantization parameter), but also to translate (exposure boost) and dilate (contrast boost)
58  * the exposure histogram through the control octaves, to center it on the control view
59  * and make maximum use of the available channels.
60  *
61  * Users should be aware that not all the available octaves will be useful on every pictures.
62  * Some automatic options will help them to optimize the luminance mask, performing histogram
63  * analyse, mapping the average exposure to -4EV, and mapping the first and last deciles of
64  * the histogram on its average ± 4EV. These automatic helpers usually fail on X-Trans sensors,
65  * maybe because of bad demosaicing, possibly resulting in outliers\negative RGB values.
66  * Since they fail the same way on filmic's auto-tuner, we might need to investigate X-Trans
67  * algos at some point.
68  *
69 ***/
70 
71 #ifdef HAVE_CONFIG_H
72 #include "config.h"
73 #endif
74 #include <assert.h>
75 #include <math.h>
76 #include <stdlib.h>
77 #include <stdio.h>
78 #include <string.h>
79 #include <time.h>
80 
81 #include "bauhaus/bauhaus.h"
82 #include "common/darktable.h"
83 #include "common/fast_guided_filter.h"
84 #include "common/eigf.h"
85 #include "common/interpolation.h"
86 #include "common/luminance_mask.h"
87 #include "common/opencl.h"
88 #include "common/collection.h"
89 #include "control/conf.h"
90 #include "control/control.h"
91 #include "develop/blend.h"
92 #include "develop/develop.h"
93 #include "develop/imageop.h"
94 #include "develop/imageop_math.h"
95 #include "develop/imageop_gui.h"
96 #include "dtgtk/drawingarea.h"
97 #include "dtgtk/expander.h"
98 #include "gui/accelerators.h"
99 #include "gui/color_picker_proxy.h"
100 #include "gui/draw.h"
101 #include "gui/gtk.h"
102 #include "gui/presets.h"
103 #include "gui/color_picker_proxy.h"
104 #include "iop/iop_api.h"
105 #include "iop/choleski.h"
106 #include "common/iop_group.h"
107 
108 #ifdef _OPENMP
109 #include <omp.h>
110 #endif
111 
112 
113 DT_MODULE_INTROSPECTION(2, dt_iop_toneequalizer_params_t)
114 
115 
116 /** Note :
117  * we use finite-math-only and fast-math because divisions by zero are manually avoided in the code
118  * fp-contract=fast enables hardware-accelerated Fused Multiply-Add
119  * the rest is loop reorganization and vectorization optimization
120  **/
121 #if defined(__GNUC__)
122 #pragma GCC optimize ("unroll-loops", "tree-loop-if-convert", \
123                       "tree-loop-distribution", "no-strict-aliasing", \
124                       "loop-interchange",  "tree-loop-im", \
125                       "unswitch-loops", "tree-loop-ivcanon", "ira-loop-pressure", \
126                       "split-ivs-in-unroller", "variable-expansion-in-unroller", \
127                       "split-loops", "ivopts", "predictive-commoning",\
128                          \
129                       "finite-math-only", "fp-contract=fast", "fast-math", \
130                       "tree-vectorize")
131 #endif
132 
133 #define UI_SAMPLES 256 // 128 is a bit small for 4K resolution
134 #define CONTRAST_FULCRUM exp2f(-4.0f)
135 #define MIN_FLOAT exp2f(-16.0f)
136 
137 /**
138  * Build the exposures octaves : 
139  * band-pass filters with gaussian windows spaced by 1 EV
140 **/
141 
142 #define CHANNELS 9
143 #define PIXEL_CHAN 8
144 #define LUT_RESOLUTION 10000
145 
146 // radial distances used for pixel ops
147 static const float centers_ops[PIXEL_CHAN] DT_ALIGNED_ARRAY = {-56.0f / 7.0f, // = -8.0f
148                                                                -48.0f / 7.0f,
149                                                                -40.0f / 7.0f,
150                                                                -32.0f / 7.0f,
151                                                                -24.0f / 7.0f,
152                                                                -16.0f / 7.0f,
153                                                                 -8.0f / 7.0f,
154                                                                  0.0f / 7.0f}; // split 8 EV into 7 evenly-spaced channels
155 
156 static const float centers_params[CHANNELS] DT_ALIGNED_ARRAY = { -8.0f, -7.0f, -6.0f, -5.0f,
157                                                                  -4.0f, -3.0f, -2.0f, -1.0f, 0.0f};
158 
159 
160 typedef enum dt_iop_toneequalizer_filter_t
161 {
162   DT_TONEEQ_NONE = 0,   // $DESCRIPTION: "no"
163   DT_TONEEQ_AVG_GUIDED, // $DESCRIPTION: "averaged guided filter"
164   DT_TONEEQ_GUIDED,     // $DESCRIPTION: "guided filter"
165   DT_TONEEQ_AVG_EIGF,   // $DESCRIPTION: "averaged eigf"
166   DT_TONEEQ_EIGF        // $DESCRIPTION: "eigf"
167 } dt_iop_toneequalizer_filter_t;
168 
169 
170 typedef struct dt_iop_toneequalizer_params_t
171 {
172   float noise; // $MIN: -2.0 $MAX: 2.0 $DEFAULT: 0.0  $DESCRIPTION: "blacks"
173   float ultra_deep_blacks; // $MIN: -2.0 $MAX: 2.0 $DEFAULT: 0.0  $DESCRIPTION: "deep shadows"
174   float deep_blacks; // $MIN: -2.0 $MAX: 2.0 $DEFAULT: 0.0  $DESCRIPTION: "shadows"
175   float blacks; // $MIN: -2.0 $MAX: 2.0 $DEFAULT: 0.0  $DESCRIPTION: "light shadows"
176   float shadows; // $MIN: -2.0 $MAX: 2.0 $DEFAULT: 0.0  $DESCRIPTION: "mid-tones"
177   float midtones; // $MIN: -2.0 $MAX: 2.0 $DEFAULT: 0.0  $DESCRIPTION: "dark highlights"
178   float highlights; // $MIN: -2.0 $MAX: 2.0 $DEFAULT: 0.0  $DESCRIPTION: "highlights"
179   float whites; // $MIN: -2.0 $MAX: 2.0 $DEFAULT: 0.0  $DESCRIPTION: "whites"
180   float speculars; // $MIN: -2.0 $MAX: 2.0 $DEFAULT: 0.0  $DESCRIPTION: "speculars"
181   float blending; // $MIN: 0.01 $MAX: 100.0 $DEFAULT: 5.0 $DESCRIPTION: "smoothing diameter"
182   float smoothing; // $DEFAULT: 1.414213562 sqrtf(2.0f)
183   float feathering; // $MIN: 0.01 $MAX: 10000.0 $DEFAULT: 1.0 $DESCRIPTION: "edges refinement/feathering"
184   float quantization; // $MIN: 0.0 $MAX: 2.0 $DEFAULT: 0.0 $DESCRIPTION: "mask quantization"
185   float contrast_boost; // $MIN: -16.0 $MAX: 16.0 $DEFAULT: 0.0 $DESCRIPTION: "mask contrast compensation"
186   float exposure_boost; // $MIN: -16.0 $MAX: 16.0 $DEFAULT: 0.0 $DESCRIPTION: "mask exposure compensation"
187   dt_iop_toneequalizer_filter_t details; // $DEFAULT: DT_TONEEQ_EIGF
188   dt_iop_luminance_mask_method_t method; // $DEFAULT: DT_TONEEQ_NORM_2 $DESCRIPTION: "luminance estimator"
189   int iterations; // $MIN: 1 $MAX: 20 $DEFAULT: 1 $DESCRIPTION: "filter diffusion"
190 } dt_iop_toneequalizer_params_t;
191 
192 
193 typedef struct dt_iop_toneequalizer_data_t
194 {
195   float factors[PIXEL_CHAN] DT_ALIGNED_ARRAY;
196   float correction_lut[PIXEL_CHAN * LUT_RESOLUTION + 1] DT_ALIGNED_ARRAY;
197   float blending, feathering, contrast_boost, exposure_boost, quantization, smoothing;
198   float scale;
199   int radius;
200   int iterations;
201   dt_iop_luminance_mask_method_t method;
202   dt_iop_toneequalizer_filter_t details;
203 } dt_iop_toneequalizer_data_t;
204 
205 
206 typedef struct dt_iop_toneequalizer_global_data_t
207 {
208   // TODO: put OpenCL kernels here at some point
209 } dt_iop_toneequalizer_global_data_t;
210 
211 
212 typedef struct dt_iop_toneequalizer_gui_data_t
213 {
214   // Mem arrays 64-bytes aligned - contiguous memory
215   float factors[PIXEL_CHAN] DT_ALIGNED_ARRAY;
216   float gui_lut[UI_SAMPLES] DT_ALIGNED_ARRAY; // LUT for the UI graph
217   float interpolation_matrix[CHANNELS * PIXEL_CHAN] DT_ALIGNED_ARRAY;
218   int histogram[UI_SAMPLES] DT_ALIGNED_ARRAY; // histogram for the UI graph
219   float temp_user_params[CHANNELS] DT_ALIGNED_ARRAY;
220   float cursor_exposure; // store the exposure value at current cursor position
221   float step; // scrolling step
222 
223   // 14 int to pack - contiguous memory
224   int mask_display;
225   int max_histogram;
226   int buf_width;
227   int buf_height;
228   int cursor_pos_x;
229   int cursor_pos_y;
230   int pipe_order;
231 
232   // 6 uint64 to pack - contiguous-ish memory
233   uint64_t ui_preview_hash;
234   uint64_t thumb_preview_hash;
235   size_t full_preview_buf_width, full_preview_buf_height;
236   size_t thumb_preview_buf_width, thumb_preview_buf_height;
237 
238   // Misc stuff, contiguity, length and alignment unknown
239   float scale;
240   float sigma;
241   float histogram_average;
242   float histogram_first_decile;
243   float histogram_last_decile;
244 
245   // Heap arrays, 64 bits-aligned, unknown length
246   float *thumb_preview_buf;
247   float *full_preview_buf;
248 
249   // GTK garbage, nobody cares, no SIMD here
250   GtkWidget *noise, *ultra_deep_blacks, *deep_blacks, *blacks, *shadows, *midtones, *highlights, *whites, *speculars;
251   GtkDrawingArea *area, *bar;
252   GtkWidget *blending, *smoothing, *quantization;
253   GtkWidget *method;
254   GtkWidget *details, *feathering, *contrast_boost, *iterations, *exposure_boost;
255   GtkNotebook *notebook;
256   GtkWidget *show_luminance_mask;
257 
258   // Cache Pango and Cairo stuff for the equalizer drawing
259   float line_height;
260   float sign_width;
261   float graph_width;
262   float graph_height;
263   float gradient_left_limit;
264   float gradient_right_limit;
265   float gradient_top_limit;
266   float gradient_width;
267   float legend_top_limit;
268   float x_label;
269   int inset;
270   int inner_padding;
271 
272   GtkAllocation allocation;
273   cairo_surface_t *cst;
274   cairo_t *cr;
275   PangoLayout *layout;
276   PangoRectangle ink;
277   PangoFontDescription *desc;
278   GtkStyleContext *context;
279 
280   // Event for equalizer drawing
281   float nodes_x[CHANNELS];
282   float nodes_y[CHANNELS];
283   float area_x; // x coordinate of cursor over graph/drawing area
284   float area_y; // y coordinate
285   int area_active_node;
286 
287   // Flags for UI events
288   int valid_nodes_x;        // TRUE if x coordinates of graph nodes have been inited
289   int valid_nodes_y;        // TRUE if y coordinates of graph nodes have been inited
290   int area_cursor_valid;    // TRUE if mouse cursor is over the graph area
291   int area_dragging;        // TRUE if left-button has been pushed but not released and cursor motion is recorded
292   int cursor_valid;         // TRUE if mouse cursor is over the preview image
293   int has_focus;            // TRUE if the widget has the focus from GTK
294 
295   // Flags for buffer caches invalidation
296   int interpolation_valid;  // TRUE if the interpolation_matrix is ready
297   int luminance_valid;      // TRUE if the luminance cache is ready
298   int histogram_valid;      // TRUE if the histogram cache and stats are ready
299   int lut_valid;            // TRUE if the gui_lut is ready
300   int graph_valid;          // TRUE if the UI graph view is ready
301   int user_param_valid;     // TRUE if users params set in interactive view are in bounds
302   int factors_valid;        // TRUE if radial-basis coeffs are ready
303 
304 } dt_iop_toneequalizer_gui_data_t;
305 
306 
name()307 const char *name()
308 {
309   return _("tone equalizer");
310 }
311 
aliases()312 const char *aliases()
313 {
314   return _("tone curve|tone mapping|relight|background light|shadows highlights");
315 }
316 
317 
description(struct dt_iop_module_t * self)318 const char *description(struct dt_iop_module_t *self)
319 {
320   return dt_iop_set_description(self, _("relight the scene as if the lighting was done directly on the scene"),
321                                       _("corrective and creative"),
322                                       _("linear, RGB, scene-referred"),
323                                       _("quasi-linear, RGB"),
324                                       _("quasi-linear, RGB, scene-referred"));
325 }
326 
default_group()327 int default_group()
328 {
329   return IOP_GROUP_BASIC | IOP_GROUP_GRADING;
330 }
331 
flags()332 int flags()
333 {
334   return IOP_FLAGS_INCLUDE_IN_STYLES | IOP_FLAGS_SUPPORTS_BLENDING;
335 }
336 
default_colorspace(dt_iop_module_t * self,dt_dev_pixelpipe_t * pipe,dt_dev_pixelpipe_iop_t * piece)337 int default_colorspace(dt_iop_module_t *self, dt_dev_pixelpipe_t *pipe, dt_dev_pixelpipe_iop_t *piece)
338 {
339   return iop_cs_rgb;
340 }
341 
legacy_params(dt_iop_module_t * self,const void * const old_params,const int old_version,void * new_params,const int new_version)342 int legacy_params(dt_iop_module_t *self, const void *const old_params, const int old_version, void *new_params,
343                   const int new_version)
344 {
345   if(old_version == 1 && new_version == 2)
346   {
347     typedef struct dt_iop_toneequalizer_params_v1_t
348     {
349       float noise, ultra_deep_blacks, deep_blacks, blacks, shadows, midtones, highlights, whites, speculars;
350       float blending, feathering, contrast_boost, exposure_boost;
351       dt_iop_toneequalizer_filter_t details;
352       int iterations;
353       dt_iop_luminance_mask_method_t method;
354     } dt_iop_toneequalizer_params_v1_t;
355 
356     dt_iop_toneequalizer_params_v1_t *o = (dt_iop_toneequalizer_params_v1_t *)old_params;
357     dt_iop_toneequalizer_params_t *n = (dt_iop_toneequalizer_params_t *)new_params;
358     dt_iop_toneequalizer_params_t *d = (dt_iop_toneequalizer_params_t *)self->default_params;
359 
360     *n = *d; // start with a fresh copy of default parameters
361 
362     // Olds params
363     n->noise = o->noise;
364     n->ultra_deep_blacks = o->ultra_deep_blacks;
365     n->deep_blacks = o->deep_blacks;
366     n->blacks = o->blacks;
367     n->shadows = o->shadows;
368     n->midtones = o->midtones;
369     n->highlights = o->highlights;
370     n->whites = o->whites;
371     n->speculars = o->speculars;
372 
373     n->blending = o->blending;
374     n->feathering = o->feathering;
375     n->contrast_boost = o->contrast_boost;
376     n->exposure_boost = o->exposure_boost;
377 
378     n->details = o->details;
379     n->iterations = o->iterations;
380     n->method = o->method;
381 
382     // New params
383     n->quantization = 0.01f;
384     n->smoothing = sqrtf(2.0f);
385     return 0;
386   }
387   return 1;
388 }
389 
compress_shadows_highlight_preset_set_exposure_params(dt_iop_toneequalizer_params_t * p,const float step)390 static void compress_shadows_highlight_preset_set_exposure_params(dt_iop_toneequalizer_params_t* p, const float step)
391 {
392   // this function is used to set the exposure params for the 4 "compress shadows
393   // highlights" presets, which use basically the same curve, centered around
394   // -4EV with an exposure compensation that puts middle-grey at -4EV.
395   p->noise = step;
396   p->ultra_deep_blacks = 5.f / 3.f * step;
397   p->deep_blacks = 5.f / 3.f * step;
398   p->blacks = step;
399   p->shadows = 0.0f;
400   p->midtones = -step;
401   p->highlights = -5.f / 3.f * step;
402   p->whites = -5.f / 3.f * step;
403   p->speculars = -step;
404 }
405 
406 
dilate_shadows_highlight_preset_set_exposure_params(dt_iop_toneequalizer_params_t * p,const float step)407 static void dilate_shadows_highlight_preset_set_exposure_params(dt_iop_toneequalizer_params_t* p, const float step)
408 {
409   // create a tone curve meant to be used without filter (as a flat, non-local, 1D tone curve) that reverts
410   // the local settings above.
411   p->noise = -15.f / 9.f * step;
412   p->ultra_deep_blacks = -14.f / 9.f * step;
413   p->deep_blacks = -12.f / 9.f * step;
414   p->blacks = -8.f / 9.f * step;
415   p->shadows = 0.f;
416   p->midtones = 8.f / 9.f * step;
417   p->highlights = 12.f / 9.f * step;
418   p->whites = 14.f / 9.f * step;
419   p->speculars = 15.f / 9.f * step;
420 }
421 
init_presets(dt_iop_module_so_t * self)422 void init_presets(dt_iop_module_so_t *self)
423 {
424   dt_iop_toneequalizer_params_t p;
425   memset(&p, 0, sizeof(p));
426 
427   p.method = DT_TONEEQ_NORM_POWER;
428   p.contrast_boost = 0.0f;
429   p.details = DT_TONEEQ_NONE;
430   p.exposure_boost = -0.5f;
431   p.feathering = 1.0f;
432   p.iterations = 1;
433   p.smoothing = sqrtf(2.0f);
434   p.quantization = 0.0f;
435 
436   // Init exposure settings
437   p.noise = p.ultra_deep_blacks = p.deep_blacks = p.blacks = p.shadows = p.midtones = p.highlights = p.whites = p. speculars = 0.0f;
438 
439   // No blending
440   dt_gui_presets_add_generic(_("simple tone curve"), self->op,
441                              self->version(), &p, sizeof(p), 1, DEVELOP_BLEND_CS_RGB_DISPLAY);
442 
443   // Simple utils blendings
444   p.details = DT_TONEEQ_EIGF;
445   p.method = DT_TONEEQ_NORM_2;
446 
447   p.blending = 5.0f;
448   p.feathering = 1.0f;
449   p.iterations = 1;
450   p.quantization = 0.0f;
451   p.exposure_boost = 0.0f;
452   p.contrast_boost = 0.0f;
453   dt_gui_presets_add_generic(_("mask blending : all purposes"), self->op,
454                              self->version(), &p, sizeof(p), 1, DEVELOP_BLEND_CS_RGB_DISPLAY);
455 
456   p.blending = 1.0f;
457   p.feathering = 10.0f;
458   p.iterations = 3;
459   dt_gui_presets_add_generic(_("mask blending : people with backlight"), self->op,
460                              self->version(), &p, sizeof(p), 1, DEVELOP_BLEND_CS_RGB_DISPLAY);
461 
462   // Shadows/highlights presets
463   // move middle-grey to the center of the range
464   p.exposure_boost = -1.57f;
465   p.contrast_boost = 0.0f;
466   p.blending = 2.0f;
467   p.feathering = 50.0f;
468   p.iterations = 5;
469   p.quantization = 0.0f;
470 
471   // slight modification to give higher compression
472   p.details = DT_TONEEQ_EIGF;
473   p.feathering = 20.0f;
474   compress_shadows_highlight_preset_set_exposure_params(&p, 0.65f);
475   dt_gui_presets_add_generic(_("compress shadows/highlights (eigf) : strong"), self->op,
476                              self->version(), &p, sizeof(p), 1, DEVELOP_BLEND_CS_RGB_DISPLAY);
477   p.details = DT_TONEEQ_GUIDED;
478   p.feathering = 500.0f;
479   dt_gui_presets_add_generic(_("compress shadows/highlights (gf) : strong"), self->op,
480                              self->version(), &p, sizeof(p), 1, DEVELOP_BLEND_CS_RGB_DISPLAY);
481 
482   p.details = DT_TONEEQ_EIGF;
483   p.blending = 3.0f;
484   p.feathering = 7.0f;
485   p.iterations = 3;
486   compress_shadows_highlight_preset_set_exposure_params(&p, 0.45f);
487   dt_gui_presets_add_generic(_("compress shadows/highlights (eigf) : medium"), self->op,
488                              self->version(), &p, sizeof(p), 1, DEVELOP_BLEND_CS_RGB_DISPLAY);
489   p.details = DT_TONEEQ_GUIDED;
490   p.feathering = 500.0f;
491   dt_gui_presets_add_generic(_("compress shadows/highlights (gf) : medium"), self->op,
492                              self->version(), &p, sizeof(p), 1, DEVELOP_BLEND_CS_RGB_DISPLAY);
493 
494   p.details = DT_TONEEQ_EIGF;
495   p.blending = 5.0f;
496   p.feathering = 1.0f;
497   p.iterations = 1;
498   compress_shadows_highlight_preset_set_exposure_params(&p, 0.25f);
499   dt_gui_presets_add_generic(_("compress shadows/highlights (eigf) : soft"), self->op,
500                              self->version(), &p, sizeof(p), 1, DEVELOP_BLEND_CS_RGB_DISPLAY);
501   p.details = DT_TONEEQ_GUIDED;
502   p.feathering = 500.0f;
503   dt_gui_presets_add_generic(_("compress shadows/highlights (gf) : soft"), self->op,
504                              self->version(), &p, sizeof(p), 1, DEVELOP_BLEND_CS_RGB_DISPLAY);
505 
506   // build the 1D contrast curves that revert the local compression of contrast above
507   p.details = DT_TONEEQ_NONE;
508   dilate_shadows_highlight_preset_set_exposure_params(&p, 0.25f);
509   dt_gui_presets_add_generic(_("contrast tone curve: soft"), self->op,
510                              self->version(), &p, sizeof(p), 1, DEVELOP_BLEND_CS_RGB_DISPLAY);
511 
512   dilate_shadows_highlight_preset_set_exposure_params(&p, 0.45f);
513   dt_gui_presets_add_generic(_("contrast tone curve: medium"), self->op,
514                              self->version(), &p, sizeof(p), 1, DEVELOP_BLEND_CS_RGB_DISPLAY);
515 
516   dilate_shadows_highlight_preset_set_exposure_params(&p, 0.65f);
517   dt_gui_presets_add_generic(_("contrast tone curve: strong"), self->op,
518                              self->version(), &p, sizeof(p), 1, DEVELOP_BLEND_CS_RGB_DISPLAY);
519 
520   // relight
521   p.details = DT_TONEEQ_EIGF;
522   p.blending = 5.0f;
523   p.feathering = 1.0f;
524   p.iterations = 1;
525   p.quantization = 0.0f;
526   p.exposure_boost = -0.5f;
527   p.contrast_boost = 0.0f;
528 
529   p.noise = 0.0f;
530   p.ultra_deep_blacks = 0.15f;
531   p.deep_blacks = 0.6f;
532   p.blacks = 1.15f;
533   p.shadows = 1.33f;
534   p.midtones = 1.15f;
535   p.highlights = 0.6f;
536   p.whites = 0.15f;
537   p.speculars = 0.0f;
538 
539   dt_gui_presets_add_generic(_("relight : fill-in"), self->op,
540                              self->version(), &p, sizeof(p), 1, DEVELOP_BLEND_CS_RGB_DISPLAY);
541 }
542 
543 
544 /**
545  * Helper functions
546  **/
547 
in_mask_editing(dt_iop_module_t * self)548 static gboolean in_mask_editing(dt_iop_module_t *self)
549 {
550   const dt_develop_t *dev = self->dev;
551   return dev->form_gui && dev->form_visible;
552 }
553 
hash_set_get(uint64_t * hash_in,uint64_t * hash_out,dt_pthread_mutex_t * lock)554 static void hash_set_get(uint64_t *hash_in, uint64_t *hash_out, dt_pthread_mutex_t *lock)
555 {
556   // Set or get a hash in a struct the thread-safe way
557   dt_pthread_mutex_lock(lock);
558   *hash_out = *hash_in;
559   dt_pthread_mutex_unlock(lock);
560 }
561 
562 
invalidate_luminance_cache(dt_iop_module_t * const self)563 static void invalidate_luminance_cache(dt_iop_module_t *const self)
564 {
565   // Invalidate the private luminance cache and histogram when
566   // the luminance mask extraction parameters have changed
567   dt_iop_toneequalizer_gui_data_t *const restrict g = (dt_iop_toneequalizer_gui_data_t *)self->gui_data;
568 
569   dt_iop_gui_enter_critical_section(self);
570   g->max_histogram = 1;
571   //g->luminance_valid = 0;
572   g->histogram_valid = 0;
573   g->thumb_preview_hash = 0;
574   g->ui_preview_hash = 0;
575   dt_iop_gui_leave_critical_section(self);
576   dt_iop_refresh_preview(self);
577 }
578 
579 
sanity_check(dt_iop_module_t * self)580 static int sanity_check(dt_iop_module_t *self)
581 {
582   // If tone equalizer is put after flip/orientation module,
583   // the pixel buffer will be in landscape orientation even for pictures displayed in portrait orientation
584   // so the interactive editing will fail. Disable the module and issue a warning then.
585 
586   const double position_self = self->iop_order;
587   const double position_min = dt_ioppr_get_iop_order(self->dev->iop_order_list, "flip", 0);
588 
589   if(position_self < position_min && self->enabled)
590   {
591     dt_control_log(_("tone equalizer needs to be after distortion modules in the pipeline – disabled"));
592     fprintf(stdout, "tone equalizer needs to be after distortion modules in the pipeline – disabled\n");
593     self->enabled = 0;
594     dt_dev_add_history_item(darktable.develop, self, FALSE);
595 
596     if(self->dev->gui_attached)
597     {
598       // Repaint the on/off icon
599       if(self->off)
600       {
601         ++darktable.gui->reset;
602         gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(self->off), self->enabled);
603         --darktable.gui->reset;
604       }
605     }
606     return 0;
607   }
608 
609   return 1;
610 }
611 
612 // gaussian-ish kernel - sum is == 1.0f so we don't care much about actual coeffs
613 static const dt_colormatrix_t gauss_kernel =
614   { { 0.076555024f, 0.124401914f, 0.076555024f },
615     { 0.124401914f, 0.196172249f, 0.124401914f },
616     { 0.076555024f, 0.124401914f, 0.076555024f } };
617 
618 __DT_CLONE_TARGETS__
get_luminance_from_buffer(const float * const buffer,const size_t width,const size_t height,const size_t x,const size_t y)619 static float get_luminance_from_buffer(const float *const buffer,
620                                        const size_t width, const size_t height,
621                                        const size_t x, const size_t y)
622 {
623   // Get the weighted average luminance of the 3×3 pixels region centered in (x, y)
624   // x and y are ratios in [0, 1] of the width and height
625 
626   if(y >= height || x >= width) return NAN;
627 
628   const size_t y_abs[4] DT_ALIGNED_PIXEL =
629                           { MAX(y, 1) - 1,                  // previous line
630                             y,                              // center line
631                             MIN(y + 1, height - 1),         // next line
632                             y };			    // padding for vectorization
633 
634   float luminance = 0.0f;
635   if (x > 0 && x < width - 2)
636   {
637     // no clamping needed on x, which allows us to vectorize
638     // apply the convolution
639     for(int i = 0; i < 3; ++i)
640     {
641       const size_t y_i = y_abs[i];
642       for_each_channel(j)
643         luminance += buffer[width * y_i + x-1 + j] * gauss_kernel[i][j];
644     }
645     return luminance;
646   }
647 
648   const size_t x_abs[4] DT_ALIGNED_PIXEL =
649                           { MAX(x, 1) - 1,                  // previous column
650                             x,                              // center column
651                             MIN(x + 1, width - 1),          // next column
652                             x };                            // padding for vectorization
653 
654   // convolution
655   for(int i = 0; i < 3; ++i)
656   {
657     const size_t y_i = y_abs[i];
658     for_each_channel(j)
659       luminance += buffer[width * y_i + x_abs[j]] * gauss_kernel[i][j];
660   }
661   return luminance;
662 }
663 
664 
665 /***
666  * Exposure compensation computation
667  *
668  * Construct the final correction factor by summing the octaves channels gains weighted by
669  * the gaussian of the radial distance (pixel exposure - octave center)
670  *
671  ***/
672 
673 #ifdef _OPENMP
674 #pragma omp declare simd
675 #endif
676 __DT_CLONE_TARGETS__
gaussian_denom(const float sigma)677 static float gaussian_denom(const float sigma)
678 {
679   // Gaussian function denominator such that y = exp(- radius^2 / denominator)
680   // this is the constant factor of the exponential, so we don't need to recompute it
681   // for every single pixel
682   return 2.0f * sigma * sigma;
683 }
684 
685 
686 #ifdef _OPENMP
687 #pragma omp declare simd
688 #endif
689 __DT_CLONE_TARGETS__
gaussian_func(const float radius,const float denominator)690 static float gaussian_func(const float radius, const float denominator)
691 {
692   // Gaussian function without normalization
693   // this is the variable part of the exponential
694   // the denominator should be evaluated with `gaussian_denom`
695   // ahead of the array loop for optimal performance
696   return expf(- radius * radius / denominator);
697 }
698 
699 #define DT_TONEEQ_USE_LUT TRUE
700 #if DT_TONEEQ_USE_LUT
701 
702 // this is the version currently used, as using a lut gives a
703 // big performance speedup on some cpus
704 __DT_CLONE_TARGETS__
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)705 static inline void apply_toneequalizer(const float *const restrict in,
706                                        const float *const restrict luminance,
707                                        float *const restrict out,
708                                        const dt_iop_roi_t *const roi_in, const dt_iop_roi_t *const roi_out,
709                                        const size_t ch,
710                                        const dt_iop_toneequalizer_data_t *const d)
711 {
712   const size_t num_elem = (size_t)roi_in->width * roi_in->height;
713   const int min_ev = -8;
714   const int max_ev = 0;
715   const float* restrict lut = d->correction_lut;
716 
717 #ifdef _OPENMP
718 #pragma omp parallel for default(none) schedule(static) \
719   dt_omp_firstprivate(in, out, num_elem, luminance, lut, min_ev, max_ev, ch)
720 #endif
721   for(size_t k = 0; k < num_elem; ++k)
722   {
723     // The radial-basis interpolation is valid in [-8; 0] EV and can quickely diverge outside
724     const float exposure = fast_clamp(log2f(luminance[k]), min_ev, max_ev);
725     float correction = lut[(unsigned)roundf((exposure - min_ev) * LUT_RESOLUTION)];
726     // apply correction
727     for(size_t c = 0; c < ch; c++)
728     {
729       if(c == 3)
730         out[k * ch + c] = in[k * ch + c];
731       else
732         out[k * ch + c] = correction * in[k * ch + c];
733     }
734   }
735 }
736 
737 #else
738 
739 // we keep this version for further reference (e.g. for implementing
740 // a gpu version)
741 __DT_CLONE_TARGETS__
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)742 static inline void apply_toneequalizer(const float *const restrict in,
743                                        const float *const restrict luminance,
744                                        float *const restrict out,
745                                        const dt_iop_roi_t *const roi_in, const dt_iop_roi_t *const roi_out,
746                                        const size_t ch,
747                                        const dt_iop_toneequalizer_data_t *const d)
748 {
749   const size_t num_elem = roi_in->width * roi_in->height;
750   const float *const restrict factors = d->factors;
751   const float sigma = d->smoothing;
752   const float gauss_denom = gaussian_denom(sigma);
753 
754 #ifdef _OPENMP
755 #pragma omp parallel for default(none) schedule(static) \
756   dt_omp_firstprivate(in, out, num_elem, luminance, factors, centers_ops, gauss_denom, ch)
757 #endif
758   for(size_t k = 0; k < num_elem; ++k)
759   {
760     // build the correction for the current pixel
761     // as the sum of the contribution of each luminance channelcorrection
762     float result = 0.0f;
763 
764     // The radial-basis interpolation is valid in [-8; 0] EV and can quickely diverge outside
765     const float exposure = fast_clamp(log2f(luminance[k]), -8.0f, 0.0f);
766 
767 #ifdef _OPENMP
768 #pragma omp simd aligned(luminance, centers_ops, factors:64) safelen(PIXEL_CHAN) reduction(+:result)
769 #endif
770     for(int i = 0; i < PIXEL_CHAN; ++i)
771       result += gaussian_func(exposure - centers_ops[i], gauss_denom) * factors[i];
772 
773     // the user-set correction is expected in [-2;+2] EV, so is the interpolated one
774     float correction = fast_clamp(result, 0.25f, 4.0f);
775 
776     // apply correction
777     for(size_t c = 0; c < ch; c++)
778     {
779       if(c == 3)
780         out[k * ch + c] = in[k * ch + c];
781       else
782         out[k * ch + c] = correction * in[k * ch + c];
783     }
784   }
785 }
786 #endif // USE_LUT
787 
788 __DT_CLONE_TARGETS__
pixel_correction(const float exposure,const float * const restrict factors,const float sigma)789 static inline float pixel_correction(const float exposure,
790                                      const float *const restrict factors,
791                                      const float sigma)
792 {
793   // build the correction for the current pixel
794   // as the sum of the contribution of each luminance channel
795   float result = 0.0f;
796   const float gauss_denom = gaussian_denom(sigma);
797   const float expo = fast_clamp(exposure, -8.0f, 0.0f);
798 
799 #ifdef _OPENMP
800 #pragma omp simd aligned(centers_ops, factors:64) safelen(PIXEL_CHAN) reduction(+:result)
801 #endif
802   for(int i = 0; i < PIXEL_CHAN; ++i)
803     result += gaussian_func(expo - centers_ops[i], gauss_denom) * factors[i];
804 
805   return fast_clamp(result, 0.25f, 4.0f);
806 }
807 
808 
809 __DT_CLONE_TARGETS__
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)810 static inline void compute_luminance_mask(const float *const restrict in, float *const restrict luminance,
811                                           const size_t width, const size_t height, const size_t ch,
812                                           const dt_iop_toneequalizer_data_t *const d)
813 {
814   switch(d->details)
815   {
816     case(DT_TONEEQ_NONE):
817     {
818       // No contrast boost here
819       luminance_mask(in, luminance, width, height, ch, d->method, d->exposure_boost, 0.0f, 1.0f);
820       break;
821     }
822 
823     case(DT_TONEEQ_AVG_GUIDED):
824     {
825       // Still no contrast boost
826       luminance_mask(in, luminance, width, height, ch, d->method, d->exposure_boost, 0.0f, 1.0f);
827       fast_surface_blur(luminance, width, height, d->radius, d->feathering, d->iterations,
828                     DT_GF_BLENDING_GEOMEAN, d->scale, d->quantization, exp2f(-14.0f), 4.0f);
829       break;
830     }
831 
832     case(DT_TONEEQ_GUIDED):
833     {
834       // Contrast boosting is done around the average luminance of the mask.
835       // This is to make exposure corrections easier to control for users, by spreading
836       // the dynamic range along all exposure channels, because guided filters
837       // tend to flatten the luminance mask a lot around an average ± 2 EV
838       // which makes only 2-3 channels usable.
839       // we assume the distribution is centered around -4EV, e.g. the center of the nodes
840       // the exposure boost should be used to make this assumption true
841       luminance_mask(in, luminance, width, height, ch, d->method, d->exposure_boost,
842                       CONTRAST_FULCRUM, d->contrast_boost);
843       fast_surface_blur(luminance, width, height, d->radius, d->feathering, d->iterations,
844                     DT_GF_BLENDING_LINEAR, d->scale, d->quantization, exp2f(-14.0f), 4.0f);
845       break;
846     }
847 
848     case(DT_TONEEQ_AVG_EIGF):
849     {
850       // Still no contrast boost
851       luminance_mask(in, luminance, width, height, ch, d->method, d->exposure_boost, 0.0f, 1.0f);
852       fast_eigf_surface_blur(luminance, width, height, d->radius, d->feathering, d->iterations,
853                     DT_GF_BLENDING_GEOMEAN, d->scale, d->quantization, exp2f(-14.0f), 4.0f);
854       break;
855     }
856 
857     case(DT_TONEEQ_EIGF):
858     {
859       luminance_mask(in, luminance, width, height, ch, d->method, d->exposure_boost,
860                       CONTRAST_FULCRUM, d->contrast_boost);
861       fast_eigf_surface_blur(luminance, width, height, d->radius, d->feathering, d->iterations,
862                     DT_GF_BLENDING_LINEAR, d->scale, d->quantization, exp2f(-14.0f), 4.0f);
863       break;
864     }
865 
866     default:
867     {
868       luminance_mask(in, luminance, width, height, ch, d->method, d->exposure_boost, 0.0f, 1.0f);
869       break;
870     }
871   }
872 }
873 
874 
875 /***
876  * Actual transfer functions
877  **/
878 
879 __DT_CLONE_TARGETS__
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 size_t ch)880 static inline void display_luminance_mask(const float *const restrict in,
881                                           const float *const restrict luminance,
882                                           float *const restrict out,
883                                           const dt_iop_roi_t *const roi_in, const dt_iop_roi_t *const roi_out,
884                                           const size_t ch)
885 {
886   const size_t offset_x = (roi_in->x < roi_out->x) ? -roi_in->x + roi_out->x : 0;
887   const size_t offset_y = (roi_in->y < roi_out->y) ? -roi_in->y + roi_out->y : 0;
888 
889   // The output dimensions need to be smaller or equal to the input ones
890   // there is no logical reason they shouldn't, except some weird bug in the pipe
891   // in this case, ensure we don't segfault
892   const size_t in_width = roi_in->width;
893   const size_t out_width = (roi_in->width > roi_out->width) ? roi_out->width : roi_in->width;
894   const size_t out_height = (roi_in->height > roi_out->height) ? roi_out->height : roi_in->height;
895 
896 #ifdef _OPENMP
897 #pragma omp parallel for default(none) \
898   dt_omp_firstprivate(luminance, out, in, in_width, out_width, out_height, offset_x, offset_y, ch) \
899   schedule(static) collapse(2)
900 #endif
901   for(size_t i = 0 ; i < out_height; ++i)
902     for(size_t j = 0; j < out_width; ++j)
903     {
904       // normalize the mask intensity between -8 EV and 0 EV for clarity,
905       // and add a "gamma" 2.0 for better legibility in shadows
906       const float intensity = sqrtf(fminf(fmaxf(luminance[(i + offset_y) * in_width  + (j + offset_x)] - 0.00390625f, 0.f) / 0.99609375f, 1.f));
907       const size_t index = (i * out_width + j) * ch;
908       // set gray level for the mask
909       for_each_channel(c,aligned(out))
910       {
911         out[index + c] = intensity;
912       }
913       // copy alpha channel
914       out[index + 3] = in[((i + offset_y) * in_width + (j + offset_x)) * ch + 3];
915     }
916 }
917 
918 
919 __DT_CLONE_TARGETS__
920 static
toneeq_process(struct dt_iop_module_t * self,dt_dev_pixelpipe_iop_t * piece,const void * const restrict ivoid,void * const restrict ovoid,const dt_iop_roi_t * const roi_in,const dt_iop_roi_t * const roi_out)921 void toneeq_process(struct dt_iop_module_t *self, dt_dev_pixelpipe_iop_t *piece,
922              const void *const restrict ivoid, void *const restrict ovoid,
923              const dt_iop_roi_t *const roi_in, const dt_iop_roi_t *const roi_out)
924 {
925   const dt_iop_toneequalizer_data_t *const d = (const dt_iop_toneequalizer_data_t *const)piece->data;
926   dt_iop_toneequalizer_gui_data_t *const g = (dt_iop_toneequalizer_gui_data_t *)self->gui_data;
927 
928   const float *const restrict in = dt_check_sse_aligned((float *const)ivoid);
929   float *const restrict out = dt_check_sse_aligned((float *const)ovoid);
930   float *restrict luminance = NULL;
931 
932   if(in == NULL || out == NULL)
933   {
934     // Pointers are not 64-bits aligned, and SSE code will segfault
935     dt_control_log(_("tone equalizer in/out buffer are ill-aligned, please report the bug to the developers"));
936     fprintf(stdout, "tone equalizer in/out buffer are ill-aligned, please report the bug to the developers\n");
937     return;
938   }
939 
940   const size_t width = roi_in->width;
941   const size_t height = roi_in->height;
942   const size_t num_elem = width * height;
943   const size_t ch = 4;
944 
945   // Get the hash of the upstream pipe to track changes
946   const int position = self->iop_order;
947   uint64_t hash = dt_dev_pixelpipe_cache_hash(piece->pipe->image.id, roi_out, piece->pipe, position);
948 
949   // Sanity checks
950   if(width < 1 || height < 1) return;
951   if(roi_in->width < roi_out->width || roi_in->height < roi_out->height) return; // input should be at least as large as output
952   if(piece->colors != 4) return;  // we need RGB signal
953 
954   if(!sanity_check(self))
955   {
956     // if module just got disabled by sanity checks, due to pipe position, just pass input through
957     dt_simd_memcpy(in, out, num_elem * ch);
958     return;
959   }
960 
961   // Init the luminance masks buffers
962   gboolean cached = FALSE;
963 
964   if(self->dev->gui_attached)
965   {
966     // If the module instance has changed order in the pipe, invalidate the caches
967     if(g->pipe_order != position)
968     {
969       dt_iop_gui_enter_critical_section(self);
970       g->ui_preview_hash = 0;
971       g->thumb_preview_hash = 0;
972       g->pipe_order = position;
973       g->luminance_valid = FALSE;
974       g->histogram_valid = FALSE;
975       dt_iop_gui_leave_critical_section(self);
976     }
977 
978     if((piece->pipe->type & DT_DEV_PIXELPIPE_FULL) == DT_DEV_PIXELPIPE_FULL)
979     {
980       // For DT_DEV_PIXELPIPE_FULL, we cache the luminance mask for performance
981       // but it's not accessed from GUI
982       // no need for threads lock since no other function is writing/reading that buffer
983 
984       // Re-allocate a new buffer if the full preview size has changed
985       if(g->full_preview_buf_width != width || g->full_preview_buf_height != height)
986       {
987         if(g->full_preview_buf) dt_free_align(g->full_preview_buf);
988         g->full_preview_buf = dt_alloc_sse_ps(num_elem);
989         g->full_preview_buf_width = width;
990         g->full_preview_buf_height = height;
991       }
992 
993       luminance = g->full_preview_buf;
994       cached = TRUE;
995     }
996 
997     else if((piece->pipe->type & DT_DEV_PIXELPIPE_PREVIEW) == DT_DEV_PIXELPIPE_PREVIEW)
998     {
999       // For DT_DEV_PIXELPIPE_PREVIEW, we need to cache is too to compute the full image stats
1000       // upon user request in GUI
1001       // threads locks are required since GUI reads and writes on that buffer.
1002 
1003       // Re-allocate a new buffer if the thumb preview size has changed
1004       dt_iop_gui_enter_critical_section(self);
1005       if(g->thumb_preview_buf_width != width || g->thumb_preview_buf_height != height)
1006       {
1007         if(g->thumb_preview_buf) dt_free_align(g->thumb_preview_buf);
1008         g->thumb_preview_buf = dt_alloc_sse_ps(num_elem);
1009         g->thumb_preview_buf_width = width;
1010         g->thumb_preview_buf_height = height;
1011         g->luminance_valid = FALSE;
1012       }
1013 
1014       luminance = g->thumb_preview_buf;
1015       cached = TRUE;
1016 
1017       dt_iop_gui_leave_critical_section(self);
1018     }
1019     else // just to please GCC
1020     {
1021       luminance = dt_alloc_sse_ps(num_elem);
1022     }
1023 
1024   }
1025   else
1026   {
1027     // no interactive editing/caching : just allocate a local temp buffer
1028     luminance = dt_alloc_sse_ps(num_elem);
1029   }
1030 
1031   // Check if the luminance buffer exists
1032   if(!luminance)
1033   {
1034     dt_control_log(_("tone equalizer failed to allocate memory, check your RAM settings"));
1035     return;
1036   }
1037 
1038   // Compute the luminance mask
1039   if(cached)
1040   {
1041     // caching path : store the luminance mask for GUI access
1042 
1043     if((piece->pipe->type & DT_DEV_PIXELPIPE_FULL) == DT_DEV_PIXELPIPE_FULL)
1044     {
1045       uint64_t saved_hash;
1046       hash_set_get(&g->ui_preview_hash, &saved_hash, &self->gui_lock);
1047 
1048       dt_iop_gui_enter_critical_section(self);
1049       const int luminance_valid = g->luminance_valid;
1050       dt_iop_gui_leave_critical_section(self);
1051 
1052       if(hash != saved_hash || !luminance_valid)
1053       {
1054         /* compute only if upstream pipe state has changed */
1055         compute_luminance_mask(in, luminance, width, height, ch, d);
1056         hash_set_get(&hash, &g->ui_preview_hash, &self->gui_lock);
1057       }
1058     }
1059     else if((piece->pipe->type & DT_DEV_PIXELPIPE_PREVIEW) == DT_DEV_PIXELPIPE_PREVIEW)
1060     {
1061       uint64_t saved_hash;
1062       hash_set_get(&g->thumb_preview_hash, &saved_hash, &self->gui_lock);
1063 
1064       dt_iop_gui_enter_critical_section(self);
1065       const int luminance_valid = g->luminance_valid;
1066       dt_iop_gui_leave_critical_section(self);
1067 
1068       if(saved_hash != hash || !luminance_valid)
1069       {
1070         /* compute only if upstream pipe state has changed */
1071         dt_iop_gui_enter_critical_section(self);
1072         g->thumb_preview_hash = hash;
1073         g->histogram_valid = FALSE;
1074         compute_luminance_mask(in, luminance, width, height, ch, d);
1075         g->luminance_valid = TRUE;
1076         dt_iop_gui_leave_critical_section(self);
1077       }
1078     }
1079     else // make it dummy-proof
1080     {
1081       compute_luminance_mask(in, luminance, width, height, ch, d);
1082     }
1083   }
1084   else
1085   {
1086     // no caching path : compute no matter what
1087     compute_luminance_mask(in, luminance, width, height, ch, d);
1088   }
1089 
1090   // Display output
1091   if(self->dev->gui_attached && (piece->pipe->type & DT_DEV_PIXELPIPE_FULL) == DT_DEV_PIXELPIPE_FULL)
1092   {
1093     if(g->mask_display)
1094     {
1095       display_luminance_mask(in, luminance, out, roi_in, roi_out, ch);
1096       piece->pipe->mask_display = DT_DEV_PIXELPIPE_DISPLAY_PASSTHRU;
1097     }
1098     else
1099       apply_toneequalizer(in, luminance, out, roi_in, roi_out, ch, d);
1100   }
1101   else
1102   {
1103     apply_toneequalizer(in, luminance, out, roi_in, roi_out, ch, d);
1104   }
1105 
1106   if(!cached) dt_free_align(luminance);
1107 }
1108 
process(struct dt_iop_module_t * self,dt_dev_pixelpipe_iop_t * piece,const void * const restrict ivoid,void * const restrict ovoid,const dt_iop_roi_t * const roi_in,const dt_iop_roi_t * const roi_out)1109 void process(struct dt_iop_module_t *self, dt_dev_pixelpipe_iop_t *piece,
1110              const void *const restrict ivoid, void *const restrict ovoid,
1111              const dt_iop_roi_t *const roi_in, const dt_iop_roi_t *const roi_out)
1112 {
1113   toneeq_process(self, piece, ivoid, ovoid, roi_in, roi_out);
1114 }
1115 
1116 
modify_roi_in(struct dt_iop_module_t * self,struct dt_dev_pixelpipe_iop_t * piece,const dt_iop_roi_t * roi_out,dt_iop_roi_t * roi_in)1117 void modify_roi_in(struct dt_iop_module_t *self, struct dt_dev_pixelpipe_iop_t *piece,
1118                    const dt_iop_roi_t *roi_out, dt_iop_roi_t *roi_in)
1119 {
1120   // Pad the zoomed-in view to avoid weird stuff with local averages at the borders of
1121   // the preview
1122 
1123   dt_iop_toneequalizer_data_t *const d = (dt_iop_toneequalizer_data_t *const)piece->data;
1124 
1125   // Get the scaled window radius for the box average
1126   const int max_size = (piece->iwidth > piece->iheight) ? piece->iwidth : piece->iheight;
1127   const float diameter = d->blending * max_size * roi_in->scale;
1128   const int radius = (int)((diameter - 1.0f) / ( 2.0f));
1129   d->radius = radius;
1130 
1131   /*
1132   // Enlarge the preview roi with padding if needed
1133   if(self->dev->gui_attached && sanity_check(self))
1134   {
1135     int roiy = fmaxf(roi_in->y - radius, 0.0f);
1136     int roix = fmaxf(roi_in->x - radius, 0.0f);
1137     int roir = fminf(roix + roi_in->width + 2 * radius, piece->buf_in.width * roi_in->scale);
1138     int roib = fminf(roiy + roi_in->height + 2 * radius, piece->buf_in.height * roi_in->scale);
1139 
1140     // Set the values and check
1141     roi_in->x = roix;
1142     roi_in->y = roiy;
1143     roi_in->width = roir - roi_in->x;
1144     roi_in->height = roib - roi_in->y;
1145   }
1146   */
1147 }
1148 
1149 
1150 /***
1151  * Setters and Getters for parameters
1152  *
1153  * Remember the user params split the [-8; 0] EV range in 9 channels and define a set of (x, y)
1154  * coordinates, where x are the exposure channels (evenly-spaced by 1 EV in [-8; 0] EV)
1155  * and y are the desired exposure compensation for each channel.
1156  *
1157  * This (x, y) set is interpolated by radial-basis function using a series of 8 gaussians.
1158  * Losing 1 degree of freedom makes it an approximation rather than an interpolation but
1159  * helps reducing a bit the oscillations and fills a full AVX vector.
1160  *
1161  * The coefficients/factors used in the interpolation/approximation are linear, but keep in
1162  * mind that users params are expressed as log2 gains, so we always need to do the log2/exp2
1163  * flip/flop between both.
1164  *
1165  * User params of exposure compensation are expected between [-2 ; +2] EV for practical UI reasons
1166  * and probably numerical stability reasons, but there is no theoretical obstacle to enlarge
1167  * this range. The main reason for not allowing it is tone equalizer is mostly intended
1168  * to do local changes, and these don't look so well if you are too harsh on the changes.
1169  * For heavier tonemapping, it should be used in combination with a tone curve or filmic.
1170  *
1171  ***/
1172 
compute_correction_lut(float * restrict lut,const float sigma,const float * const restrict factors)1173 static void compute_correction_lut(float* restrict lut, const float sigma, const float *const restrict factors)
1174 {
1175   const float gauss_denom = gaussian_denom(sigma);
1176   const int min_ev = -8;
1177   assert(PIXEL_CHAN == -min_ev);
1178   for(int j = 0; j <= LUT_RESOLUTION * PIXEL_CHAN; j++)
1179   {
1180     // build the correction for each pixel
1181     // as the sum of the contribution of each luminance channelcorrection
1182     float exposure = (float)j / (float)LUT_RESOLUTION + min_ev;
1183     float result = 0.0f;
1184     for(int i = 0; i < PIXEL_CHAN; ++i)
1185       result += gaussian_func(exposure - centers_ops[i], gauss_denom) * factors[i];
1186     // the user-set correction is expected in [-2;+2] EV, so is the interpolated one
1187     lut[j] = fast_clamp(result, 0.25f, 4.0f);
1188   }
1189 }
1190 
get_channels_gains(float factors[CHANNELS],const dt_iop_toneequalizer_params_t * p)1191 static void get_channels_gains(float factors[CHANNELS], const dt_iop_toneequalizer_params_t *p)
1192 {
1193   assert(CHANNELS == 9);
1194 
1195   // Get user-set channels gains in EV (log2)
1196   factors[0] = p->noise; // -8 EV
1197   factors[1] = p->ultra_deep_blacks; // -7 EV
1198   factors[2] = p->deep_blacks;       // -6 EV
1199   factors[3] = p->blacks;            // -5 EV
1200   factors[4] = p->shadows;           // -4 EV
1201   factors[5] = p->midtones;          // -3 EV
1202   factors[6] = p->highlights;        // -2 EV
1203   factors[7] = p->whites;            // -1 EV
1204   factors[8] = p->speculars;         // +0 EV
1205 }
1206 
1207 
get_channels_factors(float factors[CHANNELS],const dt_iop_toneequalizer_params_t * p)1208 static void get_channels_factors(float factors[CHANNELS], const dt_iop_toneequalizer_params_t *p)
1209 {
1210   assert(CHANNELS == 9);
1211 
1212   // Get user-set channels gains in EV (log2)
1213   get_channels_gains(factors, p);
1214 
1215   // Convert from EV offsets to linear factors
1216 #ifdef _OPENMP
1217 #pragma omp simd aligned(factors:64)
1218 #endif
1219   for(int c = 0; c < CHANNELS; ++c)
1220     factors[c] = exp2f(factors[c]);
1221 }
1222 
1223 
1224 __DT_CLONE_TARGETS__
compute_channels_factors(const float factors[PIXEL_CHAN],float out[CHANNELS],const float sigma)1225 static int compute_channels_factors(const float factors[PIXEL_CHAN], float out[CHANNELS], const float sigma)
1226 {
1227   // Input factors are the weights for the radial-basis curve approximation of user params
1228   // Output factors are the gains of the user parameters channels
1229   // aka the y coordinates of the approximation for x = { CHANNELS }
1230   assert(PIXEL_CHAN == 8);
1231 
1232   int valid = 1;
1233 
1234   #ifdef _OPENMP
1235   #pragma omp parallel for simd default(none) schedule(static) \
1236     aligned(factors, out, centers_params:64) dt_omp_firstprivate(factors, out, sigma, centers_params) shared(valid)
1237   #endif
1238   for(int i = 0; i < CHANNELS; ++i)
1239   {
1240      // Compute the new channels factors
1241     out[i] = pixel_correction(centers_params[i], factors, sigma);
1242 
1243     // check they are in [-2, 2] EV and not NAN
1244     if(isnan(out[i]) || out[i] < 0.25f || out[i] > 4.0f) valid = 0;
1245   }
1246 
1247   return valid;
1248 }
1249 
1250 
1251 __DT_CLONE_TARGETS__
compute_channels_gains(const float in[CHANNELS],float out[CHANNELS])1252 static int compute_channels_gains(const float in[CHANNELS], float out[CHANNELS])
1253 {
1254   // Helper function to compute the new channels gains (log) from the factors (linear)
1255   assert(PIXEL_CHAN == 8);
1256 
1257   const int valid = 1;
1258 
1259   for(int i = 0; i < CHANNELS; ++i)
1260     out[i] = log2f(in[i]);
1261 
1262   return valid;
1263 }
1264 
1265 
commit_channels_gains(const float factors[CHANNELS],dt_iop_toneequalizer_params_t * p)1266 static int commit_channels_gains(const float factors[CHANNELS], dt_iop_toneequalizer_params_t *p)
1267 {
1268   p->noise = factors[0];
1269   p->ultra_deep_blacks = factors[1];
1270   p->deep_blacks = factors[2];
1271   p->blacks = factors[3];
1272   p->shadows = factors[4];
1273   p->midtones = factors[5];
1274   p->highlights = factors[6];
1275   p->whites = factors[7];
1276   p->speculars = factors[8];
1277 
1278   return 1;
1279 }
1280 
1281 
1282 /***
1283  * Cache invalidation and initializatiom
1284  ***/
1285 
1286 
gui_cache_init(struct dt_iop_module_t * self)1287 static void gui_cache_init(struct dt_iop_module_t *self)
1288 {
1289   dt_iop_toneequalizer_gui_data_t *g = (dt_iop_toneequalizer_gui_data_t *)self->gui_data;
1290   if(g == NULL) return;
1291 
1292   dt_iop_gui_enter_critical_section(self);
1293   g->ui_preview_hash = 0;
1294   g->thumb_preview_hash = 0;
1295   g->max_histogram = 1;
1296   g->scale = 1.0f;
1297   g->sigma = sqrtf(2.0f);
1298   g->mask_display = FALSE;
1299 
1300   g->interpolation_valid = FALSE;  // TRUE if the interpolation_matrix is ready
1301   g->luminance_valid = FALSE;      // TRUE if the luminance cache is ready
1302   g->histogram_valid = FALSE;      // TRUE if the histogram cache and stats are ready
1303   g->lut_valid = FALSE;            // TRUE if the gui_lut is ready
1304   g->graph_valid = FALSE;          // TRUE if the UI graph view is ready
1305   g->user_param_valid = FALSE;     // TRUE if users params set in interactive view are in bounds
1306   g->factors_valid = TRUE;         // TRUE if radial-basis coeffs are ready
1307 
1308   g->valid_nodes_x = FALSE;        // TRUE if x coordinates of graph nodes have been inited
1309   g->valid_nodes_y = FALSE;        // TRUE if y coordinates of graph nodes have been inited
1310   g->area_cursor_valid = FALSE;    // TRUE if mouse cursor is over the graph area
1311   g->area_dragging = FALSE;        // TRUE if left-button has been pushed but not released and cursor motion is recorded
1312   g->cursor_valid = FALSE;         // TRUE if mouse cursor is over the preview image
1313   g->has_focus = FALSE;            // TRUE if module has focus from GTK
1314 
1315   g->full_preview_buf = NULL;
1316   g->full_preview_buf_width = 0;
1317   g->full_preview_buf_height = 0;
1318 
1319   g->thumb_preview_buf = NULL;
1320   g->thumb_preview_buf_width = 0;
1321   g->thumb_preview_buf_height = 0;
1322 
1323   g->desc = NULL;
1324   g->layout = NULL;
1325   g->cr = NULL;
1326   g->cst = NULL;
1327   g->context = NULL;
1328 
1329   g->pipe_order = 0;
1330   dt_iop_gui_leave_critical_section(self);
1331 }
1332 
1333 
build_interpolation_matrix(float A[CHANNELS * PIXEL_CHAN],const float sigma)1334 static inline void build_interpolation_matrix(float A[CHANNELS * PIXEL_CHAN],
1335                                               const float sigma)
1336 {
1337   // Build the symmetrical definite positive part of the augmented matrix
1338   // of the radial-basis interpolation weights
1339 
1340   const float gauss_denom = gaussian_denom(sigma);
1341 
1342 #ifdef _OPENMP
1343 #pragma omp simd aligned(A, centers_ops, centers_params:64) collapse(2)
1344 #endif
1345   for(int i = 0; i < CHANNELS; ++i)
1346     for(int j = 0; j < PIXEL_CHAN; ++j)
1347       A[i * PIXEL_CHAN + j] = gaussian_func(centers_params[i] - centers_ops[j], gauss_denom);
1348 }
1349 
1350 
1351 __DT_CLONE_TARGETS__
compute_log_histogram_and_stats(const float * const restrict luminance,int histogram[UI_SAMPLES],const size_t num_elem,int * max_histogram,float * first_decile,float * last_decile)1352 static inline void compute_log_histogram_and_stats(const float *const restrict luminance,
1353                                           int histogram[UI_SAMPLES],
1354                                           const size_t num_elem,
1355                                           int *max_histogram,
1356                                           float *first_decile, float *last_decile)
1357 {
1358   // (Re)init the histogram
1359   memset(histogram, 0, sizeof(int) * UI_SAMPLES);
1360 
1361   // we first calculate an extended histogram for better accuracy
1362   #define TEMP_SAMPLES 2 * UI_SAMPLES
1363   int temp_hist[TEMP_SAMPLES];
1364   memset(temp_hist, 0, sizeof(int) * TEMP_SAMPLES);
1365 
1366   // Split exposure in bins
1367 #ifdef _OPENMP
1368 #pragma omp parallel for default(none) schedule(simd:static) \
1369   dt_omp_firstprivate(luminance, num_elem) \
1370   reduction(+:temp_hist[:TEMP_SAMPLES])
1371 #endif
1372   for(size_t k = 0; k < num_elem; k++)
1373   {
1374     // extended histogram bins between [-10; +6] EV remapped between [0 ; 2 * UI_SAMPLES]
1375     const int index = CLAMP((int)(((log2f(luminance[k]) + 10.0f) / 16.0f) * (float)TEMP_SAMPLES), 0, TEMP_SAMPLES - 1);
1376     temp_hist[index] += 1;
1377   }
1378 
1379   const int first = (int)((float)num_elem * 0.05f);
1380   const int last = (int)((float)num_elem * (1.0f - 0.95f));
1381   int population = 0;
1382   int first_pos = 0;
1383   int last_pos = 0;
1384 
1385   // scout the extended histogram bins looking for deciles
1386   // these would not be accurate with the regular histogram
1387   for(int k = 0; k < TEMP_SAMPLES; ++k)
1388   {
1389     const size_t prev_population = population;
1390     population += temp_hist[k];
1391     if(prev_population < first && first <= population)
1392     {
1393       first_pos = k;
1394       break;
1395     }
1396   }
1397   population = 0;
1398   for(int k = TEMP_SAMPLES - 1; k >= 0; --k)
1399   {
1400     const size_t prev_population = population;
1401     population += temp_hist[k];
1402     if(prev_population < last && last <= population)
1403     {
1404       last_pos = k;
1405       break;
1406     }
1407   }
1408 
1409   // Convert decile positions to exposures
1410   *first_decile = 16.0 * (float)first_pos / (float)(TEMP_SAMPLES - 1) - 10.0;
1411   *last_decile = 16.0 * (float)last_pos / (float)(TEMP_SAMPLES - 1) - 10.0;
1412 
1413   // remap the extended histogram into the normal one
1414   // bins between [-8; 0] EV remapped between [0 ; UI_SAMPLES]
1415   for(size_t k = 0; k < TEMP_SAMPLES; ++k)
1416   {
1417     float EV = 16.0 * (float)k / (float)(TEMP_SAMPLES - 1) - 10.0;
1418     const int i = CLAMP((int)(((EV + 8.0f) / 8.0f) * (float)UI_SAMPLES), 0, UI_SAMPLES - 1);
1419     histogram[i] += temp_hist[k];
1420 
1421     // store the max numbers of elements in bins for later normalization
1422     *max_histogram = histogram[i] > *max_histogram ? histogram[i] : *max_histogram;
1423   }
1424 }
1425 
update_histogram(struct dt_iop_module_t * const self)1426 static inline void update_histogram(struct dt_iop_module_t *const self)
1427 {
1428   dt_iop_toneequalizer_gui_data_t *const g = (dt_iop_toneequalizer_gui_data_t *)self->gui_data;
1429   if(g == NULL) return;
1430 
1431   dt_iop_gui_enter_critical_section(self);
1432   if(!g->histogram_valid && g->luminance_valid)
1433   {
1434     const size_t num_elem = g->thumb_preview_buf_height * g->thumb_preview_buf_width;
1435     compute_log_histogram_and_stats(g->thumb_preview_buf, g->histogram, num_elem, &g->max_histogram,
1436                                       &g->histogram_first_decile, &g->histogram_last_decile);
1437     g->histogram_average = (g->histogram_first_decile + g->histogram_last_decile) / 2.0f;
1438     g->histogram_valid = TRUE;
1439   }
1440   dt_iop_gui_leave_critical_section(self);
1441 }
1442 
1443 
1444 __DT_CLONE_TARGETS__
compute_lut_correction(struct dt_iop_toneequalizer_gui_data_t * g,const float offset,const float scaling)1445 static inline void compute_lut_correction(struct dt_iop_toneequalizer_gui_data_t *g,
1446                                           const float offset,
1447                                           const float scaling)
1448 {
1449   // Compute the LUT of the exposure corrections in EV,
1450   // offset and scale it for display in GUI widget graph
1451 
1452   float *const restrict LUT = g->gui_lut;
1453   const float *const restrict factors = g->factors;
1454   const float sigma = g->sigma;
1455 
1456 #ifdef _OPENMP
1457 #pragma omp parallel for simd schedule(static) default(none) \
1458   dt_omp_firstprivate(factors, sigma, offset, scaling, LUT) \
1459   aligned(LUT, factors:64)
1460 #endif
1461   for(int k = 0; k < UI_SAMPLES; k++)
1462   {
1463     // build the inset graph curve LUT
1464     // the x range is [-14;+2] EV
1465     const float x = (8.0f * (((float)k) / ((float)(UI_SAMPLES - 1)))) - 8.0f;
1466     LUT[k] = offset - log2f(pixel_correction(x, factors, sigma)) / scaling;
1467   }
1468 }
1469 
1470 
1471 
update_curve_lut(struct dt_iop_module_t * self)1472 static inline gboolean update_curve_lut(struct dt_iop_module_t *self)
1473 {
1474   dt_iop_toneequalizer_params_t *p = (dt_iop_toneequalizer_params_t *)self->params;
1475   dt_iop_toneequalizer_gui_data_t *g = (dt_iop_toneequalizer_gui_data_t *)self->gui_data;
1476 
1477   if(g == NULL) return FALSE;
1478 
1479   gboolean valid = TRUE;
1480 
1481   dt_iop_gui_enter_critical_section(self);
1482 
1483   if(!g->interpolation_valid)
1484   {
1485     build_interpolation_matrix(g->interpolation_matrix, g->sigma);
1486     g->interpolation_valid = TRUE;
1487     g->factors_valid = FALSE;
1488   }
1489 
1490   if(!g->user_param_valid)
1491   {
1492     float factors[CHANNELS] DT_ALIGNED_ARRAY;
1493     get_channels_factors(factors, p);
1494     dt_simd_memcpy(factors, g->temp_user_params, CHANNELS);
1495     g->user_param_valid = TRUE;
1496     g->factors_valid = FALSE;
1497   }
1498 
1499   if(!g->factors_valid && g->user_param_valid)
1500   {
1501     float factors[CHANNELS] DT_ALIGNED_ARRAY;
1502     dt_simd_memcpy(g->temp_user_params, factors, CHANNELS);
1503     valid = pseudo_solve(g->interpolation_matrix, factors, CHANNELS, PIXEL_CHAN, 1);
1504     dt_simd_memcpy(factors, g->factors, PIXEL_CHAN);
1505     g->factors_valid = TRUE;
1506     g->lut_valid = FALSE;
1507   }
1508 
1509   if(!g->lut_valid && g->factors_valid)
1510   {
1511     compute_lut_correction(g, 0.5f, 4.0f);
1512     g->lut_valid = TRUE;
1513   }
1514 
1515   dt_iop_gui_leave_critical_section(self);
1516 
1517   return valid;
1518 }
1519 
1520 
init_global(dt_iop_module_so_t * module)1521 void init_global(dt_iop_module_so_t *module)
1522 {
1523   dt_iop_toneequalizer_global_data_t *gd
1524       = (dt_iop_toneequalizer_global_data_t *)malloc(sizeof(dt_iop_toneequalizer_global_data_t));
1525 
1526   module->data = gd;
1527 }
1528 
1529 
cleanup_global(dt_iop_module_so_t * module)1530 void cleanup_global(dt_iop_module_so_t *module)
1531 {
1532   free(module->data);
1533   module->data = NULL;
1534 }
1535 
1536 
commit_params(struct dt_iop_module_t * self,dt_iop_params_t * p1,dt_dev_pixelpipe_t * pipe,dt_dev_pixelpipe_iop_t * piece)1537 void commit_params(struct dt_iop_module_t *self, dt_iop_params_t *p1, dt_dev_pixelpipe_t *pipe,
1538                    dt_dev_pixelpipe_iop_t *piece)
1539 {
1540   dt_iop_toneequalizer_params_t *p = (dt_iop_toneequalizer_params_t *)p1;
1541   dt_iop_toneequalizer_data_t *d = (dt_iop_toneequalizer_data_t *)piece->data;
1542   dt_iop_toneequalizer_gui_data_t *g = (dt_iop_toneequalizer_gui_data_t *)self->gui_data;
1543 
1544   // Trivial params passing
1545   d->method = p->method;
1546   d->details = p->details;
1547   d->iterations = p->iterations;
1548   d->smoothing = p->smoothing;
1549   d->quantization = p->quantization;
1550 
1551   // UI blending param is set in % of the largest image dimension
1552   d->blending = p->blending / 100.0f;
1553 
1554   // UI guided filter feathering param increases the edges taping
1555   // but the actual regularization params applied in guided filter behaves the other way
1556   d->feathering = 1.f / (p->feathering);
1557 
1558   // UI params are in log2 offsets (EV) : convert to linear factors
1559   d->contrast_boost = exp2f(p->contrast_boost);
1560   d->exposure_boost = exp2f(p->exposure_boost);
1561 
1562   /*
1563    * Perform a radial-based interpolation using a series gaussian functions
1564    */
1565   if(self->dev->gui_attached && g)
1566   {
1567     dt_iop_gui_enter_critical_section(self);
1568     if(g->sigma != p->smoothing) g->interpolation_valid = FALSE;
1569     g->sigma = p->smoothing;
1570     g->user_param_valid = FALSE; // force updating channels factors
1571     dt_iop_gui_leave_critical_section(self);
1572 
1573     update_curve_lut(self);
1574 
1575     dt_iop_gui_enter_critical_section(self);
1576     dt_simd_memcpy(g->factors, d->factors, PIXEL_CHAN);
1577     dt_iop_gui_leave_critical_section(self);
1578   }
1579   else
1580   {
1581     // No cache : Build / Solve interpolation matrix
1582     float factors[CHANNELS] DT_ALIGNED_ARRAY;
1583     get_channels_factors(factors, p);
1584 
1585     float A[CHANNELS * PIXEL_CHAN] DT_ALIGNED_ARRAY;
1586     build_interpolation_matrix(A, p->smoothing);
1587     pseudo_solve(A, factors, CHANNELS, PIXEL_CHAN, 0);
1588 
1589     dt_simd_memcpy(factors, d->factors, PIXEL_CHAN);
1590   }
1591 
1592   // compute the correction LUT here to spare some time in process
1593   // when computing several times toneequalizer with same parameters
1594   compute_correction_lut(d->correction_lut, d->smoothing, d->factors);
1595 }
1596 
1597 
init_pipe(struct dt_iop_module_t * self,dt_dev_pixelpipe_t * pipe,dt_dev_pixelpipe_iop_t * piece)1598 void init_pipe(struct dt_iop_module_t *self, dt_dev_pixelpipe_t *pipe, dt_dev_pixelpipe_iop_t *piece)
1599 {
1600   piece->data = dt_calloc_align(64, sizeof(dt_iop_toneequalizer_data_t));
1601 }
1602 
1603 
cleanup_pipe(struct dt_iop_module_t * self,dt_dev_pixelpipe_t * pipe,dt_dev_pixelpipe_iop_t * piece)1604 void cleanup_pipe(struct dt_iop_module_t *self, dt_dev_pixelpipe_t *pipe, dt_dev_pixelpipe_iop_t *piece)
1605 {
1606   dt_free_align(piece->data);
1607   piece->data = NULL;
1608 }
1609 
show_guiding_controls(struct dt_iop_module_t * self)1610 void show_guiding_controls(struct dt_iop_module_t *self)
1611 {
1612   dt_iop_module_t *module = (dt_iop_module_t *)self;
1613   dt_iop_toneequalizer_gui_data_t *g = (dt_iop_toneequalizer_gui_data_t *)self->gui_data;
1614   const dt_iop_toneequalizer_params_t *p = (const dt_iop_toneequalizer_params_t *)module->params;
1615 
1616   switch(p->details)
1617   {
1618     case(DT_TONEEQ_NONE):
1619     {
1620       gtk_widget_set_visible(g->blending, FALSE);
1621       gtk_widget_set_visible(g->feathering, FALSE);
1622       gtk_widget_set_visible(g->iterations, FALSE);
1623       gtk_widget_set_visible(g->contrast_boost, FALSE);
1624       gtk_widget_set_visible(g->quantization, FALSE);
1625       break;
1626     }
1627 
1628     case(DT_TONEEQ_AVG_GUIDED):
1629     case(DT_TONEEQ_AVG_EIGF):
1630     {
1631       gtk_widget_set_visible(g->blending, TRUE);
1632       gtk_widget_set_visible(g->feathering, TRUE);
1633       gtk_widget_set_visible(g->iterations, TRUE);
1634       gtk_widget_set_visible(g->contrast_boost, FALSE);
1635       gtk_widget_set_visible(g->quantization, TRUE);
1636       break;
1637     }
1638 
1639     case(DT_TONEEQ_GUIDED):
1640     case(DT_TONEEQ_EIGF):
1641     {
1642       gtk_widget_set_visible(g->blending, TRUE);
1643       gtk_widget_set_visible(g->feathering, TRUE);
1644       gtk_widget_set_visible(g->iterations, TRUE);
1645       gtk_widget_set_visible(g->contrast_boost, TRUE);
1646       gtk_widget_set_visible(g->quantization, TRUE);
1647       break;
1648     }
1649   }
1650 }
1651 
update_exposure_sliders(dt_iop_toneequalizer_gui_data_t * g,dt_iop_toneequalizer_params_t * p)1652 void update_exposure_sliders(dt_iop_toneequalizer_gui_data_t *g, dt_iop_toneequalizer_params_t *p)
1653 {
1654   dt_bauhaus_slider_set_soft(g->noise, p->noise);
1655   dt_bauhaus_slider_set_soft(g->ultra_deep_blacks, p->ultra_deep_blacks);
1656   dt_bauhaus_slider_set_soft(g->deep_blacks, p->deep_blacks);
1657   dt_bauhaus_slider_set_soft(g->blacks, p->blacks);
1658   dt_bauhaus_slider_set_soft(g->shadows, p->shadows);
1659   dt_bauhaus_slider_set_soft(g->midtones, p->midtones);
1660   dt_bauhaus_slider_set_soft(g->highlights, p->highlights);
1661   dt_bauhaus_slider_set_soft(g->whites, p->whites);
1662   dt_bauhaus_slider_set_soft(g->speculars, p->speculars);
1663 }
1664 
1665 
gui_update(struct dt_iop_module_t * self)1666 void gui_update(struct dt_iop_module_t *self)
1667 {
1668   dt_iop_toneequalizer_gui_data_t *g = (dt_iop_toneequalizer_gui_data_t *)self->gui_data;
1669   dt_iop_toneequalizer_params_t *p = (dt_iop_toneequalizer_params_t *)self->params;
1670 
1671   update_exposure_sliders(g, p);
1672 
1673   dt_bauhaus_combobox_set(g->method, p->method);
1674   dt_bauhaus_combobox_set(g->details, p->details);
1675   dt_bauhaus_slider_set_soft(g->blending, p->blending);
1676   dt_bauhaus_slider_set_soft(g->feathering, p->feathering);
1677   dt_bauhaus_slider_set_soft(g->smoothing, logf(p->smoothing) / logf(sqrtf(2.0f)) - 1.0f);
1678   dt_bauhaus_slider_set_soft(g->iterations, p->iterations);
1679   dt_bauhaus_slider_set_soft(g->quantization, p->quantization);
1680   dt_bauhaus_slider_set_soft(g->contrast_boost, p->contrast_boost);
1681   dt_bauhaus_slider_set_soft(g->exposure_boost, p->exposure_boost);
1682 
1683   show_guiding_controls(self);
1684   invalidate_luminance_cache(self);
1685 
1686   dt_bauhaus_widget_set_quad_active(GTK_WIDGET(g->show_luminance_mask), g->mask_display);
1687 }
1688 
gui_changed(dt_iop_module_t * self,GtkWidget * w,void * previous)1689 void gui_changed(dt_iop_module_t *self, GtkWidget *w, void *previous)
1690 {
1691   dt_iop_toneequalizer_gui_data_t *g = (dt_iop_toneequalizer_gui_data_t *)self->gui_data;
1692   if(w == g->method     ||
1693      w == g->blending   ||
1694      w == g->feathering ||
1695      w == g->iterations ||
1696      w == g->quantization)
1697   {
1698     invalidate_luminance_cache(self);
1699   }
1700   else if (w == g->details)
1701   {
1702     invalidate_luminance_cache(self);
1703     show_guiding_controls(self);
1704   }
1705   else if (w == g->contrast_boost || w == g->exposure_boost)
1706   {
1707     invalidate_luminance_cache(self);
1708     dt_bauhaus_widget_set_quad_active(w, FALSE);
1709   }
1710 }
1711 
smoothing_callback(GtkWidget * slider,gpointer user_data)1712 static void smoothing_callback(GtkWidget *slider, gpointer user_data)
1713 {
1714   dt_iop_module_t *self = (dt_iop_module_t *)user_data;
1715   if(darktable.gui->reset) return;
1716   dt_iop_toneequalizer_params_t *p = (dt_iop_toneequalizer_params_t *)self->params;
1717   dt_iop_toneequalizer_gui_data_t *g = (dt_iop_toneequalizer_gui_data_t *)self->gui_data;
1718 
1719   p->smoothing= powf(sqrtf(2.0f), 1.0f +  dt_bauhaus_slider_get(slider));
1720 
1721   float factors[CHANNELS] DT_ALIGNED_ARRAY;
1722   get_channels_factors(factors, p);
1723 
1724   // Solve the interpolation by least-squares to check the validity of the smoothing param
1725   const int valid = update_curve_lut(self);
1726   if(!valid) dt_control_log(_("the interpolation is unstable, decrease the curve smoothing"));
1727 
1728   // Redraw graph before launching computation
1729   update_curve_lut(self);
1730   gtk_widget_queue_draw(GTK_WIDGET(g->area));
1731   dt_dev_add_history_item(darktable.develop, self, TRUE);
1732 
1733   // Unlock the colour picker so we can display our own custom cursor
1734   dt_iop_color_picker_reset(self, TRUE);
1735 }
1736 
auto_adjust_exposure_boost(GtkWidget * quad,gpointer user_data)1737 static void auto_adjust_exposure_boost(GtkWidget *quad, gpointer user_data)
1738 {
1739   dt_iop_module_t *self = (dt_iop_module_t *)user_data;
1740   dt_iop_toneequalizer_params_t *p = (dt_iop_toneequalizer_params_t *)self->params;
1741   dt_iop_toneequalizer_gui_data_t *g = (dt_iop_toneequalizer_gui_data_t *)self->gui_data;
1742 
1743   if(darktable.gui->reset) return;
1744 
1745   dt_iop_request_focus(self);
1746 
1747   if(!self->enabled)
1748   {
1749     // activate module and do nothing
1750     ++darktable.gui->reset;
1751     dt_bauhaus_slider_set_soft(g->exposure_boost, p->exposure_boost);
1752     --darktable.gui->reset;
1753 
1754     invalidate_luminance_cache(self);
1755     dt_dev_add_history_item(darktable.develop, self, TRUE);
1756     return;
1757   }
1758 
1759   if(!g->luminance_valid || self->dev->pipe->processing || !g->histogram_valid)
1760   {
1761     dt_control_log(_("wait for the preview to finish recomputing"));
1762     return;
1763   }
1764 
1765   // The goal is to get the exposure distribution centered on the equalizer view
1766   // to spread it over as many nodes as possible for better exposure control.
1767   // Controls nodes are between -8 and 0 EV,
1768   // so we aim at centering the exposure distribution on -4 EV
1769 
1770   dt_iop_gui_enter_critical_section(self);
1771   g->histogram_valid = 0;
1772   dt_iop_gui_leave_critical_section(self);
1773 
1774   update_histogram(self);
1775 
1776   // calculate exposure correction
1777   const float fd_new = exp2f(g->histogram_first_decile);
1778   const float ld_new = exp2f(g->histogram_last_decile);
1779   const float e = exp2f(p->exposure_boost);
1780   const float c = exp2f(p->contrast_boost);
1781   // revert current transformation
1782   const float fd_old = ((fd_new - CONTRAST_FULCRUM) / c + CONTRAST_FULCRUM) / e;
1783   const float ld_old = ((ld_new - CONTRAST_FULCRUM) / c + CONTRAST_FULCRUM) / e;
1784 
1785   // calculate correction
1786   const float s1 = CONTRAST_FULCRUM - exp2f(-7.0);
1787   const float s2 = exp2f(-1.0) - CONTRAST_FULCRUM;
1788   const float mix = fd_old * s2 +  ld_old * s1;
1789 
1790   p->exposure_boost = log2f(CONTRAST_FULCRUM * (s1 + s2) / mix);
1791 
1792   // Update the GUI stuff
1793   ++darktable.gui->reset;
1794   dt_bauhaus_slider_set_soft(g->exposure_boost, p->exposure_boost);
1795   --darktable.gui->reset;
1796   invalidate_luminance_cache(self);
1797   dt_dev_add_history_item(darktable.develop, self, TRUE);
1798 
1799   // Unlock the colour picker so we can display our own custom cursor
1800   dt_iop_color_picker_reset(self, TRUE);
1801 }
1802 
1803 
auto_adjust_contrast_boost(GtkWidget * quad,gpointer user_data)1804 static void auto_adjust_contrast_boost(GtkWidget *quad, gpointer user_data)
1805 {
1806   dt_iop_module_t *self = (dt_iop_module_t *)user_data;
1807   dt_iop_toneequalizer_params_t *p = (dt_iop_toneequalizer_params_t *)self->params;
1808   dt_iop_toneequalizer_gui_data_t *g = (dt_iop_toneequalizer_gui_data_t *)self->gui_data;
1809 
1810   if(darktable.gui->reset) return;
1811 
1812   dt_iop_request_focus(self);
1813 
1814   if(!self->enabled)
1815   {
1816     // activate module and do nothing
1817     ++darktable.gui->reset;
1818     dt_bauhaus_slider_set_soft(g->contrast_boost, p->contrast_boost);
1819     --darktable.gui->reset;
1820 
1821     invalidate_luminance_cache(self);
1822     dt_dev_add_history_item(darktable.develop, self, TRUE);
1823     return;
1824   }
1825 
1826   if(!g->luminance_valid || self->dev->pipe->processing || !g->histogram_valid)
1827   {
1828     dt_control_log(_("wait for the preview to finish recomputing"));
1829     return;
1830   }
1831 
1832   // The goal is to spread 90 % of the exposure histogram in the [-7, -1] EV
1833   dt_iop_gui_enter_critical_section(self);
1834   g->histogram_valid = 0;
1835   dt_iop_gui_leave_critical_section(self);
1836 
1837   update_histogram(self);
1838 
1839   // calculate contrast correction
1840   const float fd_new = exp2f(g->histogram_first_decile);
1841   const float ld_new = exp2f(g->histogram_last_decile);
1842   const float e = exp2f(p->exposure_boost);
1843   float c = exp2f(p->contrast_boost);
1844   // revert current transformation
1845   const float fd_old = ((fd_new - CONTRAST_FULCRUM) / c + CONTRAST_FULCRUM) / e;
1846   const float ld_old = ((ld_new - CONTRAST_FULCRUM) / c + CONTRAST_FULCRUM) / e;
1847 
1848   // calculate correction
1849   const float s1 = CONTRAST_FULCRUM - exp2f(-7.0);
1850   const float s2 = exp2f(-1.0) - CONTRAST_FULCRUM;
1851   const float mix = fd_old * s2 +  ld_old * s1;
1852 
1853   c = log2f(mix / (CONTRAST_FULCRUM * (ld_old - fd_old)) / c);
1854 
1855   // when adding contrast, blur filters modify the histogram in a way difficult to predict
1856   // here we implement a heuristic correction based on a set of images and regression analysis
1857   if(p->details == DT_TONEEQ_EIGF && c > 0.0f)
1858   {
1859     const float correction = -0.0276f + 0.01823 * p->feathering + (0.7566f - 1.0f) * c;
1860     if(p->feathering < 5.0f)
1861       c += correction;
1862     else if(p->feathering < 10.0f)
1863       c += correction * (2.0f - p->feathering / 5.0f);
1864   }
1865   else if(p->details == DT_TONEEQ_GUIDED && c > 0.0f)
1866       c = 0.0235f + 1.1225f * c;
1867 
1868   p->contrast_boost += c;
1869 
1870   // Update the GUI stuff
1871   ++darktable.gui->reset;
1872   dt_bauhaus_slider_set_soft(g->contrast_boost, p->contrast_boost);
1873   --darktable.gui->reset;
1874   invalidate_luminance_cache(self);
1875   dt_dev_add_history_item(darktable.develop, self, TRUE);
1876 
1877   // Unlock the colour picker so we can display our own custom cursor
1878   dt_iop_color_picker_reset(self, TRUE);
1879 }
1880 
1881 
show_luminance_mask_callback(GtkWidget * togglebutton,dt_iop_module_t * self)1882 static void show_luminance_mask_callback(GtkWidget *togglebutton, dt_iop_module_t *self)
1883 {
1884   if(darktable.gui->reset) return;
1885   dt_iop_request_focus(self);
1886 
1887   gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(self->off), TRUE);
1888 
1889   dt_iop_toneequalizer_gui_data_t *g = (dt_iop_toneequalizer_gui_data_t *)self->gui_data;
1890 
1891   // if blend module is displaying mask do not display it here
1892   if(self->request_mask_display)
1893   {
1894     dt_control_log(_("cannot display masks when the blending mask is displayed"));
1895     dt_bauhaus_widget_set_quad_active(GTK_WIDGET(g->show_luminance_mask), FALSE);
1896     g->mask_display = 0;
1897     return;
1898   }
1899   else
1900     g->mask_display = !g->mask_display;
1901 
1902   dt_bauhaus_widget_set_quad_active(GTK_WIDGET(g->show_luminance_mask), g->mask_display);
1903 //  dt_dev_reprocess_center(self->dev);
1904   dt_iop_refresh_center(self);
1905 
1906   // Unlock the colour picker so we can display our own custom cursor
1907   dt_iop_color_picker_reset(self, TRUE);
1908 }
1909 
1910 
1911 /***
1912  * GUI Interactivity
1913  **/
1914 
switch_cursors(struct dt_iop_module_t * self)1915 static void switch_cursors(struct dt_iop_module_t *self)
1916 {
1917   dt_iop_toneequalizer_gui_data_t *g = (dt_iop_toneequalizer_gui_data_t *)self->gui_data;
1918   if(!g || !self->dev->gui_attached) return;
1919 
1920   GtkWidget *widget = dt_ui_main_window(darktable.gui->ui);
1921 
1922   // if we are editing masks or using colour-pickers, do not display controls
1923   if(!sanity_check(self) || in_mask_editing(self) || dt_iop_color_picker_is_visible(self->dev))
1924   {
1925     // display default cursor
1926     GdkCursor *const cursor = gdk_cursor_new_from_name(gdk_display_get_default(), "default");
1927     gdk_window_set_cursor(gtk_widget_get_window(widget), cursor);
1928     g_object_unref(cursor);
1929 
1930     return;
1931   }
1932 
1933   // check if module is expanded
1934   dt_iop_gui_enter_critical_section(self);
1935   g->has_focus = self->expanded;
1936   dt_iop_gui_leave_critical_section(self);
1937 
1938   if(!g->has_focus)
1939   {
1940     // if module lost focus or is disabled
1941     // do nothing and let the app decide
1942     return;
1943   }
1944   else if( ((self->dev->pipe->processing) ||
1945           (self->dev->image_status == DT_DEV_PIXELPIPE_DIRTY) ||
1946           (self->dev->preview_status == DT_DEV_PIXELPIPE_DIRTY)) && g->cursor_valid)
1947   {
1948     // if pipe is busy or dirty but cursor is on preview,
1949     // display waiting cursor while pipe reprocesses
1950     GdkCursor *const cursor = gdk_cursor_new_from_name(gdk_display_get_default(), "wait");
1951     gdk_window_set_cursor(gtk_widget_get_window(widget), cursor);
1952     g_object_unref(cursor);
1953 
1954     dt_control_queue_redraw_center();
1955   }
1956   else if(g->cursor_valid && !self->dev->pipe->processing)
1957   {
1958     // if pipe is clean and idle and cursor is on preview,
1959     // hide GTK cursor because we display our custom one
1960     dt_control_change_cursor(GDK_BLANK_CURSOR);
1961     dt_control_hinter_message(darktable.control,
1962                               _("scroll over image to change tone exposure\n"
1963                                 "shift+scroll for large steps; "
1964                                 "ctrl+scroll for small steps"));
1965 
1966     dt_control_queue_redraw_center();
1967   }
1968   else if(!g->cursor_valid)
1969   {
1970     // if module is active and opened but cursor is out of the preview,
1971     // display default cursor
1972     GdkCursor *const cursor = gdk_cursor_new_from_name(gdk_display_get_default(), "default");
1973     gdk_window_set_cursor(gtk_widget_get_window(widget), cursor);
1974     g_object_unref(cursor);
1975 
1976     dt_control_queue_redraw_center();
1977   }
1978   else
1979   {
1980     // in any other situation where module has focus,
1981     // reset the cursor but don't launch a redraw
1982     GdkCursor *const cursor = gdk_cursor_new_from_name(gdk_display_get_default(), "default");
1983     gdk_window_set_cursor(gtk_widget_get_window(widget), cursor);
1984     g_object_unref(cursor);
1985   }
1986 }
1987 
1988 
mouse_moved(struct dt_iop_module_t * self,double x,double y,double pressure,int which)1989 int mouse_moved(struct dt_iop_module_t *self, double x, double y, double pressure, int which)
1990 {
1991   // Whenever the mouse moves over the picture preview, store its coordinates in the GUI struct
1992   // for later use. This works only if dev->preview_pipe perfectly overlaps with the UI preview
1993   // meaning all distortions, cropping, rotations etc. are applied before this module in the pipe.
1994 
1995   dt_develop_t *dev = self->dev;
1996   dt_iop_toneequalizer_gui_data_t *g = (dt_iop_toneequalizer_gui_data_t *)self->gui_data;
1997 
1998   dt_iop_gui_enter_critical_section(self);
1999   const int fail = !sanity_check(self);
2000   dt_iop_gui_leave_critical_section(self);
2001   if(fail) return 0;
2002 
2003   const int wd = dev->preview_pipe->backbuf_width;
2004   const int ht = dev->preview_pipe->backbuf_height;
2005 
2006   if(g == NULL) return 0;
2007   if(wd < 1 || ht < 1) return 0;
2008 
2009   float pzx, pzy;
2010   dt_dev_get_pointer_zoom_pos(dev, x, y, &pzx, &pzy);
2011   pzx += 0.5f;
2012   pzy += 0.5f;
2013 
2014   const int x_pointer = pzx * wd;
2015   const int y_pointer = pzy * ht;
2016 
2017   dt_iop_gui_enter_critical_section(self);
2018   // Cursor is valid if it's inside the picture frame
2019   if(x_pointer >= 0 && x_pointer < wd && y_pointer >= 0 && y_pointer < ht)
2020   {
2021     g->cursor_valid = TRUE;
2022     g->cursor_pos_x = x_pointer;
2023     g->cursor_pos_y = y_pointer;
2024   }
2025   else
2026   {
2027     g->cursor_valid = FALSE;
2028     g->cursor_pos_x = 0;
2029     g->cursor_pos_y = 0;
2030   }
2031   dt_iop_gui_leave_critical_section(self);
2032 
2033   // store the actual exposure too, to spare I/O op
2034   if(g->cursor_valid && !dev->pipe->processing && g->luminance_valid)
2035     g->cursor_exposure = log2f(get_luminance_from_buffer(g->thumb_preview_buf,
2036                                                          g->thumb_preview_buf_width,
2037                                                          g->thumb_preview_buf_height,
2038                                                          (size_t)x_pointer, (size_t)y_pointer));
2039 
2040   switch_cursors(self);
2041   return 1;
2042 }
2043 
2044 
mouse_leave(struct dt_iop_module_t * self)2045 int mouse_leave(struct dt_iop_module_t *self)
2046 {
2047   dt_iop_toneequalizer_gui_data_t *g = (dt_iop_toneequalizer_gui_data_t *)self->gui_data;
2048 
2049   if(g == NULL) return 0;
2050 
2051   dt_iop_gui_enter_critical_section(self);
2052   g->cursor_valid = FALSE;
2053   g->area_active_node = -1;
2054   dt_iop_gui_leave_critical_section(self);
2055 
2056   // display default cursor
2057   GtkWidget *widget = dt_ui_main_window(darktable.gui->ui);
2058   GdkCursor *cursor = gdk_cursor_new_from_name(gdk_display_get_default(), "default");
2059   gdk_window_set_cursor(gtk_widget_get_window(widget), cursor);
2060   g_object_unref(cursor);
2061   dt_control_queue_redraw_center();
2062   gtk_widget_queue_draw(GTK_WIDGET(g->area));
2063 
2064   return 1;
2065 }
2066 
2067 
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)2068 static inline int set_new_params_interactive(const float control_exposure, const float exposure_offset, const float blending_sigma,
2069                                               dt_iop_toneequalizer_gui_data_t *g,  dt_iop_toneequalizer_params_t *p)
2070 {
2071   // Apply an exposure offset optimized smoothly over all the exposure channels,
2072   // taking user instruction to apply exposure_offset EV at control_exposure EV,
2073   // and commit the new params is the solution is valid.
2074 
2075   // Raise the user params accordingly to control correction and distance from cursor exposure
2076   // to blend smoothly the desired correction
2077   const float std = gaussian_denom(blending_sigma);
2078   if(g->user_param_valid)
2079   {
2080     for(int i = 0; i < CHANNELS; ++i)
2081       g->temp_user_params[i] *= exp2f(gaussian_func(centers_params[i] - control_exposure, std) * exposure_offset);
2082   }
2083 
2084   // Get the new weights for the radial-basis approximation
2085   float factors[CHANNELS] DT_ALIGNED_ARRAY;
2086   dt_simd_memcpy(g->temp_user_params, factors, CHANNELS);
2087   if(g->user_param_valid)
2088     g->user_param_valid = pseudo_solve(g->interpolation_matrix, factors, CHANNELS, PIXEL_CHAN, 1);;
2089   if(!g->user_param_valid) dt_control_log(_("the interpolation is unstable, decrease the curve smoothing"));
2090 
2091   // Compute new user params for channels and store them locally
2092   if(g->user_param_valid)
2093     g->user_param_valid = compute_channels_factors(factors, g->temp_user_params, g->sigma);
2094   if(!g->user_param_valid) dt_control_log(_("some parameters are out-of-bounds"));
2095 
2096   const int commit = g->user_param_valid;
2097 
2098   if(commit)
2099   {
2100     // Accept the solution
2101     dt_simd_memcpy(factors, g->factors, PIXEL_CHAN);
2102     g->lut_valid = 0;
2103 
2104     // Convert the linear temp parameters to log gains and commit
2105     float gains[CHANNELS] DT_ALIGNED_ARRAY;
2106     compute_channels_gains(g->temp_user_params, gains);
2107     commit_channels_gains(gains, p);
2108   }
2109   else
2110   {
2111     // Reset the GUI copy of user params
2112     get_channels_factors(factors, p);
2113     dt_simd_memcpy(factors, g->temp_user_params, CHANNELS);
2114     g->user_param_valid = 1;
2115   }
2116 
2117   return commit;
2118 }
2119 
2120 
scrolled(struct dt_iop_module_t * self,double x,double y,int up,uint32_t state)2121 int scrolled(struct dt_iop_module_t *self, double x, double y, int up, uint32_t state)
2122 {
2123   dt_develop_t *dev = self->dev;
2124   dt_iop_toneequalizer_gui_data_t *g = (dt_iop_toneequalizer_gui_data_t *)self->gui_data;
2125   dt_iop_toneequalizer_params_t *p = (dt_iop_toneequalizer_params_t *)self->params;
2126 
2127   if(!sanity_check(self)) return 0;
2128   if(darktable.gui->reset) return 1;
2129   if(g == NULL) return 0;
2130   if(!g->has_focus) return 0;
2131 
2132   // turn-on the module if off
2133   if(!self->enabled)
2134     if(self->off) gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(self->off), 1);
2135 
2136   // add an option to allow skip mouse events while editing masks
2137   if(darktable.develop->darkroom_skip_mouse_events || in_mask_editing(self)) return 0;
2138 
2139   // if GUI buffers not ready, exit but still handle the cursor
2140   dt_iop_gui_enter_critical_section(self);
2141   const int fail = (!g->cursor_valid || !g->luminance_valid || !g->interpolation_valid || !g->user_param_valid || dev->pipe->processing || !g->has_focus);
2142   dt_iop_gui_leave_critical_section(self);
2143   if(fail) return 1;
2144 
2145   // re-read the exposure in case it has changed
2146   dt_iop_gui_enter_critical_section(self);
2147   g->cursor_exposure = log2f(get_luminance_from_buffer(g->thumb_preview_buf,
2148                                                        g->thumb_preview_buf_width,
2149                                                        g->thumb_preview_buf_height,
2150                                                        (size_t)g->cursor_pos_x, (size_t)g->cursor_pos_y));
2151   dt_iop_gui_leave_critical_section(self);
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
2159   else if(dt_modifier_is(state, GDK_CONTROL_MASK))
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   dt_iop_gui_enter_critical_section(self);
2168   const int commit = set_new_params_interactive(g->cursor_exposure, offset, g->sigma * g->sigma / 2.0f, g, p);
2169   dt_iop_gui_leave_critical_section(self);
2170 
2171   gtk_widget_queue_draw(GTK_WIDGET(g->area));
2172 
2173   if(commit)
2174   {
2175     // Update GUI with new params
2176     ++darktable.gui->reset;
2177     update_exposure_sliders(g, p);
2178     --darktable.gui->reset;
2179 
2180     dt_dev_add_history_item(darktable.develop, self, FALSE);
2181   }
2182 
2183   return 1;
2184 }
2185 
2186 /***
2187  * GTK/Cairo drawings and custom widgets
2188  **/
2189 
2190 static inline gboolean _init_drawing(dt_iop_module_t *const restrict self, GtkWidget *widget,
2191                                      dt_iop_toneequalizer_gui_data_t *const restrict g);
2192 
2193 
cairo_draw_hatches(cairo_t * cr,double center[2],double span[2],int instances,double line_width,double shade)2194 void cairo_draw_hatches(cairo_t *cr, double center[2], double span[2], int instances, double line_width, double shade)
2195 {
2196   // center is the (x, y) coordinates of the region to draw
2197   // span is the distance of the region's bounds to the center, over (x, y) axes
2198 
2199   // Get the coordinates of the corners of the bounding box of the region
2200   double C0[2] = { center[0] - span[0], center[1] - span[1] };
2201   double C2[2] = { center[0] + span[0], center[1] + span[1] };
2202 
2203   double delta[2] = { 2.0 * span[0] / (double)instances,
2204                       2.0 * span[1] / (double)instances };
2205 
2206   cairo_set_line_width(cr, line_width);
2207   cairo_set_source_rgb(cr, shade, shade, shade);
2208 
2209   for(int i = -instances / 2 - 1; i <= instances / 2 + 1; i++)
2210   {
2211     cairo_move_to(cr, C0[0] + (double)i * delta[0], C0[1]);
2212     cairo_line_to(cr, C2[0] + (double)i * delta[0], C2[1]);
2213     cairo_stroke(cr);
2214   }
2215 }
2216 
get_shade_from_luminance(cairo_t * cr,const float luminance,const float alpha)2217 static void get_shade_from_luminance(cairo_t *cr, const float luminance, const float alpha)
2218 {
2219   // TODO: fetch screen gamma from ICC display profile
2220   const float gamma = 1.0f / 2.2f;
2221   const float shade = powf(luminance, gamma);
2222   cairo_set_source_rgba(cr, shade, shade, shade, alpha);
2223 }
2224 
2225 
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)2226 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)
2227 {
2228   // Draw a circle cursor filled with a grey shade corresponding to a luminance value
2229   // or hatches if the value is above the overexposed threshold
2230 
2231   const double radius_z = radius / zoom_scale;
2232 
2233   get_shade_from_luminance(cr, luminance, alpha);
2234   cairo_arc(cr, pointerx, pointery, radius_z, 0, 2 * M_PI);
2235   cairo_fill_preserve(cr);
2236   cairo_save(cr);
2237   cairo_clip(cr);
2238 
2239   if(log2f(luminance) > 0.0f)
2240   {
2241     // if overexposed, draw hatches
2242     double pointer_coord[2] = { pointerx, pointery };
2243     double span[2] = { radius_z, radius_z };
2244     cairo_draw_hatches(cr, pointer_coord, span, instances, DT_PIXEL_APPLY_DPI(1. / zoom_scale), 0.3);
2245   }
2246   cairo_restore(cr);
2247 }
2248 
2249 
match_color_to_background(cairo_t * cr,const float exposure,const float alpha)2250 static void match_color_to_background(cairo_t *cr, const float exposure, const float alpha)
2251 {
2252   float shade = 0.0f;
2253   // TODO: put that as a preference in darktablerc
2254   const float contrast = 1.0f;
2255 
2256   if(exposure > -2.5f)
2257     shade = (fminf(exposure * contrast, 0.0f) - 2.5f);
2258   else
2259     shade = (fmaxf(exposure / contrast, -5.0f) + 2.5f);
2260 
2261   get_shade_from_luminance(cr, exp2f(shade), alpha);
2262 }
2263 
2264 
gui_post_expose(struct dt_iop_module_t * self,cairo_t * cr,int32_t width,int32_t height,int32_t pointerx,int32_t pointery)2265 void gui_post_expose(struct dt_iop_module_t *self, cairo_t *cr, int32_t width, int32_t height,
2266                      int32_t pointerx, int32_t pointery)
2267 {
2268   // Draw the custom exposure cursor over the image preview
2269 
2270   dt_develop_t *dev = self->dev;
2271   dt_iop_toneequalizer_gui_data_t *g = (dt_iop_toneequalizer_gui_data_t *)self->gui_data;
2272 
2273   // if we are editing masks, do not display controls
2274   if(in_mask_editing(self)) return;
2275 
2276   dt_iop_gui_enter_critical_section(self);
2277   const int fail = (!g->cursor_valid || !g->interpolation_valid || dev->pipe->processing || !sanity_check(self) || !g->has_focus);
2278   dt_iop_gui_leave_critical_section(self);
2279   if(fail) return;
2280 
2281   if(!g->graph_valid)
2282     if(!_init_drawing(self, self->widget, g)) return;
2283 
2284   dt_iop_gui_enter_critical_section(self);
2285 
2286   // Get coordinates
2287   const float x_pointer = g->cursor_pos_x;
2288   const float y_pointer = g->cursor_pos_y;
2289 
2290   float exposure_in = 0.0f;
2291   float luminance_in = 0.0f;
2292   float correction = 0.0f;
2293   float exposure_out = 0.0f;
2294   float luminance_out = 0.0f;
2295   if(g->luminance_valid && self->enabled)
2296   {
2297     // re-read the exposure in case it has changed
2298     g->cursor_exposure = log2f(get_luminance_from_buffer(g->thumb_preview_buf,
2299                                                          g->thumb_preview_buf_width,
2300                                                          g->thumb_preview_buf_height,
2301                                                          (size_t)g->cursor_pos_x, (size_t)g->cursor_pos_y));
2302 
2303     // Get the corresponding exposure
2304     exposure_in = g->cursor_exposure;
2305     luminance_in = exp2f(exposure_in);
2306 
2307     // Get the corresponding correction and compute resulting exposure
2308     correction = log2f(pixel_correction(exposure_in, g->factors, g->sigma));
2309     exposure_out = exposure_in + correction;
2310     luminance_out = exp2f(exposure_out);
2311   }
2312 
2313   dt_iop_gui_leave_critical_section(self);
2314 
2315   if(isnan(correction) || isnan(exposure_in)) return; // something went wrong
2316 
2317   // Rescale and shift Cairo drawing coordinates
2318   const float wd = dev->preview_pipe->backbuf_width;
2319   const float ht = dev->preview_pipe->backbuf_height;
2320   const float zoom_y = dt_control_get_dev_zoom_y();
2321   const float zoom_x = dt_control_get_dev_zoom_x();
2322   const dt_dev_zoom_t zoom = dt_control_get_dev_zoom();
2323   const int closeup = dt_control_get_dev_closeup();
2324   const float zoom_scale = dt_dev_get_zoom_scale(dev, zoom, 1<<closeup, 1);
2325   cairo_translate(cr, width / 2.0, height / 2.0);
2326   cairo_scale(cr, zoom_scale, zoom_scale);
2327   cairo_translate(cr, -.5f * wd - zoom_x * wd, -.5f * ht - zoom_y * ht);
2328 
2329 
2330   // set custom cursor dimensions
2331   const double outer_radius = 16.;
2332   const double inner_radius = outer_radius / 2.0;
2333   const double setting_offset_x = (outer_radius + 4. * g->inner_padding) / zoom_scale;
2334   const double fill_width = DT_PIXEL_APPLY_DPI(4. / zoom_scale);
2335 
2336   // setting fill bars
2337   match_color_to_background(cr, exposure_out, 1.0);
2338   cairo_set_line_width(cr, 2.0 * fill_width);
2339   cairo_move_to(cr, x_pointer - setting_offset_x, y_pointer);
2340 
2341   if(correction > 0.0f)
2342     cairo_arc(cr, x_pointer, y_pointer, setting_offset_x, M_PI, M_PI + correction * M_PI / 4.0);
2343   else
2344     cairo_arc_negative(cr, x_pointer, y_pointer, setting_offset_x, M_PI, M_PI + correction * M_PI / 4.0);
2345 
2346   cairo_stroke(cr);
2347 
2348   // setting ground level
2349   cairo_set_line_width(cr, DT_PIXEL_APPLY_DPI(1.5 / zoom_scale));
2350   cairo_move_to(cr, x_pointer + (outer_radius + 2. * g->inner_padding) / zoom_scale, y_pointer);
2351   cairo_line_to(cr, x_pointer + outer_radius / zoom_scale, y_pointer);
2352   cairo_move_to(cr, x_pointer - outer_radius / zoom_scale, y_pointer);
2353   cairo_line_to(cr, x_pointer - setting_offset_x - 4.0 * g->inner_padding / zoom_scale, y_pointer);
2354   cairo_stroke(cr);
2355 
2356   // setting cursor cross hair
2357   cairo_set_line_width(cr, DT_PIXEL_APPLY_DPI(1.5 / zoom_scale));
2358   cairo_move_to(cr, x_pointer, y_pointer + setting_offset_x + fill_width);
2359   cairo_line_to(cr, x_pointer, y_pointer + outer_radius / zoom_scale);
2360   cairo_move_to(cr, x_pointer, y_pointer - outer_radius / zoom_scale);
2361   cairo_line_to(cr, x_pointer, y_pointer - setting_offset_x - fill_width);
2362   cairo_stroke(cr);
2363 
2364   // draw exposure cursor
2365   draw_exposure_cursor(cr, x_pointer, y_pointer, outer_radius, luminance_in, zoom_scale, 6, .9);
2366   draw_exposure_cursor(cr, x_pointer, y_pointer, inner_radius, luminance_out, zoom_scale, 3, .9);
2367 
2368   // Create Pango objects : texts
2369   char text[256];
2370   PangoLayout *layout;
2371   PangoRectangle ink;
2372   PangoFontDescription *desc = pango_font_description_copy_static(darktable.bauhaus->pango_font_desc);
2373 
2374   // Avoid text resizing based on zoom level
2375   const int old_size = pango_font_description_get_size(desc);
2376   pango_font_description_set_size (desc, (int)(old_size / zoom_scale));
2377   layout = pango_cairo_create_layout(cr);
2378   pango_layout_set_font_description(layout, desc);
2379   pango_cairo_context_set_resolution(pango_layout_get_context(layout), darktable.gui->dpi);
2380 
2381   // Build text object
2382   if(g->luminance_valid && self->enabled)
2383     snprintf(text, sizeof(text), _("%+.1f EV"), exposure_in);
2384   else
2385     snprintf(text, sizeof(text), "? EV");
2386   pango_layout_set_text(layout, text, -1);
2387   pango_layout_get_pixel_extents(layout, &ink, NULL);
2388 
2389   // Draw the text plain blackground
2390   get_shade_from_luminance(cr, luminance_out, 0.75);
2391   cairo_rectangle(cr, x_pointer + (outer_radius + 2. * g->inner_padding) / zoom_scale,
2392                       y_pointer - ink.y - ink.height / 2.0 - g->inner_padding / zoom_scale,
2393                       ink.width + 2.0 * ink.x + 4. * g->inner_padding / zoom_scale,
2394                       ink.height + 2.0 * ink.y + 2. * g->inner_padding / zoom_scale);
2395   cairo_fill(cr);
2396 
2397   // Display the EV reading
2398   match_color_to_background(cr, exposure_out, 1.0);
2399   cairo_move_to(cr, x_pointer + (outer_radius + 4. * g->inner_padding) / zoom_scale,
2400                     y_pointer - ink.y - ink.height / 2.);
2401   pango_cairo_show_layout(cr, layout);
2402 
2403   cairo_stroke(cr);
2404 
2405   pango_font_description_free(desc);
2406   g_object_unref(layout);
2407 
2408   if(g->luminance_valid && self->enabled)
2409   {
2410     // Search for nearest node in graph and highlight it
2411     const float radius_threshold = 0.45f;
2412     g->area_active_node = -1;
2413     if(g->cursor_valid)
2414       for(int i = 0; i < CHANNELS; ++i)
2415       {
2416         const float delta_x = fabsf(g->cursor_exposure - centers_params[i]);
2417         if(delta_x < radius_threshold)
2418           g->area_active_node = i;
2419       }
2420 
2421     gtk_widget_queue_draw(GTK_WIDGET(g->area));
2422   }
2423 }
2424 
2425 
gui_focus(struct dt_iop_module_t * self,gboolean in)2426 void gui_focus(struct dt_iop_module_t *self, gboolean in)
2427 {
2428   dt_iop_toneequalizer_gui_data_t *g = (dt_iop_toneequalizer_gui_data_t *)self->gui_data;
2429   dt_iop_gui_enter_critical_section(self);
2430   g->has_focus = in;
2431   dt_iop_gui_leave_critical_section(self);
2432   switch_cursors(self);
2433   if(!in)
2434   {
2435     //lost focus - stop showing mask
2436     g->mask_display = FALSE;
2437     dt_bauhaus_widget_set_quad_active(GTK_WIDGET(g->show_luminance_mask), FALSE);
2438     dt_dev_reprocess_center(self->dev);
2439     dt_collection_hint_message(darktable.collection);
2440   }
2441   else
2442   {
2443     dt_control_hinter_message(darktable.control,
2444                               _("scroll over image to change tone exposure\n"
2445                                 "shift+scroll for large steps; "
2446                                 "ctrl+scroll for small steps"));
2447   }
2448 }
2449 
2450 
_init_drawing(dt_iop_module_t * const restrict self,GtkWidget * widget,dt_iop_toneequalizer_gui_data_t * const restrict g)2451 static inline gboolean _init_drawing(dt_iop_module_t *const restrict self, GtkWidget *widget,
2452                                      dt_iop_toneequalizer_gui_data_t *const restrict g)
2453 {
2454   // Cache the equalizer graph objects to avoid recomputing all the view at each redraw
2455   gtk_widget_get_allocation(widget, &g->allocation);
2456 
2457   if(g->cst) cairo_surface_destroy(g->cst);
2458   g->cst = dt_cairo_image_surface_create(CAIRO_FORMAT_ARGB32, g->allocation.width, g->allocation.height);
2459 
2460   if(g->cr) cairo_destroy(g->cr);
2461   g->cr = cairo_create(g->cst);
2462 
2463   if(g->layout) g_object_unref(g->layout);
2464   g->layout = pango_cairo_create_layout(g->cr);
2465 
2466   if(g->desc) pango_font_description_free(g->desc);
2467   g->desc = pango_font_description_copy_static(darktable.bauhaus->pango_font_desc);
2468 
2469   pango_layout_set_font_description(g->layout, g->desc);
2470   pango_cairo_context_set_resolution(pango_layout_get_context(g->layout), darktable.gui->dpi);
2471   g->context = gtk_widget_get_style_context(widget);
2472 
2473   char text[256];
2474 
2475   // Get the text line height for spacing
2476   snprintf(text, sizeof(text), "X");
2477   pango_layout_set_text(g->layout, text, -1);
2478   pango_layout_get_pixel_extents(g->layout, &g->ink, NULL);
2479   g->line_height = g->ink.height;
2480 
2481   // Get the width of a minus sign for legend labels spacing
2482   snprintf(text, sizeof(text), "-");
2483   pango_layout_set_text(g->layout, text, -1);
2484   pango_layout_get_pixel_extents(g->layout, &g->ink, NULL);
2485   g->sign_width = g->ink.width / 2.0;
2486 
2487   // Set the sizes, margins and paddings
2488   g->inner_padding = 4; // TODO: INNER_PADDING value as defined in bauhaus.c macros, sync them
2489   g->inset = g->inner_padding + darktable.bauhaus->quad_width;
2490   g->graph_width = g->allocation.width - g->inset - 2.0 * g->line_height; // align the right border on sliders
2491   g->graph_height = g->allocation.height - g->inset - 2.0 * g->line_height; // give room to nodes
2492   g->gradient_left_limit = 0.0;
2493   g->gradient_right_limit = g->graph_width;
2494   g->gradient_top_limit = g->graph_height + 2 * g->inner_padding;
2495   g->gradient_width = g->gradient_right_limit - g->gradient_left_limit;
2496   g->legend_top_limit = -0.5 * g->line_height - 2.0 * g->inner_padding;
2497   g->x_label = g->graph_width + g->sign_width + 3.0 * g->inner_padding;
2498 
2499   gtk_render_background(g->context, g->cr, 0, 0, g->allocation.width, g->allocation.height);
2500 
2501   // set the graph as the origin of the coordinates
2502   cairo_translate(g->cr, g->line_height + 2 * g->inner_padding, g->line_height + 3 * g->inner_padding);
2503 
2504   // display x-axis and y-axis legends (EV)
2505   set_color(g->cr, darktable.bauhaus->graph_fg);
2506 
2507   float value = -8.0f;
2508 
2509   for(int k = 0; k < CHANNELS; k++)
2510   {
2511     const float xn = (((float)k) / ((float)(CHANNELS - 1))) * g->graph_width - g->sign_width;
2512     snprintf(text, sizeof(text), "%+.0f", value);
2513     pango_layout_set_text(g->layout, text, -1);
2514     pango_layout_get_pixel_extents(g->layout, &g->ink, NULL);
2515     cairo_move_to(g->cr, xn - 0.5 * g->ink.width - g->ink.x,
2516                          g->legend_top_limit - 0.5 * g->ink.height - g->ink.y);
2517     pango_cairo_show_layout(g->cr, g->layout);
2518     cairo_stroke(g->cr);
2519 
2520     value += 1.0;
2521   }
2522 
2523   value = 2.0f;
2524 
2525   for(int k = 0; k < 5; k++)
2526   {
2527     const float yn = (k / 4.0f) * g->graph_height;
2528     snprintf(text, sizeof(text), "%+.0f", value);
2529     pango_layout_set_text(g->layout, text, -1);
2530     pango_layout_get_pixel_extents(g->layout, &g->ink, NULL);
2531     cairo_move_to(g->cr, g->x_label - 0.5 * g->ink.width - g->ink.x,
2532                 yn - 0.5 * g->ink.height - g->ink.y);
2533     pango_cairo_show_layout(g->cr, g->layout);
2534     cairo_stroke(g->cr);
2535 
2536     value -= 1.0;
2537   }
2538 
2539   /** x axis **/
2540   // Draw the perceptually even gradient
2541   cairo_pattern_t *grad;
2542   grad = cairo_pattern_create_linear(g->gradient_left_limit, 0.0, g->gradient_right_limit, 0.0);
2543   dt_cairo_perceptual_gradient(grad, 1.0);
2544   cairo_set_line_width(g->cr, 0.0);
2545   cairo_rectangle(g->cr, g->gradient_left_limit, g->gradient_top_limit, g->gradient_width, g->line_height);
2546   cairo_set_source(g->cr, grad);
2547   cairo_fill(g->cr);
2548   cairo_pattern_destroy(grad);
2549 
2550   /** y axis **/
2551   // Draw the perceptually even gradient
2552   grad = cairo_pattern_create_linear(0.0, g->graph_height, 0.0, 0.0);
2553   dt_cairo_perceptual_gradient(grad, 1.0);
2554   cairo_set_line_width(g->cr, 0.0);
2555   cairo_rectangle(g->cr, -g->line_height - 2 * g->inner_padding, 0.0, g->line_height, g->graph_height);
2556   cairo_set_source(g->cr, grad);
2557   cairo_fill(g->cr);
2558 
2559   cairo_pattern_destroy(grad);
2560 
2561   // Draw frame borders
2562   cairo_set_line_width(g->cr, DT_PIXEL_APPLY_DPI(0.5));
2563   set_color(g->cr, darktable.bauhaus->graph_border);
2564   cairo_rectangle(g->cr, 0, 0, g->graph_width, g->graph_height);
2565   cairo_stroke_preserve(g->cr);
2566 
2567   // end of caching section, this will not be drawn again
2568 
2569   dt_iop_gui_enter_critical_section(self);
2570   g->graph_valid = 1;
2571   dt_iop_gui_leave_critical_section(self);
2572 
2573   return TRUE;
2574 }
2575 
2576 
2577 // must be called while holding self->gui_lock
init_nodes_x(dt_iop_toneequalizer_gui_data_t * g)2578 static inline void init_nodes_x(dt_iop_toneequalizer_gui_data_t *g)
2579 {
2580   if(g == NULL) return;
2581 
2582   if(!g->valid_nodes_x && g->graph_width > 0)
2583   {
2584     for(int i = 0; i < CHANNELS; ++i)
2585       g->nodes_x[i] = (((float)i) / ((float)(CHANNELS - 1))) * g->graph_width;
2586     g->valid_nodes_x = TRUE;
2587   }
2588 }
2589 
2590 
2591 // must be called while holding self->gui_lock
init_nodes_y(dt_iop_toneequalizer_gui_data_t * g)2592 static inline void init_nodes_y(dt_iop_toneequalizer_gui_data_t *g)
2593 {
2594   if(g == NULL) return;
2595 
2596   if(g->user_param_valid && g->graph_height > 0)
2597   {
2598     for(int i = 0; i < CHANNELS; ++i)
2599       g->nodes_y[i] =  (0.5 - log2f(g->temp_user_params[i]) / 4.0) * g->graph_height; // assumes factors in [-2 ; 2] EV
2600     g->valid_nodes_y = TRUE;
2601   }
2602 }
2603 
2604 
area_draw(GtkWidget * widget,cairo_t * cr,gpointer user_data)2605 static gboolean area_draw(GtkWidget *widget, cairo_t *cr, gpointer user_data)
2606 {
2607   // Draw the widget equalizer view
2608   dt_iop_module_t *self = (dt_iop_module_t *)user_data;
2609   dt_iop_toneequalizer_gui_data_t *g = (dt_iop_toneequalizer_gui_data_t *)self->gui_data;
2610   if(g == NULL) return FALSE;
2611 
2612   // Init or refresh the drawing cache
2613   //if(!g->graph_valid)
2614   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
2615 
2616   // since the widget sizes are not cached and invalidated properly above (yet…)
2617   // force the invalidation of the nodes coordinates to account for possible widget resizing
2618   dt_iop_gui_enter_critical_section(self);
2619   g->valid_nodes_x = FALSE;
2620   g->valid_nodes_y = FALSE;
2621   dt_iop_gui_leave_critical_section(self);
2622 
2623   // Refresh cached UI elements
2624   update_histogram(self);
2625   update_curve_lut(self);
2626 
2627   // Draw graph background
2628   cairo_set_line_width(g->cr, DT_PIXEL_APPLY_DPI(0.5));
2629   cairo_rectangle(g->cr, 0, 0, g->graph_width, g->graph_height);
2630   set_color(g->cr, darktable.bauhaus->graph_bg);
2631   cairo_fill(g->cr);
2632 
2633   // draw grid
2634   cairo_set_line_width(g->cr, DT_PIXEL_APPLY_DPI(0.5));
2635   set_color(g->cr, darktable.bauhaus->graph_border);
2636   dt_draw_grid(g->cr, 8, 0, 0, g->graph_width, g->graph_height);
2637 
2638   // draw ground level
2639   set_color(g->cr, darktable.bauhaus->graph_fg);
2640   cairo_set_line_width(g->cr, DT_PIXEL_APPLY_DPI(1));
2641   cairo_move_to(g->cr, 0, 0.5 * g->graph_height);
2642   cairo_line_to(g->cr, g->graph_width, 0.5 * g->graph_height);
2643   cairo_stroke(g->cr);
2644 
2645   if(g->histogram_valid && self->enabled)
2646   {
2647     // draw the inset histogram
2648     set_color(g->cr, darktable.bauhaus->inset_histogram);
2649     cairo_set_line_width(g->cr, DT_PIXEL_APPLY_DPI(4.0));
2650     cairo_move_to(g->cr, 0, g->graph_height);
2651 
2652     for(int k = 0; k < UI_SAMPLES; k++)
2653     {
2654       // the x range is [-8;+0] EV
2655       const float x_temp = (8.0 * (float)k / (float)(UI_SAMPLES - 1)) - 8.0;
2656       const float y_temp = (float)(g->histogram[k]) / (float)(g->max_histogram) * 0.96;
2657       cairo_line_to(g->cr, (x_temp + 8.0) * g->graph_width / 8.0,
2658                            (1.0 - y_temp) * g->graph_height );
2659     }
2660     cairo_line_to(g->cr, g->graph_width, g->graph_height);
2661     cairo_close_path(g->cr);
2662     cairo_fill(g->cr);
2663 
2664     if(g->histogram_last_decile > -0.1f)
2665     {
2666       // histogram overflows controls in highlights : display warning
2667       cairo_save(g->cr);
2668       cairo_set_source_rgb(g->cr, 0.75, 0.50, 0.);
2669       dtgtk_cairo_paint_gamut_check(g->cr, g->graph_width - 2.5 * g->line_height, 0.5 * g->line_height,
2670                                            2.0 * g->line_height, 2.0 * g->line_height, 0, NULL);
2671       cairo_restore(g->cr);
2672     }
2673 
2674     if(g->histogram_first_decile < -7.9f)
2675     {
2676       // histogram overflows controls in lowlights : display warning
2677       cairo_save(g->cr);
2678       cairo_set_source_rgb(g->cr, 0.75, 0.50, 0.);
2679       dtgtk_cairo_paint_gamut_check(g->cr, 0.5 * g->line_height, 0.5 * g->line_height,
2680                                            2.0 * g->line_height, 2.0 * g->line_height, 0, NULL);
2681       cairo_restore(g->cr);
2682     }
2683   }
2684 
2685   if(g->lut_valid)
2686   {
2687     // draw the interpolation curve
2688     set_color(g->cr, darktable.bauhaus->graph_fg);
2689     cairo_move_to(g->cr, 0, g->gui_lut[0] * g->graph_height);
2690     cairo_set_line_width(g->cr, DT_PIXEL_APPLY_DPI(3));
2691 
2692     for(int k = 1; k < UI_SAMPLES; k++)
2693     {
2694       // the x range is [-8;+0] EV
2695       const float x_temp = (8.0f * (((float)k) / ((float)(UI_SAMPLES - 1)))) - 8.0f;
2696       const float y_temp = g->gui_lut[k];
2697 
2698       cairo_line_to(g->cr, (x_temp + 8.0f) * g->graph_width / 8.0f,
2699                             y_temp * g->graph_height );
2700     }
2701     cairo_stroke(g->cr);
2702   }
2703 
2704   dt_iop_gui_enter_critical_section(self);
2705   init_nodes_x(g);
2706   dt_iop_gui_leave_critical_section(self);
2707 
2708   dt_iop_gui_enter_critical_section(self);
2709   init_nodes_y(g);
2710   dt_iop_gui_leave_critical_section(self);
2711 
2712   if(g->user_param_valid)
2713   {
2714     // draw nodes positions
2715     for(int k = 0; k < CHANNELS; k++)
2716     {
2717       const float xn = g->nodes_x[k];
2718       const float yn = g->nodes_y[k];
2719 
2720       // fill bars
2721       cairo_set_line_width(g->cr, DT_PIXEL_APPLY_DPI(6));
2722       set_color(g->cr, darktable.bauhaus->color_fill);
2723       cairo_move_to(g->cr, xn, 0.5 * g->graph_height);
2724       cairo_line_to(g->cr, xn, yn);
2725       cairo_stroke(g->cr);
2726 
2727       // bullets
2728       cairo_set_line_width(g->cr, DT_PIXEL_APPLY_DPI(3));
2729       cairo_arc(g->cr, xn, yn, DT_PIXEL_APPLY_DPI(4), 0, 2. * M_PI);
2730       set_color(g->cr, darktable.bauhaus->graph_fg);
2731       cairo_stroke_preserve(g->cr);
2732 
2733       if(g->area_active_node == k)
2734         set_color(g->cr, darktable.bauhaus->graph_fg);
2735       else
2736         set_color(g->cr, darktable.bauhaus->graph_bg);
2737 
2738       cairo_fill(g->cr);
2739     }
2740   }
2741 
2742   if(self->enabled)
2743   {
2744     if(g->area_cursor_valid)
2745     {
2746       const float radius = g->sigma * g->graph_width / 8.0f / sqrtf(2.0f);
2747       cairo_set_line_width(g->cr, DT_PIXEL_APPLY_DPI(1.5));
2748       const float y =g->gui_lut[(int)CLAMP(((UI_SAMPLES - 1) * g->area_x / g->graph_width), 0, UI_SAMPLES - 1)];
2749       cairo_arc(g->cr, g->area_x, y * g->graph_height, radius, 0, 2. * M_PI);
2750       set_color(g->cr, darktable.bauhaus->graph_fg);
2751       cairo_stroke(g->cr);
2752     }
2753 
2754     if(g->cursor_valid)
2755     {
2756 
2757       float x_pos = (g->cursor_exposure + 8.0f) / 8.0f * g->graph_width;
2758 
2759       if(x_pos > g->graph_width || x_pos < 0.0f)
2760       {
2761         // exposure at current position is outside [-8; 0] EV :
2762         // bound it in the graph limits and show it in orange
2763         cairo_set_source_rgb(g->cr, 0.75, 0.50, 0.);
2764         cairo_set_line_width(g->cr, DT_PIXEL_APPLY_DPI(3));
2765         x_pos = (x_pos < 0.0f) ? 0.0f : g->graph_width;
2766       }
2767       else
2768       {
2769         set_color(g->cr, darktable.bauhaus->graph_fg);
2770         cairo_set_line_width(g->cr, DT_PIXEL_APPLY_DPI(1.5));
2771       }
2772 
2773       cairo_move_to(g->cr, x_pos, 0.0);
2774       cairo_line_to(g->cr, x_pos, g->graph_height);
2775       cairo_stroke(g->cr);
2776     }
2777   }
2778 
2779   // clean and exit
2780   cairo_set_source_surface(cr, g->cst, 0, 0);
2781   cairo_paint(cr);
2782 
2783   return TRUE;
2784 }
2785 
dt_iop_toneequalizer_bar_draw(GtkWidget * widget,cairo_t * crf,gpointer user_data)2786 static gboolean dt_iop_toneequalizer_bar_draw(GtkWidget *widget, cairo_t *crf, gpointer user_data)
2787 {
2788   // Draw the widget equalizer view
2789   dt_iop_module_t *self = (dt_iop_module_t *)user_data;
2790   dt_iop_toneequalizer_gui_data_t *g = (dt_iop_toneequalizer_gui_data_t *)self->gui_data;
2791 
2792   update_histogram(self);
2793 
2794   GtkAllocation allocation;
2795   gtk_widget_get_allocation(widget, &allocation);
2796   cairo_surface_t *cst = dt_cairo_image_surface_create(CAIRO_FORMAT_ARGB32, allocation.width, allocation.height);
2797   cairo_t *cr = cairo_create(cst);
2798 
2799   // draw background
2800   set_color(cr, darktable.bauhaus->graph_bg);
2801   cairo_rectangle(cr, 0, 0, allocation.width, allocation.height);
2802   cairo_fill_preserve(cr);
2803   cairo_clip(cr);
2804 
2805   dt_iop_gui_enter_critical_section(self);
2806 
2807   if(g->histogram_valid)
2808   {
2809     // draw histogram span
2810     const float left = (g->histogram_first_decile + 8.0f) / 8.0f;
2811     const float right = (g->histogram_last_decile + 8.0f) / 8.0f;
2812     const float width = (right - left);
2813     set_color(cr, darktable.bauhaus->inset_histogram);
2814     cairo_rectangle(cr, left * allocation.width, 0, width * allocation.width, allocation.height);
2815     cairo_fill(cr);
2816 
2817     // draw average bar
2818     set_color(cr, darktable.bauhaus->graph_fg);
2819     cairo_set_line_width(cr, DT_PIXEL_APPLY_DPI(3));
2820     const float average = (g->histogram_average + 8.0f) / 8.0f;
2821     cairo_move_to(cr, average * allocation.width, 0.0);
2822     cairo_line_to(cr, average * allocation.width, allocation.height);
2823     cairo_stroke(cr);
2824 
2825     // draw clipping bars
2826     cairo_set_source_rgb(cr, 0.75, 0.50, 0);
2827     cairo_set_line_width(cr, DT_PIXEL_APPLY_DPI(6));
2828     if(g->histogram_first_decile < -7.9f)
2829     {
2830       cairo_move_to(cr, DT_PIXEL_APPLY_DPI(3), 0.0);
2831       cairo_line_to(cr, DT_PIXEL_APPLY_DPI(3), allocation.height);
2832       cairo_stroke(cr);
2833     }
2834     if(g->histogram_last_decile > - 0.1f)
2835     {
2836       cairo_move_to(cr, allocation.width - DT_PIXEL_APPLY_DPI(3), 0.0);
2837       cairo_line_to(cr, allocation.width - DT_PIXEL_APPLY_DPI(3), allocation.height);
2838       cairo_stroke(cr);
2839     }
2840   }
2841 
2842   dt_iop_gui_leave_critical_section(self);
2843 
2844   cairo_set_source_surface(crf, cst, 0, 0);
2845   cairo_paint(crf);
2846   cairo_destroy(cr);
2847   cairo_surface_destroy(cst);
2848   return TRUE;
2849 }
2850 
2851 
area_enter_notify(GtkWidget * widget,GdkEventCrossing * event,gpointer user_data)2852 static gboolean area_enter_notify(GtkWidget *widget, GdkEventCrossing *event, gpointer user_data)
2853 {
2854   dt_iop_module_t *self = (dt_iop_module_t *)user_data;
2855   if(darktable.gui->reset) return 1;
2856   if(!self->enabled) return 0;
2857 
2858   dt_iop_toneequalizer_gui_data_t *g = (dt_iop_toneequalizer_gui_data_t *)self->gui_data;
2859 
2860   dt_iop_gui_enter_critical_section(self);
2861   g->area_x = (event->x - g->inset);
2862   g->area_y = (event->y - g->inset);
2863   g->area_dragging = FALSE;
2864   g->area_active_node = -1;
2865   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);
2866   dt_iop_gui_leave_critical_section(self);
2867 
2868   gtk_widget_queue_draw(GTK_WIDGET(g->area));
2869   return TRUE;
2870 }
2871 
2872 
area_leave_notify(GtkWidget * widget,GdkEventCrossing * event,gpointer user_data)2873 static gboolean area_leave_notify(GtkWidget *widget, GdkEventCrossing *event, gpointer user_data)
2874 {
2875   dt_iop_module_t *self = (dt_iop_module_t *)user_data;
2876   if(darktable.gui->reset) return 1;
2877   if(!self->enabled) return 0;
2878 
2879   dt_iop_toneequalizer_gui_data_t *g = (dt_iop_toneequalizer_gui_data_t *)self->gui_data;
2880   dt_iop_toneequalizer_params_t *p = (dt_iop_toneequalizer_params_t *)self->params;
2881 
2882   if(g->area_dragging)
2883   {
2884     // cursor left area : force commit to avoid glitches
2885     ++darktable.gui->reset;
2886     update_exposure_sliders(g, p);
2887     --darktable.gui->reset;
2888 
2889     dt_dev_add_history_item(darktable.develop, self, FALSE);
2890   }
2891   dt_iop_gui_enter_critical_section(self);
2892   g->area_x = (event->x - g->inset);
2893   g->area_y = (event->y - g->inset);
2894   g->area_dragging = FALSE;
2895   g->area_active_node = -1;
2896   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);
2897   dt_iop_gui_leave_critical_section(self);
2898 
2899   gtk_widget_queue_draw(GTK_WIDGET(g->area));
2900   return TRUE;
2901 }
2902 
2903 
area_button_press(GtkWidget * widget,GdkEventButton * event,gpointer user_data)2904 static gboolean area_button_press(GtkWidget *widget, GdkEventButton *event, gpointer user_data)
2905 {
2906   dt_iop_module_t *self = (dt_iop_module_t *)user_data;
2907   if(darktable.gui->reset) return 1;
2908 
2909   dt_iop_toneequalizer_gui_data_t *g = (dt_iop_toneequalizer_gui_data_t *)self->gui_data;
2910 
2911   dt_iop_request_focus(self);
2912 
2913   if(event->button == 1 && event->type == GDK_2BUTTON_PRESS)
2914   {
2915     dt_iop_toneequalizer_params_t *p = (dt_iop_toneequalizer_params_t *)self->params;
2916     dt_iop_toneequalizer_params_t *d = (dt_iop_toneequalizer_params_t *)self->default_params;
2917 
2918     // reset nodes params
2919     p->noise = d->noise;
2920     p->ultra_deep_blacks = d->ultra_deep_blacks;
2921     p->deep_blacks = d->deep_blacks;
2922     p->blacks = d->blacks;
2923     p->shadows = d->shadows;
2924     p->midtones = d->midtones;
2925     p->highlights = d->highlights;
2926     p->whites = d->whites;
2927     p->speculars = d->speculars;
2928 
2929     // update UI sliders
2930     ++darktable.gui->reset;
2931     update_exposure_sliders(g, p);
2932     --darktable.gui->reset;
2933 
2934     // Redraw graph
2935     gtk_widget_queue_draw(self->widget);
2936     dt_dev_add_history_item(darktable.develop, self, TRUE);
2937     return TRUE;
2938   }
2939   else if(event->button == 1)
2940   {
2941     if(self->enabled)
2942     {
2943       g->area_dragging = 1;
2944       gtk_widget_queue_draw(GTK_WIDGET(g->area));
2945     }
2946     else
2947     {
2948       dt_dev_add_history_item(darktable.develop, self, TRUE);
2949     }
2950     return TRUE;
2951   }
2952 
2953   // Unlock the colour picker so we can display our own custom cursor
2954   dt_iop_color_picker_reset(self, TRUE);
2955 
2956   return FALSE;
2957 }
2958 
2959 
area_motion_notify(GtkWidget * widget,GdkEventMotion * event,gpointer user_data)2960 static gboolean area_motion_notify(GtkWidget *widget, GdkEventMotion *event, gpointer user_data)
2961 {
2962   dt_iop_module_t *self = (dt_iop_module_t *)user_data;
2963   if(darktable.gui->reset) return 1;
2964   if(!self->enabled) return 0;
2965 
2966   dt_iop_toneequalizer_gui_data_t *g = (dt_iop_toneequalizer_gui_data_t *)self->gui_data;
2967   dt_iop_toneequalizer_params_t *p = (dt_iop_toneequalizer_params_t *)self->params;
2968 
2969   if(g->area_dragging)
2970   {
2971     // vertical distance travelled since button_pressed event
2972     dt_iop_gui_enter_critical_section(self);
2973     const float offset = (-event->y + g->area_y) / g->graph_height * 4.0f; // graph spans over 4 EV
2974     const float cursor_exposure = g->area_x / g->graph_width * 8.0f - 8.0f;
2975 
2976     // Get the desired correction on exposure channels
2977     g->area_dragging = set_new_params_interactive(cursor_exposure, offset, g->sigma * g->sigma / 2.0f, g, p);
2978     dt_iop_gui_leave_critical_section(self);
2979   }
2980 
2981   dt_iop_gui_enter_critical_section(self);
2982   g->area_x = (event->x - g->inset);
2983   g->area_y = event->y;
2984   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);
2985   g->area_active_node = -1;
2986 
2987   // Search if cursor is close to a node
2988   if(g->valid_nodes_x)
2989   {
2990     const float radius_threshold = fabsf(g->nodes_x[1] - g->nodes_x[0]) * 0.45f;
2991     for(int i = 0; i < CHANNELS; ++i)
2992     {
2993       const float delta_x = fabsf(g->area_x - g->nodes_x[i]);
2994       if(delta_x < radius_threshold)
2995       {
2996         g->area_active_node = i;
2997         g->area_cursor_valid = 1;
2998       }
2999     }
3000   }
3001   dt_iop_gui_leave_critical_section(self);
3002 
3003   gtk_widget_queue_draw(GTK_WIDGET(g->area));
3004   return TRUE;
3005 }
3006 
3007 
area_button_release(GtkWidget * widget,GdkEventButton * event,gpointer user_data)3008 static gboolean area_button_release(GtkWidget *widget, GdkEventButton *event, gpointer user_data)
3009 {
3010   dt_iop_module_t *self = (dt_iop_module_t *)user_data;
3011   if(darktable.gui->reset) return 1;
3012   if(!self->enabled) return 0;
3013 
3014   dt_iop_toneequalizer_gui_data_t *g = (dt_iop_toneequalizer_gui_data_t *)self->gui_data;
3015 
3016   // Give focus to module
3017   dt_iop_request_focus(self);
3018 
3019   if(event->button == 1)
3020   {
3021     dt_iop_toneequalizer_params_t *p = (dt_iop_toneequalizer_params_t *)self->params;
3022 
3023     if(g->area_dragging)
3024     {
3025       // Update GUI with new params
3026       ++darktable.gui->reset;
3027       update_exposure_sliders(g, p);
3028       --darktable.gui->reset;
3029 
3030       dt_dev_add_history_item(darktable.develop, self, FALSE);
3031 
3032       dt_iop_gui_enter_critical_section(self);
3033       g->area_dragging= 0;
3034       dt_iop_gui_leave_critical_section(self);
3035 
3036       return TRUE;
3037     }
3038   }
3039   return FALSE;
3040 }
3041 
3042 
notebook_button_press(GtkWidget * widget,GdkEventButton * event,gpointer user_data)3043 static gboolean notebook_button_press(GtkWidget *widget, GdkEventButton *event, gpointer user_data)
3044 {
3045   dt_iop_module_t *self = (dt_iop_module_t *)user_data;
3046   if(darktable.gui->reset) return 1;
3047 
3048   // Give focus to module
3049   dt_iop_request_focus(self);
3050 
3051   // Unlock the colour picker so we can display our own custom cursor
3052   dt_iop_color_picker_reset(self, TRUE);
3053 
3054   return 0;
3055 }
3056 
mouse_actions(struct dt_iop_module_t * self)3057 GSList *mouse_actions(struct dt_iop_module_t *self)
3058 {
3059   GSList *lm = NULL;
3060   lm = dt_mouse_action_create_format(lm, DT_MOUSE_ACTION_SCROLL, 0,
3061                                      _("[%s over image] change tone exposure"), self->name());
3062   lm = dt_mouse_action_create_format(lm, DT_MOUSE_ACTION_SCROLL, GDK_SHIFT_MASK,
3063                                      _("[%s over image] change tone exposure in large steps"), self->name());
3064   lm = dt_mouse_action_create_format(lm, DT_MOUSE_ACTION_SCROLL, GDK_CONTROL_MASK,
3065                                      _("[%s over image] change tone exposure in small steps"), self->name());
3066   return lm;
3067 }
3068 
3069 /**
3070  * Post pipe events
3071  **/
3072 
3073 
_develop_ui_pipe_started_callback(gpointer instance,gpointer user_data)3074 static void _develop_ui_pipe_started_callback(gpointer instance, gpointer user_data)
3075 {
3076   dt_iop_module_t *self = (dt_iop_module_t *)user_data;
3077   dt_iop_toneequalizer_gui_data_t *g = (dt_iop_toneequalizer_gui_data_t *)self->gui_data;
3078   if(g == NULL) return;
3079   switch_cursors(self);
3080 
3081   if(!self->expanded || !self->enabled)
3082   {
3083     // if module is not active, disable mask preview
3084     dt_iop_gui_enter_critical_section(self);
3085     g->mask_display = 0;
3086     dt_iop_gui_leave_critical_section(self);
3087   }
3088 
3089   ++darktable.gui->reset;
3090   dt_iop_gui_enter_critical_section(self);
3091   dt_bauhaus_widget_set_quad_active(GTK_WIDGET(g->show_luminance_mask), g->mask_display);
3092   dt_iop_gui_leave_critical_section(self);
3093   --darktable.gui->reset;
3094 }
3095 
3096 
_develop_preview_pipe_finished_callback(gpointer instance,gpointer user_data)3097 static void _develop_preview_pipe_finished_callback(gpointer instance, gpointer user_data)
3098 {
3099   dt_iop_module_t *self = (dt_iop_module_t *)user_data;
3100   dt_iop_toneequalizer_gui_data_t *g = (dt_iop_toneequalizer_gui_data_t *)self->gui_data;
3101   if(g == NULL) return;
3102   switch_cursors(self);
3103   gtk_widget_queue_draw(GTK_WIDGET(g->area));
3104   gtk_widget_queue_draw(GTK_WIDGET(g->bar));
3105 }
3106 
3107 
_develop_ui_pipe_finished_callback(gpointer instance,gpointer user_data)3108 static void _develop_ui_pipe_finished_callback(gpointer instance, gpointer user_data)
3109 {
3110   dt_iop_module_t *self = (dt_iop_module_t *)user_data;
3111   dt_iop_toneequalizer_gui_data_t *g = (dt_iop_toneequalizer_gui_data_t *)self->gui_data;
3112   if(g == NULL) return;
3113   switch_cursors(self);
3114 }
3115 
3116 
gui_reset(struct dt_iop_module_t * self)3117 void gui_reset(struct dt_iop_module_t *self)
3118 {
3119   dt_iop_toneequalizer_gui_data_t *g = (dt_iop_toneequalizer_gui_data_t *)self->gui_data;
3120   if(g == NULL) return;
3121   dt_iop_request_focus(self);
3122   dt_bauhaus_widget_set_quad_active(g->exposure_boost, FALSE);
3123   dt_bauhaus_widget_set_quad_active(g->contrast_boost, FALSE);
3124   dt_dev_add_history_item(darktable.develop, self, TRUE);
3125 
3126   // Redraw graph
3127   gtk_widget_queue_draw(self->widget);
3128 }
3129 
3130 
gui_init(struct dt_iop_module_t * self)3131 void gui_init(struct dt_iop_module_t *self)
3132 {
3133   dt_iop_toneequalizer_gui_data_t *g = IOP_GUI_ALLOC(toneequalizer);
3134 
3135   gui_cache_init(self);
3136 
3137   static dt_action_def_t notebook_def = { };
3138   g->notebook = dt_ui_notebook_new(&notebook_def);
3139   dt_action_define_iop(self, NULL, N_("page"), GTK_WIDGET(g->notebook), &notebook_def);
3140 
3141   // Simple view
3142 
3143   self->widget = dt_ui_notebook_page(g->notebook, N_("simple"), NULL);
3144 
3145   g->noise = dt_bauhaus_slider_from_params(self, "noise");
3146   dt_bauhaus_slider_set_step(g->noise, .05);
3147   dt_bauhaus_slider_set_format(g->noise, _("%+.2f EV"));
3148 
3149   g->ultra_deep_blacks = dt_bauhaus_slider_from_params(self, "ultra_deep_blacks");
3150   dt_bauhaus_slider_set_step(g->ultra_deep_blacks, .05);
3151   dt_bauhaus_slider_set_format(g->ultra_deep_blacks, _("%+.2f EV"));
3152 
3153   g->deep_blacks = dt_bauhaus_slider_from_params(self, "deep_blacks");
3154   dt_bauhaus_slider_set_step(g->deep_blacks, .05);
3155   dt_bauhaus_slider_set_format(g->deep_blacks, _("%+.2f EV"));
3156 
3157   g->blacks = dt_bauhaus_slider_from_params(self, "blacks");
3158   dt_bauhaus_slider_set_step(g->blacks, .05);
3159   dt_bauhaus_slider_set_format(g->blacks, _("%+.2f EV"));
3160 
3161   g->shadows = dt_bauhaus_slider_from_params(self, "shadows");
3162   dt_bauhaus_slider_set_step(g->shadows, .05);
3163   dt_bauhaus_slider_set_format(g->shadows, _("%+.2f EV"));
3164 
3165   g->midtones = dt_bauhaus_slider_from_params(self, "midtones");
3166   dt_bauhaus_slider_set_step(g->midtones, .05);
3167   dt_bauhaus_slider_set_format(g->midtones, _("%+.2f EV"));
3168 
3169   g->highlights = dt_bauhaus_slider_from_params(self, "highlights");
3170   dt_bauhaus_slider_set_step(g->highlights, .05);
3171   dt_bauhaus_slider_set_format(g->highlights, _("%+.2f EV"));
3172 
3173   g->whites = dt_bauhaus_slider_from_params(self, "whites");
3174   dt_bauhaus_slider_set_step(g->whites, .05);
3175   dt_bauhaus_slider_set_format(g->whites, _("%+.2f EV"));
3176 
3177   g->speculars = dt_bauhaus_slider_from_params(self, "speculars");
3178   dt_bauhaus_slider_set_step(g->speculars, .05);
3179   dt_bauhaus_slider_set_format(g->speculars, _("%+.2f EV"));
3180 
3181   dt_bauhaus_widget_set_label(g->noise, N_("simple"), N_("-8 EV"));
3182   dt_bauhaus_widget_set_label(g->ultra_deep_blacks, N_("simple"), N_("-7 EV"));
3183   dt_bauhaus_widget_set_label(g->deep_blacks, N_("simple"), N_("-6 EV"));
3184   dt_bauhaus_widget_set_label(g->blacks, N_("simple"), N_("-5 EV"));
3185   dt_bauhaus_widget_set_label(g->shadows, N_("simple"), N_("-4 EV"));
3186   dt_bauhaus_widget_set_label(g->midtones, N_("simple"), N_("-3 EV"));
3187   dt_bauhaus_widget_set_label(g->highlights, N_("simple"), N_("-2 EV"));
3188   dt_bauhaus_widget_set_label(g->whites, N_("simple"), N_("-1 EV"));
3189   dt_bauhaus_widget_set_label(g->speculars, N_("simple"), N_("+0 EV"));
3190 
3191   // Advanced view
3192 
3193   self->widget = dt_ui_notebook_page(g->notebook, N_("advanced"), NULL);
3194 
3195   g->area = GTK_DRAWING_AREA(gtk_drawing_area_new());
3196   g_object_set_data(G_OBJECT(g->area), "iop-instance", self);
3197   dt_action_define_iop(self, NULL, N_("graph"), GTK_WIDGET(g->area), NULL);
3198   gtk_box_pack_start(GTK_BOX(self->widget), GTK_WIDGET(g->area), TRUE, TRUE, 0);
3199   gtk_widget_add_events(GTK_WIDGET(g->area), GDK_POINTER_MOTION_MASK | darktable.gui->scroll_mask
3200                                            | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK
3201                                            | GDK_ENTER_NOTIFY_MASK | GDK_LEAVE_NOTIFY_MASK);
3202   gtk_widget_set_can_focus(GTK_WIDGET(g->area), TRUE);
3203   g_signal_connect(G_OBJECT(g->area), "draw", G_CALLBACK(area_draw), self);
3204   g_signal_connect(G_OBJECT(g->area), "button-press-event", G_CALLBACK(area_button_press), self);
3205   g_signal_connect(G_OBJECT(g->area), "button-release-event", G_CALLBACK(area_button_release), self);
3206   g_signal_connect(G_OBJECT(g->area), "leave-notify-event", G_CALLBACK(area_leave_notify), self);
3207   g_signal_connect(G_OBJECT(g->area), "enter-notify-event", G_CALLBACK(area_enter_notify), self);
3208   g_signal_connect(G_OBJECT(g->area), "motion-notify-event", G_CALLBACK(area_motion_notify), self);
3209   gtk_widget_set_tooltip_text(GTK_WIDGET(g->area), _("double-click to reset the curve"));
3210 
3211   g->smoothing = dt_bauhaus_slider_new_with_range(self, -1.0f, +1.0f, 0.1, 0.0f, 2);
3212   dt_bauhaus_slider_enable_soft_boundaries(g->smoothing, -2.33f, 1.67f);
3213   dt_bauhaus_widget_set_label(g->smoothing, NULL, N_("curve smoothing"));
3214   gtk_widget_set_tooltip_text(g->smoothing, _("positive values will produce more progressive tone transitions\n"
3215                                               "but the curve might become oscillatory in some settings.\n"
3216                                               "negative values will avoid oscillations and behave more robustly\n"
3217                                               "but may produce brutal tone transitions and damage local contrast."));
3218   gtk_box_pack_start(GTK_BOX(self->widget), g->smoothing, FALSE, FALSE, 0);
3219   g_signal_connect(G_OBJECT(g->smoothing), "value-changed", G_CALLBACK(smoothing_callback), self);
3220 
3221   // Masking options
3222 
3223   self->widget = dt_ui_notebook_page(g->notebook, N_("masking"), NULL);
3224 
3225   g->method = dt_bauhaus_combobox_from_params(self, "method");
3226   dt_bauhaus_combobox_remove_at(g->method, DT_TONEEQ_LAST);
3227   gtk_widget_set_tooltip_text(g->method, _("preview the mask and chose the estimator that gives you the\n"
3228                                            "higher contrast between areas to dodge and areas to burn"));
3229 
3230   g->details = dt_bauhaus_combobox_from_params(self, N_("details"));
3231   dt_bauhaus_widget_set_label(g->details, NULL, N_("preserve details"));
3232   gtk_widget_set_tooltip_text(g->details, _("'no' affects global and local contrast (safe if you only add contrast)\n"
3233                                             "'guided filter' only affects global contrast and tries to preserve local contrast\n"
3234                                             "'averaged guided filter' is a geometric mean of 'no' and 'guided filter' methods\n"
3235                                             "'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"
3236                                             "'averaged eigf' is a geometric mean of 'no' and 'exposure-independent guided filter' methods"));
3237 
3238   g->iterations = dt_bauhaus_slider_from_params(self, "iterations");
3239   dt_bauhaus_slider_set_soft_max(g->iterations, 5);
3240   gtk_widget_set_tooltip_text(g->iterations, _("number of passes of guided filter to apply\n"
3241                                                "helps diffusing the edges of the filter at the expense of speed"));
3242 
3243   g->blending = dt_bauhaus_slider_from_params(self, "blending");
3244   dt_bauhaus_slider_set_soft_range(g->blending, 1.0, 45.0);
3245   dt_bauhaus_slider_set_format(g->blending, "%.2f %%");
3246   gtk_widget_set_tooltip_text(g->blending, _("diameter of the blur in percent of the largest image size\n"
3247                                              "warning: big values of this parameter can make the darkroom\n"
3248                                              "preview much slower if denoise profiled is used."));
3249 
3250   g->feathering = dt_bauhaus_slider_from_params(self, "feathering");
3251   dt_bauhaus_slider_set_soft_range(g->feathering, 0.1, 50.0);
3252   dt_bauhaus_slider_set_step(g->feathering, 0.2);
3253   gtk_widget_set_tooltip_text(g->feathering, _("precision of the feathering :\n"
3254                                                "higher values force the mask to follow edges more closely\n"
3255                                                "but may void the effect of the smoothing\n"
3256                                                "lower values give smoother gradients and better smoothing\n"
3257                                                "but may lead to inaccurate edges taping and halos"));
3258 
3259   gtk_box_pack_start(GTK_BOX(self->widget), dt_ui_section_label_new(_("mask post-processing")), FALSE, FALSE, 0);
3260 
3261   g->bar = GTK_DRAWING_AREA(gtk_drawing_area_new());
3262   gtk_widget_set_size_request(GTK_WIDGET(g->bar), -1, 4);
3263   gtk_box_pack_start(GTK_BOX(self->widget), GTK_WIDGET(g->bar), TRUE, TRUE, 0);
3264   gtk_widget_set_can_focus(GTK_WIDGET(g->bar), TRUE);
3265   g_signal_connect(G_OBJECT(g->bar), "draw", G_CALLBACK(dt_iop_toneequalizer_bar_draw), self);
3266   gtk_widget_set_tooltip_text(GTK_WIDGET(g->bar), _("mask histogram span between the first and last deciles.\n"
3267                                                     "the central line shows the average. orange bars appear at extrema if clipping occurs."));
3268 
3269 
3270   g->quantization = dt_bauhaus_slider_from_params(self, "quantization");
3271   dt_bauhaus_slider_set_step(g->quantization, 0.25);
3272   dt_bauhaus_slider_set_format(g->quantization, "%+.2f EV");
3273   gtk_widget_set_tooltip_text(g->quantization, _("0 disables the quantization.\n"
3274                                                  "higher values posterize the luminance mask to help the guiding\n"
3275                                                  "produce piece-wise smooth areas when using high feathering values"));
3276 
3277   g->exposure_boost = dt_bauhaus_slider_from_params(self, "exposure_boost");
3278   dt_bauhaus_slider_set_soft_range(g->exposure_boost, -4.0, 4.0);
3279   dt_bauhaus_slider_set_format(g->exposure_boost, "%+.2f EV");
3280   gtk_widget_set_tooltip_text(g->exposure_boost, _("use this to slide the mask average exposure along channels\n"
3281                                                    "for a better control of the exposure correction with the available nodes.\n"
3282                                                    "the magic wand will auto-adjust the average exposure"));
3283   dt_bauhaus_widget_set_quad_paint(g->exposure_boost, dtgtk_cairo_paint_wand, CPF_STYLE_FLAT | CPF_DO_NOT_USE_BORDER, NULL);
3284   dt_bauhaus_widget_set_quad_toggle(g->exposure_boost, FALSE);
3285   g_signal_connect(G_OBJECT(g->exposure_boost), "quad-pressed", G_CALLBACK(auto_adjust_exposure_boost), self);
3286 
3287   g->contrast_boost = dt_bauhaus_slider_from_params(self, "contrast_boost");
3288   dt_bauhaus_slider_set_soft_range(g->contrast_boost, -2.0, 2.0);
3289   dt_bauhaus_slider_set_format(g->contrast_boost, "%+.2f EV");
3290   gtk_widget_set_tooltip_text(g->contrast_boost, _("use this to counter the averaging effect of the guided filter\n"
3291                                                    "and dilate the mask contrast around -4EV\n"
3292                                                    "this allows to spread the exposure histogram over more channels\n"
3293                                                    "for a better control of the exposure correction.\n"
3294                                                    "the magic wand will auto-adjust the contrast"));
3295   dt_bauhaus_widget_set_quad_paint(g->contrast_boost, dtgtk_cairo_paint_wand, CPF_STYLE_FLAT | CPF_DO_NOT_USE_BORDER, NULL);
3296   dt_bauhaus_widget_set_quad_toggle(g->contrast_boost, FALSE);
3297   g_signal_connect(G_OBJECT(g->contrast_boost), "quad-pressed", G_CALLBACK(auto_adjust_contrast_boost), self);
3298 
3299   // start building top level widget
3300   self->widget = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
3301 
3302   const int active_page = dt_conf_get_int("plugins/darkroom/toneequal/gui_page");
3303   gtk_widget_show(gtk_notebook_get_nth_page(g->notebook, active_page));
3304   gtk_notebook_set_current_page(g->notebook, active_page);
3305 
3306   g_signal_connect(G_OBJECT(g->notebook), "button-press-event", G_CALLBACK(notebook_button_press), self);
3307   gtk_box_pack_start(GTK_BOX(self->widget), GTK_WIDGET(g->notebook), FALSE, FALSE, 0);
3308 
3309   g->show_luminance_mask = dt_bauhaus_combobox_new(self);
3310   dt_bauhaus_widget_set_label(g->show_luminance_mask, NULL, N_("display exposure mask"));
3311   dt_bauhaus_widget_set_quad_paint(g->show_luminance_mask, dtgtk_cairo_paint_showmask,
3312                                    CPF_STYLE_FLAT | CPF_DO_NOT_USE_BORDER, NULL);
3313   dt_bauhaus_widget_set_quad_toggle(g->show_luminance_mask, TRUE);
3314   gtk_widget_set_tooltip_text(g->show_luminance_mask, _("display exposure mask"));
3315   g_signal_connect(G_OBJECT(g->show_luminance_mask), "quad-pressed", G_CALLBACK(show_luminance_mask_callback), self);
3316   gtk_box_pack_start(GTK_BOX(self->widget),  g->show_luminance_mask, TRUE, TRUE, 0);
3317 
3318   // Force UI redraws when pipe starts/finishes computing and switch cursors
3319   DT_DEBUG_CONTROL_SIGNAL_CONNECT(darktable.signals, DT_SIGNAL_DEVELOP_PREVIEW_PIPE_FINISHED,
3320                             G_CALLBACK(_develop_preview_pipe_finished_callback), self);
3321   DT_DEBUG_CONTROL_SIGNAL_CONNECT(darktable.signals, DT_SIGNAL_DEVELOP_UI_PIPE_FINISHED,
3322                             G_CALLBACK(_develop_ui_pipe_finished_callback), self);
3323 
3324   DT_DEBUG_CONTROL_SIGNAL_CONNECT(darktable.signals, DT_SIGNAL_DEVELOP_HISTORY_CHANGE,
3325                             G_CALLBACK(_develop_ui_pipe_started_callback), self);
3326 }
3327 
3328 
gui_cleanup(struct dt_iop_module_t * self)3329 void gui_cleanup(struct dt_iop_module_t *self)
3330 {
3331   dt_iop_toneequalizer_gui_data_t *g = (dt_iop_toneequalizer_gui_data_t *)self->gui_data;
3332   self->request_color_pick = DT_REQUEST_COLORPICK_OFF;
3333 
3334   dt_conf_set_int("plugins/darkroom/toneequal/gui_page", gtk_notebook_get_current_page (g->notebook));
3335 
3336   DT_DEBUG_CONTROL_SIGNAL_DISCONNECT(darktable.signals, G_CALLBACK(_develop_ui_pipe_finished_callback), self);
3337   DT_DEBUG_CONTROL_SIGNAL_DISCONNECT(darktable.signals, G_CALLBACK(_develop_ui_pipe_started_callback), self);
3338   DT_DEBUG_CONTROL_SIGNAL_DISCONNECT(darktable.signals, G_CALLBACK(_develop_preview_pipe_finished_callback), self);
3339 
3340   if(g->thumb_preview_buf) dt_free_align(g->thumb_preview_buf);
3341   if(g->full_preview_buf) dt_free_align(g->full_preview_buf);
3342   if(g->desc) pango_font_description_free(g->desc);
3343   if(g->layout) g_object_unref(g->layout);
3344   if(g->cr) cairo_destroy(g->cr);
3345   if(g->cst) cairo_surface_destroy(g->cst);
3346 
3347   IOP_GUI_FREE;
3348 }
3349 
3350 // modelines: These editor modelines have been set for all relevant files by tools/update_modelines.sh
3351 // vim: shiftwidth=2 expandtab tabstop=2 cindent
3352 // kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified;
3353