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(¬ebook_def);
3139 dt_action_define_iop(self, NULL, N_("page"), GTK_WIDGET(g->notebook), ¬ebook_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