diff --git a/data/darktableconfig.xml.in b/data/darktableconfig.xml.in
index 5cfe9440cb32..b3fd05951b0d 100644
--- a/data/darktableconfig.xml.in
+++ b/data/darktableconfig.xml.in
@@ -3173,6 +3173,13 @@
height of color balance rgb graph in per cent
height of color balance rgb graph in per cent
+
+ plugins/darkroom/toneequal/graphheight
+ int
+ 300
+ height of tone equalizer graph in per cent
+ height of tone equalizer graph in per cent
+
plugins/darkroom/histogram/mode
diff --git a/src/common/luminance_mask.h b/src/common/luminance_mask.h
index 99183c4a1957..56abdbf1473c 100644
--- a/src/common/luminance_mask.h
+++ b/src/common/luminance_mask.h
@@ -43,10 +43,12 @@ typedef enum dt_iop_luminance_mask_method_t
DT_TONEEQ_LIGHTNESS, // $DESCRIPTION: "HSL lightness"
DT_TONEEQ_VALUE, // $DESCRIPTION: "HSV value / RGB max"
DT_TONEEQ_NORM_1, // $DESCRIPTION: "RGB sum"
- DT_TONEEQ_NORM_2, // $DESCRIPTION: "RGB euclidean norm")
+ DT_TONEEQ_NORM_2, // $DESCRIPTION: "RGB euclidean norm"
DT_TONEEQ_NORM_POWER, // $DESCRIPTION: "RGB power norm"
DT_TONEEQ_GEOMEAN, // $DESCRIPTION: "RGB geometric mean"
- DT_TONEEQ_LAST
+ DT_TONEEQ_REC709W, // $DESCRIPTION: "Rec. 709 weights"
+ DT_TONEEQ_CUSTOM, // $DESCRIPTION: "Custom"
+ DT_TONEEQ_LAST,
} dt_iop_luminance_mask_method_t;
/**
@@ -78,11 +80,9 @@ static float linear_contrast(const float pixel, const float fulcrum, const float
DT_OMP_DECLARE_SIMD(aligned(image, luminance:64) uniform(image, luminance))
__DT_CLONE_TARGETS__
-static void pixel_rgb_mean(const float *const restrict image,
+static float pixel_rgb_mean(const float *const restrict image,
float *const restrict luminance,
- const size_t k,
- const float exposure_boost,
- const float fulcrum, const float contrast_boost)
+ const size_t k)
{
// mean(RGB) is the intensity
@@ -92,67 +92,59 @@ static void pixel_rgb_mean(const float *const restrict image,
for(int c = 0; c < 3; ++c)
lum += image[k + c];
- luminance[k / 4] = linear_contrast(exposure_boost * lum / 3.0f, fulcrum, contrast_boost);
+ return lum / 3.0f;
}
DT_OMP_DECLARE_SIMD(aligned(image, luminance:64) uniform(image, luminance))
__DT_CLONE_TARGETS__
-static void pixel_rgb_value(const float *const restrict image,
+static float pixel_rgb_value(const float *const restrict image,
float *const restrict luminance,
- const size_t k,
- const float exposure_boost,
- const float fulcrum, const float contrast_boost)
+ const size_t k)
{
// max(RGB) is equivalent to HSV value
- const float lum = exposure_boost * MAX(MAX(image[k], image[k + 1]), image[k + 2]);
- luminance[k / 4] = linear_contrast(lum, fulcrum, contrast_boost);
+ const float lum = MAX(MAX(image[k], image[k + 1]), image[k + 2]);
+ return lum;
}
DT_OMP_DECLARE_SIMD(aligned(image, luminance:64) uniform(image, luminance))
__DT_CLONE_TARGETS__
-static void pixel_rgb_lightness(const float *const restrict image,
+static float pixel_rgb_lightness(const float *const restrict image,
float *const restrict luminance,
- const size_t k,
- const float exposure_boost,
- const float fulcrum, const float contrast_boost)
+ const size_t k)
{
// (max(RGB) + min(RGB)) / 2 is equivalent to HSL lightness
const float max_rgb = MAX(MAX(image[k], image[k + 1]), image[k + 2]);
const float min_rgb = MIN(MIN(image[k], image[k + 1]), image[k + 2]);
- luminance[k / 4] = linear_contrast(exposure_boost * (max_rgb + min_rgb) / 2.0f, fulcrum, contrast_boost);
+ return (max_rgb + min_rgb) / 2.0f;
}
DT_OMP_DECLARE_SIMD(aligned(image, luminance:64) uniform(image, luminance))
__DT_CLONE_TARGETS__
-static void pixel_rgb_norm_1(const float *const restrict image,
+static float pixel_rgb_norm_1(const float *const restrict image,
float *const restrict luminance,
- const size_t k,
- const float exposure_boost,
- const float fulcrum, const float contrast_boost)
+ const size_t k)
{
// vector norm L1
float lum = 0.0f;
- DT_OMP_SIMD(reduction(+:lum) aligned(image:64))
- for(int c = 0; c < 3; ++c)
- lum += fabsf(image[k + c]);
+ DT_OMP_SIMD(reduction(+:lum) aligned(image:64))
+ for(int c = 0; c < 3; ++c)
+ lum += fabsf(image[k + c]);
- luminance[k / 4] = linear_contrast(exposure_boost * lum, fulcrum, contrast_boost);
+ return lum;
}
DT_OMP_DECLARE_SIMD(aligned(image, luminance:64) uniform(image, luminance))
__DT_CLONE_TARGETS__
-static void pixel_rgb_norm_2(const float *const restrict image,
+static float pixel_rgb_norm_2(const float *const restrict image,
float *const restrict luminance,
- const size_t k,
- const float exposure_boost,
- const float fulcrum, const float contrast_boost)
+ const size_t k)
{
// vector norm L2 : euclidean norm
@@ -162,17 +154,15 @@ static void pixel_rgb_norm_2(const float *const restrict image,
for(int c = 0; c < 3; ++c)
result += image[k + c] * image[k + c];
- luminance[k / 4] = linear_contrast(exposure_boost * sqrtf(result), fulcrum, contrast_boost);
+ return sqrtf(result);
}
DT_OMP_DECLARE_SIMD(aligned(image, luminance:64) uniform(image, luminance))
__DT_CLONE_TARGETS__
-static void pixel_rgb_norm_power(const float *const restrict image,
+static float pixel_rgb_norm_power(const float *const restrict image,
float *const restrict luminance,
- const size_t k,
- const float exposure_boost,
- const float fulcrum, const float contrast_boost)
+ const size_t k)
{
// weird norm sort of perceptual. This is black magic really, but it looks good.
@@ -189,16 +179,14 @@ static void pixel_rgb_norm_power(const float *const restrict image,
denominator += RGB_square;
}
- luminance[k / 4] = linear_contrast(exposure_boost * numerator / denominator, fulcrum, contrast_boost);
+ return numerator / denominator;
}
DT_OMP_DECLARE_SIMD(aligned(image, luminance:64) uniform(image, luminance))
__DT_CLONE_TARGETS__
-static void pixel_rgb_geomean(const float *const restrict image,
+static float pixel_rgb_geomean(const float *const restrict image,
float *const restrict luminance,
- const size_t k,
- const float exposure_boost,
- const float fulcrum, const float contrast_boost)
+ const size_t k)
{
// geometric_mean(RGB). Kind of interesting for saturated colours (maps them to shadows)
@@ -210,7 +198,35 @@ static void pixel_rgb_geomean(const float *const restrict image,
lum *= fabsf(image[k + c]);
}
- luminance[k / 4] = linear_contrast(exposure_boost * powf(lum, 1.0f / 3.0f), fulcrum, contrast_boost);
+ return powf(lum, 1.0f / 3.0f);
+}
+
+DT_OMP_DECLARE_SIMD(aligned(image, luminance:64) uniform(image, luminance))
+__DT_CLONE_TARGETS__
+static float pixel_rgb_r709w(const float *const restrict image,
+ float *const restrict luminance,
+ const size_t k)
+{
+ // Rec. 709 weights. Green has more influence than red and blue.
+
+ const float lum = 0.2126 * image[k] + 0.7152 * image[k + 1] + 0.0722 * image[k + 2];
+ return lum;
+}
+
+
+DT_OMP_DECLARE_SIMD(aligned(image, luminance:64) uniform(image, luminance))
+__DT_CLONE_TARGETS__
+static float pixel_rgb_custom(const float *const restrict image,
+ float *const restrict luminance,
+ const size_t k,
+ const float r_weight,
+ const float g_weight,
+ const float b_weight)
+{
+ // User can mix the greyscale image using sliders.
+
+ const float lum = MAX(MIN_FLOAT, r_weight * image[k] + g_weight * image[k + 1] + b_weight * image[k + 2]);
+ return lum;
}
@@ -221,11 +237,16 @@ static void pixel_rgb_geomean(const float *const restrict image,
#define LOOP(fn) \
{ \
_Pragma ("omp parallel for simd default(none) schedule(static) \
- dt_omp_firstprivate(num_elem, in, out, exposure_boost, fulcrum, contrast_boost)\
+ dt_omp_firstprivate(num_elem, in, out, exposure_boost, fulcrum, contrast_boost, r_weight, g_weight, b_weight)\
+ reduction(min:min_lum) reduction(max:max_lum) \
aligned(in, out:64)" ) \
for(size_t k = 0; k < num_elem; k += 4) \
{ \
- fn(in, out, k, exposure_boost, fulcrum, contrast_boost); \
+ const float lum = COMPUTE_LUM(fn); \
+ const float lum_boosted = linear_contrast(exposure_boost * lum, fulcrum, contrast_boost); \
+ min_lum = MIN(min_lum, lum_boosted); \
+ max_lum = MAX(max_lum, lum_boosted); \
+ out[k / 4] = lum_boosted; \
} \
break; \
}
@@ -234,7 +255,11 @@ static void pixel_rgb_geomean(const float *const restrict image,
{ \
for(size_t k = 0; k < num_elem; k += 4) \
{ \
- fn(in, out, k, exposure_boost, fulcrum, contrast_boost); \
+ const float lum = COMPUTE_LUM(fn); \
+ const float lum_boosted = linear_contrast(exposure_boost * lum, fulcrum, contrast_boost); \
+ min_lum = MIN(min_lum, lum_boosted); \
+ max_lum = MAX(max_lum, lum_boosted); \
+ out[k / 4] = lum_boosted; \
} \
break; \
}
@@ -249,11 +274,20 @@ static inline void luminance_mask(const float *const restrict in,
const dt_iop_luminance_mask_method_t method,
const float exposure_boost,
const float fulcrum,
- const float contrast_boost)
+ const float contrast_boost,
+ const float r_weight,
+ const float g_weight,
+ const float b_weight,
+ float *image_min_ev,
+ float *image_max_ev)
{
const size_t num_elem = width * height * 4;
+ float min_lum = INFINITY;
+ float max_lum = -INFINITY;
+
switch(method)
{
+ #define COMPUTE_LUM(fn) fn(in, out, k)
case DT_TONEEQ_MEAN:
LOOP(pixel_rgb_mean);
@@ -275,11 +309,21 @@ static inline void luminance_mask(const float *const restrict in,
case DT_TONEEQ_GEOMEAN:
LOOP(pixel_rgb_geomean);
+ case DT_TONEEQ_REC709W:
+ LOOP(pixel_rgb_r709w);
+
+ #undef COMPUTE_LUM
+ #define COMPUTE_LUM(fn) fn(in, out, k, r_weight, g_weight, b_weight)
+ case DT_TONEEQ_CUSTOM:
+ LOOP(pixel_rgb_custom);
+
default:
break;
}
-}
+ *image_min_ev = log2f(min_lum);
+ *image_max_ev = log2f(max_lum);
+}
// clang-format off
// modelines: These editor modelines have been set for all relevant files by tools/update_modelines.py
diff --git a/src/dtgtk/paint.c b/src/dtgtk/paint.c
index 374b543fe2b3..ea404b356864 100644
--- a/src/dtgtk/paint.c
+++ b/src/dtgtk/paint.c
@@ -1174,6 +1174,28 @@ void dtgtk_cairo_paint_linear_scale(cairo_t *cr, const gint x, const gint y, con
FINISH
}
+void dtgtk_cairo_paint_linear_scale_ignore_border(cairo_t *cr, const gint x, const gint y, const gint w, const gint h, const gint flags, void *data)
+{
+ PREAMBLE(1, 1, 0, 0)
+
+ // Main line with reduced slope
+ cairo_move_to(cr, 0.0, 0.7);
+ cairo_line_to(cr, 1.0, 0.3);
+ cairo_stroke(cr);
+
+ // Small marks at edges to indicate "ignore border"
+ cairo_save(cr);
+ cairo_set_line_width(cr, 0.15);
+ cairo_move_to(cr, 0.0, 0.85);
+ cairo_line_to(cr, 0.0, 0.55);
+ cairo_move_to(cr, 1.0, 0.45);
+ cairo_line_to(cr, 1.0, 0.15);
+ cairo_stroke(cr);
+ cairo_restore(cr);
+
+ FINISH
+}
+
void dtgtk_cairo_paint_logarithmic_scale(cairo_t *cr, const gint x, const gint y, const gint w, const gint h, gint flags, void *data)
{
PREAMBLE(1, 1, 0, 0)
diff --git a/src/dtgtk/paint.h b/src/dtgtk/paint.h
index 2951009a4138..8bffd83bda32 100644
--- a/src/dtgtk/paint.h
+++ b/src/dtgtk/paint.h
@@ -217,6 +217,8 @@ void dtgtk_cairo_paint_waveform_scope(cairo_t *cr, gint x, gint y, gint w, gint
void dtgtk_cairo_paint_vectorscope(cairo_t *cr, gint x, gint y, gint w, gint h, gint flags, void *data);
/** paint linear scale icon */
void dtgtk_cairo_paint_linear_scale(cairo_t *cr, gint x, gint y, gint w, gint h, gint flags, void *data);
+/** paint linear scale ignore border icon */
+void dtgtk_cairo_paint_linear_scale_ignore_border(cairo_t *cr, gint x, gint y, gint w, gint h, gint flags, void *data);
/** paint logarithmic scale icon */
void dtgtk_cairo_paint_logarithmic_scale(cairo_t *cr, gint x, gint y, gint w, gint h, gint flags, void *data);
/** paint waveform overlaid icon */
diff --git a/src/gui/draw.h b/src/gui/draw.h
index ea8b84faebc9..db699d08533f 100644
--- a/src/gui/draw.h
+++ b/src/gui/draw.h
@@ -121,6 +121,50 @@ static inline void dt_draw_grid(cairo_t *cr,
}
}
+static inline void dt_draw_grid_xy(cairo_t *cr,
+ const int num_x,
+ const int num_y,
+ const int left,
+ const int top,
+ const int right,
+ const int bottom,
+ const gboolean border_left,
+ const gboolean border_top,
+ const gboolean border_right,
+ const gboolean border_bottom)
+{
+ float width = right - left;
+ float height = bottom - top;
+
+ // Draw vertical lines (num_x divisions)
+ const int k_min_x = border_left ? 0 : 1;
+ const int k_max_x = border_right ? num_x : num_x - 1;
+
+ for(int k = k_min_x; k <= k_max_x; k++)
+ {
+ dt_draw_line(cr,
+ left + k / (float)num_x * width,
+ top,
+ left + k / (float)num_x * width,
+ bottom);
+ cairo_stroke(cr);
+ }
+
+ // Draw horizontal lines (num_y divisions)
+ const int k_min_y = border_top ? 0 : 1;
+ const int k_max_y = border_bottom ? num_y : num_y - 1;
+
+ for(int k = k_min_y; k <= k_max_y; k++)
+ {
+ dt_draw_line(cr,
+ left,
+ top + k / (float)num_y * height,
+ right,
+ top + k / (float)num_y * height);
+ cairo_stroke(cr);
+ }
+}
+
static inline float dt_curve_to_mouse(const float x,
const float zoom_factor,
const float offset)
diff --git a/src/iop/toneequal.c b/src/iop/toneequal.c
index 81a29351fdf8..6e77a1eeba99 100644
--- a/src/iop/toneequal.c
+++ b/src/iop/toneequal.c
@@ -37,14 +37,14 @@
* perfect, and I'm still looking forward to a real spectral energy
* estimator. The best physically-accurate norm should be the
* euclidean norm, but the best looking is often the power norm, which
- * has no theoretical background. The geometric mean also display
+ * has no theoretical background. The geometric mean also display
* interesting properties as it interprets saturated colours as
* low-lights, allowing to lighten and desaturate them in a realistic
* way.
*
* The exposure correction is computed as a series of each octave's
* gain weighted by the gaussian of the radial distance between the
- * current pixel exposure and each octave's center. This allows for a
+ * current pixel exposure and each octave's center. This allows for a
* smooth and continuous infinite-order interpolation, preserving
* exposure gradients as best as possible. The radius of the kernel is
* user-defined and can be tweaked to get a smoother interpolation
@@ -73,7 +73,7 @@
*
* Users should be aware that not all the available octaves will be
* useful on every pictures. Some automatic options will help them to
- * optimize the luminance mask, performing histogram analyse, mapping
+ * optimize the luminance mask, performing histogram analysis, mapping
* the average exposure to -4EV, and mapping the first and last
* deciles of the histogram on its average ± 4EV. These automatic
* helpers usually fail on X-Trans sensors, maybe because of bad
@@ -92,6 +92,7 @@
#include
#include
+
#include "bauhaus/bauhaus.h"
#include "common/darktable.h"
#include "common/fast_guided_filter.h"
@@ -123,25 +124,52 @@
#include
#endif
+#include
+#include
-DT_MODULE_INTROSPECTION(2, dt_iop_toneequalizer_params_t)
+DT_MODULE_INTROSPECTION(3, dt_iop_toneequalizer_params_t)
+/****************************************************************************
+ *
+ * Definition of constants
+ *
+ ****************************************************************************/
-#define UI_SAMPLES 256 // 128 is a bit small for 4K resolution
+#define UI_HISTO_SAMPLES 256 // 128 is a bit small for 4K resolution
+#define HDR_HISTO_SAMPLES UI_HISTO_SAMPLES * 32
#define CONTRAST_FULCRUM exp2f(-4.0f)
#define MIN_FLOAT exp2f(-16.0f)
+#define DT_TONEEQ_MIN_EV (-8.0f)
+#define DT_TONEEQ_MAX_EV (0.0f)
+
+// We need more space for the histogram and also for the LUT
+// This needs to comprise the possible scaled/shifted range of
+// the original 8EV.
+#define HDR_MIN_EV -16.0f
+#define HDR_MAX_EV 8.0f
+
/**
* Build the exposures octaves :
* band-pass filters with gaussian windows spaced by 1 EV
**/
-#define CHANNELS 9
-#define PIXEL_CHAN 8
-#define LUT_RESOLUTION 10000
+#define NUM_SLIDERS 9
+#define NUM_OCTAVES 8
+
+// TODO MF: Alway use the term "octave" for "graph EVs"
+
+// Resolution per Octave. 2048 * 8 = 16k
+// So the final size with floats in it should be 64k.
+// We use 2047, so we have space for one extra border
+// point (the 0 EV point) in the lut and are still
+// below 16k entries for cache efficiency.
+#define LUT_RESOLUTION 2047
+
+#define EPSILON 1e-6f
// radial distances used for pixel ops
-static const float centers_ops[PIXEL_CHAN] DT_ALIGNED_ARRAY =
+static const float centers_base_fns[NUM_OCTAVES] DT_ALIGNED_ARRAY =
{-56.0f / 7.0f, // = -8.0f
-48.0f / 7.0f,
-40.0f / 7.0f,
@@ -149,12 +177,28 @@ static const float centers_ops[PIXEL_CHAN] DT_ALIGNED_ARRAY =
-24.0f / 7.0f,
-16.0f / 7.0f,
-8.0f / 7.0f,
- 0.0f / 7.0f}; // split 8 EV into 7 evenly-spaced channels
+ 0.0f / 7.0f}; // split 8 EV into 7 evenly-spaced NUM_SLIDERS
-static const float centers_params[CHANNELS] DT_ALIGNED_ARRAY =
+static const float centers_sliders[NUM_SLIDERS] DT_ALIGNED_ARRAY =
{ -8.0f, -7.0f, -6.0f, -5.0f,
-4.0f, -3.0f, -2.0f, -1.0f, 0.0f};
+// gaussian-ish kernel - sum is == 1.0f so we don't care much about actual coeffs
+static const dt_colormatrix_t gauss_kernel =
+ { { 0.076555024f, 0.124401914f, 0.076555024f },
+ { 0.124401914f, 0.196172249f, 0.124401914f },
+ { 0.076555024f, 0.124401914f, 0.076555024f } };
+
+// Hardcoded colors for gamut warnings
+GdkRGBA warning_color = {.red = 0.8f, .green = 0.4f, .blue = 0.0f, .alpha = 1.0f};
+GdkRGBA error_color = {.red = 1.0f, .green = 0.0f, .blue = 0.0f, .alpha = 1.0f};
+
+
+/****************************************************************************
+ *
+ * Types
+ *
+ ****************************************************************************/
typedef enum dt_iop_toneequalizer_filter_t
{
@@ -165,40 +209,70 @@ typedef enum dt_iop_toneequalizer_filter_t
DT_TONEEQ_EIGF // $DESCRIPTION: "EIGF"
} dt_iop_toneequalizer_filter_t;
+typedef enum dt_iop_toneequalizer_curve_t
+{
+ DT_TONEEQ_CURVE_GAUSS = 2, // $DESCRIPTION: "Gauss (for large changes)"
+ DT_TONEEQ_CURVE_CATMULL = 3, // $DESCRIPTION: "Catmull-Rom (for small changes)"
+} dt_iop_toneequalizer_curve_t;
typedef struct dt_iop_toneequalizer_params_t
{
- float noise; // $MIN: -2.0 $MAX: 2.0 $DEFAULT: 0.0 $DESCRIPTION: "blacks"
+ // v1
+ float noise; // $MIN: -2.0 $MAX: 2.0 $DEFAULT: 0.0 $DESCRIPTION: "blacks"
float ultra_deep_blacks; // $MIN: -2.0 $MAX: 2.0 $DEFAULT: 0.0 $DESCRIPTION: "deep shadows"
- float deep_blacks; // $MIN: -2.0 $MAX: 2.0 $DEFAULT: 0.0 $DESCRIPTION: "shadows"
- float blacks; // $MIN: -2.0 $MAX: 2.0 $DEFAULT: 0.0 $DESCRIPTION: "light shadows"
- float shadows; // $MIN: -2.0 $MAX: 2.0 $DEFAULT: 0.0 $DESCRIPTION: "mid-tones"
- float midtones; // $MIN: -2.0 $MAX: 2.0 $DEFAULT: 0.0 $DESCRIPTION: "dark highlights"
- float highlights; // $MIN: -2.0 $MAX: 2.0 $DEFAULT: 0.0 $DESCRIPTION: "highlights"
- float whites; // $MIN: -2.0 $MAX: 2.0 $DEFAULT: 0.0 $DESCRIPTION: "whites"
- float speculars; // $MIN: -2.0 $MAX: 2.0 $DEFAULT: 0.0 $DESCRIPTION: "speculars"
- float blending; // $MIN: 0.01 $MAX: 100.0 $DEFAULT: 5.0 $DESCRIPTION: "smoothing diameter"
- float smoothing; // $DEFAULT: 1.414213562 sqrtf(2.0f)
- float feathering; // $MIN: 0.01 $MAX: 10000.0 $DEFAULT: 1.0 $DESCRIPTION: "edges refinement/feathering"
- float quantization; // $MIN: 0.0 $MAX: 2.0 $DEFAULT: 0.0 $DESCRIPTION: "mask quantization"
- float contrast_boost; // $MIN: -16.0 $MAX: 16.0 $DEFAULT: 0.0 $DESCRIPTION: "mask contrast compensation"
- float exposure_boost; // $MIN: -16.0 $MAX: 16.0 $DEFAULT: 0.0 $DESCRIPTION: "mask exposure compensation"
- dt_iop_toneequalizer_filter_t details; // $DEFAULT: DT_TONEEQ_EIGF $DESCRIPTION: "preserve details"
- dt_iop_luminance_mask_method_t method; // $DEFAULT: DT_TONEEQ_NORM_2 $DESCRIPTION: "luminance estimator"
- int iterations; // $MIN: 1 $MAX: 20 $DEFAULT: 1 $DESCRIPTION: "filter diffusion"
+ float deep_blacks; // $MIN: -2.0 $MAX: 2.0 $DEFAULT: 0.0 $DESCRIPTION: "shadows"
+ float blacks; // $MIN: -2.0 $MAX: 2.0 $DEFAULT: 0.0 $DESCRIPTION: "light shadows"
+ float shadows; // $MIN: -2.0 $MAX: 2.0 $DEFAULT: 0.0 $DESCRIPTION: "mid-tones"
+ float midtones; // $MIN: -2.0 $MAX: 2.0 $DEFAULT: 0.0 $DESCRIPTION: "dark highlights"
+ float highlights; // $MIN: -2.0 $MAX: 2.0 $DEFAULT: 0.0 $DESCRIPTION: "highlights"
+ float whites; // $MIN: -2.0 $MAX: 2.0 $DEFAULT: 0.0 $DESCRIPTION: "whites"
+ float speculars; // $MIN: -2.0 $MAX: 2.0 $DEFAULT: 0.0 $DESCRIPTION: "speculars"
+ float blending; // $MIN: 0.01 $MAX: 100.0 $DEFAULT: 5.0 $DESCRIPTION: "smoothing diameter"
+ float smoothing; // $DEFAULT: 0.0
+ float feathering; // $MIN: 0.01 $MAX: 10000.0 $DEFAULT: 1.0 $DESCRIPTION: "edges refinement/feathering"
+ float quantization; // $MIN: 0.0 $MAX: 2.0 $DEFAULT: 0.0 $DESCRIPTION: "mask quantization"
+ float contrast_boost; // $MIN: -16.0 $MAX: 16.0 $DEFAULT: 0.0 $DESCRIPTION: "mask contrast compensation"
+ float exposure_boost; // $MIN: -16.0 $MAX: 16.0 $DEFAULT: 0.0 $DESCRIPTION: "mask exposure compensation"
+ dt_iop_toneequalizer_filter_t filter; // $DEFAULT: DT_TONEEQ_EIGF $DESCRIPTION: "preserve details"
+ dt_iop_luminance_mask_method_t lum_estimator; // $DEFAULT: DT_TONEEQ_CUSTOM $DESCRIPTION: "luminance estimator"
+ int iterations; // $MIN: 1 $MAX: 20 $DEFAULT: 1 $DESCRIPTION: "filter diffusion"
+
+ // v3
+ dt_iop_toneequalizer_curve_t curve_type; // $DEFAULT: DT_TONEEQ_CURVE_GAUSS $DESCRIPTION: "curve type"
+ float post_scale_base; // $DEFAULT: 0.0f
+ float post_shift_base; // $DEFAULT: 0.0f
+ float post_scale; // $MIN: -3.0 $MAX: 3.0 $DEFAULT: 0.0 $DESCRIPTION: "mask contrast / scale histogram"
+ float post_shift; // $MIN: -8.0 $MAX: 8.0 $DEFAULT: 0.0 $DESCRIPTION: "mask brightness / shift histogram"
+ float post_pivot; // $MIN: -8.0 $MAX: 0.0 $DEFAULT: -4.0 $DESCRIPTION: "histogram scale pivot"
+ float global_exposure; // $MIN: -8.0 $MAX: 8.0 $DEFAULT: 0.0 $DESCRIPTION: "global exposure"
+ float scale_curve; // $MIN: -2.0 $MAX: 2.0 $DEFAULT: 1.0 $DESCRIPTION: "scale curve vertically"
+ float lum_estimator_R; // $MIN: -1.0 $MAX: 1.0 $DEFAULT: 0.33333 $DESCRIPTION: "red channel weight"
+ float lum_estimator_G; // $MIN: -1.0 $MAX: 1.0 $DEFAULT: 0.33333 $DESCRIPTION: "green channel weight"
+ float lum_estimator_B; // $MIN: -1.0 $MAX: 1.0 $DEFAULT: 0.33333 $DESCRIPTION: "blue channel weight"
+ gboolean lum_estimator_normalize; // $DEFAULT: TRUE $DESCRIPTION: "normalize RGB weights"
+
+ gboolean auto_align_enabled; // $DEFAULT: FALSE $DESCRIPTION: "auto align enabled"
+ gboolean align_shift_only; // $DEFAULT: FALSE $DESCRIPTION: "auto align shift only"
+
} dt_iop_toneequalizer_params_t;
typedef struct dt_iop_toneequalizer_data_t
{
- float factors[PIXEL_CHAN] DT_ALIGNED_ARRAY;
- float correction_lut[PIXEL_CHAN * LUT_RESOLUTION + 1] DT_ALIGNED_ARRAY;
+ float gauss_factors[NUM_OCTAVES] DT_ALIGNED_ARRAY;
+ float catmull_nodes_y[NUM_SLIDERS+2] DT_ALIGNED_ARRAY;
+ float catmull_tangents[NUM_SLIDERS] DT_ALIGNED_ARRAY; float correction_lut[NUM_OCTAVES * LUT_RESOLUTION + 1] DT_ALIGNED_ARRAY;
+ float lut_min_ev, lut_max_ev;
float blending, feathering, contrast_boost, exposure_boost, quantization, smoothing;
+ float post_scale_base, post_shift_base, post_scale, post_shift, post_pivot, global_exposure, scale_curve;
+ dt_iop_toneequalizer_curve_t curve_type;
float scale;
int radius;
int iterations;
- dt_iop_luminance_mask_method_t method;
- dt_iop_toneequalizer_filter_t details;
+ dt_iop_luminance_mask_method_t lum_estimator;
+ float lum_estimator_R, lum_estimator_G, lum_estimator_B;
+ gboolean auto_align_enabled, align_shift_only;
+ dt_iop_toneequalizer_filter_t filter;
} dt_iop_toneequalizer_data_t;
@@ -207,51 +281,159 @@ typedef struct dt_iop_toneequalizer_global_data_t
// TODO: put OpenCL kernels here at some point
} dt_iop_toneequalizer_global_data_t;
+typedef struct dt_iop_toneequalizer_histogram_stats_t
+{
+ int samples[HDR_HISTO_SAMPLES] DT_ALIGNED_ARRAY;
+ unsigned int num_samples;
+ float min_ev;
+ float max_ev;
+ float lo_percentile_ev;
+ float hi_percentile_ev;
+} dt_iop_toneequalizer_histogram_stats_t;
+
+static void _histogram_stats_init(dt_iop_toneequalizer_histogram_stats_t *histo)
+{
+ histo->num_samples = HDR_HISTO_SAMPLES;
+ histo->min_ev = HDR_MIN_EV;
+ histo->max_ev = HDR_MAX_EV;
+ histo->lo_percentile_ev = HDR_MIN_EV;
+ histo->hi_percentile_ev = HDR_MAX_EV;
+}
+
+typedef struct dt_iop_toneequalizer_ui_histogram_t
+{
+ int samples[UI_HISTO_SAMPLES] DT_ALIGNED_ARRAY;
+ GdkRGBA curve_colors[UI_HISTO_SAMPLES] DT_ALIGNED_ARRAY;
+ int max_val;
+ int max_val_ignore_border_bins;
+ float scsh_min_ev; // post scaled- and shifted version of min_ev
+ float scsh_max_ev;
+ float scsh_lo_percentile_ev;
+ float scsh_hi_percentile_ev;
+
+} dt_iop_toneequalizer_ui_histogram_t;
+
+static void _ui_histogram_init(dt_iop_toneequalizer_ui_histogram_t *histo)
+{
+ histo->max_val = 1;
+ histo->max_val_ignore_border_bins = 1;
+ histo->scsh_min_ev = HDR_MIN_EV; // post scaled- and shifted version of min_ev
+ histo->scsh_max_ev = HDR_MAX_EV;
+ histo->scsh_lo_percentile_ev = HDR_MIN_EV;
+ histo->scsh_hi_percentile_ev = HDR_MAX_EV;
+}
+
+typedef enum dt_iop_toneequalizer_histogram_scale_t {
+ HISTOGRAM_SCALE_LINEAR = 0,
+ HISTOGRAM_SCALE_LINEAR_IGNORE_BORDER,
+ HISTOGRAM_SCALE_LOG
+} dt_iop_toneequalizer_histogram_scale_t;
+
+typedef enum dt_iop_toneequalizer_last_align_button_t {
+ LAST_ALIGN_NONE = 0,
+ LAST_ALIGN_FULLY,
+ LAST_ALIGN_SHIFT
+} dt_iop_toneequalizer_last_align_button_t;
typedef struct dt_iop_toneequalizer_gui_data_t
{
// Mem arrays 64-bytes aligned - contiguous memory
- float factors[PIXEL_CHAN] DT_ALIGNED_ARRAY;
- float gui_lut[UI_SAMPLES] DT_ALIGNED_ARRAY; // LUT for the UI graph
- float interpolation_matrix[CHANNELS * PIXEL_CHAN] DT_ALIGNED_ARRAY;
- int histogram[UI_SAMPLES] DT_ALIGNED_ARRAY; // histogram for the UI graph
- float temp_user_params[CHANNELS] DT_ALIGNED_ARRAY;
+ float gauss_factors[NUM_OCTAVES] DT_ALIGNED_ARRAY;
+ float gauss_interpolation_matrix[NUM_SLIDERS * NUM_OCTAVES] DT_ALIGNED_ARRAY;
+ float catmull_nodes_y[NUM_SLIDERS+2] DT_ALIGNED_ARRAY;
+ float catmull_tangents[NUM_SLIDERS] DT_ALIGNED_ARRAY;
+
+ float gui_curve[UI_HISTO_SAMPLES] DT_ALIGNED_ARRAY; // LUT for the UI graph
+ GdkRGBA gui_curve_colors[UI_HISTO_SAMPLES] DT_ALIGNED_ARRAY; // color for the UI graph
+ dt_iop_toneequalizer_histogram_stats_t mask_hdr_histo; // HDR mask histogram
+ dt_iop_toneequalizer_histogram_stats_t mask_hq_histo; // Mask histogram in HQ mode
+ dt_iop_toneequalizer_ui_histogram_t ui_histo; // Histogram for the UI graph
+ dt_iop_toneequalizer_ui_histogram_t ui_hq_histo; // HQ mode version of the UI histogram
+
+ float prv_image_ev_min, prv_image_ev_max;
+
+ float temp_user_params[NUM_SLIDERS] DT_ALIGNED_ARRAY;
float cursor_exposure; // store the exposure value at current cursor position
- float step; // scrolling step
// 14 int to pack - contiguous memory
gboolean mask_display;
- int max_histogram;
- int buf_width;
- int buf_height;
int cursor_pos_x;
int cursor_pos_y;
int pipe_order;
// 6 uint64 to pack - contiguous-ish memory
- dt_hash_t ui_preview_hash;
- dt_hash_t thumb_preview_hash;
- size_t full_preview_buf_width, full_preview_buf_height;
- size_t thumb_preview_buf_width, thumb_preview_buf_height;
+ dt_hash_t full_upstream_hash;
+ dt_hash_t preview_upstream_hash;
+
+ size_t preview_buf_width, preview_buf_height;
+ size_t full_buf_width, full_buf_height;
+ int full_buf_x, full_buf_y; // top left corner of the main window
+
+ // Heap arrays, 64 bits-aligned, unknown length
+ float *preview_buf; // For performance and to get the mask luminance under the mouse cursor
+ float *full_buf; // For performance and for displaying the mask as greyscale
// Misc stuff, contiguity, length and alignment unknown
+ float image_EV_per_UI_sample;
float scale;
float sigma;
- float histogram_average;
- float histogram_first_decile;
- float histogram_last_decile;
- // Heap arrays, 64 bits-aligned, unknown length
- float *thumb_preview_buf;
- float *full_preview_buf;
+ // automatic values for post scale/shift
+ float temp_post_scale_base;
+ float temp_post_shift_base;
+ gboolean temp_post_values_valid;
+ gboolean post_realign_required;
+ dt_iop_toneequalizer_last_align_button_t last_align_button; // Last clicked align button
+ gboolean post_realign_possible_false_alert;
+
+ // Hash for synchronization between PREVIEW and FULL
+ dt_hash_t prv_full_sync_hash;
// GTK garbage, nobody cares, no SIMD here
- GtkWidget *noise, *ultra_deep_blacks, *deep_blacks, *blacks, *shadows, *midtones, *highlights, *whites, *speculars;
GtkDrawingArea *area;
- GtkWidget *blending, *smoothing, *quantization;
- GtkWidget *method;
- GtkWidget *details, *feathering, *contrast_boost, *iterations, *exposure_boost;
+
+ // Histogram control buttons
+ GtkWidget *button_box; // Container for buttons
+ GtkWidget *histogram_mode_button_linear; // Linear scale
+ GtkWidget *histogram_mode_button_ignore; // Linear-ignore-border scale
+ GtkWidget *histogram_mode_button_log; // Log scale
+ GtkWidget *histogram_range_button; // -2..2 / -4..4 EV (v3 only)
+ GtkWidget *histogram_hq_button; // Preview/HQ toggle
+ GtkWidget *histogram_align_button; // Quick align
+
+ dt_iop_toneequalizer_histogram_scale_t histogram_scale_mode; // Current scale mode
+ int histogram_vrange; // 2 or 4 EV per side
+ gboolean histogram_show_hq; // TRUE = show HQ histogram
+
GtkNotebook *notebook;
+
+ // Align Tab
+ GtkWidget *align_button, *shift_button;
+
+ GtkDrawingArea *warning_icon_area;
+ GtkWidget *post_scale, *post_shift, *post_pivot; // New main mask alignment controls
+ GtkWidget *smoothing; // curve smoothing
+
+ // Exposure Tab
+ GtkWidget *global_exposure, *scale_curve, *curve_type;
+ dt_gui_collapsible_section_t sliders_section;
+ GtkWidget *noise, *ultra_deep_blacks, *deep_blacks, *blacks, *shadows, *midtones, *highlights, *whites, *speculars;
+
+ // Masking Tab
+ dt_gui_collapsible_section_t guided_filter_section;
+ GtkWidget *filter;
+ GtkWidget *iterations, *blending, *feathering; // Filter diffusion, smoothing diameter, edges refinement
+
+ dt_gui_collapsible_section_t pre_processing_section;
+ GtkWidget *exposure_boost, *contrast_boost; // Exposure compensation, contrast compensation
+
+ dt_gui_collapsible_section_t lum_estimator_section;
+ GtkWidget *lum_estimator;
+ GtkWidget *lum_estimator_R, *lum_estimator_G, *lum_estimator_B, *lum_estimator_normalize;
+
+ dt_gui_collapsible_section_t adv_section;
+ GtkWidget *quantization;
+
GtkWidget *show_luminance_mask;
// Cache Pango and Cairo stuff for the equalizer drawing
@@ -259,12 +441,13 @@ typedef struct dt_iop_toneequalizer_gui_data_t
float sign_width;
float graph_width;
float graph_height;
+ float graph_w_gradients_height;
float gradient_left_limit;
float gradient_right_limit;
float gradient_top_limit;
float gradient_width;
float legend_top_limit;
- float x_label;
+ float x_label; // TODO: No reason why this needs to be in g
int inset;
int inner_padding;
@@ -277,15 +460,13 @@ typedef struct dt_iop_toneequalizer_gui_data_t
GtkStyleContext *context;
// Event for equalizer drawing
- float nodes_x[CHANNELS] DT_ALIGNED_ARRAY;
- float nodes_y[CHANNELS] DT_ALIGNED_ARRAY;
+ float nodes_x[NUM_SLIDERS] DT_ALIGNED_ARRAY;
+ float nodes_y[NUM_SLIDERS] DT_ALIGNED_ARRAY;
float area_x; // x coordinate of cursor over graph/drawing area
float area_y; // y coordinate
int area_active_node;
// Flags for UI events
- gboolean valid_nodes_x; // TRUE if x coordinates of graph nodes have been inited
- gboolean valid_nodes_y; // TRUE if y coordinates of graph nodes have been inited
gboolean area_cursor_valid; // TRUE if mouse cursor is over the graph area
gboolean area_dragging; // TRUE if left-button has been pushed
// but not released and cursor motion
@@ -294,23 +475,41 @@ typedef struct dt_iop_toneequalizer_gui_data_t
gboolean has_focus; // TRUE if the widget has the focus from GTK
// Flags for buffer caches invalidation
- gboolean interpolation_valid; // TRUE if the interpolation_matrix is ready
- gboolean luminance_valid; // TRUE if the luminance cache is ready
- gboolean histogram_valid; // TRUE if the histogram cache and stats are ready
- gboolean lut_valid; // TRUE if the gui_lut is ready
+ gboolean prv_luminance_valid; // TRUE if the preview luminance cache is ready,
+ // HDR_histogram and prv deciles are valid
+ gboolean full_luminance_valid;// TRUE if the full luminance cache is ready,
+ // full deciles are valid
+ gboolean hq_histogram_valid; // TRUE if we are in late scaling mode and a HQ histogram was computed
+
+ gboolean gui_histogram_valid; // TRUE if the histogram cache and stats are ready
+ gboolean gui_hq_histogram_valid; // TRUE if the HQ histogram cache and stats are ready
gboolean graph_valid; // TRUE if the UI graph view is ready
+
+ // For the curve interpolation
+ gboolean interpolation_valid; // TRUE if the gauss_interpolation_matrix is ready
+
gboolean user_param_valid; // TRUE if users params set in
// interactive view are in bounds
- gboolean factors_valid; // TRUE if radial-basis coeffs are ready
+ gboolean gauss_factors_valid; // TRUE if radial-basis coeffs are ready
+ gboolean gui_curve_valid; // TRUE if the gui_curve is ready
- gboolean distort_signal_actif;
+ gboolean distort_signal_active;
} dt_iop_toneequalizer_gui_data_t;
+
/* the signal DT_SIGNAL_DEVELOP_DISTORT is used to refresh the internal
cached image buffer used for the on-canvas luminance picker. */
static void _set_distort_signal(dt_iop_module_t *self);
static void _unset_distort_signal(dt_iop_module_t *self);
+/****************************************************************************
+ *
+ * Darktable housekeeping functions
+ *
+ ****************************************************************************/
+// empty marker function to make navigating the function list easier
+__attribute__((unused)) static void HOUSEKEEPING_FUNCTIONS_MARKER() {}
+
const char *name()
{
return _("tone equalizer");
@@ -321,7 +520,6 @@ const char *aliases()
return _("tone curve|tone mapping|relight|background light|shadows highlights");
}
-
const char **description(dt_iop_module_t *self)
{
return dt_iop_set_description
@@ -356,8 +554,9 @@ int legacy_params(dt_iop_module_t *self,
int32_t *new_params_size,
int *new_version)
{
- typedef struct dt_iop_toneequalizer_params_v2_t
+ typedef struct dt_iop_toneequalizer_params_v3_t
{
+ // v1
float noise;
float ultra_deep_blacks;
float deep_blacks;
@@ -368,15 +567,32 @@ int legacy_params(dt_iop_module_t *self,
float whites;
float speculars;
float blending;
- float smoothing;
+ float smoothing; //v2
float feathering;
- float quantization;
+ float quantization; //v2
float contrast_boost;
float exposure_boost;
- dt_iop_toneequalizer_filter_t details;
- dt_iop_luminance_mask_method_t method;
+ dt_iop_toneequalizer_filter_t filter;
+ dt_iop_luminance_mask_method_t lum_estimator;
int iterations;
- } dt_iop_toneequalizer_params_v2_t;
+
+ // v3
+ dt_iop_toneequalizer_curve_t curve_type;
+ float post_scale_base;
+ float post_shift_base;
+ float post_scale;
+ float post_shift;
+ float post_pivot;
+ float global_exposure;
+ float scale_curve;
+ float lum_estimator_R;
+ float lum_estimator_G;
+ float lum_estimator_B;
+ gboolean lum_estimator_normalize;
+
+ gboolean auto_align_enabled;
+ gboolean align_shift_only;
+ } dt_iop_toneequalizer_params_v3_t;
if(old_version == 1)
{
@@ -385,15 +601,15 @@ int legacy_params(dt_iop_module_t *self,
float noise, ultra_deep_blacks, deep_blacks, blacks;
float shadows, midtones, highlights, whites, speculars;
float blending, feathering, contrast_boost, exposure_boost;
- dt_iop_toneequalizer_filter_t details;
+ dt_iop_toneequalizer_filter_t filter;
int iterations;
- dt_iop_luminance_mask_method_t method;
+ dt_iop_luminance_mask_method_t lum_estimator;
} dt_iop_toneequalizer_params_v1_t;
const dt_iop_toneequalizer_params_v1_t *o = old_params;
- dt_iop_toneequalizer_params_v2_t *n = malloc(sizeof(dt_iop_toneequalizer_params_v2_t));
+ dt_iop_toneequalizer_params_v3_t *n = malloc(sizeof(dt_iop_toneequalizer_params_v3_t));
- // Olds params
+ // Old params
n->noise = o->noise;
n->ultra_deep_blacks = o->ultra_deep_blacks;
n->deep_blacks = o->deep_blacks;
@@ -409,29 +625,100 @@ int legacy_params(dt_iop_module_t *self,
n->contrast_boost = o->contrast_boost;
n->exposure_boost = o->exposure_boost;
- n->details = o->details;
+ n->filter = o->filter;
n->iterations = o->iterations;
- n->method = o->method;
+ n->lum_estimator = o->lum_estimator;
- // New params
+ // V2 params
n->quantization = 0.0f;
- n->smoothing = sqrtf(2.0f);
+ n->smoothing = 0.0f;
+
+ // V3 params
+ n->curve_type = DT_TONEEQ_CURVE_GAUSS;
+ n->post_scale_base = 0.0f;
+ n->post_shift_base = 0.0f;
+ n->post_scale = 0.0f;
+ n->post_shift = 0.0f;
+ n->post_pivot = -4.0f;
+ n->global_exposure = 0.0f;
+ n->scale_curve = 1.0f;
+ n->lum_estimator_R = 1.0f / 3.0f;
+ n->lum_estimator_G = 1.0f / 3.0f;
+ n->lum_estimator_B = 1.0f / 3.0f;
+ n->lum_estimator_normalize = TRUE;
+
+ n->auto_align_enabled = FALSE;
+ n->align_shift_only = FALSE;
+
+ *new_params = n;
+ *new_params_size = sizeof(dt_iop_toneequalizer_params_v3_t);
+ *new_version = 3;
+ return 0;
+ }
+
+ if(old_version == 2)
+ {
+ typedef struct dt_iop_toneequalizer_params_v2_t
+ {
+ float noise; float ultra_deep_blacks; float deep_blacks; float blacks;
+ float shadows; float midtones; float highlights; float whites;
+ float speculars; float blending; float smoothing; float feathering;
+ float quantization; float contrast_boost; float exposure_boost;
+ dt_iop_toneequalizer_filter_t filter;
+ dt_iop_luminance_mask_method_t lum_estimator;
+ int iterations;
+ } dt_iop_toneequalizer_params_v2_t;
+
+ const dt_iop_toneequalizer_params_v2_t *o = (dt_iop_toneequalizer_params_v2_t *)old_params;
+ dt_iop_toneequalizer_params_v3_t *n =
+ (dt_iop_toneequalizer_params_v3_t *)malloc(sizeof(dt_iop_toneequalizer_params_v3_t));
+
+ // changed smoothing
+ // old smoothing was sigma with a range of 1...sqrt(2)...2
+ // new smoothing is the displayed value of -1...0...1
+ n->smoothing = logf(o->smoothing) / logf(sqrtf(2.0f)) - 1.0f;
+
+ // V3 params
+ n->curve_type = DT_TONEEQ_CURVE_GAUSS;
+ n->post_scale_base = 0.0f;
+ n->post_shift_base = 0.0f;
+ n->post_scale = 0.0f;
+ n->post_shift = 0.0f;
+ n->post_pivot = -4.0f;
+ n->global_exposure = 0.0f;
+ n->scale_curve = 1.0f;
+ n->lum_estimator_R = 1.0f / 3.0f;
+ n->lum_estimator_G = 1.0f / 3.0f;
+ n->lum_estimator_B = 1.0f / 3.0f;
+ n->lum_estimator_normalize = TRUE;
+ n->auto_align_enabled = FALSE;
+ n->align_shift_only = FALSE;
*new_params = n;
- *new_params_size = sizeof(dt_iop_toneequalizer_params_v2_t);
- *new_version = 2;
+ *new_params_size = sizeof(dt_iop_toneequalizer_params_v3_t);
+ *new_version = 3;
return 0;
}
+
return 1;
}
-static void compress_shadows_highlight_preset_set_exposure_params
- (dt_iop_toneequalizer_params_t* p,
- const float step)
+
+/****************************************************************************
+ *
+ * Presets
+ *
+ ****************************************************************************/
+// empty marker function to make navigating the function list easier
+__attribute__((unused)) static void PRESETS_MARKER() {}
+
+static void _compress_shadows_highlights_preset_set_exposure_params
+ (dt_iop_toneequalizer_params_t* p)
{
// this function is used to set the exposure params for the 4 "compress shadows
// highlights" presets, which use basically the same curve, centered around
// -4EV with an exposure compensation that puts middle-grey at -4EV.
+ const float step = 0.65f; // old "strong" preset
p->noise = step;
p->ultra_deep_blacks = 5.f / 3.f * step;
p->deep_blacks = 5.f / 3.f * step;
@@ -443,11 +730,61 @@ static void compress_shadows_highlight_preset_set_exposure_params
p->speculars = -step;
}
+static void _compress_shadows_highlights_v3_preset_set_exposure_params
+ (dt_iop_toneequalizer_params_t* p)
+{
+ // this function is used to set the exposure params for the 4 "compress shadows
+ // highlights" presets, which use basically the same curve, centered around
+ // -4EV with an exposure compensation that puts middle-grey at -4EV.
+ p->noise = 1.6f;
+ p->ultra_deep_blacks = 1.49f;
+ p->deep_blacks = 1.16f;
+ p->blacks = 0.66;
+ p->shadows = 0.0f;
+ p->midtones = -0.66;
+ p->highlights = -1.16f;
+ p->whites = -1.49f;
+ p->speculars = -1.6f;
+}
+
+static void _compress_shadows_v3_preset_set_exposure_params
+ (dt_iop_toneequalizer_params_t* p)
+{
+ // this function is used to set the exposure params for the 4 "compress shadows
+ // highlights" presets, which use basically the same curve, centered around
+ // -4EV with an exposure compensation that puts middle-grey at -4EV.
+ p->noise = 1.6f;
+ p->ultra_deep_blacks = 1.49f;
+ p->deep_blacks = 1.16f;
+ p->blacks = 0.66;
+ p->shadows = 0.21f;
+ p->midtones = 0.0;
+ p->highlights = 0.0f;
+ p->whites = 0.0f;
+ p->speculars = 0.0f;
+}
+
+static void _compress_highlights_v3_preset_set_exposure_params
+ (dt_iop_toneequalizer_params_t* p)
+{
+ // this function is used to set the exposure params for the 4 "compress shadows
+ // highlights" presets, which use basically the same curve, centered around
+ // -4EV with an exposure compensation that puts middle-grey at -4EV.
+ p->noise = 0.0f;
+ p->ultra_deep_blacks = 0.0f;
+ p->deep_blacks = 0.0f;
+ p->blacks = 0.0f;
+ p->shadows = -0.21f;
+ p->midtones = -0.66;
+ p->highlights = -1.16f;
+ p->whites = -1.49f;
+ p->speculars = -1.6f;
+}
-static void dilate_shadows_highlight_preset_set_exposure_params
- (dt_iop_toneequalizer_params_t* p,
- const float step)
+static void _dilate_shadows_highlight_preset_set_exposure_params
+ (dt_iop_toneequalizer_params_t* p)
{
+ const float step = 0.65f; // old "strong" preset
// create a tone curve meant to be used without filter (as a flat,
// non-local, 1D tone curve) that reverts the local settings above.
p->noise = -15.f / 9.f * step;
@@ -466,15 +803,33 @@ void init_presets(dt_iop_module_so_t *self)
dt_iop_toneequalizer_params_t p;
memset(&p, 0, sizeof(p));
- p.method = DT_TONEEQ_NORM_POWER;
+ p.lum_estimator = DT_TONEEQ_NORM_POWER;
p.contrast_boost = 0.0f;
- p.details = DT_TONEEQ_NONE;
+ p.filter = DT_TONEEQ_NONE;
p.exposure_boost = -0.5f;
p.feathering = 1.0f;
p.iterations = 1;
- p.smoothing = sqrtf(2.0f);
+ p.smoothing = 0.0f;
p.quantization = 0.0f;
+ // V3 params
+ p.post_scale_base = 0.0f;
+ p.post_shift_base = 0.0f;
+ p.post_scale = 0.0f;
+ p.post_shift = 0.0f;
+ p.post_pivot = -4.0f;
+ p.global_exposure = 0.0f;
+ p.scale_curve = 1.0f;
+ p.curve_type = DT_TONEEQ_CURVE_GAUSS;
+
+ p.lum_estimator_R = 1.0f / 3.0f;
+ p.lum_estimator_G = 1.0f / 3.0f;
+ p.lum_estimator_B = 1.0f / 3.0f;
+ p.lum_estimator_normalize = TRUE;
+
+ p.auto_align_enabled = FALSE;
+ p.align_shift_only = FALSE;
+
// Init exposure settings
p.noise = p.ultra_deep_blacks = p.deep_blacks = p.blacks = 0.0f;
p.shadows = p.midtones = p.highlights = p.whites = p. speculars = 0.0f;
@@ -485,8 +840,8 @@ void init_presets(dt_iop_module_so_t *self)
self->version(), &p, sizeof(p), TRUE, DEVELOP_BLEND_CS_RGB_SCENE);
// Simple utils blendings
- p.details = DT_TONEEQ_EIGF;
- p.method = DT_TONEEQ_NORM_2;
+ p.filter = DT_TONEEQ_EIGF;
+ p.lum_estimator = DT_TONEEQ_NORM_2;
p.blending = 5.0f;
p.feathering = 1.0f;
@@ -515,66 +870,50 @@ void init_presets(dt_iop_module_so_t *self)
p.quantization = 0.0f;
// slight modification to give higher compression
- p.details = DT_TONEEQ_EIGF;
+ p.filter = DT_TONEEQ_EIGF;
p.feathering = 20.0f;
- compress_shadows_highlight_preset_set_exposure_params(&p, 0.65f);
+ _compress_shadows_highlights_preset_set_exposure_params(&p);
dt_gui_presets_add_generic
- (_("compress shadows/highlights | EIGF | strong"), self->op,
+ (_("compress shadows/highlights | classic EIGF"), self->op,
self->version(), &p, sizeof(p), TRUE, DEVELOP_BLEND_CS_RGB_SCENE);
- p.details = DT_TONEEQ_GUIDED;
+ p.filter = DT_TONEEQ_GUIDED;
p.feathering = 500.0f;
dt_gui_presets_add_generic
- (_("compress shadows/highlights | GF | strong"), self->op,
+ (_("compress shadows/highlights | classic GF"), self->op,
self->version(), &p, sizeof(p), TRUE, DEVELOP_BLEND_CS_RGB_SCENE);
- p.details = DT_TONEEQ_EIGF;
- p.blending = 3.0f;
- p.feathering = 7.0f;
- p.iterations = 3;
- compress_shadows_highlight_preset_set_exposure_params(&p, 0.45f);
- dt_gui_presets_add_generic
- (_("compress shadows/highlights | EIGF | medium"), self->op,
- self->version(), &p, sizeof(p), TRUE, DEVELOP_BLEND_CS_RGB_SCENE);
- p.details = DT_TONEEQ_GUIDED;
- p.feathering = 500.0f;
+ // slight modification to give higher compression
+ p.filter = DT_TONEEQ_EIGF;
+ p.feathering = 20.0f;
+ p.curve_type = DT_TONEEQ_CURVE_CATMULL;
+ p.smoothing = 0.5f;
+ _compress_shadows_highlights_v3_preset_set_exposure_params(&p);
dt_gui_presets_add_generic
- (_("compress shadows/highlights | GF | medium"), self->op,
+ (_("compress shadows/highlights | v3 EIGF"), self->op,
self->version(), &p, sizeof(p), TRUE, DEVELOP_BLEND_CS_RGB_SCENE);
- p.details = DT_TONEEQ_EIGF;
- p.blending = 5.0f;
- p.feathering = 1.0f;
- p.iterations = 1;
- compress_shadows_highlight_preset_set_exposure_params(&p, 0.25f);
- dt_gui_presets_add_generic
- (_("compress shadows/highlights | EIGF | soft"), self->op,
- self->version(), &p, sizeof(p), TRUE, DEVELOP_BLEND_CS_RGB_SCENE);
- p.details = DT_TONEEQ_GUIDED;
- p.feathering = 500.0f;
+ _compress_shadows_v3_preset_set_exposure_params(&p);
dt_gui_presets_add_generic
- (_("compress shadows/highlights | GF | soft"), self->op,
+ (_("compress shadows/highlights | v3 EIGF Raise Shadows"), self->op,
self->version(), &p, sizeof(p), TRUE, DEVELOP_BLEND_CS_RGB_SCENE);
- // build the 1D contrast curves that revert the local compression of
- // contrast above
- p.details = DT_TONEEQ_NONE;
- dilate_shadows_highlight_preset_set_exposure_params(&p, 0.25f);
+ _compress_highlights_v3_preset_set_exposure_params(&p);
dt_gui_presets_add_generic
- (_("contrast tone curve | soft"), self->op,
+ (_("compress shadows/highlights | v3 EIGF Lower Highlights"), self->op,
self->version(), &p, sizeof(p), TRUE, DEVELOP_BLEND_CS_RGB_SCENE);
- dilate_shadows_highlight_preset_set_exposure_params(&p, 0.45f);
- dt_gui_presets_add_generic
- (_("contrast tone curve | medium"), self->op,
- self->version(), &p, sizeof(p), TRUE, DEVELOP_BLEND_CS_RGB_SCENE);
+ p.smoothing = 0.0f;
- dilate_shadows_highlight_preset_set_exposure_params(&p, 0.65f);
+ // build the 1D contrast curves that revert the local compression of
+ // contrast above
+ p.filter = DT_TONEEQ_NONE;
+ _dilate_shadows_highlight_preset_set_exposure_params(&p);
dt_gui_presets_add_generic
- (_("contrast tone curve | strong"), self->op,
+ (_("contrast tone curve"), self->op,
self->version(), &p, sizeof(p), TRUE, DEVELOP_BLEND_CS_RGB_SCENE);
// relight
- p.details = DT_TONEEQ_EIGF;
+ p.filter = DT_TONEEQ_EIGF;
p.blending = 5.0f;
p.feathering = 1.0f;
p.iterations = 1;
@@ -598,356 +937,277 @@ void init_presets(dt_iop_module_so_t *self)
}
-/**
- * Helper functions
- **/
-
-static gboolean in_mask_editing(const dt_iop_module_t *self)
-{
- const dt_develop_t *dev = self->dev;
- return dev->form_gui && dev->form_visible;
-}
+/****************************************************************************
+ *
+ * Functions that are needed by process and therefore
+ * are part of worker threads
+ *
+ ****************************************************************************/
+// empty marker function to make navigating the function list easier
+__attribute__((unused)) static void PROCESS_DEPENDENCIES_MARKER() {}
-static void hash_set_get(const dt_hash_t *hash_in,
- dt_hash_t *hash_out,
- dt_pthread_mutex_t *lock)
+__DT_CLONE_TARGETS__
+static void _compute_min_max_ev(const float *const restrict luminance,
+ const size_t num_elem,
+ float *const restrict min_ev,
+ float *const restrict max_ev)
{
- // Set or get a hash in a struct the thread-safe way
- dt_pthread_mutex_lock(lock);
- *hash_out = *hash_in;
- dt_pthread_mutex_unlock(lock);
-}
-
+ float min_lum = INFINITY;
+ float max_lum = -INFINITY;
-static void invalidate_luminance_cache(dt_iop_module_t *const self)
-{
- // Invalidate the private luminance cache and histogram when
- // the luminance mask extraction parameters have changed
- dt_iop_toneequalizer_gui_data_t *const restrict g = self->gui_data;
+ DT_OMP_FOR_SIMD(reduction(min:min_lum) reduction(max:max_lum))
+ for(size_t k = 0; k < num_elem; k++)
+ {
+ min_lum = MIN(min_lum, luminance[k]);
+ max_lum = MAX(max_lum, luminance[k]);
+ }
- dt_iop_gui_enter_critical_section(self);
- g->max_histogram = 1;
- g->luminance_valid = FALSE;
- g->histogram_valid = FALSE;
- g->thumb_preview_hash = DT_INVALID_HASH;
- g->ui_preview_hash = DT_INVALID_HASH;
- dt_iop_gui_leave_critical_section(self);
- dt_iop_refresh_all(self);
+ *min_ev = log2f(MAX(min_lum, MIN_FLOAT));
+ *max_ev = log2f(MAX(max_lum, MIN_FLOAT));
}
-// gaussian-ish kernel - sum is == 1.0f so we don't care much about actual coeffs
-static const dt_colormatrix_t gauss_kernel =
- { { 0.076555024f, 0.124401914f, 0.076555024f },
- { 0.124401914f, 0.196172249f, 0.124401914f },
- { 0.076555024f, 0.124401914f, 0.076555024f } };
-
__DT_CLONE_TARGETS__
-static float get_luminance_from_buffer(const float *const buffer,
- const size_t width,
- const size_t height,
- const size_t x,
- const size_t y)
+static void _compute_hdr_histogram_and_stats(const float *const restrict luminance,
+ const size_t num_elem,
+ dt_iop_toneequalizer_histogram_stats_t *const restrict histo,
+ dt_dev_pixelpipe_type_t const debug_pipe)
{
- // Get the weighted average luminance of the 3×3 pixels region centered in (x, y)
- // x and y are ratios in [0, 1] of the width and height
+ // This expects histo to be pre-polulated with min_ev and max_ev
- if(y >= height || x >= width) return NAN;
+ // The GUI histogram comprises 8 EV (UI_HISTO_SAMPLES, -8 to 0).
+ // The high resolution histogram extends this to an exta 8 EV before and
+ // 8EV after, for a total of 24.
+ // Also the resolution is increased to compensate for the fact that the user
+ // can scale the histogram.
+ const float temp_ev_range = histo->max_ev - histo->min_ev;
- const size_t y_abs[4] DT_ALIGNED_PIXEL =
- { MAX(y, 1) - 1, // previous line
- y, // center line
- MIN(y + 1, height - 1), // next line
- y }; // padding for vectorization
+ // (Re)init the histogram
+ int* samples = histo->samples;
+ memset(samples, 0, sizeof(int) * histo->num_samples);
- float luminance = 0.0f;
- if(x > 1 && x < width - 2)
+ // Split exposure in bins
+ DT_OMP_FOR_SIMD(reduction(+:samples[:HDR_HISTO_SAMPLES]))
+ for(size_t k = 0; k < num_elem; k++)
{
- // no clamping needed on x, which allows us to vectorize
- // apply the convolution
- for(int i = 0; i < 3; ++i)
- {
- const size_t y_i = y_abs[i];
- for_each_channel(j)
- luminance += buffer[width * y_i + x-1 + j] * gauss_kernel[i][j];
- }
- return luminance;
+ const int index =
+ CLAMP((int)(((log2f(luminance[k]) - histo->min_ev) / temp_ev_range) * (float)histo->num_samples),
+ 0, histo->num_samples - 1);
+ samples[index] += 1;
}
- const size_t x_abs[4] DT_ALIGNED_PIXEL =
- { MAX(x, 1) - 1, // previous column
- x, // center column
- MIN(x + 1, width - 1), // next column
- x }; // padding for vectorization
+ const int low_percentile_pop = (int)((float)num_elem * 0.05f);
+ const int high_percentile_pop = (int)((float)num_elem * (1.0f - 0.95f));
- // convolution
- for(int i = 0; i < 3; ++i)
- {
- const size_t y_i = y_abs[i];
- for_each_channel(j)
- luminance += buffer[width * y_i + x_abs[j]] * gauss_kernel[i][j];
- }
- return luminance;
-}
+ int low_percentile_pos = 0;
+ int high_percentile_pos = 0;
-static void _get_point(const dt_iop_module_t *self,
- const int c_x,
- const int c_y,
- int *x,
- int *y)
-{
- // TODO: For this to fully work non depending on the place of the module
- // in the pipe we need a dt_dev_distort_backtransform_plus that
- // can skip crop only. With the current version if toneequalizer
- // is moved below rotation & perspective it will fail as we are
- // then missing all the transform after tone-eq.
- const double crop_order =
- dt_ioppr_get_iop_order(self->dev->iop_order_list, "crop", 0);
+ // Scout the extended histogram bins looking for deciles.
+ // These would not be accurate with the gui histogram.
+ DT_OMP_PRAGMA(parallel sections)
+ {
+ DT_OMP_PRAGMA(section)
+ {
+ int population = 0;
+ for(int k=0; k < histo->num_samples; ++k)
+ {
+ population += samples[k];
+ if(population >= low_percentile_pop)
+ {
+ low_percentile_pos = k;
+ break;
+ }
+ }
+ }
- float pts[2] = { c_x, c_y };
+ DT_OMP_PRAGMA(section)
+ {
+ int population = 0;
+ for(int k = histo->num_samples - 1; k >= 0; --k)
+ {
+ population += samples[k];
+ if(population >= high_percentile_pop)
+ {
+ high_percentile_pos = k;
+ break;
+ }
+ }
+ }
+ }
- // only a forward backtransform as the buffer already contains all the transforms
- // done before toneequal and we are speaking of on-screen cursor coordinates.
- // also we do transform only after crop as crop does change roi for the whole pipe
- // and so it is already part of the preview buffer cached in this implementation.
- dt_dev_distort_backtransform_plus(darktable.develop, darktable.develop->preview_pipe,
- crop_order,
- DT_DEV_TRANSFORM_DIR_FORW_EXCL, pts, 1);
- *x = pts[0];
- *y = pts[1];
+ // Convert positions to exposures
+ histo->lo_percentile_ev = (temp_ev_range * ((float)low_percentile_pos / (float)(histo->num_samples - 1))) + histo->min_ev;
+ histo->hi_percentile_ev = (temp_ev_range * ((float)high_percentile_pos / (float)(histo->num_samples - 1))) + histo->min_ev;
}
-static float _luminance_from_module_buffer(const dt_iop_module_t *self)
+__DT_CLONE_TARGETS__
+static void _compute_luminance_mask(const float *const restrict in,
+ float *const restrict luminance,
+ const size_t width,
+ const size_t height,
+ const dt_iop_toneequalizer_data_t *const restrict d,
+ float *const restrict image_min_lum,
+ float *const restrict image_max_lum,
+ dt_dev_pixelpipe_type_t const debug_pipe)
{
- const dt_iop_toneequalizer_gui_data_t *g = self->gui_data;
-
- const size_t c_x = g->cursor_pos_x;
- const size_t c_y = g->cursor_pos_y;
+ switch(d->filter)
+ {
+ case(DT_TONEEQ_NONE):
+ {
+ // No contrast boost here
+ luminance_mask(in, luminance, width, height,
+ d->lum_estimator, d->exposure_boost, 0.0f, 1.0f,
+ d->lum_estimator_R, d->lum_estimator_G, d->lum_estimator_B,
+ image_min_lum, image_max_lum);
+ break;
+ }
- // get buffer x,y given the cursor position
- int b_x = 0;
- int b_y = 0;
+ case(DT_TONEEQ_AVG_GUIDED):
+ {
+ // Still no contrast boost
+ luminance_mask(in, luminance, width, height,
+ d->lum_estimator, d->exposure_boost, 0.0f, 1.0f,
+ d->lum_estimator_R, d->lum_estimator_G, d->lum_estimator_B,
+ image_min_lum, image_max_lum);
+ fast_surface_blur(luminance, width, height, d->radius, d->feathering, d->iterations,
+ DT_GF_BLENDING_GEOMEAN, d->scale, d->quantization,
+ exp2f(-14.0f), 4.0f);
+ break;
+ }
- _get_point(self, c_x, c_y, &b_x, &b_y);
+ case(DT_TONEEQ_GUIDED):
+ {
+ // Contrast boosting is done around the average luminance of the mask.
+ // This is to make exposure corrections easier to control for users, by spreading
+ // the dynamic range along all exposure NUM_SLIDERS, because guided filters
+ // tend to flatten the luminance mask a lot around an average ± 2 EV
+ // which makes only 2-3 NUM_SLIDERS usable.
+ // we assume the distribution is centered around -4EV, e.g. the center of the nodes
+ // the exposure boost should be used to make this assumption true
+ luminance_mask(in, luminance, width, height, d->lum_estimator, d->exposure_boost,
+ CONTRAST_FULCRUM, d->contrast_boost,
+ d->lum_estimator_R, d->lum_estimator_G, d->lum_estimator_B,
+ image_min_lum, image_max_lum);
+ fast_surface_blur(luminance, width, height, d->radius, d->feathering, d->iterations,
+ DT_GF_BLENDING_LINEAR, d->scale, d->quantization,
+ exp2f(-14.0f), 4.0f);
+ break;
+ }
- return get_luminance_from_buffer(g->thumb_preview_buf,
- g->thumb_preview_buf_width,
- g->thumb_preview_buf_height,
- b_x,
- b_y);
-}
+ case(DT_TONEEQ_AVG_EIGF):
+ {
+ // Still no contrast boost
+ luminance_mask(in, luminance, width, height,
+ d->lum_estimator, d->exposure_boost, 0.0f, 1.0f,
+ d->lum_estimator_R, d->lum_estimator_G, d->lum_estimator_B,
+ image_min_lum, image_max_lum);
+ fast_eigf_surface_blur(luminance, width, height,
+ d->radius, d->feathering, d->iterations,
+ DT_GF_BLENDING_GEOMEAN, d->scale,
+ d->quantization, exp2f(-14.0f), 4.0f);
+ break;
+ }
-/***
- * Exposure compensation computation
- *
- * Construct the final correction factor by summing the octaves
- * channels gains weighted by the gaussian of the radial distance
- * (pixel exposure - octave center)
- *
- ***/
+ case(DT_TONEEQ_EIGF):
+ {
+ luminance_mask(in, luminance, width, height, d->lum_estimator, d->exposure_boost,
+ CONTRAST_FULCRUM, d->contrast_boost,
+ d->lum_estimator_R, d->lum_estimator_G, d->lum_estimator_B,
+ image_min_lum, image_max_lum);
+ fast_eigf_surface_blur(luminance, width, height,
+ d->radius, d->feathering, d->iterations,
+ DT_GF_BLENDING_LINEAR, d->scale,
+ d->quantization, exp2f(-14.0f), 4.0f);
+ break;
+ }
-DT_OMP_DECLARE_SIMD()
-__DT_CLONE_TARGETS__
-static float gaussian_denom(const float sigma)
-{
- // Gaussian function denominator such that y = exp(- radius^2 / denominator)
- // this is the constant factor of the exponential, so we don't need to recompute it
- // for every single pixel
- return 2.0f * sigma * sigma;
+ default:
+ {
+ luminance_mask(in, luminance, width, height,
+ d->lum_estimator, d->exposure_boost, 0.0f, 1.0f,
+ d->lum_estimator_R, d->lum_estimator_G, d->lum_estimator_B,
+ image_min_lum, image_max_lum);
+ break;
+ }
+ }
}
-
+// This is similar to exposure/contrast boost.
+// However it is applied AFTER the guided filter calculation, so it is much
+// easier to control and does not mess with the detail detection of the
+// guided filter.
+// Note: Scaling is stored logarithmically in p for UI, but
+// this function needs the linear version (i.e. from d)!
+// TODO: base-shift first, then scale? (consistently always shift first?)
DT_OMP_DECLARE_SIMD()
__DT_CLONE_TARGETS__
-static float gaussian_func(const float radius, const float denominator)
+static inline float _post_scale_shift(const float v,
+ const float post_scale_base,
+ const float post_shift_base,
+ const float post_scale,
+ const float post_shift,
+ const float post_pivot)
{
- // Gaussian function without normalization
- // this is the variable part of the exponential
- // the denominator should be evaluated with `gaussian_denom`
- // ahead of the array loop for optimal performance
- return expf(- radius * radius / denominator);
-}
-#define DT_TONEEQ_MIN_EV (-8.0f)
-#define DT_TONEEQ_MAX_EV (0.0f)
-#define DT_TONEEQ_USE_LUT TRUE
-#if DT_TONEEQ_USE_LUT
-// this is the version currently used, as using a lut gives a
-// big performance speedup on some cpus
-__DT_CLONE_TARGETS__
-static inline void apply_toneequalizer(const float *const restrict in,
- const float *const restrict luminance,
- float *const restrict out,
- const dt_iop_roi_t *const roi_in,
- const dt_iop_roi_t *const roi_out,
- const dt_iop_toneequalizer_data_t *const d)
-{
- const size_t npixels = (size_t)roi_in->width * roi_in->height;
- const float* restrict lut = d->correction_lut;
- const float lutres = LUT_RESOLUTION;
+ const float base_aligned = (v * post_scale_base) + post_shift_base;
- DT_OMP_FOR()
- for(size_t k = 0; k < npixels; k++)
- {
- // The radial-basis interpolation is valid in [-8; 0] EV and can quickly diverge outside.
- // Note: not doing an explicit lut[index] check is safe as long we take care of proper
- // DT_TONEEQ_MIN_EV and DT_TONEEQ_MAX_EV and allocated lut size LUT_RESOLUTION+1
- const float exposure = fast_clamp(log2f(luminance[k]), DT_TONEEQ_MIN_EV, DT_TONEEQ_MAX_EV);
- const float correction = lut[(unsigned)roundf((exposure - DT_TONEEQ_MIN_EV) * lutres)];
- // apply correction
- for_each_channel(c)
- out[4 * k + c] = correction * in[4 * k + c];
- }
+ return ((base_aligned + post_shift - post_pivot) * post_scale) + post_pivot;
}
-#else
-
-// we keep this version for further reference (e.g. for implementing
-// a gpu version)
+DT_OMP_DECLARE_SIMD()
__DT_CLONE_TARGETS__
-static inline void apply_toneequalizer(const float *const restrict in,
- const float *const restrict luminance,
- float *const restrict out,
- const dt_iop_roi_t *const roi_in,
- const dt_iop_roi_t *const roi_out,
- const dt_iop_toneequalizer_data_t *const d)
+static inline float _inverse_post_scale_shift(const float v,
+ const float post_scale_base,
+ const float post_shift_base,
+ const float post_scale,
+ const float post_shift,
+ const float post_pivot)
{
- const size_t num_elem = roi_in->width * roi_in->height;
- const float *const restrict factors = d->factors;
- const float sigma = d->smoothing;
- const float gauss_denom = gaussian_denom(sigma);
-
- DT_OMP_FOR(shared(centers_ops))
- for(size_t k = 0; k < num_elem; ++k)
- {
- // build the correction for the current pixel
- // as the sum of the contribution of each luminance channelcorrection
- float result = 0.0f;
+ // Reverse step 2: undo the scale around pivot and post_shift
+ const float base_aligned = ((v - post_pivot) / post_scale) + post_pivot - post_shift;
- // The radial-basis interpolation is valid in [-8; 0] EV and can
- // quickely diverge outside
- const float exposure = fast_clamp(log2f(luminance[k]), DT_TONEEQ_MIN_EV, DT_TONEEQ_MAX_EV);
-
- DT_OMP_SIMD(aligned(luminance, centers_ops, factors:64) safelen(PIXEL_CHAN) reduction(+:result))
- for(int i = 0; i < PIXEL_CHAN; ++i)
- result += gaussian_func(exposure - centers_ops[i], gauss_denom) * factors[i];
-
- // the user-set correction is expected in [-2;+2] EV, so is the interpolated one
- const float correction = fast_clamp(result, 0.25f, 4.0f);
-
- // apply correction
- for_each_channel(c)
- out[4 * k + c] = correction * in[4 * k + c];
- }
+ // Reverse step 1: undo base scale and shift
+ return (base_aligned - post_shift_base) / post_scale_base;
}
-#endif // USE_LUT
-__DT_CLONE_TARGETS__
-static inline float pixel_correction(const float exposure,
- const float *const restrict factors,
- const float sigma)
+static void _alignment_calculation(const dt_iop_toneequalizer_histogram_stats_t *const restrict hdr_histogram,
+ const gboolean shift_only,
+ float *const restrict out_post_scale_base,
+ float *const restrict out_post_shift_base)
{
- // build the correction for the current pixel
- // as the sum of the contribution of each luminance channel
- float result = 0.0f;
- const float gauss_denom = gaussian_denom(sigma);
- const float expo = fast_clamp(exposure, DT_TONEEQ_MIN_EV, DT_TONEEQ_MAX_EV);
+ const float percentile_range = hdr_histogram->hi_percentile_ev - hdr_histogram->lo_percentile_ev;
+ const float target_range = 6.0f; // -7 to -1 EV span
- DT_OMP_SIMD(aligned(centers_ops, factors:64) safelen(PIXEL_CHAN) reduction(+:result))
- for(int i = 0; i < PIXEL_CHAN; ++i)
- result += gaussian_func(expo - centers_ops[i], gauss_denom) * factors[i];
-
- return fast_clamp(result, 0.25f, 4.0f);
-}
-
-
-__DT_CLONE_TARGETS__
-static inline void compute_luminance_mask(const float *const restrict in,
- float *const restrict luminance,
- const size_t width,
- const size_t height,
- const dt_iop_toneequalizer_data_t *const d)
-{
- switch(d->details)
+ if (shift_only) // align shift, keep scale neutral
{
- case(DT_TONEEQ_NONE):
- {
- // No contrast boost here
- luminance_mask(in, luminance, width, height,
- d->method, d->exposure_boost, 0.0f, 1.0f);
- break;
- }
-
- case(DT_TONEEQ_AVG_GUIDED):
- {
- // Still no contrast boost
- luminance_mask(in, luminance, width, height,
- d->method, d->exposure_boost, 0.0f, 1.0f);
- fast_surface_blur(luminance, width, height, d->radius, d->feathering, d->iterations,
- DT_GF_BLENDING_GEOMEAN, d->scale, d->quantization,
- exp2f(-14.0f), 4.0f);
- break;
- }
-
- case(DT_TONEEQ_GUIDED):
- {
- // Contrast boosting is done around the average luminance of the mask.
- // This is to make exposure corrections easier to control for users, by spreading
- // the dynamic range along all exposure channels, because guided filters
- // tend to flatten the luminance mask a lot around an average ± 2 EV
- // which makes only 2-3 channels usable.
- // we assume the distribution is centered around -4EV, e.g. the center of the nodes
- // the exposure boost should be used to make this assumption true
- luminance_mask(in, luminance, width, height, d->method, d->exposure_boost,
- CONTRAST_FULCRUM, d->contrast_boost);
- fast_surface_blur(luminance, width, height, d->radius, d->feathering, d->iterations,
- DT_GF_BLENDING_LINEAR, d->scale, d->quantization,
- exp2f(-14.0f), 4.0f);
- break;
- }
-
- case(DT_TONEEQ_AVG_EIGF):
- {
- // Still no contrast boost
- luminance_mask(in, luminance, width, height,
- d->method, d->exposure_boost, 0.0f, 1.0f);
- fast_eigf_surface_blur(luminance, width, height,
- d->radius, d->feathering, d->iterations,
- DT_GF_BLENDING_GEOMEAN, d->scale,
- d->quantization, exp2f(-14.0f), 4.0f);
- break;
- }
+ *out_post_scale_base = 0.0f;
- case(DT_TONEEQ_EIGF):
- {
- luminance_mask(in, luminance, width, height, d->method, d->exposure_boost,
- CONTRAST_FULCRUM, d->contrast_boost);
- fast_eigf_surface_blur(luminance, width, height,
- d->radius, d->feathering, d->iterations,
- DT_GF_BLENDING_LINEAR, d->scale,
- d->quantization, exp2f(-14.0f), 4.0f);
- break;
- }
+ // Use midpoint of percentiles, target center is -4 EV
+ const float percentile_midpoint = (hdr_histogram->lo_percentile_ev + hdr_histogram->hi_percentile_ev) / 2.0f;
+ *out_post_shift_base = -4.0f - percentile_midpoint;
+ }
+ else // align shift and scale
+ {
+ const float post_scale_base_ev = target_range / percentile_range;
- default:
- {
- luminance_mask(in, luminance, width, height,
- d->method, d->exposure_boost, 0.0f, 1.0f);
- break;
- }
+ // We are already in EV here. This is log because we want the scale factor to be in the -2 to 2 range.
+ *out_post_scale_base = log2f(post_scale_base_ev);
+ *out_post_shift_base = -7.0f - (hdr_histogram->lo_percentile_ev * post_scale_base_ev);
}
}
-/***
- * Actual transfer functions
- **/
-
__DT_CLONE_TARGETS__
-static inline void display_luminance_mask(const float *const restrict in,
- const float *const restrict luminance,
- float *const restrict out,
- const dt_iop_roi_t *const roi_in,
- const dt_iop_roi_t *const roi_out)
+static void _display_luminance_mask(const float *const restrict in,
+ const float *const restrict luminance,
+ float *const restrict out,
+ const dt_iop_roi_t *const restrict roi_in,
+ const dt_iop_roi_t *const restrict roi_out,
+ const float post_scale_base,
+ const float post_shift_base,
+ const float post_scale,
+ const float post_shift,
+ const float post_pivot,
+ dt_dev_pixelpipe_type_t const debug_pipe)
{
const size_t offset_x = (roi_in->x < roi_out->x) ? -roi_in->x + roi_out->x : 0;
const size_t offset_y = (roi_in->y < roi_out->y) ? -roi_in->y + roi_out->y : 0;
@@ -964,17 +1224,27 @@ static inline void display_luminance_mask(const float *const restrict in,
? roi_out->height
: roi_in->height;
- DT_OMP_FOR(collapse(2))
+ DT_OMP_FOR_SIMD(collapse(2) aligned(in, luminance, out:64))
for(size_t i = 0 ; i < out_height; ++i)
for(size_t j = 0; j < out_width; ++j)
{
// normalize the mask intensity between -8 EV and 0 EV for clarity,
// and add a "gamma" 2.0 for better legibility in shadows
+ const int lum_index = (i + offset_y) * in_width + (j + offset_x);
+ const float lum_log = log2f(luminance[lum_index]);
+ const float lum_corrected = _post_scale_shift(lum_log, post_scale_base, post_shift_base, post_scale, post_shift, post_pivot);
+
+ // IMHO it would be fine, to show the log version of the mask to the user.
+ // const float intensity =
+ // fminf(fmaxf((lum_corrected + 8.0f) / 8.0f, 0.f), 1.f);
+ // However to keep everything identical to before, we go back to linear
+ // space and apply the square root/"gamma".
+ const float lum_linear = exp2f(lum_corrected);
const float intensity =
sqrtf(fminf(
- fmaxf(luminance[(i + offset_y) * in_width + (j + offset_x)] - 0.00390625f,
- 0.f) / 0.99609375f,
+ fmaxf(lum_linear - 0.00390625f, 0.f) / 0.99609375f,
1.f));
+
const size_t index = (i * out_width + j) * 4;
// set gray level for the mask
for_each_channel(c,aligned(out))
@@ -986,583 +1256,671 @@ static inline void display_luminance_mask(const float *const restrict in,
}
}
-
+/***
+ * Exposure compensation computation
+ *
+ * Construct the final correction factor by summing the octaves
+ * NUM_SLIDERS gains weighted by the gaussian of the radial distance
+ * (pixel exposure - octave center)
+ *
+ ***/
+DT_OMP_DECLARE_SIMD()
__DT_CLONE_TARGETS__
-static
-void toneeq_process(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)
+static float _compute_gaussian_denom(const float sigma)
{
- const dt_iop_toneequalizer_data_t *const d = piece->data;
- dt_iop_toneequalizer_gui_data_t *const g = self->gui_data;
+ // Gaussian function denominator such that y = exp(- radius^2 / denominator)
+ // this is the constant factor of the exponential, so we don't need to recompute it
+ // for every single pixel
+ return 2.0f * sigma * sigma;
+}
- const float *const restrict in = (float *const)ivoid;
- float *const restrict out = (float *const)ovoid;
- float *restrict luminance = NULL;
+DT_OMP_DECLARE_SIMD()
+__DT_CLONE_TARGETS__
+static inline float _compute_gaussian_weight(const float radius,
+ const float denominator)
+{
+ // Gaussian function without normalization
+ // this is the variable part of the exponential
+ // the denominator should be evaluated with `_compute_gaussian_denom`
+ // ahead of the array loop for optimal performance
+ return expf(- radius * radius / denominator);
+}
- const size_t width = roi_in->width;
- const size_t height = roi_in->height;
- const size_t num_elem = width * height;
+// This assumes "ghost points", an extra point in the beginning and an extra
+// point in the end, that are used for tangent calcculation but don't need
+// tangents themselves.
+DT_OMP_DECLARE_SIMD()
+__DT_CLONE_TARGETS__
+static inline void _catmull_rom_tangents(const float *const restrict y,
+ float *const restrict tangents,
+ const float tangent_scale)
+{
- // Get the hash of the upstream pipe to track changes
- const dt_hash_t hash = dt_dev_pixelpipe_piece_hash(piece, roi_out, TRUE);
+ const float scale = fabsf(tangent_scale) * 2.0f;
- // Sanity checks
- if(width < 1 || height < 1) return;
- if(roi_in->width < roi_out->width || roi_in->height < roi_out->height)
- return; // input should be at least as large as output
- if(piece->colors != 4) return; // we need RGB signal
+ DT_OMP_FOR_SIMD(aligned(y, tangents:64))
+ for (int i = 1; i <= NUM_SLIDERS; i++) {
+ tangents[i - 1] = ((y[i + 1] - y[i - 1]) / 2.0f) * scale;
+ }
+}
- // Init the luminance masks buffers
- gboolean cached = FALSE;
- if(self->dev->gui_attached)
- {
- // If the module instance has changed order in the pipe, invalidate the caches
- if(g->pipe_order != piece->module->iop_order)
- {
- dt_iop_gui_enter_critical_section(self);
- g->ui_preview_hash = DT_INVALID_HASH;
- g->thumb_preview_hash = DT_INVALID_HASH;
- g->pipe_order = piece->module->iop_order;
- g->luminance_valid = FALSE;
- g->histogram_valid = FALSE;
- dt_iop_gui_leave_critical_section(self);
- }
+// Similar to curve_tools.c catmull_rom_val, but with OpenMP
+DT_OMP_DECLARE_SIMD()
+__DT_CLONE_TARGETS__
+static inline float _catmull_rom_val(const int n, const float start_x, const float xval,
+ const float *const restrict y,
+ const float *const restrict tangents)
+{
+ assert(xval >= start_x);
+ assert(xval <= start_x + n - 1);
- if(piece->pipe->type & DT_DEV_PIXELPIPE_FULL)
- {
- // For DT_DEV_PIXELPIPE_FULL, we cache the luminance mask for performance
- // but it's not accessed from GUI
- // no need for threads lock since no other function is writing/reading that buffer
+ // Min becase of corner case xval == start_x + n - 1
+ const int ival = MIN((int)(xval - start_x), n - 2);
- // Re-allocate a new buffer if the full preview size has changed
- if(g->full_preview_buf_width != width || g->full_preview_buf_height != height)
- {
- dt_free_align(g->full_preview_buf);
- g->full_preview_buf = dt_alloc_align_float(num_elem);
- g->full_preview_buf_width = width;
- g->full_preview_buf_height = height;
- }
+ const float m0 = tangents[ival];
+ const float m1 = tangents[ival + 1];
- luminance = g->full_preview_buf;
- cached = TRUE;
- }
- else if(piece->pipe->type & DT_DEV_PIXELPIPE_PREVIEW)
- {
- // For DT_DEV_PIXELPIPE_PREVIEW, we need to cache it too to
- // compute the full image stats upon user request in GUI threads
- // locks are required since GUI reads and writes on that buffer.
+ const float dx = (xval - (float)(start_x + ival));
+ const float dx2 = dx * dx;
+ const float dx3 = dx * dx2;
- // Re-allocate a new buffer if the thumb preview size has changed
- dt_iop_gui_enter_critical_section(self);
- if(g->thumb_preview_buf_width != width || g->thumb_preview_buf_height != height)
- {
- dt_free_align(g->thumb_preview_buf);
- g->thumb_preview_buf = dt_alloc_align_float(num_elem);
- g->thumb_preview_buf_width = width;
- g->thumb_preview_buf_height = height;
- g->luminance_valid = FALSE;
- }
+ const float h00 = (2.0f * dx3) - (3.0f * dx2) + 1.0f;
+ const float h10 = (1.0f * dx3) - (2.0f * dx2) + dx;
+ const float h01 = (-2.0f * dx3) + (3.0f * dx2);
+ const float h11 = (1.0f * dx3) - (1.0f * dx2);
- luminance = g->thumb_preview_buf;
- cached = TRUE;
+ return (h00 * y[ival + 1])
+ + (h10 * m0)
+ + (h01 * y[ival + 2])
+ + (h11 * m1);
+}
- dt_iop_gui_leave_critical_section(self);
- }
- else // just to please GCC
- {
- luminance = dt_alloc_align_float(num_elem);
- }
+__DT_CLONE_TARGETS__
+static void _compute_correction_lut(dt_iop_toneequalizer_data_t *const d,
+ dt_dev_pixelpipe_type_t const debug_pipe,
+ const float debug_first_decile, const float debug_last_decile
+ )
+{
+ // The LUT only needs to be calculated for the -8 to 0 graph EV range
+ d->lut_min_ev = _inverse_post_scale_shift(-8.0f, d->post_scale_base, d->post_shift_base, d->post_scale, d->post_shift, d->post_pivot);
+ d->lut_max_ev = _inverse_post_scale_shift(0.0f, d->post_scale_base, d->post_shift_base, d->post_scale, d->post_shift, d->post_pivot);
- }
- else
- {
- // no interactive editing/caching : just allocate a local temp buffer
- luminance = dt_alloc_align_float(num_elem);
- }
+ const unsigned int lut_max_index = LUT_RESOLUTION * NUM_OCTAVES; // the array has space for 0...max (inclusive)
- // Check if the luminance buffer exists
- if(!luminance)
- {
- dt_control_log(_("tone equalizer failed to allocate memory, check your RAM settings"));
- return;
- }
+ const float sigma = powf(sqrtf(2.0f), 1.0f + d->smoothing);
+ const float gauss_denom = _compute_gaussian_denom(sigma);
+ const float global_exposure_exp = exp2f(d->global_exposure);
- // Compute the luminance mask
- if(cached)
+ if (d->curve_type == DT_TONEEQ_CURVE_GAUSS)
{
- // caching path : store the luminance mask for GUI access
-
- if(piece->pipe->type & DT_DEV_PIXELPIPE_FULL)
+ float* gauss_factors = d->gauss_factors;
+ DT_OMP_FOR_SIMD(shared(centers_base_fns) aligned(centers_base_fns, gauss_factors:64))
+ for(int j = 0; j <= lut_max_index; j++)
{
- dt_hash_t saved_hash;
- hash_set_get(&g->ui_preview_hash, &saved_hash, &self->gui_lock);
+ // build the correction for each pixel
+ // as the sum of the contribution of each luminance channelcorrection
- dt_iop_gui_enter_critical_section(self);
- const gboolean luminance_valid = g->luminance_valid;
- dt_iop_gui_leave_critical_section(self);
+ // 0..1 (inclusive)
+ const float normalized = (float)j / (float)(lut_max_index);
- if(hash != saved_hash || !luminance_valid)
- {
- /* compute only if upstream pipe state has changed */
- compute_luminance_mask(in, luminance, width, height, d);
- hash_set_get(&hash, &g->ui_preview_hash, &self->gui_lock);
- }
- }
- else if(piece->pipe->type & DT_DEV_PIXELPIPE_PREVIEW)
- {
- dt_hash_t saved_hash;
- hash_set_get(&g->thumb_preview_hash, &saved_hash, &self->gui_lock);
+ // -8..0 (inclusive)
+ const float exposure = normalized * NUM_OCTAVES + DT_TONEEQ_MIN_EV;
- dt_iop_gui_enter_critical_section(self);
- const gboolean luminance_valid = g->luminance_valid;
- dt_iop_gui_leave_critical_section(self);
+ float result = 0.0f;
- if(saved_hash != hash || !luminance_valid)
+ DT_OMP_SIMD(safelen(NUM_OCTAVES) reduction(+:result))
+ for(int i = 0; i < NUM_OCTAVES; i++)
{
- /* compute only if upstream pipe state has changed */
- dt_iop_gui_enter_critical_section(self);
- g->thumb_preview_hash = hash;
- g->histogram_valid = FALSE;
- compute_luminance_mask(in, luminance, width, height, d);
- g->luminance_valid = TRUE;
- dt_iop_gui_leave_critical_section(self);
- dt_dev_pixelpipe_cache_invalidate_later(piece->pipe, self->iop_order);
+ result += _compute_gaussian_weight(exposure - centers_base_fns[i], gauss_denom) * gauss_factors[i];
}
- }
- else // make it dummy-proof
- {
- compute_luminance_mask(in, luminance, width, height, d);
+
+ // the user-set correction is expected in [-2;+2] EV, so is the interpolated one
+ d->correction_lut[j] = fast_clamp(pow(result, d->scale_curve), 0.25f, 4.0f) * global_exposure_exp;
}
}
else
{
- // no caching path : compute no matter what
- compute_luminance_mask(in, luminance, width, height, d);
- }
-
- // Display output
- if(self->dev->gui_attached && (piece->pipe->type & DT_DEV_PIXELPIPE_FULL))
- {
- if(g->mask_display)
+ float* catmull_nodes_y = d->catmull_nodes_y;
+ float* catmull_tangents = d->catmull_tangents;
+ DT_OMP_FOR_SIMD(shared(centers_base_fns) aligned(centers_base_fns, catmull_nodes_y, catmull_tangents:64))
+ for(int j = 0; j <= lut_max_index; j++)
{
- display_luminance_mask(in, luminance, out, roi_in, roi_out);
- piece->pipe->mask_display = DT_DEV_PIXELPIPE_DISPLAY_PASSTHRU;
+ // build the correction for each pixel
+ // as the sum of the contribution of each luminance channelcorrection
+
+ // 0..1 (inclusive)
+ const float normalized = (float)j / (float)(lut_max_index);
+
+ // -8..0 (inclusive)
+ const float exposure = normalized * NUM_OCTAVES + DT_TONEEQ_MIN_EV;
+
+ const float result = fast_clamp(_catmull_rom_val(NUM_SLIDERS, DT_TONEEQ_MIN_EV, exposure, catmull_nodes_y, catmull_tangents), 0.25f, 4.0f);
+
+ // the user-set correction is expected in [-2;+2] EV, so is the interpolated one
+ d->correction_lut[j] = fast_clamp(pow(result, d->scale_curve), 0.25f, 4.0f) * global_exposure_exp;
}
- else
- apply_toneequalizer(in, luminance, out, roi_in, roi_out, d);
- }
- else
- {
- apply_toneequalizer(in, luminance, out, roi_in, roi_out, d);
}
-
- if(!cached) dt_free_align(luminance);
}
-void process(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)
+// This is the nearest neighbor version that the v2 tone equalizer used
+// Leaving it here for now for comparison/debugging purposes
+__DT_CLONE_TARGETS__
+__attribute__((unused)) static void _apply_toneequalizer(const float *const restrict in,
+ const float *const restrict luminance,
+ float *const restrict out,
+ const dt_iop_roi_t *const roi_in,
+ const dt_iop_toneequalizer_data_t *const restrict d,
+ dt_dev_pixelpipe_type_t const debug_pipe)
{
- toneeq_process(self, piece, ivoid, ovoid, roi_in, roi_out);
-}
+ const size_t npixels = (size_t)roi_in->width * roi_in->height;
+ const float* restrict lut = d->correction_lut;
+ const float lut_range_ev = d->lut_max_ev - d->lut_min_ev;
+ const float lut_max_index = LUT_RESOLUTION * NUM_OCTAVES;
-void modify_roi_in(dt_iop_module_t *self,
- dt_dev_pixelpipe_iop_t *piece,
- const dt_iop_roi_t *roi_out,
- dt_iop_roi_t *roi_in)
-{
- // Pad the zoomed-in view to avoid weird stuff with local averages
- // at the borders of the preview
-
- dt_iop_toneequalizer_data_t *const d = piece->data;
-
- // Get the scaled window radius for the box average
- const int max_size = (piece->iwidth > piece->iheight) ? piece->iwidth : piece->iheight;
- const float diameter = d->blending * max_size * roi_in->scale;
- const int radius = (int)((diameter - 1.0f) / ( 2.0f));
- d->radius = radius;
-}
-
+ DT_OMP_FOR()
+ for(size_t k = 0; k < npixels; k++)
+ {
+ // The radial-basis interpolation is valid in [-8; 0] EV and can quickly diverge outside.
+ // Note: not doing an explicit lut[index] check is safe as long we take care of proper
+ // DT_TONEEQ_MIN_EV and DT_TONEEQ_MAX_EV and allocated lut size LUT_RESOLUTION+1
-/***
- * Setters and Getters for parameters
- *
- * Remember the user params split the [-8; 0] EV range in 9 channels
- * and define a set of (x, y) coordinates, where x are the exposure
- * channels (evenly-spaced by 1 EV in [-8; 0] EV) and y are the
- * desired exposure compensation for each channel.
- *
- * This (x, y) set is interpolated by radial-basis function using a
- * series of 8 gaussians. Losing 1 degree of freedom makes it an
- * approximation rather than an interpolation but helps reducing a bit
- * the oscillations and fills a full AVX vector.
- *
- * The coefficients/factors used in the interpolation/approximation
- * are linear, but keep in mind that users params are expressed as
- * log2 gains, so we always need to do the log2/exp2 flip/flop between
- * both.
- *
- * User params of exposure compensation are expected between [-2 ; +2]
- * EV for practical UI reasons and probably numerical stability
- * reasons, but there is no theoretical obstacle to enlarge this
- * range. The main reason for not allowing it is tone equalizer is
- * mostly intended to do local changes, and these don't look so well
- * if you are too harsh on the changes. For heavier tonemapping, it
- * should be used in combination with a tone curve or filmic.
- *
- ***/
+ // lut_min_ev to (including) lut_max_ev
+ const float exposure = fast_clamp(log2f(luminance[k]), d->lut_min_ev, d->lut_max_ev);
-static void compute_correction_lut(float* restrict lut,
- const float sigma,
- const float *const restrict factors)
-{
- const float gauss_denom = gaussian_denom(sigma);
- assert(PIXEL_CHAN == 8);
+ // 0.0 to (including) 1.0
+ const float normalized = (exposure - d->lut_min_ev) / lut_range_ev;
- DT_OMP_FOR(shared(centers_ops))
- for(int j = 0; j <= LUT_RESOLUTION * PIXEL_CHAN; j++)
- {
- // build the correction for each pixel
- // as the sum of the contribution of each luminance channelcorrection
- const float exposure = (float)j / (float)LUT_RESOLUTION + DT_TONEEQ_MIN_EV;
- float result = 0.0f;
- for(int i = 0; i < PIXEL_CHAN; i++)
- result += gaussian_func(exposure - centers_ops[i], gauss_denom) * factors[i];
- // the user-set correction is expected in [-2;+2] EV, so is the interpolated one
- lut[j] = fast_clamp(result, 0.25f, 4.0f);
+ // 0 to (including) lut_max_index
+ const unsigned int lookup = (unsigned)roundf(normalized * lut_max_index);
+ const float correction = lut[lookup];
+ // apply correction
+ for_each_channel(c)
+ out[4 * k + c] = correction * in[4 * k + c];
}
}
-static void get_channels_gains(float factors[CHANNELS],
- const dt_iop_toneequalizer_params_t *p)
+// linearly interpolated version
+// This is both more accurate and faster than the nearest neighbor version.
+// The speedup comes from the smaller lookup table that fits better in cache
+__DT_CLONE_TARGETS__
+static void _apply_toneequalizer_linear(
+ const float *const restrict in,
+ const float *const restrict luminance,
+ float *const restrict out,
+ const dt_iop_roi_t *const restrict roi_in,
+ const dt_iop_toneequalizer_data_t *const restrict d,
+ dt_dev_pixelpipe_type_t const debug_pipe)
{
- assert(CHANNELS == 9);
+ const size_t npixels = (size_t)roi_in->width * roi_in->height;
+ const float* restrict lut = d->correction_lut;
- // Get user-set channels gains in EV (log2)
- factors[0] = p->noise; // -8 EV
- factors[1] = p->ultra_deep_blacks; // -7 EV
- factors[2] = p->deep_blacks; // -6 EV
- factors[3] = p->blacks; // -5 EV
- factors[4] = p->shadows; // -4 EV
- factors[5] = p->midtones; // -3 EV
- factors[6] = p->highlights; // -2 EV
- factors[7] = p->whites; // -1 EV
- factors[8] = p->speculars; // +0 EV
-}
+ // Number of elements in LUT.
+ // The LUT still starts at 0, so the max index is lut_elem-1
+ const int lut_elem = NUM_OCTAVES * LUT_RESOLUTION + 1;
+ const float lut_range_ev = d->lut_max_ev - d->lut_min_ev;
+ const float lut_max_index = LUT_RESOLUTION * NUM_OCTAVES;
-static void get_channels_factors(float factors[CHANNELS],
- const dt_iop_toneequalizer_params_t *p)
-{
- assert(CHANNELS == 9);
+ DT_OMP_FOR_SIMD(aligned(in, out, lut:64))
+ for(size_t k = 0; k < npixels; k++)
+ {
+ // The radial-basis interpolation is valid in [-8; 0] EV and can quickly diverge outside.
+ const float exposure = fast_clamp(log2f(luminance[k]), d->lut_min_ev, d->lut_max_ev);
- // Get user-set channels gains in EV (log2)
- get_channels_gains(factors, p);
+ // 0.0 to (including) 1.0
+ const float normalized = (exposure - d->lut_min_ev) / lut_range_ev;
- // Convert from EV offsets to linear factors
- DT_OMP_SIMD(aligned(factors:64))
- for(int c = 0; c < CHANNELS; ++c)
- factors[c] = exp2f(factors[c]);
-}
+ // 0 to (including) lut_max_index
+ const float pos = normalized * lut_max_index;
+ int i0 = (int)pos;
+ int i1 = i0 + 1;
+ float w = pos - (float)i0;
-__DT_CLONE_TARGETS__
-static gboolean compute_channels_factors(const float factors[PIXEL_CHAN],
- float out[CHANNELS],
- const float sigma)
-{
- // Input factors are the weights for the radial-basis curve
- // approximation of user params Output factors are the gains of the
- // user parameters channels aka the y coordinates of the
- // approximation for x = { CHANNELS }
- assert(PIXEL_CHAN == 8);
+ // Lowest allowed i0 is 0 in case that exposure = HDR_MIN_EV.
+ // Highest allowed i1 is LUT_OCTAVES * LUT_RESOLUTION
+ if(i0 < 0) { i0 = 0; i1 = 0; w = 0.0f; }
+ if(i1 >= lut_elem) { i1 = lut_elem - 1; i0 = lut_elem - 1; w = 0.0f; }
- DT_OMP_FOR_SIMD(aligned(factors, out, centers_params:64) firstprivate(centers_params))
- for(int i = 0; i < CHANNELS; ++i)
- {
- // Compute the new channels factors; pixel_correction clamps the factors, so we don't
- // need to check for validity here
- out[i] = pixel_correction(centers_params[i], factors, sigma);
+ const float lut0 = lut[i0];
+ const float lut1 = lut[i1];
+ const float correction = lut0 * (1.0f - w) + lut1 * w;
+
+ // apply correction
+ for_each_channel(c)
+ out[4 * k + c] = correction * in[4 * k + c];
}
- return TRUE;
}
-
-__DT_CLONE_TARGETS__
-static void compute_channels_gains(const float in[CHANNELS],
- float out[CHANNELS])
+static float _adjust_radius_to_scale(const dt_dev_pixelpipe_iop_t *const restrict piece,
+ const dt_iop_roi_t *const restrict roi,
+ const int full_width, const int full_height)
{
- // Helper function to compute the new channels gains (log) from the factors (linear)
- assert(PIXEL_CHAN == 8);
-
- for(int i = 0; i < CHANNELS; ++i)
- out[i] = log2f(in[i]);
-}
-
+ dt_iop_toneequalizer_data_t *const d = piece->data;
-static void commit_channels_gains(const float factors[CHANNELS],
- dt_iop_toneequalizer_params_t *p)
-{
- p->noise = factors[0];
- p->ultra_deep_blacks = factors[1];
- p->deep_blacks = factors[2];
- p->blacks = factors[3];
- p->shadows = factors[4];
- p->midtones = factors[5];
- p->highlights = factors[6];
- p->whites = factors[7];
- p->speculars = factors[8];
+ // Get the scaled window radius for the box average
+ // This should be relative to the current full image dimensions.
+ // roi.width/height refer to a segment instead of the full image, so these
+ // values are not useful for us.
+ const int max_size = (full_width > full_height) ? full_width : full_height;
+ const float diameter = d->blending * max_size * roi->scale;
+ const int radius = (int)((diameter - 1.0f) / ( 2.0f));
+ return radius;
}
+// empty marker function to make navigating the function list easier
+__attribute__((unused)) static void PROCESS_MARKER() {}
/***
- * Cache invalidation and initializatiom
+ * PROCESS
+ *
+ * General steps:
+ * 1. Compute the luminance mask
+ * 2. Compute the histogram of the luminance mask. In this step
+ * we also get the 5th and 95th percentiles.
+ * 3. Optionally compute auto align -> post scale and shift
+ * 4. Compute a correction LUT based on the user's curve in the GUI
+ * and post scale/shift
+ * 5. Correct the image based on curve and luminance mask
+ *
+ * The guided filter is scale dependent, so the results with differently
+ * sized input images will be different.
+ *
+ *
+ * We can be in one of four pipelines:
+ *
+ * DT_DEV_PIXELPIPE_PREVIEW (4)
+ * - In this case the input is a slightly blurry version of the full image,
+ * which is about 900px high. The fact that the input is blurry makes
+ * guided filter calculations deviate from the other pipes.
+ * - We cache the calculated mask in g. This version of the mask is used to
+ * determine the luminance at the mouse cursor position for interactive
+ * editing. It is also cached, so it does not need to be re-calculated
+ * as long as nothing changes upstream.
+ * - The histogram is cached in g and used for the GUI. The percentiles are
+ * calculated and stored in g. These are needed for the original
+ * exposure/contrast boost magic wands. All these statictics calculations
+ * deviate from the full image signifficantly, but this has been the case
+ * for tone equalizer from the beginning.
+ * DT_DEV_PIXELPIPE_FULL (2)
+ * - The input image is whatever is displayed in the main view. This can be
+ * a segment of the full image if the user has zoomed in.
+ * - The luminance mask for this pipe is also cached, so it does not need to
+ * be re-calculated as long as nothing changes upstream. This also allows
+ * for switching to the mask view (greyscale) quickly.
+ * supplied to process. This allows for calculating an exact histogram
+ * which can then be displayed in the GUI to inform the user about the
+ * differences. The HQ histogram is not used for any calculations.
+ * DT_DEV_PIXELPIPE_EXPORT (1)
+ * - Input is the full image. The current setting are applied.
+ * OTHER (i.e. DT_DEV_PIXELPIPE_THUMBNAIL)
+ * - The input image is a scaled-down version of the full image. The output
+ * will deviate from FULL/EXPORT, but since the image is very small,
+ * nobody will notice.
***/
-
-
-static void gui_cache_init(dt_iop_module_t *self)
+__DT_CLONE_TARGETS__
+static
+void _toneeq_process(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 restrict roi_in,
+ const dt_iop_roi_t *const restrict roi_out)
{
- dt_iop_toneequalizer_gui_data_t *g = self->gui_data;
- if(g == NULL) return;
+ dt_iop_toneequalizer_data_t *const d = piece->data;
+ dt_iop_toneequalizer_gui_data_t *const g = self->gui_data;
- dt_iop_gui_enter_critical_section(self);
- g->ui_preview_hash = DT_INVALID_HASH;
- g->thumb_preview_hash = DT_INVALID_HASH;
- g->max_histogram = 1;
- g->scale = 1.0f;
- g->sigma = sqrtf(2.0f);
- g->mask_display = FALSE;
+ // TODO
+ // imageop.h
+ // Align so that DT_ALIGNED_ARRAY may be used within gui_data struct
+ // module->gui_data = dt_calloc_aligned(size);
- g->interpolation_valid = FALSE; // TRUE if the interpolation_matrix is ready
- g->luminance_valid = FALSE; // TRUE if the luminance cache is ready
- g->histogram_valid = FALSE; // TRUE if the histogram cache and stats are ready
- g->lut_valid = FALSE; // TRUE if the gui_lut is ready
- g->graph_valid = FALSE; // TRUE if the UI graph view is ready
- g->user_param_valid = FALSE; // TRUE if users params set in interactive view are in bounds
- g->factors_valid = TRUE; // TRUE if radial-basis coeffs are ready
+ const float *const restrict in = (float *const)ivoid;
+ float *const restrict out = (float *const)ovoid;
- g->valid_nodes_x = FALSE; // TRUE if x coordinates of graph nodes have been inited
- g->valid_nodes_y = FALSE; // TRUE if y coordinates of graph nodes have been inited
- g->area_cursor_valid = FALSE; // TRUE if mouse cursor is over the graph area
- g->area_dragging = FALSE; // TRUE if left-button has been pushed but not released and cursor motion is recorded
- g->cursor_valid = FALSE; // TRUE if mouse cursor is over the preview image
- g->has_focus = FALSE; // TRUE if module has focus from GTK
+ const size_t num_elem = roi_out->width * roi_out->height;
+ const gboolean hq = darktable.develop->late_scaling.enabled;
- g->full_preview_buf = NULL;
- g->full_preview_buf_width = 0;
- g->full_preview_buf_height = 0;
+ // Sanity checks
+ if(roi_in->width < 1 || roi_in->height < 1) return;
+ if(roi_in->width < roi_out->width || roi_in->height < roi_out->height)
+ return; // input should be at least as large as output
+ if(piece->colors != 4) return; // we need RGB signal
- g->thumb_preview_buf = NULL;
- g->thumb_preview_buf_width = 0;
- g->thumb_preview_buf_height = 0;
+ // Get the hash of the upstream pipe to track changes
+ const dt_hash_t current_upstream_hash = dt_dev_pixelpipe_piece_hash(piece, roi_out, FALSE);
- g->desc = NULL;
- g->layout = NULL;
- g->cr = NULL;
- g->cst = NULL;
- g->context = NULL;
+ // This will be either local memory or cache stored in g
+ float *restrict luminance = NULL;
+ dt_iop_toneequalizer_histogram_stats_t *hdr_histogram = NULL;
- g->pipe_order = 0;
- dt_iop_gui_leave_critical_section(self);
-}
+ // Remember to free local buffers that are allocated in this function
+ gboolean local_luminance = FALSE;
+ gboolean local_hdr_hist = FALSE;
+ /**************************************************************************
+ * Initialization
+ **************************************************************************/
+ if(self->dev->gui_attached)
+ {
+ // If the module instance has changed order in the pipe, invalidate the caches
+ if(g->pipe_order != piece->module->iop_order)
+ {
+ dt_iop_gui_enter_critical_section(self);
+ g->pipe_order = piece->module->iop_order;
-static inline void build_interpolation_matrix(float A[CHANNELS * PIXEL_CHAN],
- const float sigma)
-{
- // Build the symmetrical definite positive part of the augmented matrix
- // of the radial-basis interpolation weights
+ g->full_upstream_hash = DT_INVALID_HASH;
+ g->preview_upstream_hash = DT_INVALID_HASH;
+ g->prv_luminance_valid = FALSE;
+ g->full_luminance_valid = FALSE;
+ g->gui_histogram_valid = FALSE;
+ dt_iop_gui_leave_critical_section(self);
+ }
- const float gauss_denom = gaussian_denom(sigma);
+ if(piece->pipe->type & DT_DEV_PIXELPIPE_PREVIEW)
+ {
+ // For DT_DEV_PIXELPIPE_PREVIEW, we need to cache the luminace mask
+ // and the HDR histogram for GUI.
+ // Locks are required since GUI reads and writes on that buffer.
- DT_OMP_SIMD(aligned(A, centers_ops, centers_params:64) collapse(2))
- for(int i = 0; i < CHANNELS; ++i)
- for(int j = 0; j < PIXEL_CHAN; ++j)
- A[i * PIXEL_CHAN + j] =
- gaussian_func(centers_params[i] - centers_ops[j], gauss_denom);
-}
+ // Re-allocate a new buffer if the thumb preview size has changed
+ dt_iop_gui_enter_critical_section(self);
+ if(g->preview_buf_width != roi_out->width || g->preview_buf_height != roi_out->height)
+ {
+ dt_free_align(g->preview_buf);
+ g->preview_buf = dt_alloc_align_float(num_elem);
+ g->preview_buf_width = roi_out->width;
+ g->preview_buf_height = roi_out->height;
+ g->prv_luminance_valid = FALSE;
+ g->gui_histogram_valid = FALSE;
+ }
+ luminance = g->preview_buf;
+ hdr_histogram = &g->mask_hdr_histo;
-__DT_CLONE_TARGETS__
-static inline void 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)
-{
- // (Re)init the histogram
- memset(histogram, 0, sizeof(int) * UI_SAMPLES);
+ dt_iop_gui_leave_critical_section(self);
+ }
+ else if (piece->pipe->type & DT_DEV_PIXELPIPE_FULL)
+ {
+ // For DT_DEV_PIXELPIPE_FULL, we cache the luminance mask for performance
+ // but it's not accessed from GUI
+ // no need for threads lock since no other function is writing/reading that buffer
- // we first calculate an extended histogram for better accuracy
- #define TEMP_SAMPLES 2 * UI_SAMPLES
- int temp_hist[TEMP_SAMPLES];
- memset(temp_hist, 0, sizeof(int) * TEMP_SAMPLES);
+ // Re-allocate a new buffer if the full preview size has changed
+ if(g->full_buf_width != roi_out->width || g->full_buf_height != roi_out->height)
+ {
+ dt_free_align(g->full_buf);
+ g->full_buf = dt_alloc_align_float(num_elem);
+ g->full_buf_width = roi_out->width;
+ g->full_buf_height = roi_out->height;
+ g->full_luminance_valid = FALSE; // TODO: critial needed for this value?
+ }
- // Split exposure in bins
- DT_OMP_FOR_SIMD(reduction(+:temp_hist[:TEMP_SAMPLES]))
- for(size_t k = 0; k < num_elem; k++)
- {
- // extended histogram bins between [-10; +6] EV remapped between [0 ; 2 * UI_SAMPLES]
- const int index =
- CLAMP((int)(((log2f(luminance[k]) + 10.0f) / 16.0f) * (float)TEMP_SAMPLES),
- 0, TEMP_SAMPLES - 1);
- temp_hist[index] += 1;
- }
+ // handle scrolling of the main area
+ if (g->full_buf_x != roi_out->x || g->full_buf_y != roi_out->y)
+ {
+ g->full_buf_x = roi_out->x;
+ g->full_buf_y = roi_out->y;
+ g->full_luminance_valid = FALSE;
+ }
- const int first = (int)((float)num_elem * 0.05f);
- const int last = (int)((float)num_elem * (1.0f - 0.95f));
- int population = 0;
- int first_pos = 0;
- int last_pos = 0;
+ luminance = g->full_buf;
- // scout the extended histogram bins looking for deciles
- // these would not be accurate with the regular histogram
- for(int k = 0; k < TEMP_SAMPLES; ++k)
- {
- const size_t prev_population = population;
- population += temp_hist[k];
- if(prev_population < first && first <= population)
+ hdr_histogram = dt_calloc_aligned(sizeof(dt_iop_toneequalizer_histogram_stats_t));
+ _histogram_stats_init(hdr_histogram);
+ local_hdr_hist = TRUE;
+ }
+ else // Should not happen. GUI, but neither PREVIEW nor FULL.
{
- first_pos = k;
- break;
+ luminance = dt_alloc_align_float(num_elem);
+ local_luminance = TRUE;
+ hdr_histogram = dt_calloc_aligned(sizeof(dt_iop_toneequalizer_histogram_stats_t));
+ _histogram_stats_init(hdr_histogram);
+ local_hdr_hist = TRUE;
}
}
- population = 0;
- for(int k = TEMP_SAMPLES - 1; k >= 0; --k)
+ else
{
- const size_t prev_population = population;
- population += temp_hist[k];
- if(prev_population < last && last <= population)
- {
- last_pos = k;
- break;
- }
+ // no interactive editing/caching: just allocate local temp buffers
+ luminance = dt_alloc_align_float(num_elem);
+ local_luminance = TRUE;
+ hdr_histogram = dt_calloc_aligned(sizeof(dt_iop_toneequalizer_histogram_stats_t));
+ _histogram_stats_init(hdr_histogram);
+ local_hdr_hist = TRUE;
}
- // Convert decile positions to exposures
- *first_decile = 16.0 * (float)first_pos / (float)(TEMP_SAMPLES - 1) - 10.0;
- *last_decile = 16.0 * (float)last_pos / (float)(TEMP_SAMPLES - 1) - 10.0;
-
- // remap the extended histogram into the normal one
- // bins between [-8; 0] EV remapped between [0 ; UI_SAMPLES]
- for(size_t k = 0; k < TEMP_SAMPLES; ++k)
+ // Check if the luminance buffer exists
+ if(!luminance || !hdr_histogram)
{
- const float EV = 16.0 * (float)k / (float)(TEMP_SAMPLES - 1) - 10.0;
- const int i =
- CLAMP((int)(((EV + 8.0f) / 8.0f) * (float)UI_SAMPLES),
- 0, UI_SAMPLES - 1);
- histogram[i] += temp_hist[k];
-
- // store the max numbers of elements in bins for later normalization
- *max_histogram = histogram[i] > *max_histogram ? histogram[i] : *max_histogram;
+ dt_control_log(_("tone equalizer failed to allocate memory, check your RAM settings"));
+ return;
}
-}
-static inline void update_histogram(dt_iop_module_t *const self)
-{
- dt_iop_toneequalizer_gui_data_t *const g = self->gui_data;
- if(g == NULL) return;
+ d->radius = _adjust_radius_to_scale(piece, roi_in, piece->iwidth, piece->iheight);
- dt_iop_gui_enter_critical_section(self);
- if(!g->histogram_valid && g->luminance_valid)
+ /**************************************************************************
+ * Compute the luminance mask
+ **************************************************************************/
+ if(self->dev->gui_attached && (piece->pipe->type & DT_DEV_PIXELPIPE_PREVIEW))
{
- const size_t num_elem = g->thumb_preview_buf_height * g->thumb_preview_buf_width;
- compute_log_histogram_and_stats(g->thumb_preview_buf, g->histogram, num_elem,
- &g->max_histogram,
- &g->histogram_first_decile, &g->histogram_last_decile);
- g->histogram_average = (g->histogram_first_decile + g->histogram_last_decile) / 2.0f;
- g->histogram_valid = TRUE;
- }
- dt_iop_gui_leave_critical_section(self);
-}
+ // DT_DEV_PIXELPIPE_PREVIEW:
+ // - Sees the whole image, but at a lower resolution and blurry.
+ // - Needs to store the luminance mask, HDR histogram and deciles
+ // for GUI.
+ dt_iop_gui_enter_critical_section(self);
+ const dt_hash_t saved_upstream_hash = g->preview_upstream_hash;
+ const gboolean prv_luminance_valid = g->prv_luminance_valid;
+ g->temp_post_values_valid = FALSE;
+ dt_iop_gui_leave_critical_section(self);
-__DT_CLONE_TARGETS__
-static inline void compute_lut_correction(dt_iop_toneequalizer_gui_data_t *g,
- const float offset,
- const float scaling)
-{
- // Compute the LUT of the exposure corrections in EV,
- // offset and scale it for display in GUI widget graph
+ if(saved_upstream_hash != current_upstream_hash || !prv_luminance_valid)
+ {
+ /* compute only if upstream pipe state has changed */
- if(g == NULL) return;
+ // it auto_align is on, the FULL pipe will have to wait for this one
+ const dt_hash_t prv_full_sync_hash = dt_dev_hash_plus(self->dev, piece->pipe, self->iop_order, DT_DEV_TRANSFORM_DIR_BACK_INCL);
- float *const restrict LUT = g->gui_lut;
- const float *const restrict factors = g->factors;
- const float sigma = g->sigma;
+ dt_iop_gui_enter_critical_section(self);
+ g->preview_upstream_hash = current_upstream_hash;
+ g->gui_histogram_valid = FALSE;
+ g->prv_luminance_valid = FALSE;
+ dt_iop_gui_leave_critical_section(self);
- DT_OMP_FOR_SIMD(aligned(LUT, factors:64))
- for(int k = 0; k < UI_SAMPLES; k++)
- {
- // build the inset graph curve LUT
- // the x range is [-14;+2] EV
- const float x = (8.0f * (((float)k) / ((float)(UI_SAMPLES - 1)))) - 8.0f;
- LUT[k] = offset - log2f(pixel_correction(x, factors, sigma)) / scaling;
- }
-}
+ _compute_luminance_mask(in, luminance, roi_out->width, roi_out->height, d,
+ &g->prv_image_ev_min, &g->prv_image_ev_max,
+ piece->pipe->type);
+ // min/max ev
+ float min_ev, max_ev;
+ _compute_min_max_ev(luminance, num_elem, &min_ev, &max_ev);
+ hdr_histogram->min_ev = min_ev;
+ hdr_histogram->max_ev = max_ev;
+ // Histogram and deciles
+ _compute_hdr_histogram_and_stats(luminance, num_elem, hdr_histogram,
+ piece->pipe->type);
-static inline gboolean update_curve_lut(dt_iop_module_t *self)
-{
- const dt_iop_toneequalizer_params_t *p = self->params;
- dt_iop_toneequalizer_gui_data_t *g = self->gui_data;
+ dt_iop_gui_enter_critical_section(self);
+ // GUI can assume that mask, histogram and deciles are valid
+ g->prv_luminance_valid = TRUE;
- if(g == NULL) return FALSE;
+ // if auto-alignment is enable, we will basically "auto-click" an alignment button
+ // PREVIEW computes the necessary base shift/scale valuyes and stores them
+ // temporarily in g.
+ // - FULL can read it there later
+ // - the GUI will also read these values and store them in p
- gboolean valid = TRUE;
+ if(d->auto_align_enabled)
+ {
+ float post_scale_base, post_shift_base;
+ _alignment_calculation(&g->mask_hdr_histo, d->align_shift_only,
+ &post_scale_base, &post_shift_base);
+ g->temp_post_scale_base = post_scale_base;
+ g->temp_post_shift_base = post_shift_base;
+ g->temp_post_values_valid = TRUE;
+
+ // inform FULL pipe that PREVIEW has updated
+ g->prv_full_sync_hash = prv_full_sync_hash;
+ }
+ else if (!g->post_realign_possible_false_alert)
+ g->post_realign_required = TRUE;
- dt_iop_gui_enter_critical_section(self);
+ dt_iop_gui_leave_critical_section(self);
+ }
- if(!g->interpolation_valid)
- {
- build_interpolation_matrix(g->interpolation_matrix, g->sigma);
- g->interpolation_valid = TRUE;
- g->factors_valid = FALSE;
+ // g->post_realign_required should be current now
+ dt_iop_gui_enter_critical_section(self);
+ g->post_realign_possible_false_alert = FALSE;
+ dt_iop_gui_leave_critical_section(self);
+
+ // TODO MF: Not completely sure in which cases this must be called.
+ // Assumption is once per output image change by this module and
+ // only when the GUI is active.
+ dt_dev_pixelpipe_cache_invalidate_later(piece->pipe, self->iop_order);
}
+ else if(self->dev->gui_attached && (piece->pipe->type & DT_DEV_PIXELPIPE_FULL))
+ {
+ // FULL may only see a part of the image if the user has zoomed in.
+ // We need to compute a luminance mask for this pipe and cache it for
+ // quick reuse (i.e. if the user only changes the curve).
+ // But we can not compute deciles here.
+
+ // roi_in and roi_out must be the same
+ assert(roi_in->x == roi_out->x && roi_in->y == roi_out->y && roi_in->width == roi_out->width
+ && roi_in->height == roi_out->height && roi_in->scale == roi_out->scale);
+
+ dt_iop_gui_enter_critical_section(self);
+ const dt_hash_t saved_upstream_hash = g->full_upstream_hash;
+ const gboolean full_luminance_valid = g->full_luminance_valid;
+ dt_iop_gui_leave_critical_section(self);
+
+ // If the upstream state has changed, re-compute the mask of the displayed image part
+ if(current_upstream_hash != saved_upstream_hash || !full_luminance_valid)
+ {
+ /* compute only if upstream pipe state has changed */
+ dt_iop_gui_enter_critical_section(self);
+ g->full_upstream_hash = current_upstream_hash;
+ dt_iop_gui_leave_critical_section(self);
+
+ float image_lum_min, image_lum_max;
+ _compute_luminance_mask(in, luminance, roi_out->width, roi_out->height, d,
+ &image_lum_min, &image_lum_max,
+ piece->pipe->type);
+ g->full_luminance_valid = TRUE;
+
+ if(d->auto_align_enabled)
+ {
+ // If auto-align is enabled, wait for PREVIEW and get the base shift/scale
+ // values that were left in g.
+ dt_iop_gui_enter_critical_section(self);
+ const dt_hash_t prv_full_sync_hash = g->prv_full_sync_hash;
+ dt_iop_gui_leave_critical_section(self);
+
+ if(prv_full_sync_hash != DT_INVALID_HASH
+ && !dt_dev_sync_pixelpipe_hash(self->dev, piece->pipe, self->iop_order, DT_DEV_TRANSFORM_DIR_BACK_INCL,
+ &self->gui_lock, &g->prv_full_sync_hash))
+ {
+ dt_control_log(_("inconsistent output"));
+ }
+ else
+ {
+ dt_iop_gui_enter_critical_section(self);
+ if (g->temp_post_values_valid)
+ {
+ d->post_scale_base = g->temp_post_scale_base;
+ d->post_shift_base = g->temp_post_shift_base;
+ }
+ dt_iop_gui_leave_critical_section(self);
+ }
+ }
+ }
- if(!g->user_param_valid)
+ if (hq)
+ {
+ // Compute the high quality histogram
+ dt_iop_gui_enter_critical_section(self);
+ float min_ev, max_ev;
+ _compute_min_max_ev(luminance, num_elem, &min_ev, &max_ev);
+ hdr_histogram->min_ev = min_ev;
+ hdr_histogram->max_ev = max_ev;
+ _compute_hdr_histogram_and_stats(luminance, num_elem,
+ &g->mask_hq_histo, piece->pipe->type);
+ g->hq_histogram_valid = TRUE;
+ dt_iop_gui_leave_critical_section(self);
+ }
+ else
+ {
+ dt_iop_gui_enter_critical_section(self);
+ g->hq_histogram_valid = FALSE;
+ g->gui_hq_histogram_valid = FALSE;
+ dt_iop_gui_leave_critical_section(self);
+ }
+ }
+ else
{
- float factors[CHANNELS] DT_ALIGNED_ARRAY;
- get_channels_factors(factors, p);
- dt_simd_memcpy(factors, g->temp_user_params, CHANNELS);
- g->user_param_valid = TRUE;
- g->factors_valid = FALSE;
+ // No caching path: compute no matter what
+ // We are in PIXELPIPE_EXPORT or PIXELPIPE_THUMBNAIL
+
+ float image_lum_min, image_lum_max;
+ _compute_luminance_mask(in, luminance, roi_out->width, roi_out->height, d,
+ &image_lum_min, &image_lum_max,
+ piece->pipe->type);
+
}
- if(!g->factors_valid && g->user_param_valid)
+ /**************************************************************************
+ * Display output
+ **************************************************************************/
+ if(self->dev->gui_attached && (piece->pipe->type & DT_DEV_PIXELPIPE_FULL) && g->mask_display)
{
- float factors[CHANNELS] DT_ALIGNED_ARRAY;
- dt_simd_memcpy(g->temp_user_params, factors, CHANNELS);
- valid = pseudo_solve(g->interpolation_matrix, factors, CHANNELS, PIXEL_CHAN, TRUE);
- if(valid) dt_simd_memcpy(factors, g->factors, PIXEL_CHAN);
- else dt_print(DT_DEBUG_PIPE, "tone equalizer pseudo solve problem");
- g->factors_valid = TRUE;
- g->lut_valid = FALSE;
+ _display_luminance_mask(in, luminance, out, roi_in, roi_out,
+ d->post_scale_base, d->post_shift_base,
+ d->post_scale, d->post_shift, d->post_pivot,
+ piece->pipe->type);
+ piece->pipe->mask_display = DT_DEV_PIXELPIPE_DISPLAY_PASSTHRU;
}
-
- if(!g->lut_valid && g->factors_valid)
+ else
{
- compute_lut_correction(g, 0.5f, 4.0f);
- g->lut_valid = TRUE;
+
+
+ _compute_correction_lut(d, piece->pipe->type, NAN, NAN);
+
+ _apply_toneequalizer_linear(in, luminance, out,
+ roi_in,
+ d, piece->pipe->type);
}
- dt_iop_gui_leave_critical_section(self);
+ /**************************************************************************
+ * Cleanup
+ **************************************************************************/
+ if(local_luminance) {
+ dt_free_align(luminance);
+ }
+ if(local_hdr_hist) {
+ dt_free_align(hdr_histogram);
+ }
+}
- return valid;
+void process(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)
+{
+ _toneeq_process(self, piece, ivoid, ovoid, roi_in, roi_out);
}
+/****************************************************************************
+ *
+ * Initialization and Cleanup
+ *
+ ****************************************************************************/
+
void init_global(dt_iop_module_so_t *self)
{
dt_iop_toneequalizer_global_data_t *gd = malloc(sizeof(dt_iop_toneequalizer_global_data_t));
@@ -1570,26 +1928,469 @@ void init_global(dt_iop_module_so_t *self)
self->data = gd;
}
-
void cleanup_global(dt_iop_module_so_t *self)
{
free(self->data);
self->data = NULL;
}
+void init_pipe(dt_iop_module_t *self,
+ dt_dev_pixelpipe_t *pipe,
+ dt_dev_pixelpipe_iop_t *piece)
+{
+ piece->data = dt_calloc1_align_type(dt_iop_toneequalizer_data_t);
+}
+
+void cleanup_pipe(dt_iop_module_t *self,
+ dt_dev_pixelpipe_t *pipe,
+ dt_dev_pixelpipe_iop_t *piece)
+{
+ dt_free_align(piece->data);
+ piece->data = NULL;
+}
+
+void modify_roi_in(dt_iop_module_t *self,
+ dt_dev_pixelpipe_iop_t *piece,
+ const dt_iop_roi_t *roi_out,
+ dt_iop_roi_t *roi_in)
+{
+ // Nothing to do here for now...
+}
+
+
+/***
+ * Setters and Getters for parameters
+ *
+ * Remember the user params split the [-8; 0] EV range in 9 NUM_SLIDERS
+ * and define a set of (x, y) coordinates, where x are the exposure
+ * NUM_SLIDERS (evenly-spaced by 1 EV in [-8; 0] EV) and y are the
+ * desired exposure compensation for each channel.
+ *
+ * This (x, y) set is interpolated by radial-basis function using a
+ * series of 8 gaussians. Losing 1 degree of freedom makes it an
+ * approximation rather than an interpolation but helps reducing a bit
+ * the oscillations and fills a full AVX vector.
+ *
+ * The coefficients/factors used in the interpolation/approximation
+ * are linear, but keep in mind that users params are expressed as
+ * log2 gains, so we always need to do the log2/exp2 flip/flop between
+ * both.
+ *
+ * User params of exposure compensation are expected between [-2 ; +2]
+ * EV for practical UI reasons and probably numerical stability
+ * reasons, but there is no theoretical obstacle to enlarge this
+ * range. The main reason for not allowing it is tone equalizer is
+ * mostly intended to do local changes, and these don't look so well
+ * if you are too harsh on the changes. For heavier tonemapping, it
+ * should be used in combination with a tone curve or filmic.
+ *
+ ***/
+static void _get_slider_values_ev(float *const restrict gauss_factors,
+ const dt_iop_toneequalizer_params_t *const restrict p)
+{
+ assert(NUM_SLIDERS == 9);
+
+ // Get user-set NUM_SLIDERS gains in EV (log2)
+ gauss_factors[0] = p->noise; // -8 EV
+ gauss_factors[1] = p->ultra_deep_blacks; // -7 EV
+ gauss_factors[2] = p->deep_blacks; // -6 EV
+ gauss_factors[3] = p->blacks; // -5 EV
+ gauss_factors[4] = p->shadows; // -4 EV
+ gauss_factors[5] = p->midtones; // -3 EV
+ gauss_factors[6] = p->highlights; // -2 EV
+ gauss_factors[7] = p->whites; // -1 EV
+ gauss_factors[8] = p->speculars; // +0 EV
+}
+
+static void _get_slider_values_linear(float *const restrict gauss_factors,
+ const dt_iop_toneequalizer_params_t *const restrict p)
+{
+ assert(NUM_SLIDERS == 9);
+
+ // Get user-set NUM_SLIDERS gains in EV (log2)
+ _get_slider_values_ev(gauss_factors, p);
+
+ // Convert from EV offsets to linear factors
+ DT_OMP_SIMD(aligned(gauss_factors:64))
+ for(int c = 0; c < NUM_SLIDERS; ++c)
+ gauss_factors[c] = exp2f(gauss_factors[c]);
+}
+
+static inline void _catmull_fill_array(float *const restrict cat_y,
+ const dt_iop_toneequalizer_params_t *const restrict p)
+{
+ assert(NUM_SLIDERS+2 == 11);
+
+ // For positive smoothing, place the shadow points horizontal (y = same as next point).
+ // For negative smoothing continue the slope.
+ // In the smoothing switch zone, interpolate linearly between the two.
+ const float smoothing_switch_zone = 0.1;
+
+ // map -0.1...0.1 to -1...1
+ const float zone_pos = fast_clamp(p->smoothing / smoothing_switch_zone, -1.0f, 1.0f);
+ // map -1...1 to 0...1
+ const float t = (zone_pos + 1.0f) * 0.5f;
+
+ // phantom point
+ cat_y[0] = exp2f(t * p->noise + (1-t) * (2*p->noise - p->ultra_deep_blacks));
+
+ // regular points
+ cat_y[1] = exp2f(p->noise);
+ cat_y[2] = exp2f(p->ultra_deep_blacks);
+ cat_y[3] = exp2f(p->deep_blacks);
+ cat_y[4] = exp2f(p->blacks);
+ cat_y[5] = exp2f(p->shadows);
+ cat_y[6] = exp2f(p->midtones);
+ cat_y[7] = exp2f(p->highlights);
+ cat_y[8] = exp2f(p->whites);
+ cat_y[9] = exp2f(p->speculars);
+
+ // phantom point
+ cat_y[10] = exp2f(t * p->speculars + (1-t) * (2*p->speculars - p->whites));
+}
+
+
+__DT_CLONE_TARGETS__
+DT_OMP_DECLARE_SIMD()
+static inline float _gauss_pixel_correction(const float exposure,
+ const float *const restrict gauss_factors,
+ const float sigma)
+{
+ // build the correction for the current pixel
+ // as the sum of the contribution of each luminance channel
+ float result = 0.0f;
+ const float gauss_denom = _compute_gaussian_denom(sigma);
+ const float expo = fast_clamp(exposure, DT_TONEEQ_MIN_EV, DT_TONEEQ_MAX_EV);
+
+ DT_OMP_SIMD(aligned(centers_base_fns, gauss_factors:64) safelen(NUM_OCTAVES) reduction(+:result))
+ for(int i = 0; i < NUM_OCTAVES; ++i)
+ result += _compute_gaussian_weight(expo - centers_base_fns[i], gauss_denom) * gauss_factors[i];
+
+ return fast_clamp(result, 0.25f, 4.0f);
+}
+
+
+__DT_CLONE_TARGETS__
+static gboolean _gauss_compute_channels_factors(const float *const restrict gauss_factors,
+ float *const restrict out,
+ const float sigma)
+{
+ // Input factors are the weights for the radial-basis curve
+ // approximation of user params Output factors are the gains of the
+ // user parameters NUM_SLIDERS aka the y coordinates of the
+ // approximation for x = { NUM_SLIDERS }
+ assert(NUM_OCTAVES == 8);
+
+ DT_OMP_FOR_SIMD(aligned(gauss_factors, out, centers_sliders:64) firstprivate(centers_sliders))
+ for(int i = 0; i < NUM_SLIDERS; ++i)
+ {
+ // Compute the new NUM_SLIDERS factors; _gauss_pixel_correction clamps the factors, so we don't
+ // need to check for validity here
+ out[i] = _gauss_pixel_correction(centers_sliders[i], gauss_factors, sigma);
+ }
+ return TRUE;
+}
+
+__DT_CLONE_TARGETS__
+static void _compute_channels_gains(const float *const restrict in,
+ float *const restrict out)
+{
+ // Helper function to compute the new gains (log) from the factors (linear)
+ assert(NUM_OCTAVES == 8);
+
+ for(int i = 0; i < NUM_SLIDERS; ++i)
+ out[i] = log2f(in[i]);
+}
+
+static void _commit_channels_gains(const float gauss_factors[NUM_SLIDERS],
+ dt_iop_toneequalizer_params_t *p)
+{
+ p->noise = gauss_factors[0];
+ p->ultra_deep_blacks = gauss_factors[1];
+ p->deep_blacks = gauss_factors[2];
+ p->blacks = gauss_factors[3];
+ p->shadows = gauss_factors[4];
+ p->midtones = gauss_factors[5];
+ p->highlights = gauss_factors[6];
+ p->whites = gauss_factors[7];
+ p->speculars = gauss_factors[8];
+}
+
+
+/****************************************************************************
+ *
+ * Cache invalidation and initialization
+ *
+ ****************************************************************************/
+static void _gui_cache_init(dt_iop_module_t *self)
+{
+ dt_iop_toneequalizer_gui_data_t *g = self->gui_data;
+ if(g == NULL) return;
+
+ dt_iop_gui_enter_critical_section(self);
+ g->full_upstream_hash = DT_INVALID_HASH;
+ g->preview_upstream_hash = DT_INVALID_HASH;
+ g->prv_full_sync_hash = DT_INVALID_HASH;
+
+ _ui_histogram_init(&g->ui_histo);
+ _ui_histogram_init(&g->ui_hq_histo);
+
+ _histogram_stats_init(&g->mask_hdr_histo);
+ _histogram_stats_init(&g->mask_hq_histo);
+
+ g->scale = 1.0f;
+ g->sigma = sqrtf(2.0f);
+ g->mask_display = FALSE;
+ g->image_EV_per_UI_sample = 0.00001; // In case no value is calculated yet, use something small, but not 0
+
+ g->interpolation_valid = FALSE; // TRUE if the gauss_interpolation_matrix is ready
+
+ g->prv_luminance_valid = FALSE; // TRUE if the luminance cache is ready
+ g->full_luminance_valid = FALSE; // TRUE if the luminance cache is ready
+ g->hq_histogram_valid = FALSE; // TRUE if we are in late scaling mode and a HQ histogram was computed
+ g->gui_histogram_valid = FALSE; // TRUE if the histogram cache and stats are ready
+ g->gui_hq_histogram_valid = FALSE; // TRUE if the HQ histogram is ready
+ g->gui_curve_valid = FALSE; // TRUE if the gui_curve_lut is ready
+ g->graph_valid = FALSE; // TRUE if the UI graph view is ready
+ g->user_param_valid = FALSE; // TRUE if users params set in interactive view are in bounds
+ g->gauss_factors_valid = TRUE; // TRUE if radial-basis coeffs are ready
+
+ // Initialize catmull arrays with safe default values
+ for(int i = 0; i < NUM_SLIDERS + 2; i++)
+ g->catmull_nodes_y[i] = 1.0f;
+ for(int i = 0; i < NUM_SLIDERS; i++)
+ g->catmull_tangents[i] = 0.0f;
+
+ g->area_cursor_valid = FALSE; // TRUE if mouse cursor is over the graph area
+ g->area_dragging = FALSE; // TRUE if left-button has been pushed but not released and cursor motion is recorded
+ g->cursor_valid = FALSE; // TRUE if mouse cursor is over the preview image
+ g->has_focus = FALSE; // TRUE if module has focus from GTK
+
+ g->temp_post_scale_base = 0.0f;
+ g->temp_post_shift_base = 0.0f;
+ g->temp_post_values_valid = FALSE;
+ g->post_realign_required = FALSE;
+ g->last_align_button = LAST_ALIGN_NONE; // Last clicked align button
+ g->post_realign_possible_false_alert = FALSE;
+
+ g->histogram_scale_mode = HISTOGRAM_SCALE_LINEAR_IGNORE_BORDER;
+ g->histogram_vrange = 2;
+ g->histogram_show_hq = FALSE; // TRUE = show HQ histogram
+
+ g->preview_buf = NULL;
+ g->preview_buf_width = 0;
+ g->preview_buf_height = 0;
+
+ g->full_buf = NULL;
+ g->full_buf_width = 0;
+ g->full_buf_height = 0;
+ g->full_buf_x = 0;
+ g->full_buf_y = 0;
+
+ g->desc = NULL;
+ g->layout = NULL;
+ g->cr = NULL;
+ g->cst = NULL;
+ g->context = NULL;
+
+ g->pipe_order = 0;
+ dt_iop_gui_leave_critical_section(self);
+}
+
+static void _invalidate_luminance_cache(dt_iop_module_t *const self)
+{
+ // Invalidate the private luminance cache and histogram when
+ // the luminance mask extraction parameters have changed
+ dt_iop_toneequalizer_gui_data_t *const restrict g = self->gui_data;
+
+ dt_iop_gui_enter_critical_section(self);
+ g->prv_luminance_valid = FALSE;
+ g->preview_upstream_hash = DT_INVALID_HASH;
+ g->full_luminance_valid = FALSE;
+ g->full_upstream_hash = DT_INVALID_HASH;
+
+ g->gui_curve_valid = FALSE;
+ g->gui_histogram_valid = FALSE;
+ g->gui_hq_histogram_valid = FALSE;
+ g->ui_histo.max_val = 1;
+ g->ui_histo.max_val_ignore_border_bins = 1;
+ g->ui_hq_histo.max_val = 1;
+ g->ui_hq_histo.max_val_ignore_border_bins = 1;
+ dt_iop_gui_leave_critical_section(self);
+ dt_iop_refresh_all(self);
+}
+
+static void _invalidate_lut_and_histogram(dt_iop_module_t *const self)
+{
+ dt_iop_toneequalizer_gui_data_t *const restrict g = self->gui_data;
+
+ dt_iop_gui_enter_critical_section(self);
+ g->gui_curve_valid = FALSE;
+ g->gui_histogram_valid = FALSE;
+ g->gui_hq_histogram_valid = FALSE;
+ g->ui_histo.max_val = 1;
+ g->ui_histo.max_val_ignore_border_bins = 1;
+ g->ui_hq_histo.max_val = 1;
+ g->ui_hq_histo.max_val_ignore_border_bins = 1;
+ dt_iop_gui_leave_critical_section(self);
+ dt_iop_refresh_all(self);
+}
+
+
+/****************************************************************************
+ *
+ * Curve Interpolation
+ *
+ ****************************************************************************/
+// empty marker function to make navigating the function list easier
+__attribute__((unused)) static void CURVE_INTERPOLATION_MARKER() {}
+
+static void _gauss_build_interpolation_matrix(float A[NUM_SLIDERS * NUM_OCTAVES],
+ const float sigma)
+{
+ // Build the symmetrical definite positive part of the augmented matrix
+ // of the radial-basis interpolation weights
+
+ const float gauss_denom = _compute_gaussian_denom(sigma);
-void commit_params(dt_iop_module_t *self,
- dt_iop_params_t *p1,
- dt_dev_pixelpipe_t *pipe,
+ DT_OMP_SIMD(aligned(A, centers_base_fns, centers_sliders:64) collapse(2))
+ for(int i = 0; i < NUM_SLIDERS; ++i)
+ for(int j = 0; j < NUM_OCTAVES; ++j)
+ A[i * NUM_OCTAVES + j] =
+ _compute_gaussian_weight(centers_sliders[i] - centers_base_fns[j], gauss_denom);
+}
+
+__DT_CLONE_TARGETS__
+static void _compute_gui_curve(dt_iop_toneequalizer_gui_data_t *const restrict g,
+ const dt_iop_toneequalizer_params_t *const restrict p)
+{
+ // Compute the curve of the exposure corrections in EV,
+ // offset and scale it for display in GUI widget graph
+
+ if(g == NULL) return;
+
+ float *const restrict curve = g->gui_curve;
+ const float sigma = g->sigma;
+
+ if (p->curve_type == DT_TONEEQ_CURVE_GAUSS)
+ {
+ const float *const restrict gauss_factors = g->gauss_factors;
+
+ DT_OMP_FOR_SIMD(aligned(curve, gauss_factors:64))
+ for(int k = 0; k < UI_HISTO_SAMPLES; k++)
+ {
+ // build the inset graph curve LUT
+ const float x = (8.0f * (((float)k) / ((float)(UI_HISTO_SAMPLES - 1)))) - 8.0f;
+ curve[k] = log2f(_gauss_pixel_correction(x, gauss_factors, sigma));
+ }
+ }
+ else
+ { // CATMULL ROM
+
+ const float *const restrict catmull_nodes_y = g->catmull_nodes_y;
+ const float *const restrict catmull_tangents = g->catmull_tangents;
+
+ DT_OMP_FOR_SIMD(aligned(curve, catmull_nodes_y, catmull_tangents:64))
+ for(int k = 0; k < UI_HISTO_SAMPLES; k++)
+ {
+ // build the inset graph curve LUT
+ const float x = (8.0f * (((float)k) / ((float)(UI_HISTO_SAMPLES - 1)))) - 8.0f;
+ curve[k] = log2f(fast_clamp(_catmull_rom_val(NUM_SLIDERS, DT_TONEEQ_MIN_EV, x, catmull_nodes_y, catmull_tangents), 0.25f, 4.0f));
+ }
+ }
+}
+
+
+static gboolean _curve_interpolation(dt_iop_module_t *self)
+{
+ dt_iop_toneequalizer_gui_data_t *g = self->gui_data;
+ const dt_iop_toneequalizer_params_t *p = self->params;
+
+ if(g == NULL) return FALSE;
+
+ gboolean valid = TRUE;
+
+ dt_iop_gui_enter_critical_section(self);
+
+ if(p->curve_type == DT_TONEEQ_CURVE_GAUSS)
+ {
+
+ if(!g->interpolation_valid)
+ {
+
+ g->sigma = powf(sqrtf(2.0f), 1.0f + p->smoothing);
+
+ _gauss_build_interpolation_matrix(g->gauss_interpolation_matrix, g->sigma);
+
+ g->interpolation_valid = TRUE;
+ g->gauss_factors_valid = FALSE;
+ }
+
+ if(!g->user_param_valid)
+ {
+ float gauss_factors[NUM_SLIDERS] DT_ALIGNED_ARRAY;
+
+ _get_slider_values_linear(gauss_factors, p);
+ dt_simd_memcpy(gauss_factors, g->temp_user_params, NUM_SLIDERS);
+
+ g->user_param_valid = TRUE;
+ g->gauss_factors_valid = FALSE;
+ }
+
+ if(!g->gauss_factors_valid && g->user_param_valid)
+ {
+ float gauss_factors[NUM_SLIDERS] DT_ALIGNED_ARRAY;
+
+ dt_simd_memcpy(g->temp_user_params, gauss_factors, NUM_SLIDERS);
+ valid = pseudo_solve(g->gauss_interpolation_matrix, gauss_factors, NUM_SLIDERS, NUM_OCTAVES, TRUE);
+ if(valid)
+ dt_simd_memcpy(gauss_factors, g->gauss_factors, NUM_OCTAVES);
+ else
+ dt_print(DT_DEBUG_PIPE, "tone equalizer pseudo solve problem");
+
+ g->gauss_factors_valid = TRUE;
+ g->gui_curve_valid = FALSE;
+ }
+
+ if(!g->gui_curve_valid && g->gauss_factors_valid)
+ {
+ _compute_gui_curve(g, p);
+ g->gui_curve_valid = TRUE;
+ }
+ }
+ else // catmull rom curve
+ {
+ _catmull_fill_array(g->catmull_nodes_y, p);
+ _catmull_rom_tangents(g->catmull_nodes_y, g->catmull_tangents, p->smoothing);
+ _compute_gui_curve(g, p);
+ g->gui_curve_valid = TRUE;
+
+ dt_simd_memcpy(&g->catmull_nodes_y[1], g->temp_user_params, NUM_SLIDERS);
+ g->user_param_valid = TRUE;
+ valid = TRUE;
+ }
+
+ dt_iop_gui_leave_critical_section(self);
+ return valid;
+}
+
+
+/****************************************************************************
+ *
+ * Commit Params
+ *
+ ****************************************************************************/
+void commit_params(dt_iop_module_t *self, dt_iop_params_t *p1, dt_dev_pixelpipe_t *pipe,
dt_dev_pixelpipe_iop_t *piece)
{
+
const dt_iop_toneequalizer_params_t *p = (dt_iop_toneequalizer_params_t *)p1;
dt_iop_toneequalizer_data_t *d = piece->data;
dt_iop_toneequalizer_gui_data_t *g = self->gui_data;
// Trivial params passing
- d->method = p->method;
- d->details = p->details;
+ d->lum_estimator = p->lum_estimator;
+ d->filter = p->filter;
d->iterations = p->iterations;
d->smoothing = p->smoothing;
d->quantization = p->quantization;
@@ -1605,65 +2406,207 @@ void commit_params(dt_iop_module_t *self,
d->contrast_boost = exp2f(p->contrast_boost);
d->exposure_boost = exp2f(p->exposure_boost);
+ // scaling is also stored as log
+ d->post_scale_base = exp2f(p->post_scale_base);
+ d->post_shift_base = p->post_shift_base;
+ d->post_scale = exp2f(p->post_scale);
+ d->post_shift = p->post_shift;
+ d->post_pivot = p->post_pivot;
+
+ d->global_exposure = p->global_exposure;
+ d->scale_curve = p->scale_curve;
+ d->curve_type = p->curve_type;
+
+ // RGB weights for luminance estimator
+ if (p->lum_estimator_normalize)
+ {
+ const float sum = MAX(NORM_MIN, p->lum_estimator_R + p->lum_estimator_G + p->lum_estimator_B);
+ d->lum_estimator_R = p->lum_estimator_R / sum;
+ d->lum_estimator_G = p->lum_estimator_G / sum;
+ d->lum_estimator_B = p->lum_estimator_B / sum;
+ }
+ else
+ {
+ d->lum_estimator_R = p->lum_estimator_R;
+ d->lum_estimator_G = p->lum_estimator_G;
+ d->lum_estimator_B = p->lum_estimator_B;
+ }
+
+ d->auto_align_enabled = p->auto_align_enabled;
+ d->align_shift_only = p->align_shift_only;
+
/*
* Perform a radial-based interpolation using a series gaussian functions
*/
- if(self->dev->gui_attached && g)
+
+ if (self->dev->gui_attached && g)
{
dt_iop_gui_enter_critical_section(self);
- if(g->sigma != p->smoothing)
- g->interpolation_valid = FALSE;
- g->sigma = p->smoothing;
- g->user_param_valid = FALSE; // force updating channels factors
- dt_iop_gui_leave_critical_section(self);
- update_curve_lut(self);
+ float sigma = powf(sqrtf(2.0f), 1.0f + p->smoothing);
- dt_iop_gui_enter_critical_section(self);
- dt_simd_memcpy(g->factors, d->factors, PIXEL_CHAN);
+ if(g->sigma != sigma) g->interpolation_valid = FALSE;
+
+ g->sigma = sigma;
+ g->user_param_valid = FALSE; // force updating NUM_SLIDERS factors // TODO MF: Comment
dt_iop_gui_leave_critical_section(self);
+
+ _curve_interpolation(self);
+
+ if (p->curve_type == DT_TONEEQ_CURVE_GAUSS) {
+ dt_iop_gui_enter_critical_section(self);
+ dt_simd_memcpy(g->gauss_factors, d->gauss_factors, NUM_OCTAVES);
+ dt_iop_gui_leave_critical_section(self);
+ }
+ else
+ {
+ dt_iop_gui_enter_critical_section(self);
+ dt_simd_memcpy(g->catmull_nodes_y, d->catmull_nodes_y, NUM_SLIDERS+2);
+ dt_simd_memcpy(g->catmull_tangents, d->catmull_tangents, NUM_SLIDERS);
+ dt_iop_gui_leave_critical_section(self);
+ }
}
- else
+ else // no GUI
{
- // No cache : Build / Solve interpolation matrix
- float factors[CHANNELS] DT_ALIGNED_ARRAY;
- get_channels_factors(factors, p);
+ if (p->curve_type == DT_TONEEQ_CURVE_GAUSS)
+ {
+ // No cache : Build / Solve interpolation matrix
+ float gauss_factors[NUM_SLIDERS] DT_ALIGNED_ARRAY;
+ _get_slider_values_linear(gauss_factors, p);
- float A[CHANNELS * PIXEL_CHAN] DT_ALIGNED_ARRAY;
- build_interpolation_matrix(A, p->smoothing);
- pseudo_solve(A, factors, CHANNELS, PIXEL_CHAN, FALSE);
+ const float sigma = powf(sqrtf(2.0f), 1.0f + p->smoothing);
- dt_simd_memcpy(factors, d->factors, PIXEL_CHAN);
- }
+ float A[NUM_SLIDERS * NUM_OCTAVES] DT_ALIGNED_ARRAY;
+ _gauss_build_interpolation_matrix(A, sigma);
+ pseudo_solve(A, gauss_factors, NUM_SLIDERS, NUM_OCTAVES, FALSE);
- // compute the correction LUT here to spare some time in process
- // when computing several times toneequalizer with same parameters
- compute_correction_lut(d->correction_lut, d->smoothing, d->factors);
+ dt_simd_memcpy(gauss_factors, d->gauss_factors, NUM_OCTAVES);
+ }
+ else
+ {
+ // TODO MF: changing parameters in front or back?
+ _catmull_fill_array(d->catmull_nodes_y, p);
+ _catmull_rom_tangents(d->catmull_nodes_y, d->catmull_tangents, p->smoothing);
+ }
+ }
}
-void init_pipe(dt_iop_module_t *self,
- dt_dev_pixelpipe_t *pipe,
- dt_dev_pixelpipe_iop_t *piece)
+/****************************************************************************
+ *
+ * GUI Helpers
+ *
+ ****************************************************************************/
+// empty marker function to make navigating the function list easier
+__attribute__((unused)) static void GUI_HELPERS_MARKER() {}
+
+static void _compute_gui_histogram(const dt_iop_toneequalizer_histogram_stats_t *const restrict hdr_histogram,
+ dt_iop_toneequalizer_ui_histogram_t *const restrict ui_histogram,
+ const float post_scale_base_log,
+ const float post_shift_base,
+ const float post_scale_log,
+ const float post_shift,
+ const float post_pivot)
{
- piece->data = dt_calloc1_align_type(dt_iop_toneequalizer_data_t);
-}
+ // (Re)init the histogram
+ memset(ui_histogram->samples, 0, sizeof(int) * UI_HISTO_SAMPLES);
+ const float hdr_ev_range = hdr_histogram->max_ev - hdr_histogram->min_ev;
-void cleanup_pipe(dt_iop_module_t *self,
- dt_dev_pixelpipe_t *pipe,
- dt_dev_pixelpipe_iop_t *piece)
+ // scaling values shown to the user in the UI are logs
+ const float post_scale_base = exp2f(post_scale_base_log);
+ const float post_scale = exp2f(post_scale_log);
+
+ // remap the extended histogram into the gui histogram
+ // bins between [-8; 0] EV remapped between [0 ; UI_HISTO_SAMPLES]
+ // TODO: OpenMP, parallel may be possible if collisions
+ // in ui_histogram->samples[i] are rare.
+ for(size_t k = 0; k < hdr_histogram->num_samples; ++k)
+ {
+ // from [0...num_samples] to [-16...8EV]
+ const float EV = hdr_ev_range * (float)k / (float)(hdr_histogram->num_samples - 1) + hdr_histogram->min_ev;
+
+ // apply shift & scale to the EV value, clamp to [-8...0]
+ const float shift_scaled_EV = fast_clamp(_post_scale_shift(EV, post_scale_base, post_shift_base, post_scale, post_shift, post_pivot), DT_TONEEQ_MIN_EV, DT_TONEEQ_MAX_EV);
+
+ // from [-8...0] EV to [0...UI_HISTO_SAMPLES]
+ const int i = CLAMP((int)((shift_scaled_EV + 8.0f) / 8.0f * (float)(UI_HISTO_SAMPLES - 1)),
+ 0, UI_HISTO_SAMPLES - 1);
+
+ ui_histogram->samples[i] += hdr_histogram->samples[k];
+ }
+
+ // store the max numbers of elements in bins for later normalization
+ // ignore the first and last value to keep the histogram readable
+ int max_val_ignore_border_bins = 1;
+ DT_OMP_SIMD(reduction(max: max_val_ignore_border_bins))
+ for (int i = 1; i < UI_HISTO_SAMPLES - 1; i++)
+ {
+ max_val_ignore_border_bins = MAX(ui_histogram->samples[i], max_val_ignore_border_bins);
+ }
+
+ // Compare the fist and last values too, so we have
+ // the overall maximum available as well
+ ui_histogram->max_val = MAX(ui_histogram->samples[0], max_val_ignore_border_bins);
+ ui_histogram->max_val = MAX(ui_histogram->samples[UI_HISTO_SAMPLES - 1], ui_histogram->max_val);
+ ui_histogram->max_val_ignore_border_bins = max_val_ignore_border_bins;
+}
+
+static inline void _update_gui_histogram(dt_iop_module_t *const self)
{
- dt_free_align(piece->data);
- piece->data = NULL;
+ dt_iop_toneequalizer_gui_data_t *const g = self->gui_data;
+ const dt_iop_toneequalizer_params_t *const p = self->params;
+ if(g == NULL) return;
+
+
+ dt_iop_gui_enter_critical_section(self);
+ // TODO: check for the hdr histogram insteam of prv luminance
+ if(!g->gui_histogram_valid && g->prv_luminance_valid)
+ {
+ _compute_gui_histogram(&g->mask_hdr_histo, &g->ui_histo,
+ p->post_scale_base, p->post_shift_base,
+ p->post_scale, p->post_shift, p->post_pivot);
+
+ // Computation of "image_EV_per_UI_sample"
+ // The graph shows 8EV, but when we align the histogram, we consider 6EV [-7; -1] ("target")
+ const float target_EV_range = 6.0f;
+ const float full_EV_range = 8.0f;
+ const float target_to_full = full_EV_range / target_EV_range;
+
+ // What is the real dynamic range of the histogram-part [-7; -1])? We unscale.
+ const float mask_EV_of_target = target_EV_range / (exp2f(p->post_scale));
+
+ // The histogram shows mask EV, but for evaluating curve steepness, we need image EVs
+ const float mask_to_image = (g->prv_image_ev_max - g->prv_image_ev_min)
+ / (g->mask_hdr_histo.max_ev - g->mask_hdr_histo.min_ev);
+
+ g->image_EV_per_UI_sample = (mask_EV_of_target * mask_to_image * target_to_full) / (float)UI_HISTO_SAMPLES;
+
+ g->gui_histogram_valid = TRUE;
+ }
+ // Also update HQ histogram if available
+ if(g->hq_histogram_valid && !g->gui_hq_histogram_valid)
+ {
+ // HQ histogram needs to be converted to UI histogram too
+ _compute_gui_histogram(&g->mask_hq_histo, &g->ui_hq_histo,
+ p->post_scale_base, p->post_shift_base,
+ p->post_scale, p->post_shift, p->post_pivot);
+ g->gui_hq_histogram_valid = TRUE;
+ }
+ if (!g->hq_histogram_valid)
+ {
+ g->gui_hq_histogram_valid = FALSE;
+ }
+
+ dt_iop_gui_leave_critical_section(self);
}
-static void show_guiding_controls(const dt_iop_module_t *self)
+static void _show_guiding_controls(const dt_iop_module_t *self)
{
const dt_iop_toneequalizer_gui_data_t *g = self->gui_data;
const dt_iop_toneequalizer_params_t *p = self->params;
- switch(p->details)
+ switch(p->filter)
{
case(DT_TONEEQ_NONE):
{
@@ -1697,92 +2640,413 @@ static void show_guiding_controls(const dt_iop_module_t *self)
break;
}
}
+
+ switch (p->lum_estimator)
+ {
+ case (DT_TONEEQ_CUSTOM):
+ gtk_widget_set_visible(g->lum_estimator_R, TRUE);
+ gtk_widget_set_visible(g->lum_estimator_G, TRUE);
+ gtk_widget_set_visible(g->lum_estimator_B, TRUE);
+ gtk_widget_set_visible(g->lum_estimator_normalize, TRUE);
+ break;
+ default:
+ gtk_widget_set_visible(g->lum_estimator_R, FALSE);
+ gtk_widget_set_visible(g->lum_estimator_G, FALSE);
+ gtk_widget_set_visible(g->lum_estimator_B, FALSE);
+ gtk_widget_set_visible(g->lum_estimator_normalize, FALSE);
+ }
+}
+
+
+/****************************************************************************
+ *
+ * GUI Callbacks
+ *
+ ****************************************************************************/
+// empty marker function to make navigating the function list easier
+__attribute__((unused)) static void GUI_CALLBACKS_MARKER() {}
+
+static void _fill_button_tooltip(GtkWidget* btn,
+ const float base_scale,
+ const float base_shift,
+ const dt_iop_module_t *self)
+{
+ const dt_iop_toneequalizer_gui_data_t *g = self->gui_data;
+ const char *tooltip_template;
+ char tooltip[256];
+
+ if(btn == g->align_button)
+ tooltip_template = _("auto-align base scale and shift to image histogram.\n"
+ "ctrl-click to set alignment to 0.\n"
+ "current base shift: %.2f\n"
+ "current base scale: %.2f");
+ else
+ tooltip_template = _("auto-align base shift to image histogram.\n"
+ "ctrl-click to set alignment to 0.\n"
+ "current base shift: %.2f\n"
+ "current base scale: %.2f");
+
+ snprintf(tooltip, sizeof(tooltip), tooltip_template, base_shift, base_scale);
+ gtk_widget_set_tooltip_text(btn, tooltip);
}
-void update_exposure_sliders(const dt_iop_toneequalizer_gui_data_t *g,
- const dt_iop_toneequalizer_params_t *p)
+void gui_update(dt_iop_module_t *self)
{
+ const dt_iop_toneequalizer_gui_data_t *g = self->gui_data;
+ const dt_iop_toneequalizer_params_t *p = self->params;
+
+ // TODO MF: not needed any more?
+ dt_bauhaus_slider_set(g->smoothing, p->smoothing);
+
+ _fill_button_tooltip(g->align_button, p->post_scale_base, p->post_shift_base, self);
+ _fill_button_tooltip(g->shift_button, p->post_scale_base, p->post_shift_base, self);
+
+ // Set button states from params
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->align_button), p->auto_align_enabled && !p->align_shift_only);
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->shift_button), p->auto_align_enabled && p->align_shift_only);
+
+ _show_guiding_controls(self);
+
+ // TODO MF: check if this sufficient or if invalidate_luminance_cache is needed
+ _invalidate_lut_and_histogram(self);
+
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->show_luminance_mask), g->mask_display);
+}
+
+void gui_changed(dt_iop_module_t *self,
+ GtkWidget *w,
+ void *previous)
+{
+ dt_iop_toneequalizer_gui_data_t *g = self->gui_data;
+ dt_iop_toneequalizer_params_t *p = self->params;
+
+ if(w == g->blending
+ || w == g->feathering
+ || w == g->iterations
+ || w == g->quantization
+ || w == g->lum_estimator_R
+ || w == g->lum_estimator_G
+ || w == g->lum_estimator_B
+ || w == g->lum_estimator_normalize)
+ {
+ _invalidate_luminance_cache(self);
+ }
+ else if(w == g->filter
+ || w == g->lum_estimator)
+ {
+ _invalidate_luminance_cache(self);
+ _show_guiding_controls(self);
+ }
+ else if(w == g->contrast_boost
+ || w == g->exposure_boost)
+ {
+ _invalidate_luminance_cache(self);
+ dt_bauhaus_widget_set_quad_active(w, FALSE);
+ }
+ else if (w == g->post_scale
+ || w == g->post_shift
+ || w == g->post_pivot)
+ {
+ _invalidate_lut_and_histogram(self);
+ }
+ else if (w == g->curve_type)
+ {
+ g->interpolation_valid = FALSE;
+ g->gauss_factors_valid = FALSE;
+ g->user_param_valid = FALSE;
+ g->gui_curve_valid = FALSE;
+
+ switch(p->curve_type)
+ {
+ case(DT_TONEEQ_CURVE_GAUSS):
+ {
+ p->smoothing = 0.0f;
+ ++darktable.gui->reset;
+ dt_bauhaus_slider_set(g->smoothing, p->smoothing);
+ --darktable.gui->reset;
+ break;
+ }
+ case(DT_TONEEQ_CURVE_CATMULL):
+ {
+ p->smoothing = 0.5f;
+ ++darktable.gui->reset;
+ dt_bauhaus_slider_set(g->smoothing, p->smoothing);
+ --darktable.gui->reset;
+ break;
+ }
+ }
+ }
+}
+
+static void _smoothing_callback(GtkWidget *slider,
+ dt_iop_module_t *self)
+{
+ if(!self || !self->gui_data) return;
+ if(darktable.gui->reset) return;
+ dt_iop_toneequalizer_params_t *p = self->params;
+ const dt_iop_toneequalizer_gui_data_t *g = self->gui_data;
+
+ p->smoothing = dt_bauhaus_slider_get(slider);
+
+ // Solve the interpolation by least-squares to check the validity of the smoothing param
+ if(!_curve_interpolation(self))
+ dt_control_log
+ (_("the interpolation is unstable, decrease the curve smoothing"));
+
+ // Redraw graph before launching computation
+ // Don't do this again: update_curve_lut(self);
+ gtk_widget_queue_draw(GTK_WIDGET(g->area));
+ dt_dev_add_history_item(darktable.develop, self, TRUE);
+
+ // Unlock the colour picker so we can display our own custom cursor
+ dt_iop_color_picker_reset(self, TRUE);
+}
+
+static gboolean _smoothing_button_press(GtkWidget *slider,
+ GdkEventButton *event,
+ dt_iop_module_t *self)
+{
+ if(!self || !self->gui_data) return FALSE;
+
+ // Double-click: reset to correct default value
+ if (event->type == GDK_2BUTTON_PRESS)
+ {
+ dt_iop_toneequalizer_params_t *p = self->params;
+ switch(p->curve_type)
+ {
+ case(DT_TONEEQ_CURVE_GAUSS):
+ {
+ p->smoothing = 0.0f;
+ ++darktable.gui->reset;
+ dt_bauhaus_slider_set(slider, p->smoothing);
+ --darktable.gui->reset;
+ break;
+ }
+ case(DT_TONEEQ_CURVE_CATMULL):
+ {
+ p->smoothing = 0.5f;
+ ++darktable.gui->reset;
+ dt_bauhaus_slider_set(slider, p->smoothing);
+ --darktable.gui->reset;
+ break;
+ }
+ }
+
+ return TRUE; // Event handled
+ }
+ return FALSE; // Event not handled, propagate further
+}
+
+static gboolean _align_button_clicked(GtkWidget *btn,
+ GdkEventButton *event,
+ dt_iop_module_t *self)
+{
+ dt_iop_toneequalizer_params_t *p = self->params;
+ dt_iop_toneequalizer_gui_data_t *g = self->gui_data;
+
+ if(darktable.gui->reset) return FALSE;
+
+ dt_iop_request_focus(self);
+
+ if(!self->enabled)
+ {
+ // activate module and do nothing
+ ++darktable.gui->reset;
+ dt_bauhaus_slider_set(g->contrast_boost, p->contrast_boost);
+ --darktable.gui->reset;
+
+ _invalidate_luminance_cache(self);
+ dt_dev_add_history_item(darktable.develop, self, TRUE);
+ return TRUE;
+ }
+
+ if(!g->prv_luminance_valid || self->dev->full.pipe->processing || !g->gui_histogram_valid)
+ {
+ dt_control_log(_("wait for the preview to finish recomputing"));
+ return TRUE;
+ }
+
+ if (event->type != GDK_BUTTON_PRESS)
+ {
+ return FALSE;
+ }
+
+ const gboolean clicked_shift_only = (btn == g->shift_button);
+
+ switch(event->button)
+ {
+ case(1): // left click
+ {
+ p->auto_align_enabled = FALSE;
+ p->align_shift_only = clicked_shift_only;
+ break;
+ }
+ case(3): // right click
+ {
+ // Check if we are clicking the button that corresponds to the current mode
+ const gboolean same_button_as_current_mode = (clicked_shift_only == p->align_shift_only);
+
+ if (same_button_as_current_mode)
+ {
+ // If clicking the same button/mode, toggle the enabled state
+ p->auto_align_enabled = !p->auto_align_enabled;
+ }
+ else
+ {
+ // If clicking the *other* button, switch mode and ensure it is enabled
+ p->auto_align_enabled = TRUE;
+ p->align_shift_only = clicked_shift_only;
+ }
+ break;
+ }
+ default:
+ return FALSE;
+ }
+
+ // Set button states from params
++darktable.gui->reset;
- dt_bauhaus_slider_set(g->noise, p->noise);
- dt_bauhaus_slider_set(g->ultra_deep_blacks, p->ultra_deep_blacks);
- dt_bauhaus_slider_set(g->deep_blacks, p->deep_blacks);
- dt_bauhaus_slider_set(g->blacks, p->blacks);
- dt_bauhaus_slider_set(g->shadows, p->shadows);
- dt_bauhaus_slider_set(g->midtones, p->midtones);
- dt_bauhaus_slider_set(g->highlights, p->highlights);
- dt_bauhaus_slider_set(g->whites, p->whites);
- dt_bauhaus_slider_set(g->speculars, p->speculars);
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->align_button), p->auto_align_enabled && !p->align_shift_only);
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->shift_button), p->auto_align_enabled && p->align_shift_only);
--darktable.gui->reset;
-}
+ // Ctrl-leftclick
+ if ((event->state & GDK_CONTROL_MASK) && (event->button == 1))
+ {
+
+ const gboolean alignment_changed = (fabsf(0.0f - p->post_scale_base) > EPSILON)
+ || (fabsf(0.0f - p->post_shift_base) > EPSILON);
+
+ p->post_scale_base = 0.0f;
+ p->post_shift_base = 0.0f;
+
+ dt_iop_gui_enter_critical_section(self);
+ _fill_button_tooltip(g->align_button, p->post_scale_base, p->post_shift_base, self);
+ _fill_button_tooltip(g->shift_button, p->post_scale_base, p->post_shift_base, self);
+ dt_iop_gui_leave_critical_section(self);
+
+ if (alignment_changed)
+ dt_control_log(_("base shift set to %.2f, base scale to %.2f"), p->post_shift_base, p->post_scale_base);
+
+ _invalidate_lut_and_histogram(self);
+
+ dt_dev_add_history_item(darktable.develop, self, TRUE);
+
+ dt_iop_gui_enter_critical_section(self);
+ // Re-align hash workaround
+ g->post_realign_possible_false_alert = TRUE;
+ dt_iop_gui_leave_critical_section(self);
+ return TRUE;
+ }
+
+ const float old_post_scale_base = p->post_scale_base;
+ const float old_post_shift_base = p->post_shift_base;
+ _alignment_calculation(&g->mask_hdr_histo, p->align_shift_only, &p->post_scale_base, &p->post_shift_base);
-void gui_update(dt_iop_module_t *self)
-{
- const dt_iop_toneequalizer_gui_data_t *g = self->gui_data;
- const dt_iop_toneequalizer_params_t *p = self->params;
+ const gboolean alignment_changed = (fabsf(old_post_scale_base - p->post_scale_base) > EPSILON)
+ || (fabsf(old_post_shift_base - p->post_shift_base) > EPSILON);
- dt_bauhaus_slider_set(g->smoothing, logf(p->smoothing) / logf(sqrtf(2.0f)) - 1.0f);
+ if (alignment_changed)
+ dt_control_log(_("base shift set to %.2f, base scale to %.2f"), p->post_shift_base, p->post_scale_base);
- show_guiding_controls(self);
- invalidate_luminance_cache(self);
+ dt_iop_gui_enter_critical_section(self);
+ g->gui_histogram_valid = FALSE;
+ g->post_realign_required = FALSE;
+ g->last_align_button = clicked_shift_only ? LAST_ALIGN_SHIFT : LAST_ALIGN_FULLY;
+ _fill_button_tooltip(g->align_button, p->post_scale_base, p->post_shift_base, self);
+ _fill_button_tooltip(g->shift_button, p->post_scale_base, p->post_shift_base, self);
+ dt_iop_gui_leave_critical_section(self);
- gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->show_luminance_mask), g->mask_display);
+ _invalidate_lut_and_histogram(self);
+
+ dt_dev_add_history_item(darktable.develop, self, TRUE);
+ dt_iop_gui_enter_critical_section(self);
+ // Re-align hash workaround
+ // g->post_realign_possible_false_alert = TRUE;
+ dt_iop_gui_leave_critical_section(self);
+
+ // Unlock the colour picker so we can display our own custom cursor
+ dt_iop_color_picker_reset(self, TRUE);
+ return TRUE;
}
-void gui_changed(dt_iop_module_t *self,
- GtkWidget *w,
- void *previous)
+static void _re_adjust_alignment(GtkWidget *btn,
+ GdkEventButton *event,
+ dt_iop_module_t *self)
{
- const dt_iop_toneequalizer_gui_data_t *g = self->gui_data;
- if(w == g->method
- || w == g->blending
- || w == g->feathering
- || w == g->iterations
- || w == g->quantization)
+ dt_iop_toneequalizer_params_t *p = self->params;
+ dt_iop_toneequalizer_gui_data_t *g = self->gui_data;
+
+ if(darktable.gui->reset) return;
+
+ dt_iop_request_focus(self);
+
+ if(!self->enabled)
{
- invalidate_luminance_cache(self);
+ // activate module and do nothing
+ ++darktable.gui->reset;
+ dt_bauhaus_slider_set(g->contrast_boost, p->contrast_boost);
+ --darktable.gui->reset;
+
+ _invalidate_luminance_cache(self);
+ dt_dev_add_history_item(darktable.develop, self, TRUE);
+ return;
}
- else if(w == g->details)
+
+ if (event->type != GDK_BUTTON_PRESS)
{
- invalidate_luminance_cache(self);
- show_guiding_controls(self);
+ return;
}
- else if(w == g->contrast_boost
- || w == g->exposure_boost)
+
+ ++darktable.gui->reset;
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->histogram_align_button), TRUE); // Keep the button looking active
+
+ // Set button states from params
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->align_button), p->auto_align_enabled && !p->align_shift_only);
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->shift_button), p->auto_align_enabled && p->align_shift_only);
+ --darktable.gui->reset;
+
+ if(!g->prv_luminance_valid || self->dev->full.pipe->processing || !g->gui_histogram_valid)
{
- invalidate_luminance_cache(self);
- dt_bauhaus_widget_set_quad_active(w, FALSE);
+ dt_control_log(_("wait for the preview to finish recomputing"));
+ return;
}
-}
-static void smoothing_callback(GtkWidget *slider, dt_iop_module_t *self)
-{
- if(darktable.gui->reset) return;
- dt_iop_toneequalizer_params_t *p = self->params;
- const dt_iop_toneequalizer_gui_data_t *g = self->gui_data;
+ _alignment_calculation(&g->mask_hdr_histo, (g->last_align_button == LAST_ALIGN_SHIFT), &p->post_scale_base, &p->post_shift_base);
+ dt_control_log(_("base shift set to %.2f, base scale set to %.2f"), p->post_shift_base, p->post_scale_base);
- p->smoothing= powf(sqrtf(2.0f), 1.0f + dt_bauhaus_slider_get(slider));
+ // Hide the align button histogram
+ gtk_widget_hide(g->histogram_align_button);
- float factors[CHANNELS] DT_ALIGNED_ARRAY;
- get_channels_factors(factors, p);
+ dt_iop_gui_enter_critical_section(self);
+ g->post_realign_required = FALSE;
+ g->gui_histogram_valid = FALSE;
+ _fill_button_tooltip(g->align_button, p->post_scale_base, p->post_shift_base, self);
+ _fill_button_tooltip(g->shift_button, p->post_scale_base, p->post_shift_base, self);
+ dt_iop_gui_leave_critical_section(self);
- // Solve the interpolation by least-squares to check the validity of the smoothing param
- if(!update_curve_lut(self))
- dt_control_log
- (_("the interpolation is unstable, decrease the curve smoothing"));
+ _invalidate_lut_and_histogram(self);
- // Redraw graph before launching computation
- // Don't do this again: update_curve_lut(self);
- gtk_widget_queue_draw(GTK_WIDGET(g->area));
dt_dev_add_history_item(darktable.develop, self, TRUE);
+ dt_iop_gui_enter_critical_section(self);
+ // Ugly workaround:
+ // Changing the history changes the upstream hash, even though nothing has really
+ // changed upstream. During the next call of _process, we will not
+ // set post_realign_required to true, so the re-align button does not automagically
+ // re-appear when the user has just aligned.
+ g->post_realign_possible_false_alert = TRUE;
+ dt_iop_gui_leave_critical_section(self);
// Unlock the colour picker so we can display our own custom cursor
dt_iop_color_picker_reset(self, TRUE);
+
}
-static void auto_adjust_exposure_boost(GtkWidget *quad, dt_iop_module_t *self)
+static void _auto_adjust_exposure_boost(GtkWidget *quad,
+ dt_iop_module_t *self)
{
+ if(!self || !self->gui_data) return;
+
dt_iop_toneequalizer_params_t *p = self->params;
dt_iop_toneequalizer_gui_data_t *g = self->gui_data;
@@ -1797,12 +3061,12 @@ static void auto_adjust_exposure_boost(GtkWidget *quad, dt_iop_module_t *self)
dt_bauhaus_slider_set(g->exposure_boost, p->exposure_boost);
--darktable.gui->reset;
- invalidate_luminance_cache(self);
+ _invalidate_luminance_cache(self);
dt_dev_add_history_item(darktable.develop, self, TRUE);
return;
}
- if(!g->luminance_valid || self->dev->full.pipe->processing || !g->histogram_valid)
+ if(!g->prv_luminance_valid || self->dev->full.pipe->processing || !g->gui_histogram_valid)
{
dt_control_log(_("wait for the preview to finish recomputing"));
return;
@@ -1812,16 +3076,15 @@ static void auto_adjust_exposure_boost(GtkWidget *quad, dt_iop_module_t *self)
// to spread it over as many nodes as possible for better exposure control.
// Controls nodes are between -8 and 0 EV,
// so we aim at centering the exposure distribution on -4 EV
-
dt_iop_gui_enter_critical_section(self);
- g->histogram_valid = FALSE;
+ g->gui_histogram_valid = FALSE;
dt_iop_gui_leave_critical_section(self);
- update_histogram(self);
+ _update_gui_histogram(self);
// calculate exposure correction
- const float fd_new = exp2f(g->histogram_first_decile);
- const float ld_new = exp2f(g->histogram_last_decile);
+ const float fd_new = exp2f(g->mask_hdr_histo.lo_percentile_ev);
+ const float ld_new = exp2f(g->mask_hdr_histo.hi_percentile_ev);
const float e = exp2f(p->exposure_boost);
const float c = exp2f(p->contrast_boost);
// revert current transformation
@@ -1839,128 +3102,419 @@ static void auto_adjust_exposure_boost(GtkWidget *quad, dt_iop_module_t *self)
++darktable.gui->reset;
dt_bauhaus_slider_set(g->exposure_boost, p->exposure_boost);
--darktable.gui->reset;
- invalidate_luminance_cache(self);
+ _invalidate_luminance_cache(self);
+ dt_dev_add_history_item(darktable.develop, self, TRUE);
+
+ // Unlock the colour picker so we can display our own custom cursor
+ dt_iop_color_picker_reset(self, TRUE);
+}
+
+static void _auto_adjust_contrast_boost(GtkWidget *quad,
+ dt_iop_module_t *self)
+{
+ if(!self || !self->gui_data) return;
+
+ dt_iop_toneequalizer_params_t *p = self->params;
+ dt_iop_toneequalizer_gui_data_t *g = self->gui_data;
+
+ if(darktable.gui->reset) return;
+
+ dt_iop_request_focus(self);
+
+ if(!self->enabled)
+ {
+ // activate module and do nothing
+ ++darktable.gui->reset;
+ dt_bauhaus_slider_set(g->contrast_boost, p->contrast_boost);
+ --darktable.gui->reset;
+
+ _invalidate_luminance_cache(self);
+ dt_dev_add_history_item(darktable.develop, self, TRUE);
+ return;
+ }
+
+ if(!g->prv_luminance_valid || self->dev->full.pipe->processing || !g->gui_histogram_valid)
+ {
+ dt_control_log(_("wait for the preview to finish recomputing"));
+ return;
+ }
+
+ // The goal is to spread 90 % of the exposure histogram in the [-7, -1] EV
+ dt_iop_gui_enter_critical_section(self);
+ g->gui_histogram_valid = FALSE;
+ dt_iop_gui_leave_critical_section(self);
+
+ _update_gui_histogram(self);
+
+ // calculate contrast correction
+ const float fd_new = exp2f(g->mask_hdr_histo.lo_percentile_ev);
+ const float ld_new = exp2f(g->mask_hdr_histo.hi_percentile_ev);
+ const float e = exp2f(p->exposure_boost);
+ float c = exp2f(p->contrast_boost);
+ // revert current transformation
+ const float fd_old = ((fd_new - CONTRAST_FULCRUM) / c + CONTRAST_FULCRUM) / e;
+ const float ld_old = ((ld_new - CONTRAST_FULCRUM) / c + CONTRAST_FULCRUM) / e;
+
+ // calculate correction
+ const float s1 = CONTRAST_FULCRUM - exp2f(-7.0);
+ const float s2 = exp2f(-1.0) - CONTRAST_FULCRUM;
+ const float mix = fd_old * s2 + ld_old * s1;
+
+ c = log2f(mix / (CONTRAST_FULCRUM * (ld_old - fd_old)) / c);
+
+ // when adding contrast, blur filters modify the histogram in a way
+ // difficult to predict here we implement a heuristic correction
+ // based on a set of images and regression analysis
+ if(p->filter == DT_TONEEQ_EIGF && c > 0.0f)
+ {
+ const float correction = -0.0276f + 0.01823 * p->feathering + (0.7566f - 1.0f) * c;
+ if(p->feathering < 5.0f)
+ c += correction;
+ else if(p->feathering < 10.0f)
+ c += correction * (2.0f - p->feathering / 5.0f);
+ }
+ else if(p->filter == DT_TONEEQ_GUIDED && c > 0.0f)
+ c = 0.0235f + 1.1225f * c;
+
+ p->contrast_boost += c;
+
+ // Update the GUI stuff
+ ++darktable.gui->reset;
+ dt_bauhaus_slider_set(g->contrast_boost, p->contrast_boost);
+ --darktable.gui->reset;
+ _invalidate_luminance_cache(self);
dt_dev_add_history_item(darktable.develop, self, TRUE);
// Unlock the colour picker so we can display our own custom cursor
dt_iop_color_picker_reset(self, TRUE);
}
+static void _show_luminance_mask_callback(GtkWidget *togglebutton,
+ GdkEventButton *event,
+ dt_iop_module_t *self)
+{
+ if(!self || !self->gui_data) return;
+ if(darktable.gui->reset) return;
+ dt_iop_request_focus(self);
+
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(self->off), TRUE);
+
+ dt_iop_toneequalizer_gui_data_t *g = self->gui_data;
+
+ // if blend module is displaying mask do not display it here
+ if(self->request_mask_display)
+ {
+ dt_control_log(_("cannot display masks when the blending mask is displayed"));
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->show_luminance_mask), FALSE);
+ g->mask_display = FALSE;
+ return;
+ }
+ else
+ g->mask_display = !g->mask_display;
+
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->show_luminance_mask), g->mask_display);
+// dt_dev_reprocess_center(self->dev);
+ dt_iop_refresh_center(self);
+
+ // Unlock the colour picker so we can display our own custom cursor
+ dt_iop_color_picker_reset(self, TRUE);
+}
+
+static void _histogram_mode_cycle(GtkWidget *button, dt_iop_toneequalizer_gui_data_t *g) {
+ if(!g)
+ {
+ return;
+ }
+
+ // Determine which button was clicked and cycle to next
+ ++darktable.gui->reset;
+ if(button == g->histogram_mode_button_linear)
+ {
+ g->histogram_scale_mode = HISTOGRAM_SCALE_LINEAR_IGNORE_BORDER;
+ gtk_widget_hide(g->histogram_mode_button_linear);
+ gtk_widget_show(g->histogram_mode_button_ignore);
+ gtk_widget_hide(g->histogram_mode_button_log);
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->histogram_mode_button_ignore), TRUE);
+ }
+ else if(button == g->histogram_mode_button_ignore)
+ {
+ g->histogram_scale_mode = HISTOGRAM_SCALE_LOG;
+ gtk_widget_hide(g->histogram_mode_button_linear);
+ gtk_widget_hide(g->histogram_mode_button_ignore);
+ gtk_widget_show(g->histogram_mode_button_log);
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->histogram_mode_button_log), TRUE);
+ }
+ else if(button == g->histogram_mode_button_log)
+ {
+ g->histogram_scale_mode = HISTOGRAM_SCALE_LINEAR;
+ gtk_widget_show(g->histogram_mode_button_linear);
+ gtk_widget_hide(g->histogram_mode_button_ignore);
+ gtk_widget_hide(g->histogram_mode_button_log);
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->histogram_mode_button_linear), TRUE);
+ }
+ --darktable.gui->reset;
+}
+
+static void _histogram_mode_clicked(GtkWidget *button, GdkEventButton *event, const dt_iop_module_t *self)
+{
+ if(!self || !self->gui_data) return;
+ if(darktable.gui->reset) return;
+
+ dt_iop_toneequalizer_gui_data_t *g = (dt_iop_toneequalizer_gui_data_t *)self->gui_data;
+
+ _histogram_mode_cycle(button, g);
+
+ gtk_widget_queue_draw(GTK_WIDGET(g->area));
+}
+
+static void _histogram_range_clicked(GtkWidget *button, GdkEventButton *event, const dt_iop_module_t *self)
+{
+ if(!self || !self->gui_data) return;
+ if(darktable.gui->reset) return;
+
+ dt_iop_toneequalizer_gui_data_t *g = self->gui_data;
+
+ // Toggle between 2 and 4
+ g->histogram_vrange = (g->histogram_vrange == 2) ? 4 : 2;
+
+ // Update button label
+ char label[8];
+ snprintf(label, sizeof(label), "%d", g->histogram_vrange);
+ gtk_button_set_label(GTK_BUTTON(button), label);
+
+ // Redraw the graph
+ g->graph_valid = FALSE;
+ gtk_widget_queue_draw(GTK_WIDGET(g->area));
+}
+
+static gboolean _histogram_hq_clicked(GtkWidget *button, GdkEventButton *event, const dt_iop_module_t *self)
+{
+ if(!self || !self->gui_data) return FALSE;
+ if(darktable.gui->reset) return FALSE;
+ if(event->type != GDK_BUTTON_PRESS) return FALSE;
+
+ dt_iop_toneequalizer_gui_data_t *g = self->gui_data;
+
+ // Toggle HQ display
+ g->histogram_show_hq = !g->histogram_show_hq;
+
+ // Update button label
+ gtk_button_set_label(GTK_BUTTON(button), g->histogram_show_hq ? "HQ" : "LQ");
+
+ // Redraw graph
+ gtk_widget_queue_draw(GTK_WIDGET(g->area));
+
+ return TRUE;
+}
+
+static gboolean _histogram_eventbox_enter(GtkWidget *widget, GdkEventCrossing *event, const dt_iop_module_t *self)
+{
+ const dt_iop_toneequalizer_gui_data_t *g = self->gui_data;
+ gtk_widget_show(g->button_box);
+
+ // Show the correct scale button based on g->histogram_scale_mode
+ switch(g->histogram_scale_mode)
+ {
+ case HISTOGRAM_SCALE_LINEAR:
+ gtk_widget_show(g->histogram_mode_button_linear);
+ break;
+ case HISTOGRAM_SCALE_LINEAR_IGNORE_BORDER:
+ gtk_widget_show(g->histogram_mode_button_ignore);
+ break;
+ case HISTOGRAM_SCALE_LOG:
+ gtk_widget_show(g->histogram_mode_button_log);
+ break;
+ }
+
+ gtk_widget_show(g->histogram_range_button);
+
+ // Only show HQ button if in late scaling mode
+ const gboolean hq = darktable.develop->late_scaling.enabled;
+ if(hq)
+ {
+ gtk_widget_show(g->histogram_hq_button);
+ // Update button label based on current state
+ gtk_button_set_label(GTK_BUTTON(g->histogram_hq_button),
+ g->histogram_show_hq ? "HQ" : "LQ");
+ }
+
+ // Show align button if re-alignment is needed
+ if (g->post_realign_required && g->last_align_button != LAST_ALIGN_NONE)
+ gtk_widget_show(g->histogram_align_button);
+
+ return FALSE;
+}
+static gboolean _histogram_eventbox_leave(GtkWidget *widget, GdkEventCrossing *event, const dt_iop_module_t *self)
+{
+ if(!(event->mode == GDK_CROSSING_UNGRAB && event->detail == GDK_NOTIFY_INFERIOR))
+ {
+ if(!self || !self->gui_data) return FALSE;
+ const dt_iop_toneequalizer_gui_data_t *g = self->gui_data;
+ gtk_widget_hide(g->button_box);
+ gtk_widget_hide(g->histogram_mode_button_linear);
+ gtk_widget_hide(g->histogram_mode_button_ignore);
+ gtk_widget_hide(g->histogram_mode_button_log);
+ gtk_widget_hide(g->histogram_range_button);
+ gtk_widget_hide(g->histogram_hq_button);
+ gtk_widget_hide(g->histogram_align_button);
+ }
+ return FALSE;
+}
+
+static gboolean _notebook_button_press(GtkWidget *widget,
+ GdkEventButton *event,
+ dt_iop_module_t *self)
+{
+ if(!self) return FALSE;
+ if(darktable.gui->reset) return TRUE;
+
+ // Give focus to module
+ dt_iop_request_focus(self);
+
+ // Unlock the colour picker so we can display our own custom cursor
+ dt_iop_color_picker_reset(self, TRUE);
+
+ return FALSE;
+}
+
+GSList *mouse_actions(dt_iop_module_t *self)
+{
+ GSList *lm = NULL;
+ lm = dt_mouse_action_create_format
+ (lm, DT_MOUSE_ACTION_SCROLL, 0,
+ _("[%s over image] change tone exposure"), self->name());
+ lm = dt_mouse_action_create_format
+ (lm, DT_MOUSE_ACTION_SCROLL, GDK_SHIFT_MASK,
+ _("[%s over image] change tone exposure in large steps"), self->name());
+ lm = dt_mouse_action_create_format
+ (lm, DT_MOUSE_ACTION_SCROLL, GDK_CONTROL_MASK,
+ _("[%s over image] change tone exposure in small steps"), self->name());
+ return lm;
+}
+
-static void auto_adjust_contrast_boost(GtkWidget *quad, dt_iop_module_t *self)
+/****************************************************************************
+ *
+ * GUI Interactivity
+ *
+ ****************************************************************************/
+static void _get_point(const dt_iop_module_t *self, const int c_x, const int c_y, int *x, int *y)
{
- dt_iop_toneequalizer_params_t *p = self->params;
- dt_iop_toneequalizer_gui_data_t *g = self->gui_data;
-
- if(darktable.gui->reset) return;
-
- dt_iop_request_focus(self);
-
- if(!self->enabled)
- {
- // activate module and do nothing
- ++darktable.gui->reset;
- dt_bauhaus_slider_set(g->contrast_boost, p->contrast_boost);
- --darktable.gui->reset;
-
- invalidate_luminance_cache(self);
- dt_dev_add_history_item(darktable.develop, self, TRUE);
- return;
- }
+ // TODO: For this to fully work non depending on the place of the module
+ // in the pipe we need a dt_dev_distort_backtransform_plus that
+ // can skip crop only. With the current version if toneequalizer
+ // is moved below rotation & perspective it will fail as we are
+ // then missing all the transform after tone-eq.
+ const double crop_order = dt_ioppr_get_iop_order(self->dev->iop_order_list, "crop", 0);
- if(!g->luminance_valid || self->dev->full.pipe->processing || !g->histogram_valid)
- {
- dt_control_log(_("wait for the preview to finish recomputing"));
- return;
- }
+ float pts[2] = { c_x, c_y };
- // The goal is to spread 90 % of the exposure histogram in the [-7, -1] EV
- dt_iop_gui_enter_critical_section(self);
- g->histogram_valid = FALSE;
- dt_iop_gui_leave_critical_section(self);
+ // only a forward backtransform as the buffer already contains all the transforms
+ // done before toneequal and we are speaking of on-screen cursor coordinates.
+ // also we do transform only after crop as crop does change roi for the whole pipe
+ // and so it is already part of the preview buffer cached in this implementation.
+ dt_dev_distort_backtransform_plus(darktable.develop, darktable.develop->preview_pipe, crop_order,
+ DT_DEV_TRANSFORM_DIR_FORW_EXCL, pts, 1);
+ *x = pts[0];
+ *y = pts[1];
+}
- update_histogram(self);
+static float _get_luminance_from_buffer(const float *const buffer,
+ const size_t width,
+ const size_t height,
+ const size_t x,
+ const size_t y)
+{
+ // Note: This code looks buggy, but it is probably just confusingly optimized.
+ // for_each_channel is used, but the input is greyscale, so it actually
+ // loops in the y direction.
- // calculate contrast correction
- const float fd_new = exp2f(g->histogram_first_decile);
- const float ld_new = exp2f(g->histogram_last_decile);
- const float e = exp2f(p->exposure_boost);
- float c = exp2f(p->contrast_boost);
- // revert current transformation
- const float fd_old = ((fd_new - CONTRAST_FULCRUM) / c + CONTRAST_FULCRUM) / e;
- const float ld_old = ((ld_new - CONTRAST_FULCRUM) / c + CONTRAST_FULCRUM) / e;
+ // Get the weighted average luminance of the 3×3 pixels region centered in (x, y)
+ // x and y are ratios in [0, 1] of the width and height
- // calculate correction
- const float s1 = CONTRAST_FULCRUM - exp2f(-7.0);
- const float s2 = exp2f(-1.0) - CONTRAST_FULCRUM;
- const float mix = fd_old * s2 + ld_old * s1;
+ if(y >= height || x >= width) return NAN;
- c = log2f(mix / (CONTRAST_FULCRUM * (ld_old - fd_old)) / c);
+ const size_t y_abs[4] DT_ALIGNED_PIXEL =
+ { MAX(y, 1) - 1, // previous line
+ y, // center line
+ MIN(y + 1, height - 1), // next line
+ y }; // padding for vectorization
- // when adding contrast, blur filters modify the histogram in a way
- // difficult to predict here we implement a heuristic correction
- // based on a set of images and regression analysis
- if(p->details == DT_TONEEQ_EIGF && c > 0.0f)
+ float luminance = 0.0f;
+ if(x > 1 && x < width - 2)
{
- const float correction = -0.0276f + 0.01823 * p->feathering + (0.7566f - 1.0f) * c;
- if(p->feathering < 5.0f)
- c += correction;
- else if(p->feathering < 10.0f)
- c += correction * (2.0f - p->feathering / 5.0f);
+ // no clamping needed on x, which allows us to vectorize
+ // apply the convolution
+ for(int i = 0; i < 3; ++i)
+ {
+ const size_t y_i = y_abs[i];
+ for_each_channel(j)
+ luminance += buffer[width * y_i + x-1 + j] * gauss_kernel[i][j];
+ }
+ return luminance;
}
- else if(p->details == DT_TONEEQ_GUIDED && c > 0.0f)
- c = 0.0235f + 1.1225f * c;
-
- p->contrast_boost += c;
- // Update the GUI stuff
- ++darktable.gui->reset;
- dt_bauhaus_slider_set(g->contrast_boost, p->contrast_boost);
- --darktable.gui->reset;
- invalidate_luminance_cache(self);
- dt_dev_add_history_item(darktable.develop, self, TRUE);
+ const size_t x_abs[4] DT_ALIGNED_PIXEL =
+ { MAX(x, 1) - 1, // previous column
+ x, // center column
+ MIN(x + 1, width - 1), // next column
+ x }; // padding for vectorization
- // Unlock the colour picker so we can display our own custom cursor
- dt_iop_color_picker_reset(self, TRUE);
+ // convolution
+ for(int i = 0; i < 3; ++i)
+ {
+ const size_t y_i = y_abs[i];
+ for_each_channel(j)
+ luminance += buffer[width * y_i + x_abs[j]] * gauss_kernel[i][j];
+ }
+ return luminance;
}
-
-static void show_luminance_mask_callback(GtkWidget *togglebutton,
- GdkEventButton *event,
- dt_iop_module_t *self)
+// unify with _get_luminance_from_buffer
+static float _luminance_from_thumb_preview_buf(const dt_iop_module_t *self)
{
- if(darktable.gui->reset) return;
- dt_iop_request_focus(self);
-
- gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(self->off), TRUE);
+ const dt_iop_toneequalizer_gui_data_t *g = self->gui_data;
- dt_iop_toneequalizer_gui_data_t *g = self->gui_data;
+ const size_t c_x = g->cursor_pos_x;
+ const size_t c_y = g->cursor_pos_y;
- // if blend module is displaying mask do not display it here
- if(self->request_mask_display)
- {
- dt_control_log(_("cannot display masks when the blending mask is displayed"));
- gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->show_luminance_mask), FALSE);
- g->mask_display = FALSE;
- return;
- }
- else
- g->mask_display = !g->mask_display;
+ // get buffer x,y given the cursor position
+ int b_x = 0;
+ int b_y = 0;
- gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->show_luminance_mask), g->mask_display);
-// dt_dev_reprocess_center(self->dev);
- dt_iop_refresh_center(self);
+ _get_point(self, c_x, c_y, &b_x, &b_y);
- // Unlock the colour picker so we can display our own custom cursor
- dt_iop_color_picker_reset(self, TRUE);
+ return _get_luminance_from_buffer(g->preview_buf,
+ g->preview_buf_width,
+ g->preview_buf_height,
+ b_x,
+ b_y);
}
+static void _update_exposure_sliders(const dt_iop_toneequalizer_gui_data_t *g,
+ const dt_iop_toneequalizer_params_t *p)
+{
+ // Params to GUI
+ ++darktable.gui->reset;
+ dt_bauhaus_slider_set(g->noise, p->noise);
+ dt_bauhaus_slider_set(g->ultra_deep_blacks, p->ultra_deep_blacks);
+ dt_bauhaus_slider_set(g->deep_blacks, p->deep_blacks);
+ dt_bauhaus_slider_set(g->blacks, p->blacks);
+ dt_bauhaus_slider_set(g->shadows, p->shadows);
+ dt_bauhaus_slider_set(g->midtones, p->midtones);
+ dt_bauhaus_slider_set(g->highlights, p->highlights);
+ dt_bauhaus_slider_set(g->whites, p->whites);
+ dt_bauhaus_slider_set(g->speculars, p->speculars);
+ --darktable.gui->reset;
+}
-/***
- * GUI Interactivity
- **/
+static gboolean _in_mask_editing(const dt_iop_module_t *self)
+{
+ const dt_develop_t *dev = self->dev;
+ return dev->form_gui && dev->form_visible;
+}
-static void switch_cursors(dt_iop_module_t *self)
+static void _switch_cursors(dt_iop_module_t *self)
{
dt_iop_toneequalizer_gui_data_t *g = self->gui_data;
@@ -1970,7 +3524,7 @@ static void switch_cursors(dt_iop_module_t *self)
GtkWidget *widget = dt_ui_main_window(darktable.gui->ui);
// if we are editing masks or using colour-pickers, do not display controls
- if(in_mask_editing(self)
+ if(_in_mask_editing(self)
|| dt_iop_canvas_not_sensitive(self->dev))
{
// display default cursor
@@ -2051,6 +3605,7 @@ int mouse_moved(dt_iop_module_t *self,
const dt_develop_t *dev = self->dev;
dt_iop_toneequalizer_gui_data_t *g = self->gui_data;
+ const dt_iop_toneequalizer_params_t *const p = self->params;
if(g == NULL) return 0;
@@ -2078,15 +3633,16 @@ int mouse_moved(dt_iop_module_t *self,
dt_iop_gui_leave_critical_section(self);
// store the actual exposure too, to spare I/O op
- if(g->cursor_valid && !dev->full.pipe->processing && g->luminance_valid)
- g->cursor_exposure = log2f(_luminance_from_module_buffer(self));
+ if(g->cursor_valid && !dev->full.pipe->processing && g->prv_luminance_valid) {
+ const float lum = log2f(_luminance_from_thumb_preview_buf(self));
+ g->cursor_exposure = fast_clamp(_post_scale_shift(lum, exp2f(p->post_scale_base), p->post_shift_base, exp2f(p->post_scale), p->post_shift, p->post_pivot), DT_TONEEQ_MIN_EV, DT_TONEEQ_MAX_EV);
+ }
- switch_cursors(self);
+ _switch_cursors(self);
return 1;
}
-
int mouse_leave(dt_iop_module_t *self)
{
dt_iop_toneequalizer_gui_data_t *g = self->gui_data;
@@ -2109,66 +3665,85 @@ int mouse_leave(dt_iop_module_t *self)
return 1;
}
-
-static inline gboolean 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)
+static gboolean _set_new_params_interactive(const float control_exposure,
+ const float exposure_offset,
+ const float sigma,
+ dt_iop_toneequalizer_gui_data_t *g,
+ dt_iop_toneequalizer_params_t *p)
{
// Apply an exposure offset optimized smoothly over all the exposure channels,
// taking user instruction to apply exposure_offset EV at control_exposure EV,
- // and commit the new params is the solution is valid.
+ // and commit the new params if the solution is valid.
// Raise the user params accordingly to control correction and
// distance from cursor exposure to blend smoothly the desired
// correction
- const float std = gaussian_denom(blending_sigma);
+
+ float std;
+ if (p->curve_type == DT_TONEEQ_CURVE_GAUSS)
+ std = _compute_gaussian_denom(sigma * sigma / 2.0f);
+ else
+ // always small region for catmull
+ std = 1.0f;
+
if(g->user_param_valid)
{
- for(int i = 0; i < CHANNELS; ++i)
+ for(int i = 0; i < NUM_SLIDERS; ++i)
g->temp_user_params[i] *=
- exp2f(gaussian_func(centers_params[i] - control_exposure, std) * exposure_offset);
+ exp2f(_compute_gaussian_weight(centers_sliders[i] - control_exposure, std) * exposure_offset);
}
- // Get the new weights for the radial-basis approximation
- float factors[CHANNELS] DT_ALIGNED_ARRAY;
- dt_simd_memcpy(g->temp_user_params, factors, CHANNELS);
- if(g->user_param_valid)
- g->user_param_valid = pseudo_solve(g->interpolation_matrix, factors, CHANNELS, PIXEL_CHAN, TRUE);
- if(!g->user_param_valid)
- dt_control_log(_("the interpolation is unstable, decrease the curve smoothing"));
-
- // Compute new user params for channels and store them locally
- if(g->user_param_valid)
- g->user_param_valid = compute_channels_factors(factors, g->temp_user_params, g->sigma);
- if(!g->user_param_valid) dt_control_log(_("some parameters are out-of-bounds"));
-
- const gboolean commit = g->user_param_valid;
-
- if(commit)
+ if (p->curve_type == DT_TONEEQ_CURVE_GAUSS)
{
- // Accept the solution
- dt_simd_memcpy(factors, g->factors, PIXEL_CHAN);
- g->lut_valid = FALSE;
+ // Get the new weights for the radial-basis approximation
+ float gauss_factors[NUM_SLIDERS] DT_ALIGNED_ARRAY;
+ dt_simd_memcpy(g->temp_user_params, gauss_factors, NUM_SLIDERS);
+ if(g->user_param_valid) // TODO MF: What is this fi for?
+ g->user_param_valid = pseudo_solve(g->gauss_interpolation_matrix, gauss_factors, NUM_SLIDERS, NUM_OCTAVES, TRUE);
+ if(!g->user_param_valid)
+ dt_control_log(_("the interpolation is unstable, decrease the curve smoothing"));
+
+ // Compute new user params for NUM_SLIDERS and store them locally
+ if(g->user_param_valid)
+ g->user_param_valid = _gauss_compute_channels_factors(gauss_factors, g->temp_user_params, g->sigma);
+ if(!g->user_param_valid) dt_control_log(_("some parameters are out-of-bounds"));
+
+ const gboolean commit = g->user_param_valid;
+
+ if(commit)
+ {
+ // Accept the solution
+ dt_simd_memcpy(gauss_factors, g->gauss_factors, NUM_OCTAVES);
+ g->gui_curve_valid = FALSE;
+
+ // Convert the linear temp parameters to log gains and commit
+ float gains[NUM_SLIDERS] DT_ALIGNED_ARRAY;
+ _compute_channels_gains(g->temp_user_params, gains);
+ _commit_channels_gains(gains, p);
+ }
+ else
+ {
+ // Reset the GUI copy of user params
+ _get_slider_values_linear(gauss_factors, p);
+ dt_simd_memcpy(gauss_factors, g->temp_user_params, NUM_SLIDERS);
+ g->user_param_valid = TRUE;
+ }
- // Convert the linear temp parameters to log gains and commit
- float gains[CHANNELS] DT_ALIGNED_ARRAY;
- compute_channels_gains(g->temp_user_params, gains);
- commit_channels_gains(gains, p);
+ return commit;
}
- else
+
+ // CATMULL
+ float gains[NUM_SLIDERS] DT_ALIGNED_ARRAY;
+ _compute_channels_gains(g->temp_user_params, gains);
+ // Check if any gain is out of bounds [-2, +2] EV
+ for(int i = 0; i < NUM_SLIDERS; ++i)
{
- // Reset the GUI copy of user params
- get_channels_factors(factors, p);
- dt_simd_memcpy(factors, g->temp_user_params, CHANNELS);
- g->user_param_valid = TRUE;
+ gains[i] = fast_clamp(gains[i], -2.0f, 2.0f);
}
-
- return commit;
+ _commit_channels_gains(gains, p);
+ return TRUE;
}
-
int scrolled(dt_iop_module_t *self,
const float x,
const float y,
@@ -2187,14 +3762,17 @@ int scrolled(dt_iop_module_t *self,
if(!self->enabled)
if(self->off) gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(self->off), 1);
- if(in_mask_editing(self)) return 0;
+ if(_in_mask_editing(self)) return 0;
+
+ // ALT/Option should work for zooming
+ if (dt_modifier_is(state, GDK_MOD1_MASK)) return 0;
// if GUI buffers not ready, exit but still handle the cursor
dt_iop_gui_enter_critical_section(self);
const gboolean fail = !g->cursor_valid
- || !g->luminance_valid
- || !g->interpolation_valid
+ || !g->prv_luminance_valid
+ || (p->curve_type == DT_TONEEQ_CURVE_GAUSS && !g->interpolation_valid)
|| !g->user_param_valid
|| dev->full.pipe->processing
|| !g->has_focus;
@@ -2204,7 +3782,9 @@ int scrolled(dt_iop_module_t *self,
// re-read the exposure in case it has changed
dt_iop_gui_enter_critical_section(self);
- g->cursor_exposure = log2f(_luminance_from_module_buffer(self));
+
+ const float lum = log2f(_luminance_from_thumb_preview_buf(self));
+ g->cursor_exposure = fast_clamp(_post_scale_shift(lum, exp2f(p->post_scale_base), p->post_shift_base, exp2f(p->post_scale), p->post_shift, p->post_pivot), DT_TONEEQ_MIN_EV, DT_TONEEQ_MAX_EV);
dt_iop_gui_leave_critical_section(self);
@@ -2221,10 +3801,10 @@ int scrolled(dt_iop_module_t *self,
const float offset = step * ((float)increment);
- // Get the desired correction on exposure channels
+ // Get the desired correction on exposure NUM_SLIDERS
dt_iop_gui_enter_critical_section(self);
- const gboolean commit = set_new_params_interactive(g->cursor_exposure, offset,
- g->sigma * g->sigma / 2.0f, g, p);
+ const gboolean commit = _set_new_params_interactive(g->cursor_exposure, offset,
+ g->sigma, g, p);
dt_iop_gui_leave_critical_section(self);
gtk_widget_queue_draw(GTK_WIDGET(g->area));
@@ -2232,7 +3812,7 @@ int scrolled(dt_iop_module_t *self,
if(commit)
{
// Update GUI with new params
- update_exposure_sliders(g, p);
+ _update_exposure_sliders(g, p);
dt_dev_add_history_item(darktable.develop, self, FALSE);
}
@@ -2240,16 +3820,22 @@ int scrolled(dt_iop_module_t *self,
return 1;
}
-/***
+
+ /****************************************************************************
+ *
* GTK/Cairo drawings and custom widgets
- **/
+ *
+ ****************************************************************************/
+// empty marker function to make navigating the function list easier
+__attribute__((unused)) static void GTK_CAIRO_MARKER() {}
-static inline gboolean _init_drawing(dt_iop_module_t *const restrict self,
- GtkWidget *widget,
- dt_iop_toneequalizer_gui_data_t *const restrict g);
+static gboolean _init_drawing(dt_iop_module_t *const restrict self,
+ GtkWidget *widget,
+ dt_iop_toneequalizer_gui_data_t *const restrict g,
+ const dt_iop_toneequalizer_params_t *const restrict p);
-void cairo_draw_hatches(cairo_t *cr,
+static void _cairo_draw_hatches(cairo_t *cr,
double center[2],
double span[2],
const int instances,
@@ -2277,9 +3863,9 @@ void cairo_draw_hatches(cairo_t *cr,
}
}
-static void get_shade_from_luminance(cairo_t *cr,
- const float luminance,
- const float alpha)
+static void _get_shade_from_luminance(cairo_t *cr,
+ const float luminance,
+ const float alpha)
{
// TODO: fetch screen gamma from ICC display profile
const float gamma = 1.0f / 2.2f;
@@ -2287,22 +3873,21 @@ static void get_shade_from_luminance(cairo_t *cr,
cairo_set_source_rgba(cr, shade, shade, shade, alpha);
}
-
-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)
+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)
{
// Draw a circle cursor filled with a grey shade corresponding to a luminance value
// or hatches if the value is above the overexposed threshold
const double radius_z = radius / zoom_scale;
- get_shade_from_luminance(cr, luminance, alpha);
+ _get_shade_from_luminance(cr, luminance, alpha);
cairo_arc(cr, pointerx, pointery, radius_z, 0, 2 * M_PI);
cairo_fill_preserve(cr);
cairo_save(cr);
@@ -2313,16 +3898,15 @@ static void draw_exposure_cursor(cairo_t *cr,
// if overexposed, draw hatches
double pointer_coord[2] = { pointerx, pointery };
double span[2] = { radius_z, radius_z };
- cairo_draw_hatches(cr, pointer_coord, span, instances,
+ _cairo_draw_hatches(cr, pointer_coord, span, instances,
DT_PIXEL_APPLY_DPI(1. / zoom_scale), 0.3);
}
cairo_restore(cr);
}
-
-static void match_color_to_background(cairo_t *cr,
- const float exposure,
- const float alpha)
+static void _match_color_to_background(cairo_t *cr,
+ const float exposure,
+ const float alpha)
{
float shade = 0.0f;
// TODO: put that as a preference in darktablerc
@@ -2333,10 +3917,9 @@ static void match_color_to_background(cairo_t *cr,
else
shade = (fmaxf(exposure / contrast, -5.0f) + 2.5f);
- get_shade_from_luminance(cr, exp2f(shade), alpha);
+ _get_shade_from_luminance(cr, exp2f(shade), alpha);
}
-
void gui_post_expose(dt_iop_module_t *self,
cairo_t *cr,
const float width,
@@ -2349,14 +3932,16 @@ void gui_post_expose(dt_iop_module_t *self,
const dt_develop_t *dev = self->dev;
dt_iop_toneequalizer_gui_data_t *g = self->gui_data;
+ const dt_iop_toneequalizer_params_t *p = self->params;
// if we are editing masks, do not display controls
- if(in_mask_editing(self)) return;
+ if(_in_mask_editing(self)) return;
dt_iop_gui_enter_critical_section(self);
const gboolean fail = !g->cursor_valid
- || !g->interpolation_valid
+ || (p->curve_type == DT_TONEEQ_CURVE_GAUSS && !g->interpolation_valid && !g->gauss_factors_valid)
+ || (p->curve_type == DT_TONEEQ_CURVE_CATMULL && (!g->gui_curve_valid || !g->user_param_valid))
|| dev->full.pipe->processing
|| !g->has_focus;
@@ -2365,12 +3950,14 @@ void gui_post_expose(dt_iop_module_t *self,
if(fail) return;
if(!g->graph_valid)
- if(!_init_drawing(self, self->widget, g))
+ if(!_init_drawing(self, self->widget, g, p))
return;
// re-read the exposure in case it has changed
- if(g->luminance_valid && self->enabled)
- g->cursor_exposure = log2f(_luminance_from_module_buffer(self));
+ if(g->prv_luminance_valid && self->enabled) {
+ const float lum = log2f(_luminance_from_thumb_preview_buf(self));
+ g->cursor_exposure = fast_clamp(_post_scale_shift(lum, exp2f(p->post_scale_base), p->post_shift_base, exp2f(p->post_scale), p->post_shift, p->post_pivot), DT_TONEEQ_MIN_EV, DT_TONEEQ_MAX_EV);
+ }
dt_iop_gui_enter_critical_section(self);
@@ -2383,14 +3970,18 @@ void gui_post_expose(dt_iop_module_t *self,
float correction = 0.0f;
float exposure_out = 0.0f;
float luminance_out = 0.0f;
- if(g->luminance_valid && self->enabled)
+ if(g->prv_luminance_valid && self->enabled)
{
// Get the corresponding exposure
exposure_in = g->cursor_exposure;
luminance_in = exp2f(exposure_in);
// Get the corresponding correction and compute resulting exposure
- correction = log2f(pixel_correction(exposure_in, g->factors, g->sigma));
+ if (p->curve_type == DT_TONEEQ_CURVE_GAUSS)
+ correction = log2f(_gauss_pixel_correction(exposure_in, g->gauss_factors, g->sigma));
+ else // CATMULL
+ correction = log2f(fast_clamp(_catmull_rom_val(NUM_SLIDERS, DT_TONEEQ_MIN_EV, exposure_in, g->catmull_nodes_y, g->catmull_tangents), 0.25f, 4.0f));
+
exposure_out = exposure_in + correction;
luminance_out = exp2f(exposure_out);
}
@@ -2406,7 +3997,7 @@ void gui_post_expose(dt_iop_module_t *self,
const double fill_width = DT_PIXEL_APPLY_DPI(4. / zoom_scale);
// setting fill bars
- match_color_to_background(cr, exposure_out, 1.0);
+ _match_color_to_background(cr, exposure_out, 1.0);
cairo_set_line_width(cr, 2.0 * fill_width);
cairo_move_to(cr, x_pointer - setting_offset_x, y_pointer);
@@ -2438,9 +4029,9 @@ void gui_post_expose(dt_iop_module_t *self,
cairo_stroke(cr);
// draw exposure cursor
- draw_exposure_cursor(cr, x_pointer, y_pointer, outer_radius,
+ _draw_exposure_cursor(cr, x_pointer, y_pointer, outer_radius,
luminance_in, zoom_scale, 6, .9);
- draw_exposure_cursor(cr, x_pointer, y_pointer, inner_radius,
+ _draw_exposure_cursor(cr, x_pointer, y_pointer, inner_radius,
luminance_out, zoom_scale, 3, .9);
// Create Pango objects : texts
@@ -2458,7 +4049,7 @@ void gui_post_expose(dt_iop_module_t *self,
pango_cairo_context_set_resolution(pango_layout_get_context(layout), darktable.gui->dpi);
// Build text object
- if(g->luminance_valid && self->enabled)
+ if(g->prv_luminance_valid && self->enabled)
snprintf(text, sizeof(text), _("%+.1f EV"), exposure_in);
else
snprintf(text, sizeof(text), "? EV");
@@ -2466,7 +4057,7 @@ void gui_post_expose(dt_iop_module_t *self,
pango_layout_get_pixel_extents(layout, &ink, NULL);
// Draw the text plain blackground
- get_shade_from_luminance(cr, luminance_out, 0.75);
+ _get_shade_from_luminance(cr, luminance_out, 0.75);
cairo_rectangle(cr,
x_pointer + (outer_radius + 2. * g->inner_padding) / zoom_scale,
y_pointer - ink.y - ink.height / 2.0 - g->inner_padding / zoom_scale,
@@ -2475,7 +4066,7 @@ void gui_post_expose(dt_iop_module_t *self,
cairo_fill(cr);
// Display the EV reading
- match_color_to_background(cr, exposure_out, 1.0);
+ _match_color_to_background(cr, exposure_out, 1.0);
cairo_move_to(cr, x_pointer + (outer_radius + 4. * g->inner_padding) / zoom_scale,
y_pointer - ink.y - ink.height / 2.);
pango_cairo_show_layout(cr, layout);
@@ -2485,15 +4076,15 @@ void gui_post_expose(dt_iop_module_t *self,
pango_font_description_free(desc);
g_object_unref(layout);
- if(g->luminance_valid && self->enabled)
+ if(g->prv_luminance_valid && self->enabled)
{
// Search for nearest node in graph and highlight it
const float radius_threshold = 0.45f;
g->area_active_node = -1;
if(g->cursor_valid)
- for(int i = 0; i < CHANNELS; ++i)
+ for(int i = 0; i < NUM_SLIDERS; ++i)
{
- const float delta_x = fabsf(g->cursor_exposure - centers_params[i]);
+ const float delta_x = fabsf(g->cursor_exposure - centers_sliders[i]);
if(delta_x < radius_threshold)
g->area_active_node = i;
}
@@ -2507,7 +4098,7 @@ static void _develop_distort_callback(gpointer instance,
{
const dt_iop_toneequalizer_gui_data_t *g = self->gui_data;
if(g == NULL) return;
- if(!g->distort_signal_actif) return;
+ if(!g->distort_signal_active) return;
/* disable the distort signal now to avoid recursive call on this signal as we are
about to reprocess the preview pipe which has some module doing distortion. */
@@ -2523,20 +4114,20 @@ static void _develop_distort_callback(gpointer instance,
static void _set_distort_signal(dt_iop_module_t *self)
{
dt_iop_toneequalizer_gui_data_t *g = self->gui_data;
- if(self->enabled && !g->distort_signal_actif)
+ if(self->enabled && !g->distort_signal_active)
{
DT_CONTROL_SIGNAL_HANDLE(DT_SIGNAL_DEVELOP_DISTORT, _develop_distort_callback);
- g->distort_signal_actif = TRUE;
+ g->distort_signal_active = TRUE;
}
}
static void _unset_distort_signal(dt_iop_module_t *self)
{
dt_iop_toneequalizer_gui_data_t *g = self->gui_data;
- if(g->distort_signal_actif)
+ if(g->distort_signal_active)
{
DT_CONTROL_SIGNAL_DISCONNECT(_develop_distort_callback, self);
- g->distort_signal_actif = FALSE;
+ g->distort_signal_active = FALSE;
}
}
@@ -2546,7 +4137,7 @@ void gui_focus(dt_iop_module_t *self, const gboolean in)
dt_iop_gui_enter_critical_section(self);
g->has_focus = in;
dt_iop_gui_leave_critical_section(self);
- switch_cursors(self);
+ _switch_cursors(self);
if(!in)
{
//lost focus - stop showing mask
@@ -2570,18 +4161,20 @@ void gui_focus(dt_iop_module_t *self, const gboolean in)
}
}
-
static inline gboolean _init_drawing(dt_iop_module_t *const restrict self,
GtkWidget *widget,
- dt_iop_toneequalizer_gui_data_t *const restrict g)
+ dt_iop_toneequalizer_gui_data_t *const restrict g,
+ const dt_iop_toneequalizer_params_t *const restrict p)
{
// Cache the equalizer graph objects to avoid recomputing all the view at each redraw
gtk_widget_get_allocation(widget, &g->allocation);
if(g->cst)
cairo_surface_destroy(g->cst);
+
+ g->graph_w_gradients_height = g->allocation.height - DT_RESIZE_HANDLE_SIZE - g->inner_padding;
g->cst = dt_cairo_image_surface_create(CAIRO_FORMAT_ARGB32,
- g->allocation.width, g->allocation.height);
+ g->allocation.width, g->graph_w_gradients_height);
if(g->cr)
cairo_destroy(g->cr);
@@ -2621,15 +4214,28 @@ static inline gboolean _init_drawing(dt_iop_module_t *const restrict self,
// align the right border on sliders:
g->graph_width = g->allocation.width - g->inset - 2.0 * g->line_height;
// give room to nodes:
- g->graph_height = g->allocation.height - g->inset - 2.0 * g->line_height;
+ g->graph_height = g->graph_w_gradients_height - g->inset - 2.0 * g->line_height;
g->gradient_left_limit = 0.0;
g->gradient_right_limit = g->graph_width;
g->gradient_top_limit = g->graph_height + 2 * g->inner_padding;
g->gradient_width = g->gradient_right_limit - g->gradient_left_limit;
g->legend_top_limit = -0.5 * g->line_height - 2.0 * g->inner_padding;
- g->x_label = g->graph_width + g->sign_width + 3.0 * g->inner_padding;
+ g->x_label = g->graph_width + 1.0 * g->inner_padding;
+
+ // Position button box at the top-right corner of the graph
+ // Only do this if button_box exists (it might not exist during early initialization)
+ if(g->button_box)
+ {
+ // The graph content area starts at (line_height + 2*inner_padding, line_height + 3*inner_padding)
+ const int button_box_width = gtk_widget_get_allocated_width(g->button_box);
+ const int left_margin = (int)(g->line_height + 2 * g->inner_padding + g->graph_width) - button_box_width;
+ const int top_margin = (int)(g->line_height + 3 * g->inner_padding);
- gtk_render_background(g->context, g->cr, 0, 0, g->allocation.width, g->allocation.height);
+ gtk_widget_set_margin_start(g->button_box, left_margin);
+ gtk_widget_set_margin_top(g->button_box, top_margin);
+ }
+
+ gtk_render_background(g->context, g->cr, 0, 0, g->allocation.width, g->graph_w_gradients_height);
// set the graph as the origin of the coordinates
cairo_translate(g->cr, g->line_height + 2 * g->inner_padding,
@@ -2640,10 +4246,10 @@ static inline gboolean _init_drawing(dt_iop_module_t *const restrict self,
float value = -8.0f;
- for(int k = 0; k < CHANNELS; k++)
+ for(int k = 0; k < NUM_SLIDERS; k++)
{
const float xn =
- (((float)k) / ((float)(CHANNELS - 1))) * g->graph_width - g->sign_width;
+ (((float)k) / ((float)(NUM_SLIDERS - 1))) * g->graph_width - g->sign_width;
snprintf(text, sizeof(text), "%+.0f", value);
pango_layout_set_text(g->layout, text, -1);
@@ -2656,20 +4262,26 @@ static inline gboolean _init_drawing(dt_iop_module_t *const restrict self,
value += 1.0;
}
- value = 2.0f;
+ // display y-axis legends (EV)
+ // vertical range can be -2...+2 EV or -4...+4 EV
+ const int num_y_labels = 5;
+ const float max_ev = (float)g->histogram_vrange;
+ const float min_ev = -(float)g->histogram_vrange;
+ value = max_ev;
+ const float decrement = (max_ev - min_ev) / (float)(num_y_labels - 1);
- for(int k = 0; k < 5; k++)
+ for(int k = 0; k < num_y_labels; k++)
{
- const float yn = (k / 4.0f) * g->graph_height;
+ const float yn = (k / (float)(num_y_labels - 1)) * g->graph_height;
snprintf(text, sizeof(text), "%+.0f", value);
pango_layout_set_text(g->layout, text, -1);
pango_layout_get_pixel_extents(g->layout, &g->ink, NULL);
- cairo_move_to(g->cr, g->x_label - 0.5 * g->ink.width - g->ink.x,
- yn - 0.5 * g->ink.height - g->ink.y);
+ cairo_move_to(g->cr, g->x_label, // - 0.5 * g->ink.width - g->ink.x,
+ yn - 0.5 * g->line_height - g->ink.y); // );
pango_cairo_show_layout(g->cr, g->layout);
cairo_stroke(g->cr);
- value -= 1.0;
+ value -= decrement;
}
/** x axis **/
@@ -2685,6 +4297,23 @@ static inline gboolean _init_drawing(dt_iop_module_t *const restrict self,
cairo_fill(g->cr);
cairo_pattern_destroy(grad);
+ /** Draw scale pivot marker **/
+ // TODO MF: clean up hard coded values
+ const float pivot_x = ((p->post_pivot + 8.0f) / 8.0f) * g->gradient_width;
+
+ // Draw a small upward-pointing triangle (^)
+ const float marker_size = 0.75 * g->inner_padding;
+ const float marker_y = g->gradient_top_limit - g->inner_padding;
+
+ cairo_set_line_width(g->cr, DT_PIXEL_APPLY_DPI(1.5));
+ set_color(g->cr, darktable.bauhaus->graph_fg);
+
+ // Draw triangle
+ cairo_move_to(g->cr, pivot_x - marker_size, marker_y);
+ cairo_line_to(g->cr, pivot_x, marker_y - marker_size);
+ cairo_line_to(g->cr, pivot_x + marker_size, marker_y);
+ cairo_stroke(g->cr);
+
/** y axis **/
// Draw the perceptually even gradient
grad = cairo_pattern_create_linear(0.0, g->graph_height, 0.0, 0.0);
@@ -2712,42 +4341,193 @@ static inline gboolean _init_drawing(dt_iop_module_t *const restrict self,
return TRUE;
}
-
// must be called while holding self->gui_lock
-static inline void init_nodes_x(dt_iop_toneequalizer_gui_data_t *g)
+static inline void _init_nodes_x(dt_iop_toneequalizer_gui_data_t *g)
{
if(g == NULL) return;
- if(!g->valid_nodes_x && g->graph_width > 0)
+ if(g->graph_width > 0)
{
- for(int i = 0; i < CHANNELS; ++i)
- g->nodes_x[i] = (((float)i) / ((float)(CHANNELS - 1))) * g->graph_width;
- g->valid_nodes_x = TRUE;
+ for(int i = 0; i < NUM_SLIDERS; ++i)
+ g->nodes_x[i] = (((float)i) / ((float)(NUM_SLIDERS - 1))) * g->graph_width;
}
}
-
// must be called while holding self->gui_lock
-static inline void init_nodes_y(dt_iop_toneequalizer_gui_data_t *g)
+static inline void _init_nodes_y(dt_iop_toneequalizer_gui_data_t *g)
{
if(g == NULL) return;
if(g->user_param_valid && g->graph_height > 0)
{
- for(int i = 0; i < CHANNELS; ++i)
- g->nodes_y[i] = // assumes factors in [-2 ; 2] EV
- (0.5 - log2f(g->temp_user_params[i]) / 4.0) * g->graph_height;
- g->valid_nodes_y = TRUE;
+ const float plus_minus_range = 2.0f * (float)g->histogram_vrange;
+ for(int i = 0; i < NUM_SLIDERS; ++i)
+ g->nodes_y[i] =
+ (0.5 - log2f(g->temp_user_params[i]) / plus_minus_range) * g->graph_height;
+ }
+}
+
+static inline void _interpolate_gui_color(const GdkRGBA a,
+ const GdkRGBA b,
+ const float t,
+ GdkRGBA *out)
+{
+ float t_clamp = fast_clamp(t, 0.0f, 1.0f);
+ out->red = a.red + t_clamp * (b.red - a.red);
+ out->green = a.green + t_clamp * (b.green - a.green);
+ out->blue = a.blue + t_clamp * (b.blue - a.blue);
+ out->alpha = a.alpha + t_clamp * (b.alpha - a.alpha);
+}
+
+static void _compute_gui_curve_colors(const dt_iop_module_t *self)
+{
+ dt_iop_toneequalizer_gui_data_t *g = self->gui_data;
+ const dt_iop_toneequalizer_params_t *p = self->params;
+
+ const float *const restrict curve = g->gui_curve;
+ GdkRGBA *const restrict colors = g->gui_curve_colors;
+ const gboolean filter_active = (p->filter != DT_TONEEQ_NONE);
+ const float ev_dx = g->image_EV_per_UI_sample;
+
+ const GdkRGBA standard = darktable.bauhaus->graph_fg;
+ const GdkRGBA warning = {0.75, 0.5, 0.0, 1.0};
+ const GdkRGBA error = {1.0, 0.0, 0.0, 1.0};
+ GdkRGBA temp_color = {0.0, 0.0, 0.0, 1.0};
+
+ const int shadows_limit = (int)UI_HISTO_SAMPLES * 0.3333;
+ const int highlights_limit = (int)UI_HISTO_SAMPLES * 0.6666;
+
+ if (!g->gui_histogram_valid || !g->gui_curve_valid)
+ {
+ // the module is not completely initialized, set all colors to standard
+ for(int k = 0; k < UI_HISTO_SAMPLES; k++)
+ colors[k] = standard;
+ return;
+ };
+
+ colors[0] = standard;
+ for(int k = 1; k < UI_HISTO_SAMPLES; k++)
+ {
+ const float scaled_curve_prev = curve[k - 1] * p->scale_curve;
+ const float scaled_curve_curr = curve[k] * p->scale_curve;
+
+ const float ev_dy = (scaled_curve_curr - scaled_curve_prev);
+ const float steepness = ev_dy / ev_dx;
+ colors[k] = standard;
+
+ if(filter_active && k < shadows_limit && scaled_curve_curr < 0.0f)
+ {
+ // Lower shadows with filter active, this does not provide the local
+ // contrast that the user probably expects.
+ const float x_dist = ((float)(shadows_limit - k) / (float)UI_HISTO_SAMPLES) * 8.0f;
+ const float color_dist = fminf(x_dist, -scaled_curve_curr);
+ _interpolate_gui_color(standard, warning, color_dist, &temp_color);
+ colors[k] = temp_color;
+ }
+ else if(filter_active && k > highlights_limit && scaled_curve_curr > 0.0f)
+ {
+ // Raise highlights with filter active, this does not provide the local
+ // contrast that the user probably expects.
+ const float x_dist = ((float)(k - highlights_limit) / (float)UI_HISTO_SAMPLES) * 8.0f;
+ const float color_dist = fminf(x_dist, scaled_curve_curr);
+ _interpolate_gui_color(standard, warning, color_dist, &temp_color);
+ colors[k] = temp_color;
+ }
+ else if(!filter_active && k < shadows_limit && scaled_curve_curr > 0.0f)
+ {
+ // Raise shadows without filter, this leads to a loss of contrast.
+ const float x_dist = ((float)(shadows_limit - k) / (float)UI_HISTO_SAMPLES) * 8.0f;
+ const float color_dist = fminf(x_dist, scaled_curve_curr);
+ _interpolate_gui_color(standard, warning, color_dist, &temp_color);
+ colors[k] = temp_color;
+ }
+ else if(!filter_active && k > highlights_limit && scaled_curve_curr < 0.0f)
+ {
+ // Lower highlights without filter, this leads to a loss of contrast.
+ const float x_dist = ((float)(k - highlights_limit) / (float)UI_HISTO_SAMPLES) * 8.0f;
+ const float color_dist = fminf(x_dist, -scaled_curve_curr);
+ _interpolate_gui_color(standard, warning, color_dist, &temp_color);
+ colors[k] = temp_color;
+ }
+
+ // Too steep downward slopes.
+ // These warnings take precedence, even if the segment was already
+ // colored, we overwrite the colors here.
+ if(steepness < -0.5f && steepness > -1.0f)
+ {
+ colors[k] = warning;
+ }
+ else if(steepness <= -1.0f)
+ {
+ colors[k] = error;
+ }
}
}
+static void _draw_histogram(cairo_t *cr, const dt_iop_toneequalizer_ui_histogram_t *histo,
+ const float graph_width, const float graph_height,
+ const dt_iop_toneequalizer_histogram_scale_t scale_mode,
+ GdkRGBA histo_color)
+{
+ set_color(cr, histo_color);
+ cairo_set_line_width(cr, DT_PIXEL_APPLY_DPI(4.0));
+ cairo_move_to(cr, 0, graph_height);
+
+ // Determine max value based on scale mode
+ int max_val
+ = (scale_mode == HISTOGRAM_SCALE_LINEAR_IGNORE_BORDER) ? histo->max_val_ignore_border_bins : histo->max_val;
+
+ if(max_val < 1) max_val = 1; // Avoid division by zero
+
+ for(int k = 0; k < UI_HISTO_SAMPLES; k++)
+ {
+ // x range is [-8;+0] EV
+ const float x_temp = (8.0 * (float)k / (float)(UI_HISTO_SAMPLES - 1)) - 8.0;
+
+ float normalized_value;
+
+ switch(scale_mode)
+ {
+ case HISTOGRAM_SCALE_LINEAR:
+ case HISTOGRAM_SCALE_LINEAR_IGNORE_BORDER:
+ // Linear scale
+ normalized_value = fast_clamp((float)(histo->samples[k]) / (float)max_val, 0.0f, 1.0f);
+ break;
+
+ case HISTOGRAM_SCALE_LOG:
+ // Logarithmic scale: log(1 + value) / log(1 + max_val)
+ if(histo->samples[k] > 0 && max_val > 0)
+ {
+ normalized_value = logf(1.0f + (float)histo->samples[k]) / logf(1.0f + (float)max_val);
+ normalized_value = fast_clamp(normalized_value, 0.0f, 1.0f);
+ }
+ else
+ {
+ normalized_value = 0.0f;
+ }
+ break;
+
+ default:
+ normalized_value = 0.0f;
+ }
+
+ const float y_temp = 1.0f - normalized_value * 0.96f;
+ cairo_line_to(cr, (x_temp + 8.0) * graph_width / 8.0, graph_height * y_temp);
+ }
+
+ cairo_line_to(cr, graph_width, graph_height);
+ cairo_close_path(cr);
+ cairo_fill(cr);
+}
-static gboolean area_draw(GtkWidget *widget,
- cairo_t *cr,
- dt_iop_module_t *self)
+static gboolean _area_draw(GtkWidget *widget,
+ cairo_t *cr,
+ dt_iop_module_t *self)
{
// Draw the widget equalizer view
dt_iop_toneequalizer_gui_data_t *g = self->gui_data;
+ const dt_iop_toneequalizer_params_t *p = self->params;
+
if(g == NULL) return FALSE;
// Init or refresh the drawing cache
@@ -2755,31 +4535,80 @@ static gboolean area_draw(GtkWidget *widget,
// this can be cached and drawn just once, but too lazy to debug a
// cache invalidation for Cairo objects
- if(!_init_drawing(self, widget, g))
+ if(!_init_drawing(self, widget, g, p))
return FALSE;
- // since the widget sizes are not cached and invalidated properly
- // above (yet…) force the invalidation of the nodes coordinates to
- // account for possible widget resizing
- dt_iop_gui_enter_critical_section(self);
- g->valid_nodes_x = FALSE;
- g->valid_nodes_y = FALSE;
- dt_iop_gui_leave_critical_section(self);
-
// Refresh cached UI elements
- update_histogram(self);
- update_curve_lut(self);
+ _update_gui_histogram(self);
+ _curve_interpolation(self);
+
+ // The colors depend on the histogram and the curve
+ _compute_gui_curve_colors(self);
- // Draw graph background
+ // Draw graph background
cairo_set_line_width(g->cr, DT_PIXEL_APPLY_DPI(0.5));
cairo_rectangle(g->cr, 0, 0, g->graph_width, g->graph_height);
set_color(g->cr, darktable.bauhaus->graph_bg);
cairo_fill(g->cr);
+ // draw warnings if outside of module range
+ if (g->gui_histogram_valid && self->enabled)
+ {
+ // Histograms are computed from -24 to +8 EV.
+ // Draw warnings if parts outside of this area end up in the graph
+ // after shift/scale.
+ GdkRGBA error_color_interpolated;
+ _interpolate_gui_color(darktable.bauhaus->graph_bg, error_color, 0.1f, &error_color_interpolated);
+ const float hdr_min_scaled = _post_scale_shift(HDR_MIN_EV, exp2f(p->post_scale_base), p->post_shift_base, exp2f(p->post_scale), p->post_shift, p->post_pivot);
+ if (hdr_min_scaled > DT_TONEEQ_MIN_EV)
+ {
+ const float end_x = fast_clamp((hdr_min_scaled + 8.0f) / 8.0f, 0.0f, 1.0f);
+ cairo_rectangle(g->cr, 0, 0, end_x * g->graph_width, g->graph_height);
+ set_color(g->cr, error_color_interpolated);
+ cairo_fill(g->cr);
+ }
+ const float hdr_max_scaled = _post_scale_shift(HDR_MAX_EV, exp2f(p->post_scale_base), p->post_shift_base, exp2f(p->post_scale), p->post_shift, p->post_pivot);
+ if (hdr_max_scaled < DT_TONEEQ_MAX_EV)
+ {
+ const float start_x = fast_clamp((hdr_max_scaled + 8.0f) / 8.0f, 0.0f, 1.0f);
+ cairo_rectangle(g->cr, start_x * g->graph_width, 0, (1.0f - start_x) * g->graph_width, g->graph_height);
+ set_color(g->cr, error_color_interpolated);
+ cairo_fill(g->cr);
+ }
+ }
+
// draw grid
cairo_set_line_width(g->cr, DT_PIXEL_APPLY_DPI(0.5));
- set_color(g->cr, darktable.bauhaus->graph_border);
- dt_draw_grid(g->cr, 8, 0, 0, g->graph_width, g->graph_height);
+
+ if(g->histogram_vrange == 4)
+ {
+ // For ±4 EV range: draw main grid for -2 to +2 (middle 50% of height)
+ // and lighter extended grids for -4 to -2 and +2 to +4
+
+ // Top extended grid (-4 to -2 EV) - lighter color
+ cairo_save(g->cr);
+ set_color(g->cr, darktable.bauhaus->inset_histogram);
+ dt_draw_grid_xy(g->cr, 8, 4, 0, 0, g->graph_width, 0.25f * g->graph_height, FALSE, FALSE, FALSE, FALSE);
+ cairo_restore(g->cr);
+
+ // Bottom extended grid (+2 to +4 EV) - lighter color
+ cairo_save(g->cr);
+ set_color(g->cr, darktable.bauhaus->inset_histogram);
+ dt_draw_grid_xy(g->cr, 8, 4, 0, 0.75f * g->graph_height, g->graph_width, g->graph_height, FALSE, FALSE, FALSE, FALSE);
+ cairo_restore(g->cr);
+
+ // Main grid (-2 to +2 EV) - normal color
+ cairo_save(g->cr);
+ set_color(g->cr, darktable.bauhaus->graph_border);
+ dt_draw_grid_xy(g->cr, 8, 8, 0, 0.25f * g->graph_height, g->graph_width, 0.75f * g->graph_height, FALSE, TRUE, FALSE, TRUE);
+ cairo_restore(g->cr);
+ }
+ else
+ {
+ // For ±2 EV range: draw normal grid over entire height
+ set_color(g->cr, darktable.bauhaus->graph_border);
+ dt_draw_grid(g->cr, 8, 0, 0, g->graph_width, g->graph_height);
+ }
// draw ground level
set_color(g->cr, darktable.bauhaus->graph_fg);
@@ -2788,81 +4617,99 @@ static gboolean area_draw(GtkWidget *widget,
cairo_line_to(g->cr, g->graph_width, 0.5 * g->graph_height);
cairo_stroke(g->cr);
- if(g->histogram_valid && self->enabled)
+ if(g->gui_histogram_valid && self->enabled)
{
- // draw the inset histogram
- set_color(g->cr, darktable.bauhaus->inset_histogram);
- cairo_set_line_width(g->cr, DT_PIXEL_APPLY_DPI(4.0));
- cairo_move_to(g->cr, 0, g->graph_height);
+ // Determine which histogram to show
+ dt_iop_toneequalizer_ui_histogram_t *histo;
+
+ GdkRGBA histo_color = darktable.bauhaus->inset_histogram;
- for(int k = 0; k < UI_SAMPLES; k++)
+ // If HQ is enabled and we have a valid HQ histogram, show it
+ if(g->histogram_show_hq && g->hq_histogram_valid)
{
- // the x range is [-8;+0] EV
- const float x_temp = (8.0 * (float)k / (float)(UI_SAMPLES - 1)) - 8.0;
- const float y_temp = (float)(g->histogram[k]) / (float)(g->max_histogram) * 0.96;
- cairo_line_to(g->cr, (x_temp + 8.0) * g->graph_width / 8.0,
- (1.0 - y_temp) * g->graph_height );
+ histo = &g->ui_hq_histo;
+
+ // Make the histogram color a bit more redish
+ const GdkRGBA red = {1.0, 0.0, 0.0, 1.0};
+ _interpolate_gui_color(histo_color, red, 0.2f, &histo_color);
}
- cairo_line_to(g->cr, g->graph_width, g->graph_height);
- cairo_close_path(g->cr);
- cairo_fill(g->cr);
+ else
+ histo = &g->ui_histo;
- if(g->histogram_last_decile > -0.1f)
+ // draw the mask histogram background
+ _draw_histogram(g->cr, histo, g->graph_width, g->graph_height, g->histogram_scale_mode, histo_color);
+
+ const float half_bar_width = DT_PIXEL_APPLY_DPI(1.0f);
+
+ if(_post_scale_shift(g->mask_hdr_histo.hi_percentile_ev, exp2f(p->post_scale_base), p->post_shift_base, exp2f(p->post_scale), p->post_shift, p->post_pivot) > -0.1f)
{
// histogram overflows controls in highlights : display warning
cairo_save(g->cr);
- cairo_set_source_rgb(g->cr, 0.75, 0.50, 0.);
- dtgtk_cairo_paint_warning
- (g->cr,
- g->graph_width - 2.5 * g->line_height, 0.5 * g->line_height,
- 2.0 * g->line_height, 2.0 * g->line_height, 0, NULL);
+ cairo_set_source_rgba(g->cr, warning_color.red, warning_color.green, warning_color.blue, warning_color.alpha);
+ // Draw vertical bar at right edge
+ cairo_rectangle(g->cr, g->graph_width - half_bar_width, 0, 2 * half_bar_width, g->graph_height);
+ cairo_fill(g->cr);
cairo_restore(g->cr);
}
- if(g->histogram_first_decile < -7.9f)
+ if(_post_scale_shift(g->mask_hdr_histo.lo_percentile_ev, exp2f(p->post_scale_base), p->post_shift_base, exp2f(p->post_scale), p->post_shift, p->post_pivot) < -7.9f)
{
// histogram overflows controls in lowlights : display warning
cairo_save(g->cr);
- cairo_set_source_rgb(g->cr, 0.75, 0.50, 0.);
- dtgtk_cairo_paint_warning
- (g->cr,
- 0.5 * g->line_height, 0.5 * g->line_height,
- 2.0 * g->line_height, 2.0 * g->line_height, 0, NULL);
+ cairo_set_source_rgba(g->cr, warning_color.red, warning_color.green, warning_color.blue, warning_color.alpha);
+ // Draw vertical bar at left edge
+ cairo_rectangle(g->cr, -half_bar_width, 0, 2 * half_bar_width, g->graph_height);
+ cairo_fill(g->cr);
cairo_restore(g->cr);
}
}
- if(g->lut_valid)
+ // TODO MF: The order of operations has become a bit complicated here
+ // - The thin (unscaled) curve should be under bullets and bars
+ // - The thick (scaled) curve should be above the bars, but under the bullets
+
+ const float plus_minus_range = 2.0f * (float)g->histogram_vrange;
+
+ if(g->gui_curve_valid && p->scale_curve != 1.0f)
{
- // draw the interpolation curve
- set_color(g->cr, darktable.bauhaus->graph_fg);
- cairo_move_to(g->cr, 0, g->gui_lut[0] * g->graph_height);
- cairo_set_line_width(g->cr, DT_PIXEL_APPLY_DPI(3));
+ // draw the thin unscaled curve
+
+ float x_draw, y_draw;
- for(int k = 1; k < UI_SAMPLES; k++)
+ // The coloring of the curve makes it necessary to draw it as individual segments.
+ // However this led to aliasing artifacts, therefore we draw overlapping segments
+ // from k-1 to k+1.
+ // unscaled curve
+ cairo_set_line_width(g->cr, DT_PIXEL_APPLY_DPI(1));
+ set_color(g->cr, darktable.bauhaus->graph_fg);
+ for(int k = 1; k < UI_HISTO_SAMPLES - 1; k++)
{
- // the x range is [-8;+0] EV
- const float x_temp = (8.0f * (((float)k) / ((float)(UI_SAMPLES - 1)))) - 8.0f;
- const float y_temp = g->gui_lut[k];
- cairo_line_to(g->cr, (x_temp + 8.0f) * g->graph_width / 8.0f,
- y_temp * g->graph_height );
+ // Map [0;UI_HISTO_SAMPLES] to [0;1] and then to g->graph_width.
+ x_draw = ((float)(k - 1) / (float)(UI_HISTO_SAMPLES - 1)) * g->graph_width;
+ // Map [-2;+2] EV to graph_height, with graph pixel 0 at the top
+ y_draw = (0.5f - g->gui_curve[k - 1] / plus_minus_range) * g->graph_height;
+
+ cairo_move_to(g->cr, x_draw, y_draw);
+
+ // Map [0;UI_HISTO_SAMPLES] to [0;1] and then to g->graph_width.
+ x_draw = ((float)(k+1) / (float)(UI_HISTO_SAMPLES - 1)) * g->graph_width;
+ // Map [-2;+2] EV to graph_height, with graph pixel 0 at the top
+ y_draw = (0.5f - g->gui_curve[k+1] / plus_minus_range) * g->graph_height;
+
+ cairo_line_to(g->cr, x_draw, y_draw);
+ cairo_stroke(g->cr);
}
- cairo_stroke(g->cr);
}
dt_iop_gui_enter_critical_section(self);
- init_nodes_x(g);
- dt_iop_gui_leave_critical_section(self);
-
- dt_iop_gui_enter_critical_section(self);
- init_nodes_y(g);
+ _init_nodes_x(g);
+ _init_nodes_y(g);
dt_iop_gui_leave_critical_section(self);
if(g->user_param_valid)
{
- // draw nodes positions
- for(int k = 0; k < CHANNELS; k++)
+ for(int k = 0; k < NUM_SLIDERS; k++)
{
const float xn = g->nodes_x[k];
const float yn = g->nodes_y[k];
@@ -2873,6 +4720,73 @@ static gboolean area_draw(GtkWidget *widget,
cairo_move_to(g->cr, xn, 0.5 * g->graph_height);
cairo_line_to(g->cr, xn, yn);
cairo_stroke(g->cr);
+ }
+ }
+
+ if(g->gui_curve_valid)
+ {
+ // draw the interpolation curve
+
+ float x1_draw, y1_draw, x2_draw, y2_draw;
+
+ cairo_set_line_width(g->cr, DT_PIXEL_APPLY_DPI(3));
+ for(int k = 1; k < UI_HISTO_SAMPLES - 1; k++)
+ {
+ set_color(g->cr, g->gui_curve_colors[k]);
+
+ // Map [0;UI_HISTO_SAMPLES] to [0;1] and then to g->graph_width.
+ x1_draw = ((float)(k - 1) / (float)(UI_HISTO_SAMPLES - 1)) * g->graph_width;
+ // Map [-2;+2] EV to graph_height, with graph pixel 0 at the top
+ y1_draw = (0.5f - (g->gui_curve[k - 1] * p->scale_curve) / plus_minus_range) * g->graph_height;
+
+ gboolean first_point_is_outside = FALSE;
+ if (y1_draw < 0.0f - EPSILON)
+ {
+ first_point_is_outside = TRUE;
+ y1_draw = 0.0f;
+ }
+ else if (y1_draw > g->graph_height + EPSILON)
+ {
+ first_point_is_outside = TRUE;
+ y1_draw = g->graph_height;
+ }
+
+
+ // Map [0;UI_HISTO_SAMPLES] to [0;1] and then to g->graph_width.
+ x2_draw = ((float)(k+1) / (float)(UI_HISTO_SAMPLES - 1)) * g->graph_width;
+ // Map [-2;+2] EV to graph_height, with graph pixel 0 at the top
+ y2_draw = (0.5f - (g->gui_curve[k + 1] * p->scale_curve) / plus_minus_range) * g->graph_height;
+
+ if (y2_draw < 0.0f - EPSILON)
+ {
+ if (first_point_is_outside)
+ // both points are outside : skip drawing
+ continue;
+ else
+ y2_draw = 0.0f;
+ }
+ else if (y2_draw > g->graph_height + EPSILON)
+ {
+ if (first_point_is_outside)
+ // both points are outside : skip drawing
+ continue;
+ else
+ y2_draw = g->graph_height;
+ }
+
+ cairo_move_to(g->cr, x1_draw, y1_draw);
+ cairo_line_to(g->cr, x2_draw, y2_draw);
+ cairo_stroke(g->cr);
+ }
+ }
+
+ if(g->user_param_valid)
+ {
+ // draw nodes positions
+ for(int k = 0; k < NUM_SLIDERS; k++)
+ {
+ const float xn = g->nodes_x[k];
+ const float yn = g->nodes_y[k];
// bullets
cairo_set_line_width(g->cr, DT_PIXEL_APPLY_DPI(3));
@@ -2893,11 +4807,17 @@ static gboolean area_draw(GtkWidget *widget,
{
if(g->area_cursor_valid)
{
- const float radius = g->sigma * g->graph_width / 8.0f / sqrtf(2.0f);
+ float radius;
+ if (p->curve_type == DT_TONEEQ_CURVE_GAUSS)
+ radius = g->sigma * g->graph_width / 8.0f / sqrtf(2.0f);
+ else
+ // small ring for catmull
+ radius = g->graph_width / 10.0f;
+
cairo_set_line_width(g->cr, DT_PIXEL_APPLY_DPI(1.5));
- const float y =
- g->gui_lut[(int)CLAMP(((UI_SAMPLES - 1) * g->area_x / g->graph_width),
- 0, UI_SAMPLES - 1)];
+ const float y = 0.5f -
+ g->gui_curve[(int)CLAMP(((UI_HISTO_SAMPLES - 1) * g->area_x / g->graph_width),
+ 0, UI_HISTO_SAMPLES - 1)] / plus_minus_range;
cairo_arc(g->cr, g->area_x, y * g->graph_height, radius, 0, 2. * M_PI);
set_color(g->cr, darktable.bauhaus->graph_fg);
cairo_stroke(g->cr);
@@ -2908,13 +4828,13 @@ static gboolean area_draw(GtkWidget *widget,
float x_pos = (g->cursor_exposure + 8.0f) / 8.0f * g->graph_width;
- if(x_pos > g->graph_width || x_pos < 0.0f)
+ if(x_pos >= g->graph_width || x_pos <= 0.0f)
{
// exposure at current position is outside [-8; 0] EV :
// bound it in the graph limits and show it in orange
cairo_set_source_rgb(g->cr, 0.75, 0.50, 0.);
cairo_set_line_width(g->cr, DT_PIXEL_APPLY_DPI(3));
- x_pos = (x_pos < 0.0f) ? 0.0f : g->graph_width;
+ x_pos = (x_pos <= 0.0f) ? 0.0f : g->graph_width;
}
else
{
@@ -2932,14 +4852,14 @@ static gboolean area_draw(GtkWidget *widget,
cairo_set_source_surface(cr, g->cst, 0, 0);
cairo_paint(cr);
- return TRUE;
+ return FALSE; // Draw the handle next
}
-
-static gboolean area_enter_leave_notify(GtkWidget *widget,
- const GdkEventCrossing *event,
- dt_iop_module_t *self)
+static gboolean _area_enter_leave_notify(GtkWidget *widget,
+ const GdkEventCrossing *event,
+ dt_iop_module_t *self)
{
+ if(!self || !self->gui_data) return FALSE;
if(darktable.gui->reset) return TRUE;
if(!self->enabled) return FALSE;
@@ -2950,7 +4870,7 @@ static gboolean area_enter_leave_notify(GtkWidget *widget,
if(g->area_dragging)
{
// cursor left area : force commit to avoid glitches
- update_exposure_sliders(g, p);
+ _update_exposure_sliders(g, p);
dt_dev_add_history_item(darktable.develop, self, FALSE);
}
@@ -2969,12 +4889,11 @@ static gboolean area_enter_leave_notify(GtkWidget *widget,
return FALSE;
}
-
-static gboolean area_button_press(GtkWidget *widget,
- const GdkEventButton *event,
- dt_iop_module_t *self)
+static gboolean _area_button_press(GtkWidget *widget,
+ const GdkEventButton *event,
+ dt_iop_module_t *self)
{
-
+ if(!self || !self->gui_data) return FALSE;
if(darktable.gui->reset) return TRUE;
dt_iop_toneequalizer_gui_data_t *g = self->gui_data;
@@ -2998,7 +4917,7 @@ static gboolean area_button_press(GtkWidget *widget,
p->speculars = d->speculars;
// update UI sliders
- update_exposure_sliders(g, p);
+ _update_exposure_sliders(g, p);
// Redraw graph
gtk_widget_queue_draw(GTK_WIDGET(g->area));
@@ -3016,7 +4935,7 @@ static gboolean area_button_press(GtkWidget *widget,
{
dt_dev_add_history_item(darktable.develop, self, TRUE);
}
- return TRUE;
+ return FALSE;
}
// Unlock the colour picker so we can display our own custom cursor
@@ -3025,11 +4944,11 @@ static gboolean area_button_press(GtkWidget *widget,
return FALSE;
}
-
-static gboolean area_motion_notify(GtkWidget *widget,
- const GdkEventMotion *event,
- dt_iop_module_t *self)
+static gboolean _area_motion_notify(GtkWidget *widget,
+ const GdkEventMotion *event,
+ dt_iop_module_t *self)
{
+ if(!self || !self->gui_data) return FALSE;
if(darktable.gui->reset) return TRUE;
if(!self->enabled) return FALSE;
@@ -3045,8 +4964,8 @@ static gboolean area_motion_notify(GtkWidget *widget,
const float cursor_exposure = g->area_x / g->graph_width * 8.0f - 8.0f;
// Get the desired correction on exposure channels
- g->area_dragging = set_new_params_interactive(cursor_exposure, offset,
- g->sigma * g->sigma / 2.0f, g, p);
+ g->area_dragging = _set_new_params_interactive(cursor_exposure, offset,
+ g->sigma, g, p);
dt_iop_gui_leave_critical_section(self);
}
@@ -3060,30 +4979,29 @@ static gboolean area_motion_notify(GtkWidget *widget,
g->area_active_node = -1;
// Search if cursor is close to a node
- if(g->valid_nodes_x)
+
+ const float radius_threshold = fabsf(g->nodes_x[1] - g->nodes_x[0]) * 0.45f;
+ for(int i = 0; i < NUM_SLIDERS; ++i)
{
- const float radius_threshold = fabsf(g->nodes_x[1] - g->nodes_x[0]) * 0.45f;
- for(int i = 0; i < CHANNELS; ++i)
+ const float delta_x = fabsf(g->area_x - g->nodes_x[i]);
+ if(delta_x < radius_threshold)
{
- const float delta_x = fabsf(g->area_x - g->nodes_x[i]);
- if(delta_x < radius_threshold)
- {
- g->area_active_node = i;
- g->area_cursor_valid = TRUE;
- }
+ g->area_active_node = i;
+ g->area_cursor_valid = TRUE;
}
}
+
dt_iop_gui_leave_critical_section(self);
gtk_widget_queue_draw(GTK_WIDGET(g->area));
- return TRUE;
+ return FALSE;
}
-
-static gboolean area_button_release(GtkWidget *widget,
- const GdkEventButton *event,
- dt_iop_module_t *self)
+static gboolean _area_button_release(GtkWidget *widget,
+ const GdkEventButton *event,
+ dt_iop_module_t *self)
{
+ if(!self || !self->gui_data) return FALSE;
if(darktable.gui->reset) return TRUE;
if(!self->enabled) return FALSE;
@@ -3099,7 +5017,7 @@ static gboolean area_button_release(GtkWidget *widget,
if(g->area_dragging)
{
// Update GUI with new params
- update_exposure_sliders(g, p);
+ _update_exposure_sliders(g, p);
dt_dev_add_history_item(darktable.develop, self, FALSE);
@@ -3107,60 +5025,29 @@ static gboolean area_button_release(GtkWidget *widget,
g->area_dragging = FALSE;
dt_iop_gui_leave_critical_section(self);
- return TRUE;
+ return FALSE;
}
}
return FALSE;
}
-static gboolean area_scroll(GtkWidget *widget,
- GdkEventScroll *event,
- gpointer user_data)
+static gboolean _area_scroll(GtkWidget *widget,
+ GdkEventScroll *event,
+ gpointer user_data)
{
// do not propagate to tab bar unless scrolling sidebar
return !dt_gui_ignore_scroll(event);
}
-static gboolean notebook_button_press(GtkWidget *widget,
- GdkEventButton *event,
- dt_iop_module_t *self)
-{
- if(darktable.gui->reset) return TRUE;
-
- // Give focus to module
- dt_iop_request_focus(self);
-
- // Unlock the colour picker so we can display our own custom cursor
- dt_iop_color_picker_reset(self, TRUE);
-
- return FALSE;
-}
-
-GSList *mouse_actions(dt_iop_module_t *self)
-{
- GSList *lm = NULL;
- lm = dt_mouse_action_create_format
- (lm, DT_MOUSE_ACTION_SCROLL, 0,
- _("[%s over image] change tone exposure"), self->name());
- lm = dt_mouse_action_create_format
- (lm, DT_MOUSE_ACTION_SCROLL, GDK_SHIFT_MASK,
- _("[%s over image] change tone exposure in large steps"), self->name());
- lm = dt_mouse_action_create_format
- (lm, DT_MOUSE_ACTION_SCROLL, GDK_CONTROL_MASK,
- _("[%s over image] change tone exposure in small steps"), self->name());
- return lm;
-}
-
/**
* Post pipe events
**/
-
static void _develop_ui_pipe_started_callback(gpointer instance,
dt_iop_module_t *self)
{
dt_iop_toneequalizer_gui_data_t *g = self->gui_data;
if(g == NULL) return;
- switch_cursors(self);
+ _switch_cursors(self);
if(!self->expanded || !self->enabled)
{
@@ -3178,30 +5065,53 @@ static void _develop_ui_pipe_started_callback(gpointer instance,
--darktable.gui->reset;
}
-
static void _develop_preview_pipe_finished_callback(gpointer instance,
dt_iop_module_t *self)
{
- const dt_iop_toneequalizer_gui_data_t *g = self->gui_data;
+ dt_iop_toneequalizer_gui_data_t *g = self->gui_data;
if(g == NULL) return;
+ dt_iop_toneequalizer_params_t *p = self->params;
+
// now that the preview pipe is termintated, set back the distort signal to catch
// any new changes from a module doing distortion. this signal has been disconnected
// at the time the DT_SIGNAL_DEVELOP_DISTORT has been handled (see ) and a full
// reprocess of the preview has been scheduled.
_set_distort_signal(self);
- switch_cursors(self);
+ // If auto-align is enabled and PREVIEW has computed new values, update params
+ dt_iop_gui_enter_critical_section(self);
+
+ if(p->auto_align_enabled && g->prv_luminance_valid && g->temp_post_values_valid)
+ {
+ const gboolean alignment_changed = (fabsf(g->temp_post_scale_base - p->post_scale_base) > EPSILON)
+ || (fabsf(g->temp_post_shift_base - p->post_shift_base) > EPSILON);
+ // Copy the temporary values computed in PREVIEW to params
+ p->post_scale_base = g->temp_post_scale_base;
+ p->post_shift_base = g->temp_post_shift_base;
+ if (alignment_changed)
+ dt_control_log(_("toneequalizer auto-alignment: base shift set to %.2f, base scale set to %.2f"), p->post_shift_base, p->post_scale_base);
+
+ // Update button tooltips with the new values
+ _fill_button_tooltip(g->align_button, p->post_scale_base, p->post_shift_base, self);
+ _fill_button_tooltip(g->shift_button, p->post_scale_base, p->post_shift_base, self);
+
+ // Mark that we need to recompute the histogram/LUT with new alignment
+ g->gui_histogram_valid = FALSE;
+ }
+ dt_iop_gui_leave_critical_section(self);
+
+ _switch_cursors(self);
gtk_widget_queue_draw(GTK_WIDGET(g->area));
}
-
static void _develop_ui_pipe_finished_callback(gpointer instance,
dt_iop_module_t *self)
{
const dt_iop_toneequalizer_gui_data_t *g = self->gui_data;
if(g == NULL) return;
- switch_cursors(self);
+
+ _switch_cursors(self);
}
void gui_reset(dt_iop_module_t *self)
@@ -3218,19 +5128,249 @@ void gui_reset(dt_iop_module_t *self)
}
+
+static void _dtgtk_cairo_paint_refresh_warning_color(cairo_t *cr,
+ const gint x, const gint y, const gint w, const gint h,
+ const gint flags, void *data)
+{
+ // Set the warning color
+ cairo_set_source_rgba(cr, warning_color.red, warning_color.green, warning_color.blue, warning_color.alpha);
+
+ // Call the original refresh paint function with the warning color already set
+ dtgtk_cairo_paint_refresh(cr, x, y, w, h, flags, data);
+}
+
+// empty marker function to make navigating the function list easier
+__attribute__((unused)) static void GUI_INIT_MARKER() {}
+
void gui_init(dt_iop_module_t *self)
{
dt_iop_toneequalizer_gui_data_t *g = IOP_GUI_ALLOC(toneequalizer);
- gui_cache_init(self);
+ _gui_cache_init(self);
+
+ g->area = GTK_DRAWING_AREA(dt_ui_resize_wrap(NULL,
+ 0,
+ "plugins/darkroom/toneequal/graphheight"));
+
+ gtk_widget_add_events(GTK_WIDGET(g->area),
+ GDK_POINTER_MOTION_MASK | darktable.gui->scroll_mask
+ | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK
+ | GDK_ENTER_NOTIFY_MASK | GDK_LEAVE_NOTIFY_MASK);
+ gtk_widget_set_can_focus(GTK_WIDGET(g->area), TRUE);
+ g_signal_connect(G_OBJECT(g->area), "draw", G_CALLBACK(_area_draw), self);
+ g_signal_connect(G_OBJECT(g->area), "button-press-event",
+ G_CALLBACK(_area_button_press), self);
+ g_signal_connect(G_OBJECT(g->area), "button-release-event",
+ G_CALLBACK(_area_button_release), self);
+ g_signal_connect(G_OBJECT(g->area), "leave-notify-event",
+ G_CALLBACK(_area_enter_leave_notify), self);
+ g_signal_connect(G_OBJECT(g->area), "enter-notify-event",
+ G_CALLBACK(_area_enter_leave_notify), self);
+ g_signal_connect(G_OBJECT(g->area), "motion-notify-event",
+ G_CALLBACK(_area_motion_notify), self);
+ g_signal_connect(G_OBJECT(g->area), "scroll-event",
+ G_CALLBACK(_area_scroll), self);
+ gtk_widget_set_tooltip_text(GTK_WIDGET(g->area), _("double-click to reset the curve"));
+
+ const int button_size = DT_PIXEL_APPLY_DPI(18);
+
+ // Create button box (horizontal box for all buttons)
+ g->button_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 2);
+ gtk_widget_set_valign(g->button_box, GTK_ALIGN_START);
+ gtk_widget_set_halign(g->button_box, GTK_ALIGN_START);
+
+ const int left_margin = DT_PIXEL_APPLY_DPI(0); // Will be calculated in _init_drawing
+ const int top_margin = DT_PIXEL_APPLY_DPI(0); // Will be calculated in _init_drawing
+
+ gtk_widget_set_margin_start(g->button_box, left_margin);
+ gtk_widget_set_margin_top(g->button_box, top_margin);
+ dt_gui_add_class(g->button_box, "dt_transparent_background");
+
+ // Align button - leftmost
+ g->histogram_align_button = dtgtk_togglebutton_new(_dtgtk_cairo_paint_refresh_warning_color, CPF_NONE, NULL);
+ gtk_widget_set_size_request(g->histogram_align_button, button_size, button_size);
+ gtk_widget_set_tooltip_text(g->histogram_align_button, _("align histogram"));
+ gtk_box_pack_start(GTK_BOX(g->button_box), g->histogram_align_button, FALSE, FALSE, 0);
+ g_signal_connect(G_OBJECT(g->histogram_align_button), "button-press-event",
+ G_CALLBACK(_re_adjust_alignment), self);
+
+ // FIXME: Doing this only to make the button look nice (not transparent)
+ ++darktable.gui->reset;
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->histogram_align_button), TRUE);
+ --darktable.gui->reset;
+
+ // HQ button
+ g->histogram_hq_button = gtk_toggle_button_new_with_label("LQ");
+ gtk_widget_set_size_request(g->histogram_hq_button, button_size, button_size);
+ gtk_widget_set_tooltip_text(g->histogram_hq_button, _("toggle between preview and high-quality histogram"));
+ gtk_box_pack_start(GTK_BOX(g->button_box), g->histogram_hq_button, FALSE, FALSE, 0);
+ g_signal_connect(G_OBJECT(g->histogram_hq_button), "button-press-event",
+ G_CALLBACK(_histogram_hq_clicked), self);
+
+ // Range button
+ g->histogram_range_button = gtk_button_new_with_label("2");
+ gtk_widget_set_size_request(g->histogram_range_button, button_size, button_size);
+ gtk_widget_set_tooltip_text(g->histogram_range_button, _("toggle y-axis range"));
+ gtk_box_pack_start(GTK_BOX(g->button_box), g->histogram_range_button, FALSE, FALSE, 0);
+ g_signal_connect(G_OBJECT(g->histogram_range_button), "button-press-event",
+ G_CALLBACK(_histogram_range_clicked), self);
+
+ // Create 3 scale buttons - packed left to right like other buttons
+ g->histogram_mode_button_linear = dtgtk_togglebutton_new(dtgtk_cairo_paint_linear_scale, CPF_NONE, NULL);
+ gtk_widget_set_size_request(g->histogram_mode_button_linear, button_size, button_size);
+ gtk_widget_set_tooltip_text(g->histogram_mode_button_linear, _("linear histogram"));
+ gtk_box_pack_start(GTK_BOX(g->button_box), g->histogram_mode_button_linear, FALSE, FALSE, 0);
+ g_signal_connect(G_OBJECT(g->histogram_mode_button_linear), "button-press-event",
+ G_CALLBACK(_histogram_mode_clicked), self);
+
+ g->histogram_mode_button_ignore = dtgtk_togglebutton_new(dtgtk_cairo_paint_linear_scale_ignore_border, CPF_NONE, NULL);
+ gtk_widget_set_size_request(g->histogram_mode_button_ignore, button_size, button_size);
+ gtk_widget_set_tooltip_text(g->histogram_mode_button_ignore, _("linear histogram (ignore border bins)"));
+ gtk_box_pack_start(GTK_BOX(g->button_box), g->histogram_mode_button_ignore, FALSE, FALSE, 0);
+ g_signal_connect(G_OBJECT(g->histogram_mode_button_ignore), "button-press-event",
+ G_CALLBACK(_histogram_mode_clicked), self);
+
+ g->histogram_mode_button_log = dtgtk_togglebutton_new(dtgtk_cairo_paint_logarithmic_scale, CPF_NONE, NULL);
+ gtk_widget_set_size_request(g->histogram_mode_button_log, button_size, button_size);
+ gtk_widget_set_tooltip_text(g->histogram_mode_button_log, _("logarithmic histogram"));
+ gtk_box_pack_start(GTK_BOX(g->button_box), g->histogram_mode_button_log, FALSE, FALSE, 0);
+ g_signal_connect(G_OBJECT(g->histogram_mode_button_log), "button-press-event",
+ G_CALLBACK(_histogram_mode_clicked), self);
+
+ // To get all histogram mode buttons into the correct initial state,
+ // we call the button that is "1 click before" what we want.
+ _histogram_mode_cycle(g->histogram_mode_button_linear, g);
+
+ // Hide button box initially (will show on mouseover)
+ gtk_widget_set_no_show_all(g->button_box, TRUE);
+
+ // Create overlay
+ GtkWidget *histogram_overlay = gtk_overlay_new();
+ gtk_container_add(GTK_CONTAINER(histogram_overlay), GTK_WIDGET(g->area));
+ gtk_overlay_add_overlay(GTK_OVERLAY(histogram_overlay), g->button_box);
+
+ // Event box to capture enter/leave events
+ GtkWidget *histogram_eventbox = gtk_event_box_new();
+ gtk_container_add(GTK_CONTAINER(histogram_eventbox), histogram_overlay);
+ gtk_widget_add_events(histogram_eventbox, GDK_ENTER_NOTIFY_MASK | GDK_LEAVE_NOTIFY_MASK);
+
+ g_signal_connect(G_OBJECT(histogram_eventbox), "enter-notify-event",
+ G_CALLBACK(_histogram_eventbox_enter), self);
+ g_signal_connect(G_OBJECT(histogram_eventbox), "leave-notify-event",
+ G_CALLBACK(_histogram_eventbox_leave), self);
+
+ // Main Drawing area wrapper
+ GtkWidget *wrapper = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); // for CSS size
+ gtk_box_pack_start(GTK_BOX(wrapper), histogram_eventbox, TRUE, TRUE, 0);
+ g_object_set_data(G_OBJECT(wrapper), "iop-instance", self);
+ gtk_widget_set_name(GTK_WIDGET(wrapper), "toneeqgraph");
+ dt_action_define_iop(self, NULL, N_("graph"), GTK_WIDGET(wrapper), NULL);
+ // Notebook (tabs)
static dt_action_def_t notebook_def = { };
g->notebook = dt_ui_notebook_new(¬ebook_def);
dt_action_define_iop(self, NULL, N_("page"), GTK_WIDGET(g->notebook), ¬ebook_def);
- // Simple view
+ // Main view (former "advanced" page)
+ self->widget = dt_ui_notebook_page(g->notebook, N_("alignment"), NULL);
+ gtk_widget_set_vexpand(GTK_WIDGET(self->widget), TRUE);
+
+ // Row with align button
+ GtkWidget *box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
+
+ g->align_button = gtk_toggle_button_new_with_label(N_("fully align"));
+ _fill_button_tooltip(g->align_button, 0.0f, 0.0f, self);
+ g_signal_connect(g->align_button, "button-press-event", G_CALLBACK(_align_button_clicked), self);
+
+ g->shift_button = gtk_toggle_button_new_with_label(N_("align exposure/shift"));
+ _fill_button_tooltip(g->shift_button, 0.0f, 0.0f, self);
+ g_signal_connect(g->shift_button, "button-press-event", G_CALLBACK(_align_button_clicked), self);
+
+ // Calculate the width of the longer label and set both buttons to that width
+ PangoLayout *layout = gtk_widget_create_pango_layout(g->shift_button, gtk_button_get_label(GTK_BUTTON(g->shift_button)));
+ int w, h;
+ pango_layout_get_pixel_size(layout, &w, &h);
+ g_object_unref(layout);
+
+ w += 20; // Add padding for button borders/margins
+ gtk_widget_set_size_request(g->align_button, w, -1);
+ gtk_widget_set_size_request(g->shift_button, w, -1);
+
+ gtk_box_pack_start(GTK_BOX(box), g->align_button, TRUE, TRUE, 0);
+ gtk_box_pack_start(GTK_BOX(box), g->shift_button, TRUE, TRUE, 0);
+
+ gtk_box_pack_start(GTK_BOX(self->widget), box, FALSE, FALSE, 0);
+
+ GtkWidget *section_label = dt_ui_section_label_new(C_("section", "custom alignment"));
+ gtk_box_pack_start(GTK_BOX(self->widget),
+ section_label,
+ FALSE, FALSE, 0);
+
+ g->post_shift = dt_bauhaus_slider_from_params(self, "post_shift");
+ dt_bauhaus_slider_set_soft_range(g->post_shift, -4.0, 4.0);
+ gtk_widget_set_tooltip_text
+ (g->post_shift,
+ _("set the mask exposure / shift the histogram"));
+
+ g->post_scale = dt_bauhaus_slider_from_params(self, "post_scale");
+ dt_bauhaus_slider_set_soft_range(g->post_scale, -2.0, 2.0);
+ gtk_widget_set_tooltip_text
+ (g->post_scale,
+ _("set the mask contrast / scale the histogram"));
+
+ g->post_pivot = dt_bauhaus_slider_from_params(self, "post_pivot");
+ gtk_widget_set_tooltip_text
+ (g->post_pivot,
+ _("pivot point for scaling the histogram"));
+
+ self->widget = dt_ui_notebook_page(g->notebook, N_("exposure"), NULL);
+
+ g->global_exposure = dt_bauhaus_slider_from_params(self, "global_exposure");
+ dt_bauhaus_slider_set_soft_range(g->global_exposure, -4.0, 4.0);
+ gtk_widget_set_tooltip_text
+ (g->global_exposure,
+ _("globally brighten/darken the image"));
+
+ g->scale_curve = dt_bauhaus_slider_from_params(self, "scale_curve");
+ dt_bauhaus_slider_set_soft_range(g->scale_curve, 0.0, 2.0);
+ gtk_widget_set_tooltip_text
+ (g->global_exposure,
+ _("scale the curve vertically"));
- self->widget = dt_ui_notebook_page(g->notebook, N_("simple"), NULL);
+ g->smoothing = dt_bauhaus_slider_new_with_range(self, -2.33f, +1.67f, 0, 0.0f, 2);
+ dt_bauhaus_slider_set_soft_range(g->smoothing, -1.0f, 1.0f);
+ dt_bauhaus_widget_set_label(g->smoothing, NULL, N_("curve smoothing"));
+ gtk_widget_set_tooltip_text(g->smoothing,
+ _("positive values will produce more progressive tone transitions\n"
+ "but the curve might become oscillatory in some settings.\n"
+ "negative values will avoid oscillations and behave more robustly\n"
+ "but may produce brutal tone transitions and damage local contrast."));
+ gtk_box_pack_start(GTK_BOX(self->widget), g->smoothing, FALSE, FALSE, 0);
+ g_signal_connect(G_OBJECT(g->smoothing), "value-changed", G_CALLBACK(_smoothing_callback), self);
+ g_signal_connect(G_OBJECT(g->smoothing), "button-press-event", G_CALLBACK(_smoothing_button_press), self);
+
+ g->curve_type = dt_bauhaus_combobox_from_params(self, "curve_type");
+ dt_bauhaus_widget_set_label(g->curve_type, NULL, N_("curve type"));
+ gtk_widget_set_tooltip_text(
+ g->curve_type, _("curve_type"));
+
+ // sliders section (former "simple" page)
+ dt_gui_new_collapsible_section(&g->sliders_section, "plugins/darkroom/toneequal/expand_sliders", _("sliders"),
+ GTK_BOX(self->widget), DT_ACTION(self));
+ gtk_widget_set_tooltip_text(g->sliders_section.expander, _("sliders"));
+
+ // TODO MF dirty hack:
+ // dt_gui_new_collapsible_section always uses gtk_box_pack_end to align the collapsible
+ // section at the bottom of the parent container.
+ // I want the collapsible section to be aligned at the top, therefore I remove it from the
+ // parent again and pack it manually.
+ g_object_ref(g->sliders_section.expander);
+ gtk_container_remove(GTK_CONTAINER(self->widget), g->sliders_section.expander);
+ gtk_box_pack_start(GTK_BOX(self->widget), g->sliders_section.expander, FALSE, FALSE, 0);
+ g_object_unref(g->sliders_section.expander);
+
+ self->widget = GTK_WIDGET(g->sliders_section.container);
g->noise = dt_bauhaus_slider_from_params(self, "noise");
dt_bauhaus_slider_set_format(g->noise, _(" EV"));
@@ -3269,118 +5409,138 @@ void gui_init(dt_iop_module_t *self)
dt_bauhaus_widget_set_label(g->whites, N_("simple"), N_("-1 EV"));
dt_bauhaus_widget_set_label(g->speculars, N_("simple"), N_("+0 EV"));
- // Advanced view
+ // Masking options
+ self->widget = dt_ui_notebook_page(g->notebook, N_("masking"), NULL);
+ GtkWidget *masking_page = self->widget;
+
+ // guided filter section
+ dt_gui_new_collapsible_section(&g->guided_filter_section, "plugins/darkroom/toneequal/expand_sliders",
+ _("guided filter"), GTK_BOX(masking_page), DT_ACTION(self));
+ gtk_widget_set_tooltip_text(g->guided_filter_section.expander, _("guided filter"));
+
+ // Hack to make the collapsible section align at the top
+ g_object_ref(g->guided_filter_section.expander);
+ gtk_container_remove(GTK_CONTAINER(masking_page), g->guided_filter_section.expander);
+ gtk_box_pack_start(GTK_BOX(masking_page), g->guided_filter_section.expander, FALSE, FALSE, 0);
+ g_object_unref(g->guided_filter_section.expander);
+
+ self->widget = GTK_WIDGET(g->guided_filter_section.container);
+
+ g->filter = dt_bauhaus_combobox_from_params(self, N_("filter"));
+ dt_bauhaus_widget_set_label(g->filter, NULL, N_("preserve details"));
+ gtk_widget_set_tooltip_text(
+ g->filter, _("'no' affects global and local contrast (safe if you only add contrast)\n"
+ "'guided filter' only affects global contrast and tries to preserve local contrast\n"
+ "'averaged guided filter' is a geometric mean of 'no' and 'guided filter' methods\n"
+ "'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"
+ "'averaged EIGF' is a geometric mean of 'no' and 'exposure-independent"
+ " guided filter' methods"));
- self->widget = dt_ui_notebook_page(g->notebook, N_("advanced"), NULL);
+ g->iterations = dt_bauhaus_slider_from_params(self, "iterations");
+ dt_bauhaus_slider_set_soft_max(g->iterations, 5);
+ gtk_widget_set_tooltip_text(g->iterations, _("number of passes of guided filter to apply\n"
+ "helps diffusing the edges of the filter at the expense of speed"));
- g->area = GTK_DRAWING_AREA(gtk_drawing_area_new());
- GtkWidget *wrapper = dt_gui_vbox(g->area);
- g_object_set_data(G_OBJECT(wrapper), "iop-instance", self);
- gtk_widget_set_name(GTK_WIDGET(wrapper), "toneeqgraph");
- dt_action_define_iop(self, NULL, N_("graph"), GTK_WIDGET(wrapper), NULL);
- gtk_widget_add_events(GTK_WIDGET(g->area),
- GDK_POINTER_MOTION_MASK | darktable.gui->scroll_mask
- | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK
- | GDK_ENTER_NOTIFY_MASK | GDK_LEAVE_NOTIFY_MASK);
- gtk_widget_set_vexpand(GTK_WIDGET(g->area), TRUE);
- gtk_widget_set_can_focus(GTK_WIDGET(g->area), TRUE);
- g_signal_connect(G_OBJECT(g->area), "draw", G_CALLBACK(area_draw), self);
- g_signal_connect(G_OBJECT(g->area), "button-press-event",
- G_CALLBACK(area_button_press), self);
- g_signal_connect(G_OBJECT(g->area), "button-release-event",
- G_CALLBACK(area_button_release), self);
- g_signal_connect(G_OBJECT(g->area), "leave-notify-event",
- G_CALLBACK(area_enter_leave_notify), self);
- g_signal_connect(G_OBJECT(g->area), "enter-notify-event",
- G_CALLBACK(area_enter_leave_notify), self);
- g_signal_connect(G_OBJECT(g->area), "motion-notify-event",
- G_CALLBACK(area_motion_notify), self);
- g_signal_connect(G_OBJECT(g->area), "scroll-event",
- G_CALLBACK(area_scroll), self);
- gtk_widget_set_tooltip_text(GTK_WIDGET(g->area), _("double-click to reset the curve"));
+ g->blending = dt_bauhaus_slider_from_params(self, "blending");
+ dt_bauhaus_slider_set_soft_range(g->blending, 1.0, 45.0);
+ dt_bauhaus_slider_set_format(g->blending, "%");
+ gtk_widget_set_tooltip_text(g->blending, _("diameter of the blur in percent of the largest image size\n"
+ "warning: big values of this parameter can make the darkroom\n"
+ "preview much slower if denoise profiled is used."));
+
+ g->feathering = dt_bauhaus_slider_from_params(self, "feathering");
+ dt_bauhaus_slider_set_soft_range(g->feathering, 0.1, 50.0);
+ gtk_widget_set_tooltip_text(g->feathering, _("precision of the feathering:\n"
+ "higher values force the mask to follow edges more closely\n"
+ "but may void the effect of the smoothing\n"
+ "lower values give smoother gradients and better smoothing\n"
+ "but may lead to inaccurate edges taping and halos"));
+
+ // Collapsible section mask pre-processing
+ dt_gui_new_collapsible_section(&g->pre_processing_section, "plugins/darkroom/toneequal/expand_mask_proprocessing",
+ _("mask pre-processing"), GTK_BOX(masking_page), DT_ACTION(self));
+ gtk_widget_set_tooltip_text(g->pre_processing_section.expander, _("mask pre-processing"));
+
+ // Hack to make the collapsible section align at the top
+ g_object_ref(g->pre_processing_section.expander);
+ gtk_container_remove(GTK_CONTAINER(masking_page), g->pre_processing_section.expander);
+ gtk_box_pack_start(GTK_BOX(masking_page), g->pre_processing_section.expander, FALSE, FALSE, 0);
+ g_object_unref(g->pre_processing_section.expander);
+
+ self->widget = GTK_WIDGET(g->pre_processing_section.container);
- g->smoothing = dt_bauhaus_slider_new_with_range(self, -2.33f, +1.67f, 0, 0.0f, 2);
- dt_bauhaus_slider_set_soft_range(g->smoothing, -1.0f, 1.0f);
- dt_bauhaus_widget_set_label(g->smoothing, NULL, N_("curve smoothing"));
- gtk_widget_set_tooltip_text
- (g->smoothing,
- _("positive values will produce more progressive tone transitions\n"
- "but the curve might become oscillatory in some settings.\n"
- "negative values will avoid oscillations and behave more robustly\n"
- "but may produce brutal tone transitions and damage local contrast."));
- g_signal_connect(G_OBJECT(g->smoothing), "value-changed",
- G_CALLBACK(smoothing_callback), self);
- dt_gui_box_add(self->widget, wrapper, g->smoothing);
-
g->exposure_boost = dt_bauhaus_slider_from_params(self, "exposure_boost");
dt_bauhaus_slider_set_soft_range(g->exposure_boost, -4.0, 4.0);
dt_bauhaus_slider_set_format(g->exposure_boost, _(" EV"));
gtk_widget_set_tooltip_text
(g->exposure_boost,
- _("use this to slide the mask average exposure along channels\n"
- "for a better control of the exposure correction with the available nodes."));
- dt_bauhaus_widget_set_quad(g->exposure_boost, self, dtgtk_cairo_paint_wand, FALSE, auto_adjust_exposure_boost,
- _("auto-adjust the average exposure"));
+ _("use this to slide the mask average exposure along sliders\n"
+ "for a better control of the exposure correction with the available nodes."));
+ dt_bauhaus_widget_set_quad(g->exposure_boost, self, dtgtk_cairo_paint_wand, FALSE, _auto_adjust_exposure_boost,
+ _("auto-adjust the average exposure"));
g->contrast_boost = dt_bauhaus_slider_from_params(self, "contrast_boost");
dt_bauhaus_slider_set_soft_range(g->contrast_boost, -2.0, 2.0);
dt_bauhaus_slider_set_format(g->contrast_boost, _(" EV"));
gtk_widget_set_tooltip_text
(g->contrast_boost,
- _("use this to counter the averaging effect of the guided filter\n"
- "and dilate the mask contrast around -4EV\n"
- "this allows to spread the exposure histogram over more channels\n"
- "for a better control of the exposure correction."));
- dt_bauhaus_widget_set_quad(g->contrast_boost, self, dtgtk_cairo_paint_wand, FALSE, auto_adjust_contrast_boost,
- _("auto-adjust the contrast"));
-
- // Masking options
+ _("use this to counter the averaging effect of the guided filter\n"
+ "and dilate the mask contrast around -4EV\n"
+ "this allows to spread the exposure histogram over more sliders\n"
+ "for a better control of the exposure correction."));
+ dt_bauhaus_widget_set_quad(g->contrast_boost, self, dtgtk_cairo_paint_wand, FALSE, _auto_adjust_contrast_boost,
+ _("auto-adjust the contrast"));
- self->widget = dt_ui_notebook_page(g->notebook, N_("masking"), NULL);
+ self->widget = masking_page;
- g->method = dt_bauhaus_combobox_from_params(self, "method");
- gtk_widget_set_tooltip_text
- (g->method,
- _("preview the mask and chose the estimator that gives you the\n"
- "higher contrast between areas to dodge and areas to burn"));
+ // Collapsible section "advanced"
+ dt_gui_new_collapsible_section(&g->lum_estimator_section, "plugins/darkroom/toneequal/expand_luminance_estimator",
+ _("luminance estimator"), GTK_BOX(masking_page), DT_ACTION(self));
+ gtk_widget_set_tooltip_text(g->lum_estimator_section.expander, _("luminance estimator"));
- g->details = dt_bauhaus_combobox_from_params(self, N_("details"));
- gtk_widget_set_tooltip_text
- (g->details,
- _("'no' affects global and local contrast (safe if you only add contrast)\n"
- "'guided filter' only affects global contrast and tries to preserve local contrast\n"
- "'averaged guided filter' is a geometric mean of 'no' and 'guided filter' methods\n"
- "'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"
- "'averaged EIGF' is a geometric mean of 'no' and 'exposure-independent"
- " guided filter' methods"));
+ // Hack to make the collapsible section align at the top
+ g_object_ref(g->lum_estimator_section.expander);
+ gtk_container_remove(GTK_CONTAINER(masking_page), g->lum_estimator_section.expander);
+ gtk_box_pack_start(GTK_BOX(masking_page), g->lum_estimator_section.expander, FALSE, FALSE, 0);
+ g_object_unref(g->lum_estimator_section.expander);
- g->iterations = dt_bauhaus_slider_from_params(self, "iterations");
- dt_bauhaus_slider_set_soft_max(g->iterations, 5);
- gtk_widget_set_tooltip_text
- (g->iterations,
- _("number of passes of guided filter to apply\n"
- "helps diffusing the edges of the filter at the expense of speed"));
+ self->widget = GTK_WIDGET(g->lum_estimator_section.container);
- g->blending = dt_bauhaus_slider_from_params(self, "blending");
- dt_bauhaus_slider_set_soft_range(g->blending, 1.0, 45.0);
- dt_bauhaus_slider_set_format(g->blending, "%");
- gtk_widget_set_tooltip_text
- (g->blending,
- _("diameter of the blur in percent of the largest image size\n"
- "warning: big values of this parameter can make the darkroom\n"
- "preview much slower if denoise profiled is used."));
+ g->lum_estimator = dt_bauhaus_combobox_from_params(self, "lum_estimator");
+ gtk_widget_set_tooltip_text(g->lum_estimator, _("preview the mask and chose the estimator that gives you the\n"
+ "higher contrast between areas to dodge and areas to burn"));
- g->feathering = dt_bauhaus_slider_from_params(self, "feathering");
- dt_bauhaus_slider_set_soft_range(g->feathering, 0.1, 50.0);
- gtk_widget_set_tooltip_text
- (g->feathering,
- _("precision of the feathering:\n"
- "higher values force the mask to follow edges more closely\n"
- "but may void the effect of the smoothing\n"
- "lower values give smoother gradients and better smoothing\n"
- "but may lead to inaccurate edges taping and halos"));
+ g->lum_estimator_R = dt_bauhaus_slider_from_params(self, "lum_estimator_R");
+ dt_bauhaus_slider_set_soft_range(g->lum_estimator_R, 0.0, 1.0);
+ gtk_widget_set_tooltip_text(g->lum_estimator_R, _("red weight for greyscale mask\n"));
+
+ g->lum_estimator_G = dt_bauhaus_slider_from_params(self, "lum_estimator_G");
+ dt_bauhaus_slider_set_soft_range(g->lum_estimator_G, 0.0, 1.0);
+ gtk_widget_set_tooltip_text(g->lum_estimator_G, _("green weight for greyscale mask\n"));
+
+ g->lum_estimator_B = dt_bauhaus_slider_from_params(self, "lum_estimator_B");
+ dt_bauhaus_slider_set_soft_range(g->lum_estimator_B, 0.0, 1.0);
+ gtk_widget_set_tooltip_text(g->lum_estimator_B, _("blue weight for greyscale mask\n"));
+
+ g->lum_estimator_normalize = dt_bauhaus_toggle_from_params
+ (self, "lum_estimator_normalize");
+
+ self->widget = masking_page;
+
+ // Collapsible section "advanced"
+ dt_gui_new_collapsible_section(&g->adv_section, "plugins/darkroom/toneequal/expand_advanced_masking",
+ _("advanced"), GTK_BOX(masking_page), DT_ACTION(self));
+ gtk_widget_set_tooltip_text(g->adv_section.expander, _("advanced"));
+
+ // Hack to make the collapsible section align at the top
+ g_object_ref(g->adv_section.expander);
+ gtk_container_remove(GTK_CONTAINER(masking_page), g->adv_section.expander);
+ gtk_box_pack_start(GTK_BOX(masking_page), g->adv_section.expander, FALSE, FALSE, 0);
+ g_object_unref(g->adv_section.expander);
+
+ self->widget = GTK_WIDGET(g->adv_section.container);
g->quantization = dt_bauhaus_slider_from_params(self, "quantization");
@@ -3391,25 +5551,32 @@ void gui_init(dt_iop_module_t *self)
"higher values posterize the luminance mask to help the guiding\n"
"produce piece-wise smooth areas when using high feathering values"));
+
// start building top level widget
+ self->widget = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
+
const int active_page = dt_conf_get_int("plugins/darkroom/toneequal/gui_page");
gtk_widget_show(gtk_notebook_get_nth_page(g->notebook, active_page));
gtk_notebook_set_current_page(g->notebook, active_page);
+ gtk_box_pack_start(GTK_BOX(self->widget), GTK_WIDGET(wrapper), TRUE, TRUE, 0);
+
g_signal_connect(G_OBJECT(g->notebook), "button-press-event",
- G_CALLBACK(notebook_button_press), self);
+ G_CALLBACK(_notebook_button_press), self);
+ gtk_box_pack_start(GTK_BOX(self->widget), GTK_WIDGET(g->notebook), FALSE, FALSE, 0);
+ GtkWidget *hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
+ gtk_box_pack_start(GTK_BOX(hbox),
+ dt_ui_label_new(_("display exposure mask")), TRUE, TRUE, 0);
g->show_luminance_mask = dt_iop_togglebutton_new
(self, NULL,
- N_("display exposure mask"), NULL, G_CALLBACK(show_luminance_mask_callback),
- FALSE, 0, 0, dtgtk_cairo_paint_showmask, NULL);
+ N_("display exposure mask"), NULL, G_CALLBACK(_show_luminance_mask_callback),
+ FALSE, 0, 0, dtgtk_cairo_paint_showmask, hbox);
dt_gui_add_class(g->show_luminance_mask, "dt_transparent_background");
dtgtk_togglebutton_set_paint(DTGTK_TOGGLEBUTTON(g->show_luminance_mask),
dtgtk_cairo_paint_showmask, 0, NULL);
dt_gui_add_class(g->show_luminance_mask, "dt_bauhaus_alignment");
- self->widget = dt_gui_vbox(g->notebook,
- dt_gui_hbox(dt_gui_expand(dt_ui_label_new(_("display exposure mask"))),
- g->show_luminance_mask));
+ gtk_box_pack_start(GTK_BOX(self->widget), hbox, FALSE, FALSE, 0);
// Force UI redraws when pipe starts/finishes computing and switch cursors
DT_CONTROL_SIGNAL_HANDLE(DT_SIGNAL_DEVELOP_PREVIEW_PIPE_FINISHED, _develop_preview_pipe_finished_callback);
@@ -3425,8 +5592,8 @@ void gui_cleanup(dt_iop_module_t *self)
dt_conf_set_int("plugins/darkroom/toneequal/gui_page",
gtk_notebook_get_current_page (g->notebook));
- dt_free_align(g->thumb_preview_buf);
- dt_free_align(g->full_preview_buf);
+ dt_free_align(g->preview_buf);
+ dt_free_align(g->full_buf);
if(g->desc) pango_font_description_free(g->desc);
if(g->layout) g_object_unref(g->layout);