From 5a24e886eabe5c35b2335bc4cf026bb92567770c Mon Sep 17 00:00:00 2001
From: Martin Straeten <39386816+MStraeten@users.noreply.github.com>
Date: Sun, 19 Jul 2026 23:37:50 +0200
Subject: [PATCH 01/12] add moudule satcurvergb
---
data/kernels/programs.conf | 1 +
data/kernels/satcurve.cl | 157 ++++++
src/common/iop_order.c | 6 +
src/iop/CMakeLists.txt | 1 +
src/iop/satcurvergb.c | 1090 ++++++++++++++++++++++++++++++++++++
5 files changed, 1255 insertions(+)
create mode 100644 data/kernels/satcurve.cl
create mode 100644 src/iop/satcurvergb.c
diff --git a/data/kernels/programs.conf b/data/kernels/programs.conf
index 15ddf324b645..16b3addde72d 100644
--- a/data/kernels/programs.conf
+++ b/data/kernels/programs.conf
@@ -42,3 +42,4 @@ capture.cl 38
agx.cl 39
colorharmonizer.cl 40
overlay.cl 41
+satcurve.cl 42
\ No newline at end of file
diff --git a/data/kernels/satcurve.cl b/data/kernels/satcurve.cl
new file mode 100644
index 000000000000..e73ac332311c
--- /dev/null
+++ b/data/kernels/satcurve.cl
@@ -0,0 +1,157 @@
+/*
+ This file is part of darktable,
+ Copyright (C) 2009-2026 darktable developers.
+
+ darktable is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ darktable is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with darktable. If not, see .
+*/
+
+#include "common.h"
+#include "colorspace.h"
+#include "color_conversion.h"
+
+
+#define DT_IOP_SATCURVE_RES 256
+
+// periodic lookup in the hue-indexed gamut LUT; mirrors satcurve_lookup_gamut() on CPU
+static inline float satcurve_lookup_gamut_cl(global const float *const gamut_lut, const float h)
+{
+ const float position = (h + M_PI_F) * (float)LUT_ELEM / (2.0f * M_PI_F);
+ const float position_floor = floor(position);
+ const int bin0 = ((int)position_floor) % LUT_ELEM;
+ const int bin1 = (bin0 + 1) % LUT_ELEM;
+ const float f = position - position_floor;
+ return gamut_lut[bin0] * (1.f - f) + gamut_lut[bin1] * f;
+}
+
+// smoothly map x from knee onwards towards maximum; mirrors satcurve_soft_clip() on CPU
+static inline float satcurve_soft_clip_cl(const float x, const float knee, const float maximum)
+{
+ if(x <= knee) return x;
+ const float range = maximum - knee;
+ if(range <= 0.f) return maximum;
+ return knee + range * (1.f - dtcl_exp(-(x - knee) / range));
+}
+
+// linear interpolation into the 256-entry curve LUT
+static inline float satcurve_lookup_lut_cl(global const float *const lut, const float x)
+{
+ const float position = clamp(x, 0.f, 1.f) * (DT_IOP_SATCURVE_RES - 1);
+ const int i = min((int)position, DT_IOP_SATCURVE_RES - 2);
+ return lut[i] + (position - i) * (lut[i + 1] - lut[i]);
+}
+
+// mirrors clip_jz_chroma() on CPU : test-converts to L'M'S' and clips Cz so
+// the JzAzBz -> XYZ back-transform never produces negative LMS values
+static inline float clip_jz_chroma_cl(const float Jz, const float Cz, const float ch, const float sh)
+{
+ const float d0 = 1.6295499532821566e-11f;
+ const float dd = -0.56f;
+ float Iz = (Jz + d0) / (1.f + dd - dd * (Jz + d0));
+ Iz = fmax(Iz, 0.f);
+
+ const float4 AI[3] = { { 1.0f, 0.1386050432715393f, 0.0580473161561189f, 0.0f },
+ { 1.0f, -0.1386050432715393f, -0.0580473161561189f, 0.0f },
+ { 1.0f, -0.0960192420263190f, -0.8118918960560390f, 0.0f } };
+ const float4 izab = { Iz, Cz * ch, Cz * sh, 0.f };
+ const float lms0 = dot(AI[0], izab);
+ const float lms1 = dot(AI[1], izab);
+ const float lms2 = dot(AI[2], izab);
+
+ float max_c = Cz;
+ if(lms0 < 0.f) max_c = fmin(max_c, -Iz / (AI[0].y * ch + AI[0].z * sh));
+ if(lms1 < 0.f) max_c = fmin(max_c, -Iz / (AI[1].y * ch + AI[1].z * sh));
+ if(lms2 < 0.f) max_c = fmin(max_c, -Iz / (AI[2].y * ch + AI[2].z * sh));
+ return fmax(max_c, 0.f);
+}
+
+typedef enum dt_iop_satcurve_formula_t
+{
+ DT_IOP_SATCURVE_JZAZBZ = 0,
+ DT_IOP_SATCURVE_DTUCS = 1
+} dt_iop_satcurve_formula_t;
+
+kernel void
+satcurvergb(read_only image2d_t in, write_only image2d_t out,
+ const int width, const int height,
+ constant const float *const matrix_in, constant const float *const matrix_out,
+ global const float *const lut, global const float *const gamut_lut,
+ const int formula, const float L_white)
+{
+ const int x = get_global_id(0);
+ const int y = get_global_id(1);
+ if(x >= width || y >= height) return;
+
+ float4 pix_in = readpixel(in, x, y);
+ float4 rgb = fmax(pix_in, 0.f);
+
+ float4 xyz = matrix_product_float4(rgb, matrix_in);
+ float4 pixout;
+
+ if(formula == DT_IOP_SATCURVE_DTUCS)
+ {
+ float4 xyY = dt_D65_XYZ_to_xyY(xyz);
+ float4 JCH = xyY_to_dt_UCS_JCH(xyY, L_white);
+ float4 HCB = dt_UCS_JCH_to_HCB(JCH);
+
+ const float max_colorfulness = fmax(satcurve_lookup_gamut_cl(gamut_lut, JCH.z), FLT_MIN);
+ const float max_chroma = 15.932993652962535f
+ * dtcl_pow(JCH.x / L_white, 0.6523997524738018f)
+ * dtcl_pow(max_colorfulness, 0.6007557017508491f) / L_white;
+
+ const float4 JCH_gamut_boundary = { JCH.x, max_chroma, JCH.z, 0.f };
+ const float4 HSB_gamut_boundary = dt_UCS_JCH_to_HSB(JCH_gamut_boundary);
+
+ float4 HSB = { HCB.x, HCB.z > 0.f ? HCB.y / HCB.z : 0.f, HCB.z, 0.f };
+
+ const float gamut_s = fmax(HSB_gamut_boundary.y, FLT_MIN);
+ const float s_in_norm = HSB.y / gamut_s;
+ const float c = clamp(satcurve_lookup_lut_cl(lut, s_in_norm), 0.f, 1.f);
+ HSB.y = fmax(HSB.y * (2.f * c), 0.f);
+ HSB.y = satcurve_soft_clip_cl(HSB.y, 0.8f * gamut_s, gamut_s);
+
+ JCH = dt_UCS_HSB_to_JCH(HSB);
+ xyY = dt_UCS_JCH_to_xyY(JCH, L_white);
+ xyz = dt_xyY_to_XYZ(xyY);
+ }
+ else
+ {
+ float4 jab = XYZ_to_JzAzBz(xyz);
+ const float Jz = fmax(jab.x, 0.f);
+ const float Cz = hypot(jab.y, jab.z);
+ const float h = atan2(jab.z, jab.y);
+ const float ch = dtcl_cos(h), sh = dtcl_sin(h);
+ const float gamut = fmax(satcurve_lookup_gamut_cl(gamut_lut, h), FLT_MIN);
+ const float s_in_norm = (Jz > 0.f ? Cz / Jz : 0.f) / gamut;
+ const float c = clamp(satcurve_lookup_lut_cl(lut, s_in_norm), 0.f, 1.f);
+ const float s_out = satcurve_soft_clip_cl(fmax(s_in_norm * (2.f * c), 0.f), 0.8f, 1.f) * gamut;
+
+ const float r = hypot(Jz, Cz);
+ // T = atan(s_out); cos(T) = 1/sqrt(1+s_out^2), sin(T) = s_out/sqrt(1+s_out^2)
+ // avoids one atan + one cos + one sin per pixel
+ const float inv_norm = 1.f / sqrt(1.f + s_out * s_out);
+ const float Jz_out = r * inv_norm;
+ const float Cz_out = clip_jz_chroma_cl(Jz_out, r * s_out * inv_norm, ch, sh);
+
+ jab.x = Jz_out;
+ jab.y = Cz_out * ch;
+ jab.z = Cz_out * sh;
+ xyz = JzAzBz_2_XYZ(jab);
+ }
+
+ pixout = matrix_product_float4(xyz, matrix_out);
+ pixout = fmax(pixout, 0.f);
+ pixout.w = pix_in.w;
+
+ write_imagef(out, (int2)(x, y), pixout);
+}
\ No newline at end of file
diff --git a/src/common/iop_order.c b/src/common/iop_order.c
index 1cb9effaa6f9..2c548318bb93 100644
--- a/src/common/iop_order.c
+++ b/src/common/iop_order.c
@@ -128,6 +128,7 @@ const dt_iop_order_entry_t legacy_order[] = {
{ {33.0f }, "colorbalance", 0},
{ {33.2f }, "colorequal", 0},
{ {33.5f }, "colorbalancergb", 0},
+ { {33.7f }, "satcurvergb", 0},
{ {34.0f }, "colorize", 0},
{ {35.0f }, "colortransfer", 0},
{ {36.0f }, "colormapping", 0},
@@ -250,6 +251,7 @@ const dt_iop_order_entry_t v30_order[] = {
{ {41.0f }, "colorbalance", 0}, // scene-referred color manipulation
{ {41.2f }, "colorequal", 0},
{ {41.5f }, "colorbalancergb", 0}, // scene-referred color manipulation
+ { {41.7f }, "satcurvergb", 0}, // scene-referred saturation adjustment
{ {42.0f }, "rgbcurve", 0}, // really versatile way to edit colour in scene-referred and display-referred workflow
{ {43.0f }, "rgblevels", 0}, // same
{ {44.0f }, "basecurve", 0}, // conversion from scene-referred to display referred, reverse-engineered
@@ -369,6 +371,7 @@ const dt_iop_order_entry_t v50_order[] = {
{ {41.0f }, "colorbalance", 0}, // scene-referred color manipulation
{ {41.2f }, "colorequal", 0},
{ {41.5f }, "colorbalancergb", 0}, // scene-referred color manipulation
+ { {41.7f }, "satcurvergb", 0}, // scene-referred saturation adjustment
{ {42.0f }, "rgbcurve", 0}, // really versatile way to edit colour in scene-referred and display-referred workflow
{ {43.0f }, "rgblevels", 0}, // same
{ {44.0f }, "basecurve", 0}, // conversion from scene-referred to display referred, reverse-engineered
@@ -488,6 +491,7 @@ const dt_iop_order_entry_t v30_jpg_order[] = {
{ { 41.0f }, "colorbalance", 0 }, // scene-referred color manipulation
{ { 41.2f }, "colorequal", 0 },
{ { 41.5f }, "colorbalancergb", 0 }, // scene-referred color manipulation
+ { { 41.7f }, "satcurvergb", 0 }, // scene-referred saturation adjustment
{ { 42.0f }, "rgbcurve", 0 }, // really versatile way to edit colour in scene-referred and display-referred
// workflow
{ { 43.0f }, "rgblevels", 0 }, // same
@@ -610,6 +614,7 @@ const dt_iop_order_entry_t v50_jpg_order[] = {
{ { 41.0f }, "colorbalance", 0 }, // scene-referred color manipulation
{ { 41.2f }, "colorequal", 0 },
{ { 41.5f }, "colorbalancergb", 0 }, // scene-referred color manipulation
+ { { 41.7f }, "satcurvergb", 0 }, // scene-referred saturation adjustment
{ { 42.0f }, "rgbcurve", 0 }, // really versatile way to edit colour in scene-referred and display-referred
// workflow
{ { 43.0f }, "rgblevels", 0 }, // same
@@ -727,6 +732,7 @@ void dt_ioppr_migrate_legacy_iop_order_list(GList *iop_order_list)
_insert_before(iop_order_list, "negadoctor", "censorize");
_insert_before(iop_order_list, "negadoctor", "primaries");
_insert_before(iop_order_list, "rgbcurve", "colorbalancergb");
+ _insert_before(iop_order_list, "rgbcurve", "satcurvergb");
_insert_before(iop_order_list, "ashift", "cacorrectrgb");
_insert_before(iop_order_list, "graduatednd", "crop");
_insert_before(iop_order_list, "flip", "enlargecanvas");
diff --git a/src/iop/CMakeLists.txt b/src/iop/CMakeLists.txt
index 92a69c1037f9..5a00eeae8206 100644
--- a/src/iop/CMakeLists.txt
+++ b/src/iop/CMakeLists.txt
@@ -157,6 +157,7 @@ add_iop(agx "agx.c")
add_iop(primaries "primaries.c")
add_iop(colorequal "colorequal.c")
add_iop(rasterfile "rasterfile.c")
+add_iop(satcurvergb "satcurvergb.c")
if(Rsvg2_FOUND)
add_iop(watermark "watermark.c")
diff --git a/src/iop/satcurvergb.c b/src/iop/satcurvergb.c
new file mode 100644
index 000000000000..51e55d45e743
--- /dev/null
+++ b/src/iop/satcurvergb.c
@@ -0,0 +1,1090 @@
+/*
+ This file is part of darktable,
+ Copyright (C) 2026 darktable developers.
+
+ darktable is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ darktable is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with darktable. If not, see .
+*/
+// our includes go first:
+#include "bauhaus/bauhaus.h"
+#include "common/chromatic_adaptation.h"
+#include "common/color_picker.h"
+#include "common/darktable_ucs_22_helpers.h"
+#include "common/dtpthread.h"
+#include "common/gamut_mapping.h"
+#include "common/imagebuf.h"
+#include "common/math.h"
+#include "develop/imageop.h"
+#include "develop/imageop_gui.h"
+#include "develop/openmp_maths.h"
+#include "dtgtk/drawingarea.h"
+#include "gui/color_picker_proxy.h"
+#include "gui/draw.h"
+#include "gui/gtk.h"
+#include "gui/presets.h"
+#include "iop/iop_api.h"
+
+#define DT_IOP_SATCURVE_MAXNODES 20
+#define DT_IOP_SATCURVE_RES 256
+#define DT_IOP_SATCURVE_HIST_RES 256
+#define DT_IOP_SATCURVE_INSET DT_PIXEL_APPLY_DPI(5)
+// thickness of, and gap to, the decorative perceptual gradient bar
+// drawn below the curve graph
+#define DT_IOP_SATCURVE_GRADIENT_SIZE DT_PIXEL_APPLY_DPI(8)
+#define DT_IOP_SATCURVE_GRADIENT_GAP DT_PIXEL_APPLY_DPI(2)
+#define DT_IOP_SATCURVE_MIN_X_DISTANCE 0.0025f
+#define DT_IOP_SATCURVE_GAMUT_STEPS 92
+
+// version of the module's serialized parameters
+DT_MODULE_INTROSPECTION(1, dt_iop_satcurve_params_t)
+
+// saturation definition used to compute S_in_norm and drive the curve
+typedef enum dt_iop_satcurve_formula_t
+{
+ DT_IOP_SATCURVE_JZAZBZ = 0,
+ DT_IOP_SATCURVE_DTUCS = 1
+} dt_iop_satcurve_formula_t;
+
+typedef struct dt_iop_satcurve_node_t
+{
+ float x, y;
+} dt_iop_satcurve_node_t;
+
+// module parameters, serialized to the database
+typedef struct dt_iop_satcurve_params_t
+{
+ dt_iop_satcurve_node_t curve[DT_IOP_SATCURVE_MAXNODES];
+ int curve_num_nodes;
+ int curve_type;
+ dt_iop_satcurve_formula_t formula; // $DEFAULT: 1 $DESCRIPTION: "saturation formula"
+} dt_iop_satcurve_params_t;
+
+// per-pixelpipe runtime data, derived from params_t in commit_params()
+typedef struct dt_iop_satcurve_data_t
+{
+ dt_draw_curve_t *curve;
+ int curve_num_nodes;
+ int curve_type;
+ dt_iop_satcurve_formula_t formula;
+ float *lut;
+ float *gamut_lut;
+ gboolean lut_inited;
+ const struct dt_iop_order_iccprofile_info_t *work_profile;
+} dt_iop_satcurve_data_t;
+
+// GUI state and widget handles
+typedef struct dt_iop_satcurve_gui_data_t
+{
+ GtkDrawingArea *area;
+ GtkWidget *colorpicker;
+ GtkWidget *formula;
+ dt_draw_curve_t *curve;
+ int curve_num_nodes;
+ int curve_type;
+ float draw_ys[DT_IOP_SATCURVE_RES];
+ int selected;
+ gboolean dragging;
+ float mouse_x, mouse_y;
+
+ // input saturation histogram (S_in_norm)
+ float histogram[DT_IOP_SATCURVE_HIST_RES];
+ float histogram_max;
+ dt_pthread_mutex_t histogram_lock;
+
+ // picked region: S_in_norm of mean/min/max
+ gboolean picker_valid;
+ float picked_s;
+ float picked_s_min;
+ float picked_s_max;
+} dt_iop_satcurve_gui_data_t;
+
+typedef struct dt_iop_satcurve_global_data_t
+{
+ int kernel_satcurvergb;
+} dt_iop_satcurve_global_data_t;
+
+const char *name()
+{
+ return _("saturation curve");
+}
+
+const char *aliases()
+{
+ return _("sat vs sat|saturation versus saturation|satcurve");
+}
+
+const char **description(dt_iop_module_t *self)
+{
+ return dt_iop_set_description(self, _("remap saturation with a curve"),
+ _("corrective or creative"),
+ _("linear, RGB, scene-referred"),
+ _("linear, RGB, scene-referred"),
+ _("linear, RGB, scene-referred"));
+}
+
+int flags()
+{
+ return IOP_FLAGS_INCLUDE_IN_STYLES | IOP_FLAGS_SUPPORTS_BLENDING | IOP_FLAGS_ALLOW_TILING;
+}
+
+int default_group()
+{
+ return IOP_GROUP_COLOR | IOP_GROUP_GRADING;
+}
+
+dt_iop_colorspace_type_t default_colorspace(dt_iop_module_t *self, dt_dev_pixelpipe_t *pipe,
+ dt_dev_pixelpipe_iop_t *piece)
+{
+ return IOP_CS_RGB;
+}
+
+// intended to run before filmic rgb; pipeline order entry is added
+// separately in iop_order.c
+
+static inline void reset_curve(dt_iop_satcurve_params_t *p)
+{
+ p->curve_num_nodes = 2;
+ p->curve_type = MONOTONE_HERMITE;
+ p->formula = DT_IOP_SATCURVE_DTUCS;
+ p->curve[0] = (dt_iop_satcurve_node_t){0.f, .5f};
+ p->curve[1] = (dt_iop_satcurve_node_t){1.f, .5f};
+}
+
+static inline float lookup_lut(const float *lut, const float x)
+{
+ const float position = CLAMP(x, 0.f, 1.f) * (DT_IOP_SATCURVE_RES - 1);
+ const int i = MIN((int)position, DT_IOP_SATCURVE_RES - 2);
+ return lut[i] + (position - i) * (lut[i + 1] - lut[i]);
+}
+
+// periodic lookup in the hue-indexed gamut LUT; h = atan2(b, a), in [-pi, pi]
+static inline float satcurve_lookup_gamut(const float *const gamut_lut, const float h)
+{
+ const float position = (h + M_PI_F) * (float)LUT_ELEM / DT_2PI_F;
+ const int bin0 = ((int)floorf(position)) % LUT_ELEM;
+ const int bin1 = (bin0 + 1) % LUT_ELEM;
+ const float f = position - floorf(position);
+
+ return gamut_lut[bin0] * (1.f - f) + gamut_lut[bin1] * f;
+}
+
+// smoothly map x from knee onwards towards maximum (reached asymptotically);
+// x <= knee is left unchanged
+static inline float satcurve_soft_clip(const float x, const float knee, const float maximum)
+{
+ if(x <= knee)
+ return x;
+
+ const float range = maximum - knee;
+ if(range <= 0.f)
+ return maximum;
+
+ return knee + range * (1.f - expf(-(x - knee) / range));
+}
+
+// mirrors the JzAzBz chroma clip in color balance rgb (test conversion to
+// L'M'S'): keeps the curve from producing colors that go negative on the
+// JzAzBz -> XYZ back-transform
+static inline float clip_jz_chroma(const float Jz, const float Cz, const float ch, const float sh)
+{
+ const float d0 = 1.6295499532821566e-11f;
+ const float dd = -0.56f;
+ float Iz = (Jz + d0) / (1.f + dd - dd * (Jz + d0));
+ Iz = MAX(Iz, 0.f);
+ static const dt_colormatrix_t AI_trans = {{1.f, 1.f, 1.f, 0.f},
+ {.1386050432715393f, -.1386050432715393f, -.0960192420263190f, 0.f},
+ {.0580473161561189f, -.0580473161561189f, -.8118918960560390f, 0.f}};
+ dt_aligned_pixel_t izab = {Iz, Cz * ch, Cz * sh, 0.f}, lms;
+ dt_apply_transposed_color_matrix(izab, AI_trans, lms);
+ float max_c = Cz;
+ if(lms[0] < 0.f)
+ max_c = MIN(max_c, -Iz / (AI_trans[1][0] * ch + AI_trans[2][0] * sh));
+ if(lms[1] < 0.f)
+ max_c = MIN(max_c, -Iz / (AI_trans[1][1] * ch + AI_trans[2][1] * sh));
+ if(lms[2] < 0.f)
+ max_c = MIN(max_c, -Iz / (AI_trans[1][2] * ch + AI_trans[2][2] * sh));
+ return MAX(max_c, 0.f);
+}
+
+// shared S_in_norm computation, used by process(), the histogram, and the
+// picker, so all three use the exact same scene-referred math
+static inline float pixel_s_in_norm_jzazbz(const float *const restrict rgb_in,
+ const dt_colormatrix_t inputmatrix_trans,
+ const float *const restrict gamut_lut,
+ float *const restrict h_out)
+{
+ dt_aligned_pixel_t rgb, xyz, jab;
+ copy_pixel(rgb, rgb_in);
+ dt_vector_clipneg(rgb);
+ dt_apply_transposed_color_matrix(rgb, inputmatrix_trans, xyz);
+ dt_XYZ_2_JzAzBz(xyz, jab);
+
+ const float Jz = MAX(jab[0], 0.f);
+ const float Cz = dt_fast_hypotf(jab[1], jab[2]);
+ const float h = atan2f(jab[2], jab[1]);
+ const float gamut = MAX(satcurve_lookup_gamut(gamut_lut, h), FLT_MIN);
+ const float s_in = Jz > 0.f ? Cz / Jz : 0.f;
+
+ if(h_out) *h_out = h;
+ return s_in / gamut;
+}
+
+static inline float pixel_s_in_norm_ucs(
+ const float *const restrict rgb_in,
+ const dt_colormatrix_t inputmatrix_trans,
+ const float *const restrict gamut_lut,
+ const float L_white,
+ float *const restrict h_out)
+{
+ dt_aligned_pixel_t rgb, xyz, xyY, JCH, HCB;
+ dt_aligned_pixel_t JCH_gamut_boundary, HSB_gamut_boundary;
+
+ copy_pixel(rgb, rgb_in);
+ dt_vector_clipneg(rgb);
+ dt_apply_transposed_color_matrix(rgb, inputmatrix_trans, xyz);
+
+ dt_D65_XYZ_to_xyY(xyz, xyY);
+ xyY_to_dt_UCS_JCH(xyY, L_white, JCH);
+ dt_UCS_JCH_to_HCB(JCH, HCB);
+
+ const float max_colorfulness
+ = MAX(satcurve_lookup_gamut(gamut_lut, JCH[2]), FLT_MIN);
+
+ const float max_chroma
+ = 15.932993652962535f
+ * powf(JCH[0] / L_white, 0.6523997524738018f)
+ * powf(max_colorfulness, 0.6007557017508491f)
+ / L_white;
+
+ JCH_gamut_boundary[0] = JCH[0];
+ JCH_gamut_boundary[1] = max_chroma;
+ JCH_gamut_boundary[2] = JCH[2];
+ JCH_gamut_boundary[3] = 0.f;
+
+ dt_UCS_JCH_to_HSB(JCH_gamut_boundary, HSB_gamut_boundary);
+
+ if(h_out) *h_out = JCH[2];
+
+ const float saturation = HCB[2] > 0.f ? HCB[1] / HCB[2] : 0.f;
+ return saturation / MAX(HSB_gamut_boundary[1], FLT_MIN);
+}
+
+static inline float pixel_s_in_norm(
+ const dt_iop_satcurve_data_t *const d,
+ const float *const restrict rgb_in,
+ const dt_colormatrix_t inputmatrix_trans,
+ const float *const restrict gamut_lut,
+ const float L_white,
+ float *const restrict h_out)
+{
+ if(d->formula == DT_IOP_SATCURVE_DTUCS)
+ return pixel_s_in_norm_ucs(rgb_in, inputmatrix_trans, gamut_lut, L_white, h_out);
+
+ return pixel_s_in_norm_jzazbz(rgb_in, inputmatrix_trans, gamut_lut, h_out);
+}
+
+// updates the input-referred saturation histogram; GUI full-preview only,
+// mirrors the display_mask gating used in colorzones.c
+static void _update_sat_histogram(dt_iop_module_t *self,
+ const dt_iop_satcurve_data_t *d,
+ const dt_colormatrix_t inputmatrix_trans,
+ const float *const restrict in,
+ const size_t npixels)
+{
+ dt_iop_satcurve_gui_data_t *g = self->gui_data;
+ if(!g) return;
+
+ float local_hist[DT_IOP_SATCURVE_HIST_RES] = { 0.f };
+ const float L_white = Y_to_dt_UCS_L_star(1.f); // constant; compute once, not per pixel
+
+ DT_OMP_FOR(reduction(+ : local_hist[:DT_IOP_SATCURVE_HIST_RES]))
+ for(size_t k = 0; k < npixels; k++)
+ {
+ const float s_in_norm = pixel_s_in_norm(d, in + 4 * k, inputmatrix_trans, d->gamut_lut, L_white, NULL);
+ const int bin = CLAMP((int)(s_in_norm * (DT_IOP_SATCURVE_HIST_RES - 1)),
+ 0, DT_IOP_SATCURVE_HIST_RES - 1);
+ local_hist[bin] += 1.f;
+ }
+
+ // log1p scaling: S_in_norm is typically skewed heavily toward low values;
+ // the x-axis (S_in_norm) itself stays linear
+ float max_val = 0.f;
+ for(int i = 0; i < DT_IOP_SATCURVE_HIST_RES; i++)
+ {
+ local_hist[i] = log1pf(local_hist[i]);
+ max_val = MAX(max_val, local_hist[i]);
+ }
+
+ dt_pthread_mutex_lock(&g->histogram_lock);
+ memcpy(g->histogram, local_hist, sizeof(local_hist));
+ g->histogram_max = MAX(max_val, 1e-6f);
+ dt_pthread_mutex_unlock(&g->histogram_lock);
+
+ dt_control_queue_redraw_widget(GTK_WIDGET(g->area));
+}
+
+void process(dt_iop_module_t *self, dt_dev_pixelpipe_iop_t *piece,
+ const void *const ivoid, void *const ovoid,
+ const dt_iop_roi_t *const roi_in, const dt_iop_roi_t *const roi_out)
+{
+ dt_iop_satcurve_data_t *d = piece->data;
+ const gboolean neutral_curve = d->curve_num_nodes == 2
+ && fabsf(d->lut[0] - .5f) < 1e-6f
+ && fabsf(d->lut[DT_IOP_SATCURVE_RES - 1] - .5f) < 1e-6f;
+ if(neutral_curve)
+ {
+ dt_iop_image_copy_by_size(ovoid, ivoid, roi_out->width, roi_out->height, piece->colors);
+ return;
+ }
+
+ const dt_iop_order_iccprofile_info_t *work_profile = dt_ioppr_get_pipe_current_profile_info(self, piece->pipe);
+ if(!work_profile || piece->colors != 4)
+ {
+ dt_iop_image_copy_by_size(ovoid, ivoid, roi_out->width, roi_out->height, piece->colors);
+ return;
+ }
+
+ dt_colormatrix_t inputmatrix = {{ 0.0f }};
+ dt_colormatrix_t outputmatrix = {{ 0.0f }};
+ dt_colormatrix_t inputmatrix_trans;
+ dt_colormatrix_t outputmatrix_trans;
+ dt_colormatrix_mul(inputmatrix, XYZ_D50_to_D65_CAT16, work_profile->matrix_in);
+ dt_colormatrix_transpose(inputmatrix_trans, inputmatrix);
+ dt_colormatrix_mul(outputmatrix, work_profile->matrix_out, XYZ_D65_to_D50_CAT16);
+ dt_colormatrix_transpose(outputmatrix_trans, outputmatrix);
+
+ const float *const restrict in = DT_IS_ALIGNED((const float *)ivoid);
+ float *const restrict out = DT_IS_ALIGNED((float *)ovoid);
+ const size_t npixels = (size_t)roi_out->width * roi_out->height;
+
+ if(self->dev->gui_attached && dt_pipe_is_full(piece->pipe) && dt_iop_has_focus(self)
+ && piece->pipe == self->dev->full.pipe)
+ _update_sat_histogram(self, d, inputmatrix_trans, in, npixels);
+
+ // constant regardless of pixel data; hoisted out of the per-pixel loop
+ const float L_white = Y_to_dt_UCS_L_star(1.f);
+
+ DT_OMP_FOR()
+ for(size_t k = 0; k < npixels; k++)
+ {
+ dt_aligned_pixel_t rgb, xyz, pixout;
+ copy_pixel(rgb, in + 4 * k);
+ dt_vector_clipneg(rgb);
+ dt_apply_transposed_color_matrix(rgb, inputmatrix_trans, xyz);
+
+ if(d->formula == DT_IOP_SATCURVE_DTUCS)
+ {
+ dt_aligned_pixel_t xyY, JCH, HCB, HSB, JCH_gamut_boundary, HSB_gamut_boundary;
+ dt_D65_XYZ_to_xyY(xyz, xyY);
+ xyY_to_dt_UCS_JCH(xyY, L_white, JCH);
+ dt_UCS_JCH_to_HCB(JCH, HCB);
+
+ const float max_colorfulness = MAX(satcurve_lookup_gamut(d->gamut_lut, JCH[2]), FLT_MIN);
+ const float max_chroma = 15.932993652962535f
+ * powf(JCH[0] / L_white, 0.6523997524738018f)
+ * powf(max_colorfulness, 0.6007557017508491f)
+ / L_white;
+ JCH_gamut_boundary[0] = JCH[0];
+ JCH_gamut_boundary[1] = max_chroma;
+ JCH_gamut_boundary[2] = JCH[2];
+ JCH_gamut_boundary[3] = 0.f;
+ dt_UCS_JCH_to_HSB(JCH_gamut_boundary, HSB_gamut_boundary);
+
+ HSB[0] = HCB[0];
+ HSB[1] = HCB[2] > 0.f ? HCB[1] / HCB[2] : 0.f;
+ HSB[2] = HCB[2];
+ HSB[3] = 0.f;
+
+ const float gamut_s = MAX(HSB_gamut_boundary[1], FLT_MIN);
+ const float s_in_norm = HSB[1] / gamut_s;
+ const float c = CLAMP(lookup_lut(d->lut, s_in_norm), 0.f, 1.f);
+ HSB[1] = MAX(HSB[1] * (2.f * c), 0.f);
+ HSB[1] = satcurve_soft_clip(HSB[1], .8f * gamut_s, gamut_s);
+
+ dt_UCS_HSB_to_JCH(HSB, JCH);
+ dt_UCS_JCH_to_xyY(JCH, L_white, xyY);
+ dt_xyY_to_XYZ(xyY, xyz);
+ }
+ else
+ {
+ dt_aligned_pixel_t jab;
+ dt_XYZ_2_JzAzBz(xyz, jab);
+ const float Jz = MAX(jab[0], 0.f);
+ const float Cz = dt_fast_hypotf(jab[1], jab[2]);
+ const float h = atan2f(jab[2], jab[1]);
+ const float ch = cosf(h), sh = sinf(h);
+ const float gamut = MAX(satcurve_lookup_gamut(d->gamut_lut, h), FLT_MIN);
+ const float s_in_norm = (Jz > 0.f ? Cz / Jz : 0.f) / gamut;
+ const float c = CLAMP(lookup_lut(d->lut, s_in_norm), 0.f, 1.f);
+ const float s_out = satcurve_soft_clip(MAX(s_in_norm * (2.f * c), 0.f), .8f, 1.f) * gamut;
+ const float r = dt_fast_hypotf(Jz, Cz);
+ // T = atanf(s_out); cosf(T) = 1/sqrt(1+s_out^2), sinf(T) = s_out/sqrt(1+s_out^2)
+ // avoids one atanf + one cosf + one sinf per pixel
+ const float inv_norm = 1.f / sqrtf(1.f + s_out * s_out);
+ const float Jz_out = r * inv_norm;
+ const float Cz_out = clip_jz_chroma(Jz_out, r * s_out * inv_norm, ch, sh);
+ jab[0] = Jz_out;
+ jab[1] = Cz_out * ch;
+ jab[2] = Cz_out * sh;
+ dt_JzAzBz_2_XYZ(jab, xyz);
+ }
+
+ dt_apply_transposed_color_matrix(xyz, outputmatrix_trans, pixout);
+ dt_vector_clipneg(pixout);
+ copy_pixel_nontemporal(out + 4 * k, pixout);
+ }
+ dt_omploop_sfence();
+}
+
+#if HAVE_OPENCL
+int process_cl(dt_iop_module_t *self, dt_dev_pixelpipe_iop_t *piece,
+ cl_mem dev_in, cl_mem dev_out,
+ const dt_iop_roi_t *const roi_in, const dt_iop_roi_t *const roi_out)
+{
+ dt_iop_satcurve_data_t *d = piece->data;
+ const dt_iop_satcurve_global_data_t *gd = self->global_data;
+
+ cl_int err = DT_OPENCL_DEFAULT_ERROR;
+ if(piece->colors != 4) return err;
+
+ const int devid = piece->pipe->devid;
+ const int width = roi_in->width;
+ const int height = roi_in->height;
+
+ const gboolean neutral_curve = d->curve_num_nodes == 2
+ && fabsf(d->lut[0] - .5f) < 1e-6f
+ && fabsf(d->lut[DT_IOP_SATCURVE_RES - 1] - .5f) < 1e-6f;
+ if(neutral_curve)
+ return dt_opencl_enqueue_copy_image(devid, dev_in, dev_out, (size_t[]){0,0,0},
+ (size_t[]){0,0,0}, (size_t[]){width, height, 1});
+
+ const dt_iop_order_iccprofile_info_t *const work_profile
+ = dt_ioppr_get_pipe_current_profile_info(self, piece->pipe);
+ if(work_profile == NULL) return err;
+
+ // Histogram needs a host-side copy of the input buffer; only do this
+ // when the GUI actually needs it, to avoid a needless GPU->CPU sync
+ // on every full-pipe run.
+ if(self->dev->gui_attached && dt_pipe_is_full(piece->pipe) && dt_iop_has_focus(self)
+ && piece->pipe == self->dev->full.pipe)
+ {
+ dt_colormatrix_t hist_input_matrix;
+ dt_colormatrix_t hist_input_matrix_trans;
+ dt_colormatrix_mul(hist_input_matrix, XYZ_D50_to_D65_CAT16, work_profile->matrix_in);
+ dt_colormatrix_transpose(hist_input_matrix_trans, hist_input_matrix);
+
+ float *host_in = dt_alloc_align_float((size_t)4 * width * height);
+ if(host_in)
+ {
+ cl_int hist_err = dt_opencl_copy_image_to_host(devid, host_in, dev_in, width, height, sizeof(float) * 4);
+ if(hist_err == CL_SUCCESS)
+ _update_sat_histogram(self, d, hist_input_matrix_trans, host_in, (size_t)width * height);
+ dt_free_align(host_in);
+ }
+ }
+
+ cl_mem lut_cl = NULL;
+ cl_mem gamut_lut_cl = NULL;
+ cl_mem input_matrix_cl = NULL;
+ cl_mem output_matrix_cl = NULL;
+
+ dt_colormatrix_t input_matrix;
+ dt_colormatrix_t output_matrix;
+ dt_colormatrix_mul(input_matrix, XYZ_D50_to_D65_CAT16, work_profile->matrix_in);
+ dt_colormatrix_mul(output_matrix, work_profile->matrix_out, XYZ_D65_to_D50_CAT16);
+
+ input_matrix_cl = dt_opencl_copy_host_to_device_constant(devid, 12 * sizeof(float), input_matrix);
+ output_matrix_cl = dt_opencl_copy_host_to_device_constant(devid, 12 * sizeof(float), output_matrix);
+ lut_cl = dt_opencl_copy_host_to_device_constant(devid, DT_IOP_SATCURVE_RES * sizeof(float), d->lut);
+ gamut_lut_cl = dt_opencl_copy_host_to_device_constant(devid, LUT_ELEM * sizeof(float), d->gamut_lut);
+
+ if(input_matrix_cl == NULL || output_matrix_cl == NULL || lut_cl == NULL || gamut_lut_cl == NULL)
+ {
+ err = CL_MEM_OBJECT_ALLOCATION_FAILURE;
+ goto error;
+ }
+
+ // constant regardless of pixel data; compute once on the host instead of
+ // once per GPU thread inside the kernel
+ const float L_white = Y_to_dt_UCS_L_star(1.f);
+
+ err = dt_opencl_enqueue_kernel_2d_args(devid, gd->kernel_satcurvergb, width, height,
+ CLARG(dev_in), CLARG(dev_out), CLARG(width), CLARG(height),
+ CLARG(input_matrix_cl), CLARG(output_matrix_cl),
+ CLARG(lut_cl), CLARG(gamut_lut_cl), CLARG(d->formula), CLARG(L_white));
+
+error:
+ dt_opencl_release_mem_object(input_matrix_cl);
+ dt_opencl_release_mem_object(output_matrix_cl);
+ dt_opencl_release_mem_object(lut_cl);
+ dt_opencl_release_mem_object(gamut_lut_cl);
+ return err;
+}
+
+void init_global(dt_iop_module_so_t *self)
+{
+ const int program = 42; // satcurve.cl in programs.conf
+ dt_iop_satcurve_global_data_t *gd = malloc(sizeof(dt_iop_satcurve_global_data_t));
+ self->data = gd;
+ gd->kernel_satcurvergb = dt_opencl_create_kernel(program, "satcurvergb");
+}
+
+void cleanup_global(dt_iop_module_so_t *self)
+{
+ const dt_iop_satcurve_global_data_t *gd = self->data;
+ dt_opencl_free_kernel(gd->kernel_satcurvergb);
+ free(self->data);
+ self->data = NULL;
+}
+#endif
+
+
+void init_pipe(dt_iop_module_t *self, dt_dev_pixelpipe_t *pipe, dt_dev_pixelpipe_iop_t *piece)
+{
+ dt_iop_satcurve_data_t *d = dt_calloc_align_type(dt_iop_satcurve_data_t, 1);
+ d->lut = dt_alloc_align_float(DT_IOP_SATCURVE_RES);
+ d->gamut_lut = dt_alloc_align_float(LUT_ELEM);
+ d->curve = dt_draw_curve_new(0.f, 1.f, MONOTONE_HERMITE);
+ d->lut_inited = FALSE;
+ piece->data = d;
+}
+
+void cleanup_pipe(dt_iop_module_t *self, dt_dev_pixelpipe_t *pipe, dt_dev_pixelpipe_iop_t *piece)
+{
+ dt_iop_satcurve_data_t *d = piece->data;
+ dt_draw_curve_destroy(d->curve);
+ dt_free_align(d->lut);
+ dt_free_align(d->gamut_lut);
+ dt_free_align(d);
+ piece->data = NULL;
+}
+
+// RGB/D50 -> XYZ/D65, the input expected by dt_XYZ_2_JzAzBz()
+static void build_jzazbz_gamut_lut(dt_iop_satcurve_data_t *d, const dt_iop_order_iccprofile_info_t *work_profile)
+{
+ dt_colormatrix_t inputmatrix = { {0.0f} };
+
+ dt_colormatrix_mul(inputmatrix, XYZ_D50_to_D65_CAT16, work_profile->matrix_in);
+ dt_colormatrix_t inputmatrix_trans;
+ dt_colormatrix_transpose(inputmatrix_trans, inputmatrix);
+ float *sampler = dt_calloc_align_float(LUT_ELEM);
+
+ DT_OMP_FOR(reduction(max : sampler[:LUT_ELEM]) collapse(3))
+ for(int r = 0; r < DT_IOP_SATCURVE_GAMUT_STEPS; r++)
+ for(int g = 0; g < DT_IOP_SATCURVE_GAMUT_STEPS; g++)
+ for(int b = 0; b < DT_IOP_SATCURVE_GAMUT_STEPS; b++)
+ {
+ const dt_aligned_pixel_t rgb = {r / (float)(DT_IOP_SATCURVE_GAMUT_STEPS - 1),
+ g / (float)(DT_IOP_SATCURVE_GAMUT_STEPS - 1),
+ b / (float)(DT_IOP_SATCURVE_GAMUT_STEPS - 1), 0.f};
+ dt_aligned_pixel_t xyz, jab;
+ dt_apply_transposed_color_matrix(rgb, inputmatrix_trans, xyz);
+ dt_XYZ_2_JzAzBz(xyz, jab);
+ const float saturation = jab[0] > 0.f ? dt_fast_hypotf(jab[1], jab[2]) / jab[0] : 0.f;
+ int index = roundf((LUT_ELEM - 1) * (atan2f(jab[2], jab[1]) + M_PI_F) / DT_2PI_F);
+ index = index < 0 ? LUT_ELEM - 1 : (index >= LUT_ELEM ? 0 : index);
+ sampler[index] = MAX(sampler[index], saturation);
+ }
+
+ for(size_t i = 2; i < LUT_ELEM - 2; i++)
+ d->gamut_lut[i] = (sampler[i - 2] + sampler[i - 1] + sampler[i] + sampler[i + 1] + sampler[i + 2]) / 5.f;
+ d->gamut_lut[0] = (sampler[LUT_ELEM - 2] + sampler[LUT_ELEM - 1] + sampler[0] + sampler[1] + sampler[2]) / 5.f;
+ d->gamut_lut[1] = (sampler[LUT_ELEM - 1] + sampler[0] + sampler[1] + sampler[2] + sampler[3]) / 5.f;
+ d->gamut_lut[LUT_ELEM - 2] = (sampler[LUT_ELEM - 4] + sampler[LUT_ELEM - 3] + sampler[LUT_ELEM - 2] + sampler[LUT_ELEM - 1] + sampler[0]) / 5.f;
+ d->gamut_lut[LUT_ELEM - 1] = (sampler[LUT_ELEM - 3] + sampler[LUT_ELEM - 2] + sampler[LUT_ELEM - 1] + sampler[0] + sampler[1]) / 5.f;
+ dt_free_align(sampler);
+}
+
+static void build_gamut_lut(dt_iop_satcurve_data_t *d,
+ const dt_iop_order_iccprofile_info_t *work_profile)
+{
+ if(d->formula == DT_IOP_SATCURVE_DTUCS)
+ {
+ dt_colormatrix_t inputmatrix = {{ 0.0f }};
+ dt_colormatrix_mul(inputmatrix, XYZ_D50_to_D65_CAT16, work_profile->matrix_in);
+ dt_UCS_22_build_gamut_LUT(inputmatrix, d->gamut_lut);
+ }
+ else
+ build_jzazbz_gamut_lut(d, work_profile);
+}
+
+void commit_params(dt_iop_module_t *self, dt_iop_params_t *p1, dt_dev_pixelpipe_t *pipe,
+ dt_dev_pixelpipe_iop_t *piece)
+{
+ dt_iop_satcurve_data_t *d = piece->data;
+ const dt_iop_satcurve_params_t *p = (const dt_iop_satcurve_params_t *)p1;
+ if(d->formula != p->formula)
+ {
+ d->formula = p->formula;
+ d->lut_inited = FALSE;
+ }
+ if(d->curve_type != p->curve_type || d->curve_num_nodes != p->curve_num_nodes)
+ {
+ dt_draw_curve_destroy(d->curve);
+ d->curve = dt_draw_curve_new(0.f, 1.f, p->curve_type);
+ d->curve_type = p->curve_type;
+ d->curve_num_nodes = p->curve_num_nodes;
+ for(int i = 0; i < p->curve_num_nodes; i++)
+ dt_draw_curve_add_point(d->curve, p->curve[i].x, p->curve[i].y);
+ }
+ else
+ for(int i = 0; i < p->curve_num_nodes; i++)
+ dt_draw_curve_set_point(d->curve, i, p->curve[i].x, p->curve[i].y);
+ dt_draw_curve_calc_values(d->curve, 0.f, 1.f, DT_IOP_SATCURVE_RES, NULL, d->lut);
+
+ const dt_iop_order_iccprofile_info_t *work_profile = dt_ioppr_get_pipe_current_profile_info(self, piece->pipe);
+ if(work_profile && (!d->lut_inited || work_profile != d->work_profile))
+ {
+ build_gamut_lut(d, work_profile);
+ d->work_profile = work_profile;
+ d->lut_inited = TRUE;
+ }
+}
+
+void init_presets(dt_iop_module_so_t *self)
+{
+ dt_iop_satcurve_params_t p = {0};
+ reset_curve(&p);
+ dt_gui_presets_add_generic(_("neutral"), self->op, self->version(), &p, sizeof(p), TRUE, DEVELOP_BLEND_CS_RGB_SCENE);
+
+ p.curve[0].y = .60f;
+ p.curve[1].y = .55f;
+ dt_gui_presets_add_generic(_("gentle saturation"), self->op, self->version(), &p, sizeof(p), TRUE, DEVELOP_BLEND_CS_RGB_SCENE);
+
+ reset_curve(&p);
+ p.curve_num_nodes = 3;
+ p.curve[1] = (dt_iop_satcurve_node_t){.45f, .62f};
+ p.curve[2] = (dt_iop_satcurve_node_t){1.f, .42f};
+ dt_gui_presets_add_generic(_("protect saturated colors"), self->op, self->version(), &p, sizeof(p), TRUE, DEVELOP_BLEND_CS_RGB_SCENE);
+
+ reset_curve(&p);
+ p.curve_num_nodes = 3;
+ p.curve[0].y = .38f;
+ p.curve[1] = (dt_iop_satcurve_node_t){.35f, .48f};
+ p.curve[2] = (dt_iop_satcurve_node_t){1.f, .58f};
+ dt_gui_presets_add_generic(_("boost saturated colors"), self->op, self->version(), &p, sizeof(p), TRUE, DEVELOP_BLEND_CS_RGB_SCENE);
+}
+
+static inline int add_node(dt_iop_satcurve_params_t *p, const float x, const float y)
+{
+ int at = 0;
+ while(at < p->curve_num_nodes && p->curve[at].x < x)
+ at++;
+ if((at && x - p->curve[at - 1].x < DT_IOP_SATCURVE_MIN_X_DISTANCE) ||
+ (at < p->curve_num_nodes && p->curve[at].x - x < DT_IOP_SATCURVE_MIN_X_DISTANCE))
+ return -1;
+ for(int i = p->curve_num_nodes; i > at; i--)
+ p->curve[i] = p->curve[i - 1];
+ p->curve[at] = (dt_iop_satcurve_node_t){x, y};
+ p->curve_num_nodes++;
+ return at;
+}
+
+// paints the input-referred saturation histogram as a filled area behind
+// the curve; similar to the histogram overlay in colorzones.c, but backed
+// by our own float accumulator instead of the Lab-based histogram
+static void _draw_sat_histogram(cairo_t *cr, dt_iop_satcurve_gui_data_t *g, const int w, const int h)
+{
+ dt_pthread_mutex_lock(&g->histogram_lock);
+ const float hist_max = g->histogram_max;
+ cairo_save(cr);
+ cairo_set_source_rgba(cr, .8, .8, .8, .35);
+ cairo_move_to(cr, 0, h);
+ for(int i = 0; i < DT_IOP_SATCURVE_HIST_RES; i++)
+ {
+ const float x = w * i / (float)(DT_IOP_SATCURVE_HIST_RES - 1);
+ const float y = h * (1.f - g->histogram[i] / hist_max);
+ cairo_line_to(cr, x, y);
+ }
+ cairo_line_to(cr, w, h);
+ cairo_close_path(cr);
+ cairo_fill(cr);
+ cairo_restore(cr);
+ dt_pthread_mutex_unlock(&g->histogram_lock);
+}
+
+// paints the picker overlay (mean/min/max S_in_norm of the picked region),
+// analogous to colorequal.c / colorzones.c but using our own S_in_norm
+// metric instead of Lab/LCh or dt UCS HSB
+static void _draw_sat_picker(cairo_t *cr, dt_iop_module_t *self, const int w, const int h)
+{
+ dt_iop_satcurve_gui_data_t *g = self->gui_data;
+ if(!g->picker_valid) return;
+ if(self->request_color_pick != DT_REQUEST_COLORPICK_MODULE) return;
+
+ const float x_mean = CLAMP(g->picked_s, 0.f, 1.f) * w;
+ const float x_min = CLAMP(g->picked_s_min, 0.f, 1.f) * w;
+ const float x_max = CLAMP(g->picked_s_max, 0.f, 1.f) * w;
+
+ cairo_save(cr);
+ cairo_set_source_rgba(cr, 0.5, 0.7, 0.5, 0.25);
+ cairo_rectangle(cr, x_min, 0, fmaxf(x_max - x_min, 0.f), h);
+ cairo_fill(cr);
+
+ cairo_set_source_rgba(cr, 0.5, 0.9, 0.5, 0.9);
+ cairo_set_line_width(cr, DT_PIXEL_APPLY_DPI(2.));
+ cairo_move_to(cr, x_mean, 0);
+ cairo_line_to(cr, x_mean, h);
+ cairo_stroke(cr);
+ cairo_restore(cr);
+}
+
+// shared geometry for the curve graph itself: origin is offset from the
+// widget's top-left by the inset; only the bottom reserves extra space
+// for the decorative gradient bar, so area_draw and the pointer
+// hit-testing functions always agree on where (0,0)-(1,1) map to
+static inline void _get_graph_geometry(const GtkAllocation *a, float *x0, float *y0, float *w, float *h)
+{
+ const float inset = DT_IOP_SATCURVE_INSET;
+ const float reserved = DT_IOP_SATCURVE_GRADIENT_SIZE + DT_IOP_SATCURVE_GRADIENT_GAP;
+ *x0 = inset;
+ *y0 = inset;
+ *w = a->width - 2.f * inset;
+ *h = a->height - 2.f * inset - reserved;
+}
+
+static gboolean area_draw(GtkWidget *widget, cairo_t *cr, dt_iop_module_t *self)
+{
+ dt_iop_satcurve_gui_data_t *g = self->gui_data;
+ dt_iop_satcurve_params_t *p = self->params;
+ GtkAllocation a;
+ gtk_widget_get_allocation(widget, &a);
+ float gx0, gy0, gw, gh;
+ _get_graph_geometry(&a, &gx0, &gy0, &gw, &gh);
+ const int w = (int)gw, h = (int)gh;
+ if(g->curve_type != p->curve_type || g->curve_num_nodes != p->curve_num_nodes)
+ {
+ dt_draw_curve_destroy(g->curve);
+ g->curve = dt_draw_curve_new(0.f, 1.f, p->curve_type);
+ g->curve_type = p->curve_type;
+ g->curve_num_nodes = p->curve_num_nodes;
+ for(int i = 0; i < p->curve_num_nodes; i++)
+ dt_draw_curve_add_point(g->curve, p->curve[i].x, p->curve[i].y);
+ }
+ else
+ for(int i = 0; i < p->curve_num_nodes; i++)
+ dt_draw_curve_set_point(g->curve, i, p->curve[i].x, p->curve[i].y);
+ dt_draw_curve_calc_values(g->curve, 0.f, 1.f, DT_IOP_SATCURVE_RES, NULL, g->draw_ys);
+
+ // background first: cairo_paint() fills the whole widget regardless of
+ // any later cairo_translate(), so it must happen before the gradient
+ // bars are drawn, or it would overpaint them
+ cairo_set_source_rgb(cr, .18, .18, .18);
+ cairo_paint(cr);
+
+ // purely decorative perceptual gradient bar, below the graph; same
+ // cairo_pattern_create_linear + dt_cairo_perceptual_gradient combo as
+ // used in toneequal.c, just without any axis legend
+ cairo_pattern_t *grad;
+
+ // horizontal bar, below the graph
+ grad = cairo_pattern_create_linear(gx0, 0.0, gx0 + gw, 0.0);
+ dt_cairo_perceptual_gradient(grad, 1.0);
+ cairo_set_line_width(cr, 0.0);
+ cairo_rectangle(cr, gx0, gy0 + gh + DT_IOP_SATCURVE_GRADIENT_GAP,
+ gw, DT_IOP_SATCURVE_GRADIENT_SIZE);
+ cairo_set_source(cr, grad);
+ cairo_fill(cr);
+ cairo_pattern_destroy(grad);
+
+ cairo_translate(cr, gx0, gy0);
+ cairo_set_source_rgba(cr, .4, .4, .4, .5);
+ cairo_set_line_width(cr, DT_PIXEL_APPLY_DPI(1));
+ for(int i = 1; i < 4; i++)
+ {
+ cairo_move_to(cr, w * i / 4.f, 0);
+ cairo_line_to(cr, w * i / 4.f, h);
+ cairo_move_to(cr, 0, h * i / 4.f);
+ cairo_line_to(cr, w, h * i / 4.f);
+ }
+ cairo_stroke(cr);
+
+ // input histogram first, so it stays in the background
+ _draw_sat_histogram(cr, g, w, h);
+ _draw_sat_picker(cr, self, w, h);
+
+ cairo_set_source_rgb(cr, .85, .85, .85);
+ cairo_set_line_width(cr, DT_PIXEL_APPLY_DPI(2));
+ cairo_move_to(cr, 0, h * (1.f - g->draw_ys[0]));
+ for(int i = 1; i < DT_IOP_SATCURVE_RES; i++)
+ cairo_line_to(cr, w * i / (float)(DT_IOP_SATCURVE_RES - 1), h * (1.f - g->draw_ys[i]));
+ cairo_stroke(cr);
+ cairo_set_line_width(cr, DT_PIXEL_APPLY_DPI(1));
+ for(int i = 0; i < p->curve_num_nodes; i++)
+ {
+ cairo_arc(cr, w * p->curve[i].x, h * (1.f - p->curve[i].y), DT_PIXEL_APPLY_DPI(i == g->selected ? 5 : 3), 0, DT_2PI_F);
+ cairo_stroke(cr);
+ }
+ return FALSE;
+}
+
+static gboolean area_button(GtkWidget *widget, GdkEventButton *event, dt_iop_module_t *self)
+{
+ dt_iop_satcurve_gui_data_t *g = self->gui_data;
+ dt_iop_satcurve_params_t *p = self->params;
+ GtkAllocation a;
+ gtk_widget_get_allocation(widget, &a);
+ float gx0, gy0, w, h;
+ _get_graph_geometry(&a, &gx0, &gy0, &w, &h);
+ const float x = CLAMP((event->x - gx0) / w, 0.f, 1.f), y = CLAMP(1.f - (event->y - gy0) / h, 0.f, 1.f);
+ int hit = -1;
+ for(int i = 0; i < p->curve_num_nodes; i++)
+ if(sqf(x - p->curve[i].x) + sqf(y - p->curve[i].y) < .0025f)
+ {
+ hit = i;
+ break;
+ }
+
+ if(event->type == GDK_2BUTTON_PRESS && event->button == GDK_BUTTON_PRIMARY)
+ {
+ reset_curve(p);
+ g->selected = -1;
+ dt_dev_add_history_item(darktable.develop, self, TRUE);
+ gtk_widget_queue_draw(widget);
+ return TRUE;
+ }
+ if(event->button == GDK_BUTTON_SECONDARY && hit >= 0)
+ {
+ if((event->state & GDK_CONTROL_MASK) || p->curve_num_nodes <= 2)
+ p->curve[hit].y = .5f;
+ else
+ {
+ for(int i = hit; i < p->curve_num_nodes - 1; i++)
+ p->curve[i] = p->curve[i + 1];
+ p->curve_num_nodes--;
+ }
+ g->selected = -1;
+ dt_dev_add_history_item(darktable.develop, self, TRUE);
+ gtk_widget_queue_draw(widget);
+ return TRUE;
+ }
+ if(event->button == GDK_BUTTON_PRIMARY)
+ {
+ // clicking/dragging outside an existing node inserts a new one on the curve
+ if(hit < 0 && p->curve_num_nodes < DT_IOP_SATCURVE_MAXNODES)
+ hit = add_node(p, x, CLAMP(dt_draw_curve_calc_value(g->curve, x), 0.f, 1.f));
+ g->selected = hit;
+ g->dragging = hit >= 0;
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static gboolean area_motion(GtkWidget *widget, GdkEventMotion *event, dt_iop_module_t *self)
+{
+ dt_iop_satcurve_gui_data_t *g = self->gui_data;
+ dt_iop_satcurve_params_t *p = self->params;
+ if(!g->dragging || g->selected < 0)
+ return FALSE;
+ GtkAllocation a;
+ gtk_widget_get_allocation(widget, &a);
+ float gx0, gy0, w, h;
+ _get_graph_geometry(&a, &gx0, &gy0, &w, &h);
+ const float x = CLAMP((event->x - gx0) / w, 0.f, 1.f);
+ const int n = g->selected;
+ if(n == 0)
+ p->curve[n].x = 0.f;
+ else if(n == p->curve_num_nodes - 1)
+ p->curve[n].x = 1.f;
+ else
+ p->curve[n].x = CLAMP(x, p->curve[n - 1].x + DT_IOP_SATCURVE_MIN_X_DISTANCE, p->curve[n + 1].x - DT_IOP_SATCURVE_MIN_X_DISTANCE);
+ p->curve[n].y = CLAMP(1.f - (event->y - gy0) / h, 0.f, 1.f);
+ gtk_widget_queue_draw(widget);
+ return TRUE;
+}
+
+static gboolean area_release(GtkWidget *widget, GdkEventButton *event, dt_iop_module_t *self)
+{
+ dt_iop_satcurve_gui_data_t *g = self->gui_data;
+ if(g->dragging)
+ {
+ g->dragging = FALSE;
+ dt_dev_add_history_item(darktable.develop, self, TRUE);
+ }
+ return TRUE;
+}
+
+// returns the module's committed gamut_lut if already available (module
+// processed at least once); otherwise falls back to a neutral unity LUT
+// (gamut = 1 for all hues) so the picker cannot crash right after loading,
+// though it then shows unnormalized S values
+static const float *_get_gamut_lut_for_picker(dt_iop_module_t *self)
+{
+ static float unity_gamut_lut[LUT_ELEM];
+ static gboolean unity_inited = FALSE;
+ if(!unity_inited)
+ {
+ for(int i = 0; i < LUT_ELEM; i++) unity_gamut_lut[i] = 1.f;
+ unity_inited = TRUE;
+ }
+
+ for(GList *nodes = self->dev->full.pipe ? self->dev->full.pipe->nodes : NULL; nodes; nodes = g_list_next(nodes))
+ {
+ dt_dev_pixelpipe_iop_t *piece = nodes->data;
+ if(piece->module == self && piece->data)
+ {
+ const dt_iop_satcurve_data_t *d = piece->data;
+ if(d->lut_inited && d->gamut_lut) return d->gamut_lut;
+ }
+ }
+ return unity_gamut_lut;
+}
+
+// called automatically after a color pick while g->colorpicker is active.
+// self->picked_color/-min/-max are already in pipeline RGB (default_colorspace
+// is IOP_CS_RGB), so they go straight through pixel_s_in_norm() using the
+// same metric as the curve and histogram
+void color_picker_apply(dt_iop_module_t *self, GtkWidget *widget, dt_dev_pixelpipe_t *pipe)
+{
+ dt_iop_satcurve_gui_data_t *g = self->gui_data;
+ if(!g) return;
+
+ const dt_iop_order_iccprofile_info_t *work_profile =
+ dt_ioppr_get_iop_work_profile_info(self, self->dev->iop);
+ if(!work_profile) return;
+
+ dt_colormatrix_t inputmatrix = {{ 0.0f }};
+ dt_colormatrix_t inputmatrix_trans;
+ dt_colormatrix_mul(inputmatrix, XYZ_D50_to_D65_CAT16, work_profile->matrix_in);
+ dt_colormatrix_transpose(inputmatrix_trans, inputmatrix);
+
+ const float *const gamut_lut = _get_gamut_lut_for_picker(self);
+ const float L_white = Y_to_dt_UCS_L_star(1.f);
+
+ const dt_aligned_pixel_t mean_rgb = { self->picked_color[0], self->picked_color[1], self->picked_color[2], 0.f };
+ const dt_aligned_pixel_t min_rgb = { self->picked_color_min[0], self->picked_color_min[1], self->picked_color_min[2], 0.f };
+ const dt_aligned_pixel_t max_rgb = { self->picked_color_max[0], self->picked_color_max[1], self->picked_color_max[2], 0.f };
+
+ dt_iop_satcurve_data_t picker_data = { 0 };
+ picker_data.formula = ((const dt_iop_satcurve_params_t *)self->params)->formula;
+ g->picked_s = pixel_s_in_norm(&picker_data, mean_rgb, inputmatrix_trans, gamut_lut, L_white, NULL);
+ g->picked_s_min = pixel_s_in_norm(&picker_data, min_rgb, inputmatrix_trans, gamut_lut, L_white, NULL);
+ g->picked_s_max = pixel_s_in_norm(&picker_data, max_rgb, inputmatrix_trans, gamut_lut, L_white, NULL);
+ g->picker_valid = TRUE;
+
+ gtk_widget_queue_draw(GTK_WIDGET(g->area));
+}
+
+// reset the picker state when the module loses GUI focus, so no stale
+// overlay lingers in another module
+void gui_focus(dt_iop_module_t *self, gboolean in)
+{
+ if(!in)
+ dt_iop_color_picker_reset(self, FALSE);
+}
+
+// S_in_norm is defined differently per formula (JzAzBz vs darktable UCS), so
+// a histogram computed under the previous formula is stale and must not
+// linger on screen after switching. The bauhaus binding for the formula
+// combobox already updates self->params and adds a history item, but not
+// necessarily a *forced* one (see the explicit TRUE argument used for curve
+// node edits below) - force it here as well so process() always re-runs
+// and _update_sat_histogram() refreshes the histogram in either direction.
+void gui_changed(dt_iop_module_t *self, GtkWidget *widget, void *previous)
+{
+ dt_iop_satcurve_gui_data_t *g = self->gui_data;
+ if(!g) return;
+
+ if(widget == g->formula)
+ dt_dev_add_history_item(darktable.develop, self, TRUE);
+}
+
+void gui_update(dt_iop_module_t *self)
+{
+ dt_iop_satcurve_gui_data_t *g = self->gui_data;
+ const dt_iop_satcurve_params_t *p = self->params;
+ if(!g) return;
+
+ if(g->formula) dt_bauhaus_combobox_set(g->formula, p->formula);
+ if(g->area) gtk_widget_queue_draw(GTK_WIDGET(g->area));
+}
+
+void gui_cleanup(dt_iop_module_t *self)
+{
+ dt_iop_satcurve_gui_data_t *g = self->gui_data;
+ dt_draw_curve_destroy(g->curve);
+ dt_pthread_mutex_destroy(&g->histogram_lock);
+}
+
+void gui_init(dt_iop_module_t *self)
+{
+ dt_iop_satcurve_gui_data_t *g = IOP_GUI_ALLOC(satcurve);
+ const dt_iop_satcurve_params_t *p = self->default_params;
+ g->selected = -1;
+ g->curve_type = p->curve_type;
+ g->curve_num_nodes = p->curve_num_nodes;
+ g->curve = dt_draw_curve_new(0.f, 1.f, p->curve_type);
+ for(int i = 0; i < p->curve_num_nodes; i++)
+ dt_draw_curve_add_point(g->curve, p->curve[i].x, p->curve[i].y);
+
+ memset(g->histogram, 0, sizeof(g->histogram));
+ g->histogram_max = 1e-6f;
+ dt_pthread_mutex_init(&g->histogram_lock, NULL);
+
+ g->picker_valid = FALSE;
+ g->picked_s = g->picked_s_min = g->picked_s_max = 0.f;
+
+ self->widget = dt_gui_vbox();
+
+ g->area = GTK_DRAWING_AREA(dt_ui_resize_wrap(NULL, 0, "plugins/darkroom/satcurve/graph_height"));
+ gtk_widget_set_size_request(GTK_WIDGET(g->area), -1, DT_PIXEL_APPLY_DPI(180));
+ gtk_widget_add_events(GTK_WIDGET(g->area), GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | GDK_POINTER_MOTION_MASK);
+ 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), self);
+ g_signal_connect(G_OBJECT(g->area), "button-release-event", G_CALLBACK(area_release), self);
+ g_signal_connect(G_OBJECT(g->area), "motion-notify-event", G_CALLBACK(area_motion), self);
+ dt_gui_box_add(self->widget, GTK_WIDGET(g->area));
+
+ // Row below the draw area: picker button on the left, formula combobox on
+ // the right. dt_bauhaus_combobox_from_params() auto-packs the widget into
+ // self->widget, so it is detached here and re-packed into the shared hbox
+ // (pack_start/pack_end keep both widgets at their natural width with a
+ // flexible gap in between).
+ // dt_color_picker_new_with_cst() is used instead of dt_color_picker_new()
+ // because the latter attaches the picker icon to an existing bauhaus
+ // widget rather than creating a standalone button.
+ g->formula = dt_bauhaus_combobox_from_params(self, "formula");
+ dt_bauhaus_combobox_add(g->formula, _("JzAzBz"));
+ dt_bauhaus_combobox_add(g->formula, _("darktable UCS"));
+ gtk_widget_set_tooltip_text(g->formula,
+ _("choose the perceptual saturation definition used by the curve"));
+
+ g_object_ref(g->formula);
+ gtk_container_remove(GTK_CONTAINER(self->widget), g->formula);
+
+ GtkWidget *controls_hbox = dt_gui_hbox();
+ gtk_box_set_spacing(GTK_BOX(controls_hbox), DT_PIXEL_APPLY_DPI(5));
+
+ g->colorpicker = dt_color_picker_new_with_cst(self, DT_COLOR_PICKER_POINT, NULL, IOP_CS_RGB);
+ gtk_widget_set_tooltip_text(g->colorpicker, _("pick saturation from image"));
+ gtk_box_pack_start(GTK_BOX(controls_hbox), g->colorpicker, FALSE, FALSE, 0);
+ gtk_box_pack_end(GTK_BOX(controls_hbox), g->formula, FALSE, FALSE, 0);
+ g_object_unref(g->formula);
+
+ dt_gui_box_add(self->widget, controls_hbox);
+}
+
+void init(dt_iop_module_t *self)
+{
+ self->params = calloc(1, sizeof(dt_iop_satcurve_params_t));
+ self->default_params = calloc(1, sizeof(dt_iop_satcurve_params_t));
+ self->params_size = sizeof(dt_iop_satcurve_params_t);
+ reset_curve(self->params);
+ reset_curve(self->default_params);
+}
+
+// clang-format off
+// modelines: These editor modelines have been set for all relevant files by tools/update_modelines.py
+// vim: shiftwidth=2 expandtab tabstop=2 cindent
+// kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified;
+// clang-format on
\ No newline at end of file
From 79f7bdde547bb1c068ffcc004e230ac408761951 Mon Sep 17 00:00:00 2001
From: Martin Straeten <39386816+MStraeten@users.noreply.github.com>
Date: Mon, 20 Jul 2026 15:57:24 +0200
Subject: [PATCH 02/12] Initialize hist_input_matrix to zero
---
src/iop/satcurvergb.c | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/iop/satcurvergb.c b/src/iop/satcurvergb.c
index 51e55d45e743..38201f112178 100644
--- a/src/iop/satcurvergb.c
+++ b/src/iop/satcurvergb.c
@@ -478,7 +478,7 @@ int process_cl(dt_iop_module_t *self, dt_dev_pixelpipe_iop_t *piece,
if(self->dev->gui_attached && dt_pipe_is_full(piece->pipe) && dt_iop_has_focus(self)
&& piece->pipe == self->dev->full.pipe)
{
- dt_colormatrix_t hist_input_matrix;
+ dt_colormatrix_t hist_input_matrix = {{ 0.0f }};
dt_colormatrix_t hist_input_matrix_trans;
dt_colormatrix_mul(hist_input_matrix, XYZ_D50_to_D65_CAT16, work_profile->matrix_in);
dt_colormatrix_transpose(hist_input_matrix_trans, hist_input_matrix);
@@ -1087,4 +1087,4 @@ void init(dt_iop_module_t *self)
// modelines: These editor modelines have been set for all relevant files by tools/update_modelines.py
// vim: shiftwidth=2 expandtab tabstop=2 cindent
// kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified;
-// clang-format on
\ No newline at end of file
+// clang-format on
From 7a265530c0d52ddb590bf78257bab5d885efb068 Mon Sep 17 00:00:00 2001
From: Martin Straeten <39386816+MStraeten@users.noreply.github.com>
Date: Mon, 20 Jul 2026 21:48:13 +0200
Subject: [PATCH 03/12] added brilliance curve
---
data/kernels/satcurve.cl | 127 +++--
src/iop/satcurvergb.c | 1113 ++++++++++++++++++++++++--------------
2 files changed, 790 insertions(+), 450 deletions(-)
mode change 100644 => 100755 data/kernels/satcurve.cl
diff --git a/data/kernels/satcurve.cl b/data/kernels/satcurve.cl
old mode 100644
new mode 100755
index e73ac332311c..3abe25a56bfd
--- a/data/kernels/satcurve.cl
+++ b/data/kernels/satcurve.cl
@@ -1,26 +1,22 @@
/*
- This file is part of darktable,
- Copyright (C) 2009-2026 darktable developers.
-
- darktable is free software: you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- darktable is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License
- along with darktable. If not, see .
+ This file is part of darktable,
+ Copyright (C) 2009-2026 darktable developers.
+
+ darktable is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ darktable is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
*/
#include "common.h"
#include "colorspace.h"
#include "color_conversion.h"
-
#define DT_IOP_SATCURVE_RES 256
// periodic lookup in the hue-indexed gamut LUT; mirrors satcurve_lookup_gamut() on CPU
@@ -60,9 +56,12 @@ static inline float clip_jz_chroma_cl(const float Jz, const float Cz, const floa
float Iz = (Jz + d0) / (1.f + dd - dd * (Jz + d0));
Iz = fmax(Iz, 0.f);
- const float4 AI[3] = { { 1.0f, 0.1386050432715393f, 0.0580473161561189f, 0.0f },
- { 1.0f, -0.1386050432715393f, -0.0580473161561189f, 0.0f },
- { 1.0f, -0.0960192420263190f, -0.8118918960560390f, 0.0f } };
+ const float4 AI[3] = {
+ { 1.0f, 0.1386050432715393f, 0.0580473161561189f, 0.0f },
+ { 1.0f, -0.1386050432715393f, -0.0580473161561189f, 0.0f },
+ { 1.0f, -0.0960192420263190f, -0.8118918960560390f, 0.0f }
+ };
+
const float4 izab = { Iz, Cz * ch, Cz * sh, 0.f };
const float lms0 = dot(AI[0], izab);
const float lms1 = dot(AI[1], izab);
@@ -78,14 +77,39 @@ static inline float clip_jz_chroma_cl(const float Jz, const float Cz, const floa
typedef enum dt_iop_satcurve_formula_t
{
DT_IOP_SATCURVE_JZAZBZ = 0,
- DT_IOP_SATCURVE_DTUCS = 1
+ DT_IOP_SATCURVE_DTUCS = 1
} dt_iop_satcurve_formula_t;
+static inline float curve_to_factor_cl(const float c)
+{
+ return fmax(2.f * c, 0.f);
+}
+
+// maximum HSB saturation reachable at the gamut boundary for given J, h in dt UCS;
+// mirrors satcurve_ucs_gamut_saturation() on CPU -- single source of truth for the
+// gamut-boundary fit, do not re-derive it inline in the kernel.
+static inline float satcurve_ucs_gamut_saturation_cl(const float J, const float h,
+ const float L_white,
+ global const float *const gamut_lut)
+{
+ const float max_colorfulness = fmax(satcurve_lookup_gamut_cl(gamut_lut, h), FLT_MIN);
+ const float max_chroma = 15.932993652962535f
+ * dtcl_pow(J / L_white, 0.6523997524738018f)
+ * dtcl_pow(max_colorfulness, 0.6007557017508491f)
+ / L_white;
+
+ const float4 JCH_gamut_boundary = { J, max_chroma, h, 0.f };
+ const float4 HSB_gamut_boundary = dt_UCS_JCH_to_HSB(JCH_gamut_boundary);
+
+ return fmax(HSB_gamut_boundary.y, FLT_MIN);
+}
+
kernel void
satcurvergb(read_only image2d_t in, write_only image2d_t out,
const int width, const int height,
constant const float *const matrix_in, constant const float *const matrix_out,
- global const float *const lut, global const float *const gamut_lut,
+ global const float *const sat_lut, global const float *const bri_lut,
+ global const float *const gamut_lut,
const int formula, const float L_white)
{
const int x = get_global_id(0);
@@ -104,48 +128,67 @@ satcurvergb(read_only image2d_t in, write_only image2d_t out,
float4 JCH = xyY_to_dt_UCS_JCH(xyY, L_white);
float4 HCB = dt_UCS_JCH_to_HCB(JCH);
- const float max_colorfulness = fmax(satcurve_lookup_gamut_cl(gamut_lut, JCH.z), FLT_MIN);
- const float max_chroma = 15.932993652962535f
- * dtcl_pow(JCH.x / L_white, 0.6523997524738018f)
- * dtcl_pow(max_colorfulness, 0.6007557017508491f) / L_white;
-
- const float4 JCH_gamut_boundary = { JCH.x, max_chroma, JCH.z, 0.f };
- const float4 HSB_gamut_boundary = dt_UCS_JCH_to_HSB(JCH_gamut_boundary);
-
float4 HSB = { HCB.x, HCB.z > 0.f ? HCB.y / HCB.z : 0.f, HCB.z, 0.f };
- const float gamut_s = fmax(HSB_gamut_boundary.y, FLT_MIN);
+ const float gamut_s = satcurve_ucs_gamut_saturation_cl(JCH.x, JCH.z, L_white, gamut_lut);
const float s_in_norm = HSB.y / gamut_s;
- const float c = clamp(satcurve_lookup_lut_cl(lut, s_in_norm), 0.f, 1.f);
- HSB.y = fmax(HSB.y * (2.f * c), 0.f);
+
+ const float sat_c = clamp(satcurve_lookup_lut_cl(sat_lut, s_in_norm), 0.f, 1.f);
+ const float bri_c = clamp(satcurve_lookup_lut_cl(bri_lut, s_in_norm), 0.f, 1.f);
+ const float sat_factor = curve_to_factor_cl(sat_c);
+ const float bri_factor = curve_to_factor_cl(bri_c);
+
+ HSB.y = fmax(HSB.y * sat_factor, 0.f);
HSB.y = satcurve_soft_clip_cl(HSB.y, 0.8f * gamut_s, gamut_s);
JCH = dt_UCS_HSB_to_JCH(HSB);
+ HCB = dt_UCS_JCH_to_HCB(JCH);
+
+ HCB.y = fmax(HCB.y * bri_factor, 0.f);
+ HCB.z = fmax(HCB.z * bri_factor, 0.f);
+
+ JCH = dt_UCS_HCB_to_JCH(HCB);
+
+ const float gamut_s_out = satcurve_ucs_gamut_saturation_cl(JCH.x, JCH.z, L_white, gamut_lut);
+
+ float4 HSB_out = { HCB.x, HCB.z > 0.f ? HCB.y / HCB.z : 0.f, HCB.z, 0.f };
+ HSB_out.y = satcurve_soft_clip_cl(HSB_out.y, 0.8f * gamut_s_out, gamut_s_out);
+
+ JCH = dt_UCS_HSB_to_JCH(HSB_out);
xyY = dt_UCS_JCH_to_xyY(JCH, L_white);
xyz = dt_xyY_to_XYZ(xyY);
}
else
{
float4 jab = XYZ_to_JzAzBz(xyz);
+
const float Jz = fmax(jab.x, 0.f);
const float Cz = hypot(jab.y, jab.z);
- const float h = atan2(jab.z, jab.y);
+ const float h = atan2(jab.z, jab.y);
const float ch = dtcl_cos(h), sh = dtcl_sin(h);
const float gamut = fmax(satcurve_lookup_gamut_cl(gamut_lut, h), FLT_MIN);
+
const float s_in_norm = (Jz > 0.f ? Cz / Jz : 0.f) / gamut;
- const float c = clamp(satcurve_lookup_lut_cl(lut, s_in_norm), 0.f, 1.f);
- const float s_out = satcurve_soft_clip_cl(fmax(s_in_norm * (2.f * c), 0.f), 0.8f, 1.f) * gamut;
+
+ const float sat_c = clamp(satcurve_lookup_lut_cl(sat_lut, s_in_norm), 0.f, 1.f);
+ const float bri_c = clamp(satcurve_lookup_lut_cl(bri_lut, s_in_norm), 0.f, 1.f);
+ const float sat_factor = curve_to_factor_cl(sat_c);
+ const float bri_factor = curve_to_factor_cl(bri_c);
+
+ const float s_out = satcurve_soft_clip_cl(fmax(s_in_norm * sat_factor, 0.f), 0.8f, 1.f) * gamut;
const float r = hypot(Jz, Cz);
- // T = atan(s_out); cos(T) = 1/sqrt(1+s_out^2), sin(T) = s_out/sqrt(1+s_out^2)
- // avoids one atan + one cos + one sin per pixel
const float inv_norm = 1.f / sqrt(1.f + s_out * s_out);
- const float Jz_out = r * inv_norm;
- const float Cz_out = clip_jz_chroma_cl(Jz_out, r * s_out * inv_norm, ch, sh);
- jab.x = Jz_out;
- jab.y = Cz_out * ch;
- jab.z = Cz_out * sh;
+ float Jz_tmp = r * inv_norm;
+ float Cz_tmp = clip_jz_chroma_cl(Jz_tmp, r * s_out * inv_norm, ch, sh);
+
+ Jz_tmp *= bri_factor;
+ Cz_tmp = clip_jz_chroma_cl(Jz_tmp, Cz_tmp * bri_factor, ch, sh);
+
+ jab.x = Jz_tmp;
+ jab.y = Cz_tmp * ch;
+ jab.z = Cz_tmp * sh;
xyz = JzAzBz_2_XYZ(jab);
}
diff --git a/src/iop/satcurvergb.c b/src/iop/satcurvergb.c
index 38201f112178..5b8822c60b34 100644
--- a/src/iop/satcurvergb.c
+++ b/src/iop/satcurvergb.c
@@ -1,20 +1,18 @@
/*
- This file is part of darktable,
- Copyright (C) 2026 darktable developers.
-
- darktable is free software: you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- darktable is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License
- along with darktable. If not, see .
+ This file is part of darktable,
+ Copyright (C) 2026 darktable developers.
+
+ darktable is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ darktable is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
*/
+
// our includes go first:
#include "bauhaus/bauhaus.h"
#include "common/chromatic_adaptation.h"
@@ -38,70 +36,93 @@
#define DT_IOP_SATCURVE_RES 256
#define DT_IOP_SATCURVE_HIST_RES 256
#define DT_IOP_SATCURVE_INSET DT_PIXEL_APPLY_DPI(5)
-// thickness of, and gap to, the decorative perceptual gradient bar
-// drawn below the curve graph
#define DT_IOP_SATCURVE_GRADIENT_SIZE DT_PIXEL_APPLY_DPI(8)
#define DT_IOP_SATCURVE_GRADIENT_GAP DT_PIXEL_APPLY_DPI(2)
#define DT_IOP_SATCURVE_MIN_X_DISTANCE 0.0025f
#define DT_IOP_SATCURVE_GAMUT_STEPS 92
+#define DT_IOP_SATCURVE_CHANNELS 2
-// version of the module's serialized parameters
-DT_MODULE_INTROSPECTION(1, dt_iop_satcurve_params_t)
+DT_MODULE_INTROSPECTION(2, dt_iop_satcurve_params_t)
-// saturation definition used to compute S_in_norm and drive the curve
typedef enum dt_iop_satcurve_formula_t
{
DT_IOP_SATCURVE_JZAZBZ = 0,
DT_IOP_SATCURVE_DTUCS = 1
} dt_iop_satcurve_formula_t;
+typedef enum dt_iop_satcurve_channel_t
+{
+ DT_IOP_SATCURVE_CHANNEL_SATURATION = 0,
+ DT_IOP_SATCURVE_CHANNEL_BRILLIANCE = 1
+} dt_iop_satcurve_channel_t;
+
typedef struct dt_iop_satcurve_node_t
{
float x, y;
} dt_iop_satcurve_node_t;
-// module parameters, serialized to the database
-typedef struct dt_iop_satcurve_params_t
+typedef struct dt_iop_satcurve_channel_params_t
{
dt_iop_satcurve_node_t curve[DT_IOP_SATCURVE_MAXNODES];
int curve_num_nodes;
int curve_type;
+} dt_iop_satcurve_channel_params_t;
+
+typedef struct dt_iop_satcurve_params_t
+{
+ dt_iop_satcurve_channel_params_t channel[DT_IOP_SATCURVE_CHANNELS];
dt_iop_satcurve_formula_t formula; // $DEFAULT: 1 $DESCRIPTION: "saturation formula"
} dt_iop_satcurve_params_t;
-// per-pixelpipe runtime data, derived from params_t in commit_params()
-typedef struct dt_iop_satcurve_data_t
+typedef struct dt_iop_satcurve_params_v1_t
{
- dt_draw_curve_t *curve;
+ dt_iop_satcurve_node_t curve[DT_IOP_SATCURVE_MAXNODES];
int curve_num_nodes;
int curve_type;
dt_iop_satcurve_formula_t formula;
+} dt_iop_satcurve_params_v1_t;
+
+typedef struct dt_iop_satcurve_channel_data_t
+{
+ dt_draw_curve_t *curve;
+ int curve_num_nodes;
+ int curve_type;
float *lut;
+} dt_iop_satcurve_channel_data_t;
+
+typedef struct dt_iop_satcurve_data_t
+{
+ dt_iop_satcurve_formula_t formula;
+ dt_iop_satcurve_channel_data_t channel[DT_IOP_SATCURVE_CHANNELS];
float *gamut_lut;
gboolean lut_inited;
const struct dt_iop_order_iccprofile_info_t *work_profile;
} dt_iop_satcurve_data_t;
-// GUI state and widget handles
-typedef struct dt_iop_satcurve_gui_data_t
+typedef struct dt_iop_satcurve_gui_channel_t
{
- GtkDrawingArea *area;
- GtkWidget *colorpicker;
- GtkWidget *formula;
dt_draw_curve_t *curve;
int curve_num_nodes;
int curve_type;
float draw_ys[DT_IOP_SATCURVE_RES];
+} dt_iop_satcurve_gui_channel_t;
+
+typedef struct dt_iop_satcurve_gui_data_t
+{
+ GtkDrawingArea *area;
+ GtkWidget *colorpicker;
+ GtkWidget *formula;
+ GtkNotebook *notebook;
+
+ dt_iop_satcurve_gui_channel_t channel[DT_IOP_SATCURVE_CHANNELS];
+ dt_iop_satcurve_channel_t active_channel;
int selected;
gboolean dragging;
- float mouse_x, mouse_y;
- // input saturation histogram (S_in_norm)
float histogram[DT_IOP_SATCURVE_HIST_RES];
float histogram_max;
dt_pthread_mutex_t histogram_lock;
- // picked region: S_in_norm of mean/min/max
gboolean picker_valid;
float picked_s;
float picked_s_min;
@@ -113,6 +134,12 @@ typedef struct dt_iop_satcurve_global_data_t
int kernel_satcurvergb;
} dt_iop_satcurve_global_data_t;
+typedef struct dt_iop_satcurve_factors_t
+{
+ float sat_factor;
+ float bri_factor;
+} dt_iop_satcurve_factors_t;
+
const char *name()
{
return _("saturation curve");
@@ -120,16 +147,16 @@ const char *name()
const char *aliases()
{
- return _("sat vs sat|saturation versus saturation|satcurve");
+ return _("sat vs sat|saturation versus saturation|brilliance curve|satcurve");
}
const char **description(dt_iop_module_t *self)
{
- return dt_iop_set_description(self, _("remap saturation with a curve"),
- _("corrective or creative"),
- _("linear, RGB, scene-referred"),
- _("linear, RGB, scene-referred"),
- _("linear, RGB, scene-referred"));
+ return dt_iop_set_description(self, _("remap saturation and brilliance with curves"),
+ _("corrective or creative"),
+ _("linear, RGB, scene-referred"),
+ _("linear, RGB, scene-referred"),
+ _("linear, RGB, scene-referred"));
}
int flags()
@@ -142,22 +169,26 @@ int default_group()
return IOP_GROUP_COLOR | IOP_GROUP_GRADING;
}
-dt_iop_colorspace_type_t default_colorspace(dt_iop_module_t *self, dt_dev_pixelpipe_t *pipe,
- dt_dev_pixelpipe_iop_t *piece)
+dt_iop_colorspace_type_t default_colorspace(dt_iop_module_t *self,
+ dt_dev_pixelpipe_t *pipe,
+ dt_dev_pixelpipe_iop_t *piece)
{
return IOP_CS_RGB;
}
-// intended to run before filmic rgb; pipeline order entry is added
-// separately in iop_order.c
+static inline void reset_channel_curve(dt_iop_satcurve_channel_params_t *c)
+{
+ c->curve_num_nodes = 2;
+ c->curve_type = MONOTONE_HERMITE;
+ c->curve[0] = (dt_iop_satcurve_node_t){ 0.f, .5f };
+ c->curve[1] = (dt_iop_satcurve_node_t){ 1.f, .5f };
+}
-static inline void reset_curve(dt_iop_satcurve_params_t *p)
+static inline void reset_params(dt_iop_satcurve_params_t *p)
{
- p->curve_num_nodes = 2;
- p->curve_type = MONOTONE_HERMITE;
+ reset_channel_curve(&p->channel[DT_IOP_SATCURVE_CHANNEL_SATURATION]);
+ reset_channel_curve(&p->channel[DT_IOP_SATCURVE_CHANNEL_BRILLIANCE]);
p->formula = DT_IOP_SATCURVE_DTUCS;
- p->curve[0] = (dt_iop_satcurve_node_t){0.f, .5f};
- p->curve[1] = (dt_iop_satcurve_node_t){1.f, .5f};
}
static inline float lookup_lut(const float *lut, const float x)
@@ -167,61 +198,58 @@ static inline float lookup_lut(const float *lut, const float x)
return lut[i] + (position - i) * (lut[i + 1] - lut[i]);
}
-// periodic lookup in the hue-indexed gamut LUT; h = atan2(b, a), in [-pi, pi]
+static inline float curve_to_factor(const float c)
+{
+ return MAX(2.f * c, 0.f);
+}
+
static inline float satcurve_lookup_gamut(const float *const gamut_lut, const float h)
{
const float position = (h + M_PI_F) * (float)LUT_ELEM / DT_2PI_F;
const int bin0 = ((int)floorf(position)) % LUT_ELEM;
const int bin1 = (bin0 + 1) % LUT_ELEM;
const float f = position - floorf(position);
-
return gamut_lut[bin0] * (1.f - f) + gamut_lut[bin1] * f;
}
-// smoothly map x from knee onwards towards maximum (reached asymptotically);
-// x <= knee is left unchanged
static inline float satcurve_soft_clip(const float x, const float knee, const float maximum)
{
- if(x <= knee)
- return x;
+ if(x <= knee) return x;
const float range = maximum - knee;
- if(range <= 0.f)
- return maximum;
+ if(range <= 0.f) return maximum;
return knee + range * (1.f - expf(-(x - knee) / range));
}
-// mirrors the JzAzBz chroma clip in color balance rgb (test conversion to
-// L'M'S'): keeps the curve from producing colors that go negative on the
-// JzAzBz -> XYZ back-transform
static inline float clip_jz_chroma(const float Jz, const float Cz, const float ch, const float sh)
{
const float d0 = 1.6295499532821566e-11f;
const float dd = -0.56f;
float Iz = (Jz + d0) / (1.f + dd - dd * (Jz + d0));
Iz = MAX(Iz, 0.f);
- static const dt_colormatrix_t AI_trans = {{1.f, 1.f, 1.f, 0.f},
- {.1386050432715393f, -.1386050432715393f, -.0960192420263190f, 0.f},
- {.0580473161561189f, -.0580473161561189f, -.8118918960560390f, 0.f}};
- dt_aligned_pixel_t izab = {Iz, Cz * ch, Cz * sh, 0.f}, lms;
+
+ static const dt_colormatrix_t AI_trans = {
+ { 1.f, 1.f, 1.f, 0.f },
+ { .1386050432715393f, -.1386050432715393f, -.0960192420263190f, 0.f },
+ { .0580473161561189f, -.0580473161561189f, -.8118918960560390f, 0.f }
+ };
+
+ dt_aligned_pixel_t izab = { Iz, Cz * ch, Cz * sh, 0.f }, lms;
dt_apply_transposed_color_matrix(izab, AI_trans, lms);
+
float max_c = Cz;
- if(lms[0] < 0.f)
- max_c = MIN(max_c, -Iz / (AI_trans[1][0] * ch + AI_trans[2][0] * sh));
- if(lms[1] < 0.f)
- max_c = MIN(max_c, -Iz / (AI_trans[1][1] * ch + AI_trans[2][1] * sh));
- if(lms[2] < 0.f)
- max_c = MIN(max_c, -Iz / (AI_trans[1][2] * ch + AI_trans[2][2] * sh));
+ if(lms[0] < 0.f) max_c = MIN(max_c, -Iz / (AI_trans[1][0] * ch + AI_trans[2][0] * sh));
+ if(lms[1] < 0.f) max_c = MIN(max_c, -Iz / (AI_trans[1][1] * ch + AI_trans[2][1] * sh));
+ if(lms[2] < 0.f) max_c = MIN(max_c, -Iz / (AI_trans[1][2] * ch + AI_trans[2][2] * sh));
+
return MAX(max_c, 0.f);
}
-// shared S_in_norm computation, used by process(), the histogram, and the
-// picker, so all three use the exact same scene-referred math
static inline float pixel_s_in_norm_jzazbz(const float *const restrict rgb_in,
- const dt_colormatrix_t inputmatrix_trans,
- const float *const restrict gamut_lut,
- float *const restrict h_out)
+ const dt_colormatrix_t inputmatrix_trans,
+ const float *const restrict gamut_lut,
+ float *const restrict h_out)
{
dt_aligned_pixel_t rgb, xyz, jab;
copy_pixel(rgb, rgb_in);
@@ -239,15 +267,34 @@ static inline float pixel_s_in_norm_jzazbz(const float *const restrict rgb_in,
return s_in / gamut;
}
-static inline float pixel_s_in_norm_ucs(
- const float *const restrict rgb_in,
- const dt_colormatrix_t inputmatrix_trans,
- const float *const restrict gamut_lut,
- const float L_white,
- float *const restrict h_out)
+// maximum HSB saturation reachable at the gamut boundary for a given J, h in dt UCS;
+// fit constants (15.93.../0.652.../0.600...) come from the gamut-boundary polynomial
+// model used throughout darktable_ucs_22_helpers. Single source of truth for this
+// computation -- do not re-derive it inline at call sites.
+static inline float satcurve_ucs_gamut_saturation(const float J, const float h,
+ const float L_white,
+ const float *const restrict gamut_lut)
+{
+ const float max_colorfulness = MAX(satcurve_lookup_gamut(gamut_lut, h), FLT_MIN);
+ const float max_chroma = 15.932993652962535f
+ * powf(J / L_white, 0.6523997524738018f)
+ * powf(max_colorfulness, 0.6007557017508491f)
+ / L_white;
+
+ const dt_aligned_pixel_t JCH_gamut_boundary = { J, max_chroma, h, 0.f };
+ dt_aligned_pixel_t HSB_gamut_boundary;
+ dt_UCS_JCH_to_HSB(JCH_gamut_boundary, HSB_gamut_boundary);
+
+ return MAX(HSB_gamut_boundary[1], FLT_MIN);
+}
+
+static inline float pixel_s_in_norm_ucs(const float *const restrict rgb_in,
+ const dt_colormatrix_t inputmatrix_trans,
+ const float *const restrict gamut_lut,
+ const float L_white,
+ float *const restrict h_out)
{
dt_aligned_pixel_t rgb, xyz, xyY, JCH, HCB;
- dt_aligned_pixel_t JCH_gamut_boundary, HSB_gamut_boundary;
copy_pixel(rgb, rgb_in);
dt_vector_clipneg(rgb);
@@ -257,67 +304,80 @@ static inline float pixel_s_in_norm_ucs(
xyY_to_dt_UCS_JCH(xyY, L_white, JCH);
dt_UCS_JCH_to_HCB(JCH, HCB);
- const float max_colorfulness
- = MAX(satcurve_lookup_gamut(gamut_lut, JCH[2]), FLT_MIN);
-
- const float max_chroma
- = 15.932993652962535f
- * powf(JCH[0] / L_white, 0.6523997524738018f)
- * powf(max_colorfulness, 0.6007557017508491f)
- / L_white;
-
- JCH_gamut_boundary[0] = JCH[0];
- JCH_gamut_boundary[1] = max_chroma;
- JCH_gamut_boundary[2] = JCH[2];
- JCH_gamut_boundary[3] = 0.f;
-
- dt_UCS_JCH_to_HSB(JCH_gamut_boundary, HSB_gamut_boundary);
+ const float gamut_s = satcurve_ucs_gamut_saturation(JCH[0], JCH[2], L_white, gamut_lut);
if(h_out) *h_out = JCH[2];
const float saturation = HCB[2] > 0.f ? HCB[1] / HCB[2] : 0.f;
- return saturation / MAX(HSB_gamut_boundary[1], FLT_MIN);
+ return saturation / gamut_s;
}
-static inline float pixel_s_in_norm(
- const dt_iop_satcurve_data_t *const d,
- const float *const restrict rgb_in,
- const dt_colormatrix_t inputmatrix_trans,
- const float *const restrict gamut_lut,
- const float L_white,
- float *const restrict h_out)
+static inline float pixel_s_in_norm(const dt_iop_satcurve_formula_t formula,
+ const float *const restrict rgb_in,
+ const dt_colormatrix_t inputmatrix_trans,
+ const float *const restrict gamut_lut,
+ const float L_white,
+ float *const restrict h_out)
{
- if(d->formula == DT_IOP_SATCURVE_DTUCS)
+ if(formula == DT_IOP_SATCURVE_DTUCS)
return pixel_s_in_norm_ucs(rgb_in, inputmatrix_trans, gamut_lut, L_white, h_out);
return pixel_s_in_norm_jzazbz(rgb_in, inputmatrix_trans, gamut_lut, h_out);
}
-// updates the input-referred saturation histogram; GUI full-preview only,
-// mirrors the display_mask gating used in colorzones.c
+static inline dt_iop_satcurve_channel_params_t *get_active_channel_params(dt_iop_module_t *self)
+{
+ dt_iop_satcurve_gui_data_t *g = self->gui_data;
+ dt_iop_satcurve_params_t *p = self->params;
+ return &p->channel[g->active_channel];
+}
+
+static inline dt_iop_satcurve_gui_channel_t *get_active_gui_channel(dt_iop_module_t *self)
+{
+ dt_iop_satcurve_gui_data_t *g = self->gui_data;
+ return &g->channel[g->active_channel];
+}
+
+static inline gboolean channel_is_neutral(const dt_iop_satcurve_channel_data_t *c)
+{
+ return c->curve_num_nodes == 2
+ && fabsf(c->lut[0] - .5f) < 1e-6f
+ && fabsf(c->lut[DT_IOP_SATCURVE_RES - 1] - .5f) < 1e-6f;
+}
+
+static inline dt_iop_satcurve_factors_t eval_curve_factors(const dt_iop_satcurve_data_t *d,
+ const float s_in_norm)
+{
+ const float sat_c = CLAMP(lookup_lut(d->channel[DT_IOP_SATCURVE_CHANNEL_SATURATION].lut, s_in_norm), 0.f, 1.f);
+ const float bri_c = CLAMP(lookup_lut(d->channel[DT_IOP_SATCURVE_CHANNEL_BRILLIANCE].lut, s_in_norm), 0.f, 1.f);
+
+ return (dt_iop_satcurve_factors_t){
+ .sat_factor = curve_to_factor(sat_c),
+ .bri_factor = curve_to_factor(bri_c)
+ };
+}
+
static void _update_sat_histogram(dt_iop_module_t *self,
- const dt_iop_satcurve_data_t *d,
- const dt_colormatrix_t inputmatrix_trans,
- const float *const restrict in,
- const size_t npixels)
+ const dt_iop_satcurve_data_t *d,
+ const dt_colormatrix_t inputmatrix_trans,
+ const float *const restrict in,
+ const size_t npixels)
{
dt_iop_satcurve_gui_data_t *g = self->gui_data;
if(!g) return;
float local_hist[DT_IOP_SATCURVE_HIST_RES] = { 0.f };
- const float L_white = Y_to_dt_UCS_L_star(1.f); // constant; compute once, not per pixel
+ const float L_white = Y_to_dt_UCS_L_star(1.f);
DT_OMP_FOR(reduction(+ : local_hist[:DT_IOP_SATCURVE_HIST_RES]))
for(size_t k = 0; k < npixels; k++)
{
- const float s_in_norm = pixel_s_in_norm(d, in + 4 * k, inputmatrix_trans, d->gamut_lut, L_white, NULL);
+ const float s_in_norm = pixel_s_in_norm(d->formula, in + 4 * k, inputmatrix_trans, d->gamut_lut, L_white, NULL);
const int bin = CLAMP((int)(s_in_norm * (DT_IOP_SATCURVE_HIST_RES - 1)),
- 0, DT_IOP_SATCURVE_HIST_RES - 1);
+ 0, DT_IOP_SATCURVE_HIST_RES - 1);
local_hist[bin] += 1.f;
}
- // log1p scaling: S_in_norm is typically skewed heavily toward low values;
- // the x-axis (S_in_norm) itself stays linear
float max_val = 0.f;
for(int i = 0; i < DT_IOP_SATCURVE_HIST_RES; i++)
{
@@ -333,15 +393,94 @@ static void _update_sat_histogram(dt_iop_module_t *self,
dt_control_queue_redraw_widget(GTK_WIDGET(g->area));
}
+static inline void apply_sat_and_brilliance_ucs(const dt_iop_satcurve_data_t *d,
+ dt_aligned_pixel_t xyz,
+ const float L_white)
+{
+ dt_aligned_pixel_t xyY, JCH, HCB, HSB;
+
+ dt_D65_XYZ_to_xyY(xyz, xyY);
+ xyY_to_dt_UCS_JCH(xyY, L_white, JCH);
+ dt_UCS_JCH_to_HCB(JCH, HCB);
+
+ HSB[0] = HCB[0];
+ HSB[1] = HCB[2] > 0.f ? HCB[1] / HCB[2] : 0.f;
+ HSB[2] = HCB[2];
+ HSB[3] = 0.f;
+
+ const float gamut_s = satcurve_ucs_gamut_saturation(JCH[0], JCH[2], L_white, d->gamut_lut);
+ const float s_in_norm = HSB[1] / gamut_s;
+ const dt_iop_satcurve_factors_t f = eval_curve_factors(d, s_in_norm);
+
+ HSB[1] = MAX(HSB[1] * f.sat_factor, 0.f);
+ HSB[1] = satcurve_soft_clip(HSB[1], .8f * gamut_s, gamut_s);
+
+ dt_UCS_HSB_to_JCH(HSB, JCH);
+ dt_UCS_JCH_to_HCB(JCH, HCB);
+
+ HCB[1] = MAX(HCB[1] * f.bri_factor, 0.f);
+ HCB[2] = MAX(HCB[2] * f.bri_factor, 0.f);
+
+ dt_UCS_HCB_to_JCH(HCB, JCH);
+
+ const float gamut_s_out = satcurve_ucs_gamut_saturation(JCH[0], JCH[2], L_white, d->gamut_lut);
+
+ dt_aligned_pixel_t HSB_out;
+ HSB_out[0] = HCB[0];
+ HSB_out[1] = HCB[2] > 0.f ? HCB[1] / HCB[2] : 0.f;
+ HSB_out[2] = HCB[2];
+ HSB_out[3] = 0.f;
+
+ HSB_out[1] = satcurve_soft_clip(HSB_out[1], .8f * gamut_s_out, gamut_s_out);
+
+ dt_UCS_HSB_to_JCH(HSB_out, JCH);
+ dt_UCS_JCH_to_xyY(JCH, L_white, xyY);
+ dt_xyY_to_XYZ(xyY, xyz);
+}
+
+static inline void apply_sat_and_brilliance_jzazbz(const dt_iop_satcurve_data_t *d,
+ dt_aligned_pixel_t xyz)
+{
+ dt_aligned_pixel_t jab;
+ dt_XYZ_2_JzAzBz(xyz, jab);
+
+ const float Jz = MAX(jab[0], 0.f);
+ const float Cz = dt_fast_hypotf(jab[1], jab[2]);
+ const float h = atan2f(jab[2], jab[1]);
+ const float ch = cosf(h), sh = sinf(h);
+ const float gamut = MAX(satcurve_lookup_gamut(d->gamut_lut, h), FLT_MIN);
+
+ const float s_in_norm = (Jz > 0.f ? Cz / Jz : 0.f) / gamut;
+ const dt_iop_satcurve_factors_t f = eval_curve_factors(d, s_in_norm);
+
+ const float s_out = satcurve_soft_clip(MAX(s_in_norm * f.sat_factor, 0.f), .8f, 1.f) * gamut;
+
+ const float r = dt_fast_hypotf(Jz, Cz);
+ const float inv_norm = 1.f / sqrtf(1.f + s_out * s_out);
+
+ float Jz_tmp = r * inv_norm;
+ float Cz_tmp = clip_jz_chroma(Jz_tmp, r * s_out * inv_norm, ch, sh);
+
+ Jz_tmp *= f.bri_factor;
+ Cz_tmp = clip_jz_chroma(Jz_tmp, Cz_tmp * f.bri_factor, ch, sh);
+
+ jab[0] = Jz_tmp;
+ jab[1] = Cz_tmp * ch;
+ jab[2] = Cz_tmp * sh;
+
+ dt_JzAzBz_2_XYZ(jab, xyz);
+}
+
void process(dt_iop_module_t *self, dt_dev_pixelpipe_iop_t *piece,
const void *const ivoid, void *const ovoid,
const dt_iop_roi_t *const roi_in, const dt_iop_roi_t *const roi_out)
{
dt_iop_satcurve_data_t *d = piece->data;
- const gboolean neutral_curve = d->curve_num_nodes == 2
- && fabsf(d->lut[0] - .5f) < 1e-6f
- && fabsf(d->lut[DT_IOP_SATCURVE_RES - 1] - .5f) < 1e-6f;
- if(neutral_curve)
+
+ const gboolean neutral_sat = channel_is_neutral(&d->channel[DT_IOP_SATCURVE_CHANNEL_SATURATION]);
+ const gboolean neutral_bri = channel_is_neutral(&d->channel[DT_IOP_SATCURVE_CHANNEL_BRILLIANCE]);
+
+ if(neutral_sat && neutral_bri)
{
dt_iop_image_copy_by_size(ovoid, ivoid, roi_out->width, roi_out->height, piece->colors);
return;
@@ -358,6 +497,7 @@ void process(dt_iop_module_t *self, dt_dev_pixelpipe_iop_t *piece,
dt_colormatrix_t outputmatrix = {{ 0.0f }};
dt_colormatrix_t inputmatrix_trans;
dt_colormatrix_t outputmatrix_trans;
+
dt_colormatrix_mul(inputmatrix, XYZ_D50_to_D65_CAT16, work_profile->matrix_in);
dt_colormatrix_transpose(inputmatrix_trans, inputmatrix);
dt_colormatrix_mul(outputmatrix, work_profile->matrix_out, XYZ_D65_to_D50_CAT16);
@@ -371,7 +511,6 @@ void process(dt_iop_module_t *self, dt_dev_pixelpipe_iop_t *piece,
&& piece->pipe == self->dev->full.pipe)
_update_sat_histogram(self, d, inputmatrix_trans, in, npixels);
- // constant regardless of pixel data; hoisted out of the per-pixel loop
const float L_white = Y_to_dt_UCS_L_star(1.f);
DT_OMP_FOR()
@@ -383,66 +522,16 @@ void process(dt_iop_module_t *self, dt_dev_pixelpipe_iop_t *piece,
dt_apply_transposed_color_matrix(rgb, inputmatrix_trans, xyz);
if(d->formula == DT_IOP_SATCURVE_DTUCS)
- {
- dt_aligned_pixel_t xyY, JCH, HCB, HSB, JCH_gamut_boundary, HSB_gamut_boundary;
- dt_D65_XYZ_to_xyY(xyz, xyY);
- xyY_to_dt_UCS_JCH(xyY, L_white, JCH);
- dt_UCS_JCH_to_HCB(JCH, HCB);
-
- const float max_colorfulness = MAX(satcurve_lookup_gamut(d->gamut_lut, JCH[2]), FLT_MIN);
- const float max_chroma = 15.932993652962535f
- * powf(JCH[0] / L_white, 0.6523997524738018f)
- * powf(max_colorfulness, 0.6007557017508491f)
- / L_white;
- JCH_gamut_boundary[0] = JCH[0];
- JCH_gamut_boundary[1] = max_chroma;
- JCH_gamut_boundary[2] = JCH[2];
- JCH_gamut_boundary[3] = 0.f;
- dt_UCS_JCH_to_HSB(JCH_gamut_boundary, HSB_gamut_boundary);
-
- HSB[0] = HCB[0];
- HSB[1] = HCB[2] > 0.f ? HCB[1] / HCB[2] : 0.f;
- HSB[2] = HCB[2];
- HSB[3] = 0.f;
-
- const float gamut_s = MAX(HSB_gamut_boundary[1], FLT_MIN);
- const float s_in_norm = HSB[1] / gamut_s;
- const float c = CLAMP(lookup_lut(d->lut, s_in_norm), 0.f, 1.f);
- HSB[1] = MAX(HSB[1] * (2.f * c), 0.f);
- HSB[1] = satcurve_soft_clip(HSB[1], .8f * gamut_s, gamut_s);
-
- dt_UCS_HSB_to_JCH(HSB, JCH);
- dt_UCS_JCH_to_xyY(JCH, L_white, xyY);
- dt_xyY_to_XYZ(xyY, xyz);
- }
+ apply_sat_and_brilliance_ucs(d, xyz, L_white);
else
- {
- dt_aligned_pixel_t jab;
- dt_XYZ_2_JzAzBz(xyz, jab);
- const float Jz = MAX(jab[0], 0.f);
- const float Cz = dt_fast_hypotf(jab[1], jab[2]);
- const float h = atan2f(jab[2], jab[1]);
- const float ch = cosf(h), sh = sinf(h);
- const float gamut = MAX(satcurve_lookup_gamut(d->gamut_lut, h), FLT_MIN);
- const float s_in_norm = (Jz > 0.f ? Cz / Jz : 0.f) / gamut;
- const float c = CLAMP(lookup_lut(d->lut, s_in_norm), 0.f, 1.f);
- const float s_out = satcurve_soft_clip(MAX(s_in_norm * (2.f * c), 0.f), .8f, 1.f) * gamut;
- const float r = dt_fast_hypotf(Jz, Cz);
- // T = atanf(s_out); cosf(T) = 1/sqrt(1+s_out^2), sinf(T) = s_out/sqrt(1+s_out^2)
- // avoids one atanf + one cosf + one sinf per pixel
- const float inv_norm = 1.f / sqrtf(1.f + s_out * s_out);
- const float Jz_out = r * inv_norm;
- const float Cz_out = clip_jz_chroma(Jz_out, r * s_out * inv_norm, ch, sh);
- jab[0] = Jz_out;
- jab[1] = Cz_out * ch;
- jab[2] = Cz_out * sh;
- dt_JzAzBz_2_XYZ(jab, xyz);
- }
+ apply_sat_and_brilliance_jzazbz(d, xyz);
dt_apply_transposed_color_matrix(xyz, outputmatrix_trans, pixout);
dt_vector_clipneg(pixout);
+ pixout[3] = in[4 * k + 3];
copy_pixel_nontemporal(out + 4 * k, pixout);
}
+
dt_omploop_sfence();
}
@@ -457,24 +546,22 @@ int process_cl(dt_iop_module_t *self, dt_dev_pixelpipe_iop_t *piece,
cl_int err = DT_OPENCL_DEFAULT_ERROR;
if(piece->colors != 4) return err;
+ const gboolean neutral_sat = channel_is_neutral(&d->channel[DT_IOP_SATCURVE_CHANNEL_SATURATION]);
+ const gboolean neutral_bri = channel_is_neutral(&d->channel[DT_IOP_SATCURVE_CHANNEL_BRILLIANCE]);
+ if(neutral_sat && neutral_bri)
+ return dt_opencl_enqueue_copy_image(piece->pipe->devid, dev_in, dev_out,
+ (size_t[]){ 0, 0, 0 },
+ (size_t[]){ 0, 0, 0 },
+ (size_t[]){ roi_in->width, roi_in->height, 1 });
+
+ const dt_iop_order_iccprofile_info_t *const work_profile =
+ dt_ioppr_get_pipe_current_profile_info(self, piece->pipe);
+ if(work_profile == NULL) return err;
+
const int devid = piece->pipe->devid;
const int width = roi_in->width;
const int height = roi_in->height;
- const gboolean neutral_curve = d->curve_num_nodes == 2
- && fabsf(d->lut[0] - .5f) < 1e-6f
- && fabsf(d->lut[DT_IOP_SATCURVE_RES - 1] - .5f) < 1e-6f;
- if(neutral_curve)
- return dt_opencl_enqueue_copy_image(devid, dev_in, dev_out, (size_t[]){0,0,0},
- (size_t[]){0,0,0}, (size_t[]){width, height, 1});
-
- const dt_iop_order_iccprofile_info_t *const work_profile
- = dt_ioppr_get_pipe_current_profile_info(self, piece->pipe);
- if(work_profile == NULL) return err;
-
- // Histogram needs a host-side copy of the input buffer; only do this
- // when the GUI actually needs it, to avoid a needless GPU->CPU sync
- // on every full-pipe run.
if(self->dev->gui_attached && dt_pipe_is_full(piece->pipe) && dt_iop_has_focus(self)
&& piece->pipe == self->dev->full.pipe)
{
@@ -492,41 +579,51 @@ int process_cl(dt_iop_module_t *self, dt_dev_pixelpipe_iop_t *piece,
dt_free_align(host_in);
}
}
-
- cl_mem lut_cl = NULL;
- cl_mem gamut_lut_cl = NULL;
+
cl_mem input_matrix_cl = NULL;
cl_mem output_matrix_cl = NULL;
+ cl_mem sat_lut_cl = NULL;
+ cl_mem bri_lut_cl = NULL;
+ cl_mem gamut_lut_cl = NULL;
- dt_colormatrix_t input_matrix;
- dt_colormatrix_t output_matrix;
+ dt_colormatrix_t input_matrix = {{ 0.0f }};
+ dt_colormatrix_t output_matrix = {{ 0.0f }};
dt_colormatrix_mul(input_matrix, XYZ_D50_to_D65_CAT16, work_profile->matrix_in);
dt_colormatrix_mul(output_matrix, work_profile->matrix_out, XYZ_D65_to_D50_CAT16);
- input_matrix_cl = dt_opencl_copy_host_to_device_constant(devid, 12 * sizeof(float), input_matrix);
+ input_matrix_cl = dt_opencl_copy_host_to_device_constant(devid, 12 * sizeof(float), input_matrix);
output_matrix_cl = dt_opencl_copy_host_to_device_constant(devid, 12 * sizeof(float), output_matrix);
- lut_cl = dt_opencl_copy_host_to_device_constant(devid, DT_IOP_SATCURVE_RES * sizeof(float), d->lut);
- gamut_lut_cl = dt_opencl_copy_host_to_device_constant(devid, LUT_ELEM * sizeof(float), d->gamut_lut);
-
- if(input_matrix_cl == NULL || output_matrix_cl == NULL || lut_cl == NULL || gamut_lut_cl == NULL)
+ sat_lut_cl = dt_opencl_copy_host_to_device_constant(
+ devid, DT_IOP_SATCURVE_RES * sizeof(float),
+ d->channel[DT_IOP_SATCURVE_CHANNEL_SATURATION].lut);
+ bri_lut_cl = dt_opencl_copy_host_to_device_constant(
+ devid, DT_IOP_SATCURVE_RES * sizeof(float),
+ d->channel[DT_IOP_SATCURVE_CHANNEL_BRILLIANCE].lut);
+ gamut_lut_cl = dt_opencl_copy_host_to_device_constant(
+ devid, LUT_ELEM * sizeof(float), d->gamut_lut);
+
+ if(input_matrix_cl == NULL || output_matrix_cl == NULL
+ || sat_lut_cl == NULL || bri_lut_cl == NULL || gamut_lut_cl == NULL)
{
err = CL_MEM_OBJECT_ALLOCATION_FAILURE;
goto error;
}
- // constant regardless of pixel data; compute once on the host instead of
- // once per GPU thread inside the kernel
const float L_white = Y_to_dt_UCS_L_star(1.f);
- err = dt_opencl_enqueue_kernel_2d_args(devid, gd->kernel_satcurvergb, width, height,
- CLARG(dev_in), CLARG(dev_out), CLARG(width), CLARG(height),
+ err = dt_opencl_enqueue_kernel_2d_args(
+ devid, gd->kernel_satcurvergb, width, height,
+ CLARG(dev_in), CLARG(dev_out),
+ CLARG(width), CLARG(height),
CLARG(input_matrix_cl), CLARG(output_matrix_cl),
- CLARG(lut_cl), CLARG(gamut_lut_cl), CLARG(d->formula), CLARG(L_white));
+ CLARG(sat_lut_cl), CLARG(bri_lut_cl), CLARG(gamut_lut_cl),
+ CLARG(d->formula), CLARG(L_white));
error:
dt_opencl_release_mem_object(input_matrix_cl);
dt_opencl_release_mem_object(output_matrix_cl);
- dt_opencl_release_mem_object(lut_cl);
+ dt_opencl_release_mem_object(sat_lut_cl);
+ dt_opencl_release_mem_object(bri_lut_cl);
dt_opencl_release_mem_object(gamut_lut_cl);
return err;
}
@@ -548,35 +645,49 @@ void cleanup_global(dt_iop_module_so_t *self)
}
#endif
-
void init_pipe(dt_iop_module_t *self, dt_dev_pixelpipe_t *pipe, dt_dev_pixelpipe_iop_t *piece)
{
dt_iop_satcurve_data_t *d = dt_calloc_align_type(dt_iop_satcurve_data_t, 1);
- d->lut = dt_alloc_align_float(DT_IOP_SATCURVE_RES);
+
+ for(int ch = 0; ch < DT_IOP_SATCURVE_CHANNELS; ch++)
+ {
+ d->channel[ch].lut = dt_alloc_align_float(DT_IOP_SATCURVE_RES);
+ d->channel[ch].curve = dt_draw_curve_new(0.f, 1.f, MONOTONE_HERMITE);
+ d->channel[ch].curve_type = MONOTONE_HERMITE;
+ d->channel[ch].curve_num_nodes = 0;
+ }
+
d->gamut_lut = dt_alloc_align_float(LUT_ELEM);
- d->curve = dt_draw_curve_new(0.f, 1.f, MONOTONE_HERMITE);
d->lut_inited = FALSE;
+ d->work_profile = NULL;
piece->data = d;
}
void cleanup_pipe(dt_iop_module_t *self, dt_dev_pixelpipe_t *pipe, dt_dev_pixelpipe_iop_t *piece)
{
dt_iop_satcurve_data_t *d = piece->data;
- dt_draw_curve_destroy(d->curve);
- dt_free_align(d->lut);
+ if(!d) return;
+
+ for(int ch = 0; ch < DT_IOP_SATCURVE_CHANNELS; ch++)
+ {
+ if(d->channel[ch].curve) dt_draw_curve_destroy(d->channel[ch].curve);
+ dt_free_align(d->channel[ch].lut);
+ }
+
dt_free_align(d->gamut_lut);
dt_free_align(d);
piece->data = NULL;
}
-// RGB/D50 -> XYZ/D65, the input expected by dt_XYZ_2_JzAzBz()
-static void build_jzazbz_gamut_lut(dt_iop_satcurve_data_t *d, const dt_iop_order_iccprofile_info_t *work_profile)
+static void build_jzazbz_gamut_lut(dt_iop_satcurve_data_t *d,
+ const dt_iop_order_iccprofile_info_t *work_profile)
{
- dt_colormatrix_t inputmatrix = { {0.0f} };
-
+ dt_colormatrix_t inputmatrix = {{ 0.0f }};
dt_colormatrix_mul(inputmatrix, XYZ_D50_to_D65_CAT16, work_profile->matrix_in);
+
dt_colormatrix_t inputmatrix_trans;
dt_colormatrix_transpose(inputmatrix_trans, inputmatrix);
+
float *sampler = dt_calloc_align_float(LUT_ELEM);
DT_OMP_FOR(reduction(max : sampler[:LUT_ELEM]) collapse(3))
@@ -584,24 +695,32 @@ static void build_jzazbz_gamut_lut(dt_iop_satcurve_data_t *d, const dt_iop_order
for(int g = 0; g < DT_IOP_SATCURVE_GAMUT_STEPS; g++)
for(int b = 0; b < DT_IOP_SATCURVE_GAMUT_STEPS; b++)
{
- const dt_aligned_pixel_t rgb = {r / (float)(DT_IOP_SATCURVE_GAMUT_STEPS - 1),
- g / (float)(DT_IOP_SATCURVE_GAMUT_STEPS - 1),
- b / (float)(DT_IOP_SATCURVE_GAMUT_STEPS - 1), 0.f};
+ const dt_aligned_pixel_t rgb = {
+ r / (float)(DT_IOP_SATCURVE_GAMUT_STEPS - 1),
+ g / (float)(DT_IOP_SATCURVE_GAMUT_STEPS - 1),
+ b / (float)(DT_IOP_SATCURVE_GAMUT_STEPS - 1),
+ 0.f
+ };
+
dt_aligned_pixel_t xyz, jab;
dt_apply_transposed_color_matrix(rgb, inputmatrix_trans, xyz);
dt_XYZ_2_JzAzBz(xyz, jab);
- const float saturation = jab[0] > 0.f ? dt_fast_hypotf(jab[1], jab[2]) / jab[0] : 0.f;
+
+ const float sat = jab[0] > 0.f ? dt_fast_hypotf(jab[1], jab[2]) / jab[0] : 0.f;
int index = roundf((LUT_ELEM - 1) * (atan2f(jab[2], jab[1]) + M_PI_F) / DT_2PI_F);
index = index < 0 ? LUT_ELEM - 1 : (index >= LUT_ELEM ? 0 : index);
- sampler[index] = MAX(sampler[index], saturation);
+ sampler[index] = MAX(sampler[index], sat);
}
for(size_t i = 2; i < LUT_ELEM - 2; i++)
d->gamut_lut[i] = (sampler[i - 2] + sampler[i - 1] + sampler[i] + sampler[i + 1] + sampler[i + 2]) / 5.f;
d->gamut_lut[0] = (sampler[LUT_ELEM - 2] + sampler[LUT_ELEM - 1] + sampler[0] + sampler[1] + sampler[2]) / 5.f;
d->gamut_lut[1] = (sampler[LUT_ELEM - 1] + sampler[0] + sampler[1] + sampler[2] + sampler[3]) / 5.f;
- d->gamut_lut[LUT_ELEM - 2] = (sampler[LUT_ELEM - 4] + sampler[LUT_ELEM - 3] + sampler[LUT_ELEM - 2] + sampler[LUT_ELEM - 1] + sampler[0]) / 5.f;
- d->gamut_lut[LUT_ELEM - 1] = (sampler[LUT_ELEM - 3] + sampler[LUT_ELEM - 2] + sampler[LUT_ELEM - 1] + sampler[0] + sampler[1]) / 5.f;
+ d->gamut_lut[LUT_ELEM - 2] = (sampler[LUT_ELEM - 4] + sampler[LUT_ELEM - 3] + sampler[LUT_ELEM - 2]
+ + sampler[LUT_ELEM - 1] + sampler[0]) / 5.f;
+ d->gamut_lut[LUT_ELEM - 1] = (sampler[LUT_ELEM - 3] + sampler[LUT_ELEM - 2] + sampler[LUT_ELEM - 1]
+ + sampler[0] + sampler[1]) / 5.f;
+
dt_free_align(sampler);
}
@@ -615,34 +734,51 @@ static void build_gamut_lut(dt_iop_satcurve_data_t *d,
dt_UCS_22_build_gamut_LUT(inputmatrix, d->gamut_lut);
}
else
+ {
build_jzazbz_gamut_lut(d, work_profile);
+ }
+}
+
+static void sync_channel_curve(dt_iop_satcurve_channel_data_t *dst,
+ const dt_iop_satcurve_channel_params_t *src)
+{
+ if(dst->curve_type != src->curve_type || dst->curve_num_nodes != src->curve_num_nodes)
+ {
+ if(dst->curve) dt_draw_curve_destroy(dst->curve);
+ dst->curve = dt_draw_curve_new(0.f, 1.f, src->curve_type);
+ dst->curve_type = src->curve_type;
+ dst->curve_num_nodes = src->curve_num_nodes;
+
+ for(int i = 0; i < src->curve_num_nodes; i++)
+ dt_draw_curve_add_point(dst->curve, src->curve[i].x, src->curve[i].y);
+ }
+ else
+ {
+ for(int i = 0; i < src->curve_num_nodes; i++)
+ dt_draw_curve_set_point(dst->curve, i, src->curve[i].x, src->curve[i].y);
+ }
+
+ dt_draw_curve_calc_values(dst->curve, 0.f, 1.f, DT_IOP_SATCURVE_RES, NULL, dst->lut);
}
void commit_params(dt_iop_module_t *self, dt_iop_params_t *p1, dt_dev_pixelpipe_t *pipe,
- dt_dev_pixelpipe_iop_t *piece)
+ dt_dev_pixelpipe_iop_t *piece)
{
dt_iop_satcurve_data_t *d = piece->data;
const dt_iop_satcurve_params_t *p = (const dt_iop_satcurve_params_t *)p1;
+
if(d->formula != p->formula)
{
d->formula = p->formula;
d->lut_inited = FALSE;
}
- if(d->curve_type != p->curve_type || d->curve_num_nodes != p->curve_num_nodes)
- {
- dt_draw_curve_destroy(d->curve);
- d->curve = dt_draw_curve_new(0.f, 1.f, p->curve_type);
- d->curve_type = p->curve_type;
- d->curve_num_nodes = p->curve_num_nodes;
- for(int i = 0; i < p->curve_num_nodes; i++)
- dt_draw_curve_add_point(d->curve, p->curve[i].x, p->curve[i].y);
- }
- else
- for(int i = 0; i < p->curve_num_nodes; i++)
- dt_draw_curve_set_point(d->curve, i, p->curve[i].x, p->curve[i].y);
- dt_draw_curve_calc_values(d->curve, 0.f, 1.f, DT_IOP_SATCURVE_RES, NULL, d->lut);
- const dt_iop_order_iccprofile_info_t *work_profile = dt_ioppr_get_pipe_current_profile_info(self, piece->pipe);
+ for(int ch = 0; ch < DT_IOP_SATCURVE_CHANNELS; ch++)
+ sync_channel_curve(&d->channel[ch], &p->channel[ch]);
+
+ const dt_iop_order_iccprofile_info_t *work_profile =
+ dt_ioppr_get_pipe_current_profile_info(self, piece->pipe);
+
if(work_profile && (!d->lut_inited || work_profile != d->work_profile))
{
build_gamut_lut(d, work_profile);
@@ -651,71 +787,121 @@ void commit_params(dt_iop_module_t *self, dt_iop_params_t *p1, dt_dev_pixelpipe_
}
}
+int legacy_params(dt_iop_module_t *self,
+ const void *const old_params,
+ const int old_version,
+ void **new_params,
+ int32_t *new_params_size,
+ int *new_version)
+{
+ if(old_version == 1)
+ {
+ const dt_iop_satcurve_params_v1_t *o = old_params;
+ dt_iop_satcurve_params_t *n = calloc(1, sizeof(dt_iop_satcurve_params_t));
+
+ reset_params(n);
+
+ n->formula = o->formula;
+ n->channel[DT_IOP_SATCURVE_CHANNEL_SATURATION].curve_num_nodes = o->curve_num_nodes;
+ n->channel[DT_IOP_SATCURVE_CHANNEL_SATURATION].curve_type = o->curve_type;
+
+ for(int i = 0; i < o->curve_num_nodes; i++)
+ n->channel[DT_IOP_SATCURVE_CHANNEL_SATURATION].curve[i] = o->curve[i];
+
+ *new_params = n;
+ *new_params_size = sizeof(dt_iop_satcurve_params_t);
+ *new_version = 2;
+ return 0;
+ }
+
+ return 1;
+}
+
void init_presets(dt_iop_module_so_t *self)
{
- dt_iop_satcurve_params_t p = {0};
- reset_curve(&p);
- dt_gui_presets_add_generic(_("neutral"), self->op, self->version(), &p, sizeof(p), TRUE, DEVELOP_BLEND_CS_RGB_SCENE);
-
- p.curve[0].y = .60f;
- p.curve[1].y = .55f;
- dt_gui_presets_add_generic(_("gentle saturation"), self->op, self->version(), &p, sizeof(p), TRUE, DEVELOP_BLEND_CS_RGB_SCENE);
-
- reset_curve(&p);
- p.curve_num_nodes = 3;
- p.curve[1] = (dt_iop_satcurve_node_t){.45f, .62f};
- p.curve[2] = (dt_iop_satcurve_node_t){1.f, .42f};
- dt_gui_presets_add_generic(_("protect saturated colors"), self->op, self->version(), &p, sizeof(p), TRUE, DEVELOP_BLEND_CS_RGB_SCENE);
-
- reset_curve(&p);
- p.curve_num_nodes = 3;
- p.curve[0].y = .38f;
- p.curve[1] = (dt_iop_satcurve_node_t){.35f, .48f};
- p.curve[2] = (dt_iop_satcurve_node_t){1.f, .58f};
- dt_gui_presets_add_generic(_("boost saturated colors"), self->op, self->version(), &p, sizeof(p), TRUE, DEVELOP_BLEND_CS_RGB_SCENE);
+ dt_iop_satcurve_params_t p = { 0 };
+
+ reset_params(&p);
+ dt_gui_presets_add_generic(_("neutral"), self->op, self->version(),
+ &p, sizeof(p), TRUE, DEVELOP_BLEND_CS_RGB_SCENE);
+
+ reset_params(&p);
+ p.channel[DT_IOP_SATCURVE_CHANNEL_SATURATION].curve[0].y = .60f;
+ p.channel[DT_IOP_SATCURVE_CHANNEL_SATURATION].curve[1].y = .55f;
+ dt_gui_presets_add_generic(_("gentle saturation"), self->op, self->version(),
+ &p, sizeof(p), TRUE, DEVELOP_BLEND_CS_RGB_SCENE);
+
+ reset_params(&p);
+ p.channel[DT_IOP_SATCURVE_CHANNEL_SATURATION].curve_num_nodes = 3;
+ p.channel[DT_IOP_SATCURVE_CHANNEL_SATURATION].curve[1] = (dt_iop_satcurve_node_t){ .45f, .62f };
+ p.channel[DT_IOP_SATCURVE_CHANNEL_SATURATION].curve[2] = (dt_iop_satcurve_node_t){ 1.f, .42f };
+ dt_gui_presets_add_generic(_("protect saturated colors"), self->op, self->version(),
+ &p, sizeof(p), TRUE, DEVELOP_BLEND_CS_RGB_SCENE);
+
+ reset_params(&p);
+ p.channel[DT_IOP_SATCURVE_CHANNEL_BRILLIANCE].curve[0].y = .56f;
+ p.channel[DT_IOP_SATCURVE_CHANNEL_BRILLIANCE].curve[1].y = .54f;
+ dt_gui_presets_add_generic(_("gentle brilliance"), self->op, self->version(),
+ &p, sizeof(p), TRUE, DEVELOP_BLEND_CS_RGB_SCENE);
+
+ reset_params(&p);
+ p.channel[DT_IOP_SATCURVE_CHANNEL_BRILLIANCE].curve_num_nodes = 3;
+ p.channel[DT_IOP_SATCURVE_CHANNEL_BRILLIANCE].curve[1] = (dt_iop_satcurve_node_t){ .40f, .58f };
+ p.channel[DT_IOP_SATCURVE_CHANNEL_BRILLIANCE].curve[2] = (dt_iop_satcurve_node_t){ 1.f, .48f };
+ dt_gui_presets_add_generic(_("bright mids, protect extremes"), self->op, self->version(),
+ &p, sizeof(p), TRUE, DEVELOP_BLEND_CS_RGB_SCENE);
}
-static inline int add_node(dt_iop_satcurve_params_t *p, const float x, const float y)
+static inline int add_node_to_channel(dt_iop_satcurve_channel_params_t *c, const float x, const float y)
{
+ if(c->curve_num_nodes >= DT_IOP_SATCURVE_MAXNODES) return -1;
+
int at = 0;
- while(at < p->curve_num_nodes && p->curve[at].x < x)
- at++;
- if((at && x - p->curve[at - 1].x < DT_IOP_SATCURVE_MIN_X_DISTANCE) ||
- (at < p->curve_num_nodes && p->curve[at].x - x < DT_IOP_SATCURVE_MIN_X_DISTANCE))
+ while(at < c->curve_num_nodes && c->curve[at].x < x) at++;
+
+ if((at && x - c->curve[at - 1].x < DT_IOP_SATCURVE_MIN_X_DISTANCE)
+ || (at < c->curve_num_nodes && c->curve[at].x - x < DT_IOP_SATCURVE_MIN_X_DISTANCE))
return -1;
- for(int i = p->curve_num_nodes; i > at; i--)
- p->curve[i] = p->curve[i - 1];
- p->curve[at] = (dt_iop_satcurve_node_t){x, y};
- p->curve_num_nodes++;
+
+ for(int i = c->curve_num_nodes; i > at; i--) c->curve[i] = c->curve[i - 1];
+ c->curve[at] = (dt_iop_satcurve_node_t){ x, y };
+ c->curve_num_nodes++;
return at;
}
-// paints the input-referred saturation histogram as a filled area behind
-// the curve; similar to the histogram overlay in colorzones.c, but backed
-// by our own float accumulator instead of the Lab-based histogram
+static void _get_graph_geometry(const GtkAllocation *a, float *x0, float *y0, float *w, float *h)
+{
+ *x0 = DT_IOP_SATCURVE_INSET;
+ *y0 = DT_IOP_SATCURVE_INSET;
+ *w = MAX(1, a->width - 2 * DT_IOP_SATCURVE_INSET);
+ *h = MAX(1, a->height - 2 * DT_IOP_SATCURVE_INSET - DT_IOP_SATCURVE_GRADIENT_GAP
+ - DT_IOP_SATCURVE_GRADIENT_SIZE);
+}
+
static void _draw_sat_histogram(cairo_t *cr, dt_iop_satcurve_gui_data_t *g, const int w, const int h)
{
dt_pthread_mutex_lock(&g->histogram_lock);
+
const float hist_max = g->histogram_max;
cairo_save(cr);
- cairo_set_source_rgba(cr, .8, .8, .8, .35);
+ cairo_set_source_rgba(cr, .8, .8, .8, .30);
cairo_move_to(cr, 0, h);
+
for(int i = 0; i < DT_IOP_SATCURVE_HIST_RES; i++)
{
const float x = w * i / (float)(DT_IOP_SATCURVE_HIST_RES - 1);
const float y = h * (1.f - g->histogram[i] / hist_max);
cairo_line_to(cr, x, y);
}
+
cairo_line_to(cr, w, h);
cairo_close_path(cr);
cairo_fill(cr);
cairo_restore(cr);
+
dt_pthread_mutex_unlock(&g->histogram_lock);
}
-// paints the picker overlay (mean/min/max S_in_norm of the picked region),
-// analogous to colorequal.c / colorzones.c but using our own S_in_norm
-// metric instead of Lab/LCh or dt UCS HSB
static void _draw_sat_picker(cairo_t *cr, dt_iop_module_t *self, const int w, const int h)
{
dt_iop_satcurve_gui_data_t *g = self->gui_data;
@@ -727,6 +913,7 @@ static void _draw_sat_picker(cairo_t *cr, dt_iop_module_t *self, const int w, co
const float x_max = CLAMP(g->picked_s_max, 0.f, 1.f) * w;
cairo_save(cr);
+
cairo_set_source_rgba(cr, 0.5, 0.7, 0.5, 0.25);
cairo_rectangle(cr, x_min, 0, fmaxf(x_max - x_min, 0.f), h);
cairo_fill(cr);
@@ -736,169 +923,228 @@ static void _draw_sat_picker(cairo_t *cr, dt_iop_module_t *self, const int w, co
cairo_move_to(cr, x_mean, 0);
cairo_line_to(cr, x_mean, h);
cairo_stroke(cr);
+
cairo_restore(cr);
}
-// shared geometry for the curve graph itself: origin is offset from the
-// widget's top-left by the inset; only the bottom reserves extra space
-// for the decorative gradient bar, so area_draw and the pointer
-// hit-testing functions always agree on where (0,0)-(1,1) map to
-static inline void _get_graph_geometry(const GtkAllocation *a, float *x0, float *y0, float *w, float *h)
-{
- const float inset = DT_IOP_SATCURVE_INSET;
- const float reserved = DT_IOP_SATCURVE_GRADIENT_SIZE + DT_IOP_SATCURVE_GRADIENT_GAP;
- *x0 = inset;
- *y0 = inset;
- *w = a->width - 2.f * inset;
- *h = a->height - 2.f * inset - reserved;
+static void _draw_channel_curve(cairo_t *cr,
+ const dt_iop_satcurve_channel_params_t *cp,
+ const dt_iop_satcurve_gui_channel_t *gc,
+ const int selected,
+ const gboolean active,
+ const int w, const int h,
+ const double r, const double g, const double b)
+{
+ cairo_save(cr);
+
+ cairo_set_source_rgba(cr, r, g, b, active ? .95 : .50);
+ cairo_set_line_width(cr, DT_PIXEL_APPLY_DPI(active ? 2.0 : 1.2));
+ cairo_move_to(cr, 0, h * (1.f - gc->draw_ys[0]));
+ for(int i = 1; i < DT_IOP_SATCURVE_RES; i++)
+ {
+ const float x = w * i / (float)(DT_IOP_SATCURVE_RES - 1);
+ const float y = h * (1.f - gc->draw_ys[i]);
+ cairo_line_to(cr, x, y);
+ }
+ cairo_stroke(cr);
+
+ if(active)
+ {
+ cairo_set_line_width(cr, DT_PIXEL_APPLY_DPI(1.0));
+ for(int i = 0; i < cp->curve_num_nodes; i++)
+ {
+ const float px = w * cp->curve[i].x;
+ const float py = h * (1.f - cp->curve[i].y);
+ cairo_arc(cr, px, py, DT_PIXEL_APPLY_DPI(i == selected ? 5 : 3), 0, DT_2PI_F);
+ cairo_stroke(cr);
+ }
+ }
+
+ cairo_restore(cr);
+}
+
+static void _sync_gui_curve(dt_iop_satcurve_gui_channel_t *gc,
+ const dt_iop_satcurve_channel_params_t *cp)
+{
+ if(gc->curve_type != cp->curve_type || gc->curve_num_nodes != cp->curve_num_nodes)
+ {
+ if(gc->curve) dt_draw_curve_destroy(gc->curve);
+ gc->curve = dt_draw_curve_new(0.f, 1.f, cp->curve_type);
+ gc->curve_type = cp->curve_type;
+ gc->curve_num_nodes = cp->curve_num_nodes;
+
+ for(int i = 0; i < cp->curve_num_nodes; i++)
+ dt_draw_curve_add_point(gc->curve, cp->curve[i].x, cp->curve[i].y);
+ }
+ else
+ {
+ for(int i = 0; i < cp->curve_num_nodes; i++)
+ dt_draw_curve_set_point(gc->curve, i, cp->curve[i].x, cp->curve[i].y);
+ }
+
+ dt_draw_curve_calc_values(gc->curve, 0.f, 1.f, DT_IOP_SATCURVE_RES, NULL, gc->draw_ys);
}
static gboolean area_draw(GtkWidget *widget, cairo_t *cr, dt_iop_module_t *self)
{
dt_iop_satcurve_gui_data_t *g = self->gui_data;
dt_iop_satcurve_params_t *p = self->params;
+
GtkAllocation a;
gtk_widget_get_allocation(widget, &a);
+
float gx0, gy0, gw, gh;
_get_graph_geometry(&a, &gx0, &gy0, &gw, &gh);
- const int w = (int)gw, h = (int)gh;
- if(g->curve_type != p->curve_type || g->curve_num_nodes != p->curve_num_nodes)
- {
- dt_draw_curve_destroy(g->curve);
- g->curve = dt_draw_curve_new(0.f, 1.f, p->curve_type);
- g->curve_type = p->curve_type;
- g->curve_num_nodes = p->curve_num_nodes;
- for(int i = 0; i < p->curve_num_nodes; i++)
- dt_draw_curve_add_point(g->curve, p->curve[i].x, p->curve[i].y);
- }
- else
- for(int i = 0; i < p->curve_num_nodes; i++)
- dt_draw_curve_set_point(g->curve, i, p->curve[i].x, p->curve[i].y);
- dt_draw_curve_calc_values(g->curve, 0.f, 1.f, DT_IOP_SATCURVE_RES, NULL, g->draw_ys);
-
- // background first: cairo_paint() fills the whole widget regardless of
- // any later cairo_translate(), so it must happen before the gradient
- // bars are drawn, or it would overpaint them
- cairo_set_source_rgb(cr, .18, .18, .18);
+
+ cairo_set_source_rgb(cr, .15, .15, .15);
cairo_paint(cr);
- // purely decorative perceptual gradient bar, below the graph; same
- // cairo_pattern_create_linear + dt_cairo_perceptual_gradient combo as
- // used in toneequal.c, just without any axis legend
- cairo_pattern_t *grad;
+ cairo_save(cr);
+ cairo_translate(cr, gx0, gy0);
- // horizontal bar, below the graph
- grad = cairo_pattern_create_linear(gx0, 0.0, gx0 + gw, 0.0);
- dt_cairo_perceptual_gradient(grad, 1.0);
- cairo_set_line_width(cr, 0.0);
- cairo_rectangle(cr, gx0, gy0 + gh + DT_IOP_SATCURVE_GRADIENT_GAP,
- gw, DT_IOP_SATCURVE_GRADIENT_SIZE);
- cairo_set_source(cr, grad);
- cairo_fill(cr);
- cairo_pattern_destroy(grad);
+ for(int ch = 0; ch < DT_IOP_SATCURVE_CHANNELS; ch++)
+ _sync_gui_curve(&g->channel[ch], &p->channel[ch]);
- cairo_translate(cr, gx0, gy0);
- cairo_set_source_rgba(cr, .4, .4, .4, .5);
- cairo_set_line_width(cr, DT_PIXEL_APPLY_DPI(1));
- for(int i = 1; i < 4; i++)
+ _draw_sat_histogram(cr, g, (int)gw, (int)gh);
+ _draw_sat_picker(cr, self, (int)gw, (int)gh);
+
+ cairo_set_source_rgba(cr, 1, 1, 1, .08);
+ cairo_set_line_width(cr, 1.0);
+ for(int i = 0; i <= 4; i++)
{
- cairo_move_to(cr, w * i / 4.f, 0);
- cairo_line_to(cr, w * i / 4.f, h);
- cairo_move_to(cr, 0, h * i / 4.f);
- cairo_line_to(cr, w, h * i / 4.f);
+ const float y = gh * i / 4.f;
+ cairo_move_to(cr, 0, y);
+ cairo_line_to(cr, gw, y);
+ }
+ for(int i = 0; i <= 4; i++)
+ {
+ const float x = gw * i / 4.f;
+ cairo_move_to(cr, x, 0);
+ cairo_line_to(cr, x, gh);
}
cairo_stroke(cr);
- // input histogram first, so it stays in the background
- _draw_sat_histogram(cr, g, w, h);
- _draw_sat_picker(cr, self, w, h);
+ _draw_channel_curve(cr,
+ &p->channel[DT_IOP_SATCURVE_CHANNEL_SATURATION],
+ &g->channel[DT_IOP_SATCURVE_CHANNEL_SATURATION],
+ g->active_channel == DT_IOP_SATCURVE_CHANNEL_SATURATION ? g->selected : -1,
+ g->active_channel == DT_IOP_SATCURVE_CHANNEL_SATURATION,
+ (int)gw, (int)gh,
+ .90, .90, .90);
+
+ _draw_channel_curve(cr,
+ &p->channel[DT_IOP_SATCURVE_CHANNEL_BRILLIANCE],
+ &g->channel[DT_IOP_SATCURVE_CHANNEL_BRILLIANCE],
+ g->active_channel == DT_IOP_SATCURVE_CHANNEL_BRILLIANCE ? g->selected : -1,
+ g->active_channel == DT_IOP_SATCURVE_CHANNEL_BRILLIANCE,
+ (int)gw, (int)gh,
+ 1.00, .65, .20);
+
+ cairo_restore(cr);
+
+ cairo_pattern_t *grad = cairo_pattern_create_linear(gx0, 0.0, gx0 + gw, 0.0);
+ dt_cairo_perceptual_gradient(grad, 1.0);
+ cairo_rectangle(cr, gx0, gy0 + gh + DT_IOP_SATCURVE_GRADIENT_GAP, gw, DT_IOP_SATCURVE_GRADIENT_SIZE);
+ cairo_set_source(cr, grad);
+ cairo_fill(cr);
+ cairo_pattern_destroy(grad);
- cairo_set_source_rgb(cr, .85, .85, .85);
- cairo_set_line_width(cr, DT_PIXEL_APPLY_DPI(2));
- cairo_move_to(cr, 0, h * (1.f - g->draw_ys[0]));
- for(int i = 1; i < DT_IOP_SATCURVE_RES; i++)
- cairo_line_to(cr, w * i / (float)(DT_IOP_SATCURVE_RES - 1), h * (1.f - g->draw_ys[i]));
- cairo_stroke(cr);
- cairo_set_line_width(cr, DT_PIXEL_APPLY_DPI(1));
- for(int i = 0; i < p->curve_num_nodes; i++)
- {
- cairo_arc(cr, w * p->curve[i].x, h * (1.f - p->curve[i].y), DT_PIXEL_APPLY_DPI(i == g->selected ? 5 : 3), 0, DT_2PI_F);
- cairo_stroke(cr);
- }
return FALSE;
}
static gboolean area_button(GtkWidget *widget, GdkEventButton *event, dt_iop_module_t *self)
{
dt_iop_satcurve_gui_data_t *g = self->gui_data;
- dt_iop_satcurve_params_t *p = self->params;
+ dt_iop_satcurve_channel_params_t *cp = get_active_channel_params(self);
+ dt_iop_satcurve_gui_channel_t *gc = get_active_gui_channel(self);
+
GtkAllocation a;
gtk_widget_get_allocation(widget, &a);
+
float gx0, gy0, w, h;
_get_graph_geometry(&a, &gx0, &gy0, &w, &h);
- const float x = CLAMP((event->x - gx0) / w, 0.f, 1.f), y = CLAMP(1.f - (event->y - gy0) / h, 0.f, 1.f);
+
+ const float x = CLAMP((event->x - gx0) / w, 0.f, 1.f);
+ const float y = CLAMP(1.f - (event->y - gy0) / h, 0.f, 1.f);
+
int hit = -1;
- for(int i = 0; i < p->curve_num_nodes; i++)
- if(sqf(x - p->curve[i].x) + sqf(y - p->curve[i].y) < .0025f)
+ for(int i = 0; i < cp->curve_num_nodes; i++)
+ {
+ const float dx = x - cp->curve[i].x;
+ const float dy = y - cp->curve[i].y;
+ if(dx * dx + dy * dy < .0025f)
{
hit = i;
break;
}
+ }
if(event->type == GDK_2BUTTON_PRESS && event->button == GDK_BUTTON_PRIMARY)
{
- reset_curve(p);
+ reset_channel_curve(cp);
g->selected = -1;
dt_dev_add_history_item(darktable.develop, self, TRUE);
gtk_widget_queue_draw(widget);
return TRUE;
}
+
if(event->button == GDK_BUTTON_SECONDARY && hit >= 0)
{
- if((event->state & GDK_CONTROL_MASK) || p->curve_num_nodes <= 2)
- p->curve[hit].y = .5f;
+ if((event->state & GDK_CONTROL_MASK) || cp->curve_num_nodes <= 2)
+ {
+ cp->curve[hit].y = .5f;
+ }
else
{
- for(int i = hit; i < p->curve_num_nodes - 1; i++)
- p->curve[i] = p->curve[i + 1];
- p->curve_num_nodes--;
+ for(int i = hit; i < cp->curve_num_nodes - 1; i++) cp->curve[i] = cp->curve[i + 1];
+ cp->curve_num_nodes--;
}
+
g->selected = -1;
dt_dev_add_history_item(darktable.develop, self, TRUE);
gtk_widget_queue_draw(widget);
return TRUE;
}
+
if(event->button == GDK_BUTTON_PRIMARY)
{
- // clicking/dragging outside an existing node inserts a new one on the curve
- if(hit < 0 && p->curve_num_nodes < DT_IOP_SATCURVE_MAXNODES)
- hit = add_node(p, x, CLAMP(dt_draw_curve_calc_value(g->curve, x), 0.f, 1.f));
+ if(hit < 0 && cp->curve_num_nodes < DT_IOP_SATCURVE_MAXNODES)
+ hit = add_node_to_channel(cp, x, CLAMP(dt_draw_curve_calc_value(gc->curve, x), 0.f, 1.f));
+
g->selected = hit;
g->dragging = hit >= 0;
return TRUE;
}
+
return FALSE;
}
static gboolean area_motion(GtkWidget *widget, GdkEventMotion *event, dt_iop_module_t *self)
{
dt_iop_satcurve_gui_data_t *g = self->gui_data;
- dt_iop_satcurve_params_t *p = self->params;
- if(!g->dragging || g->selected < 0)
- return FALSE;
+ dt_iop_satcurve_channel_params_t *cp = get_active_channel_params(self);
+
+ if(!g->dragging || g->selected < 0) return FALSE;
+
GtkAllocation a;
gtk_widget_get_allocation(widget, &a);
+
float gx0, gy0, w, h;
_get_graph_geometry(&a, &gx0, &gy0, &w, &h);
+
const float x = CLAMP((event->x - gx0) / w, 0.f, 1.f);
const int n = g->selected;
+
if(n == 0)
- p->curve[n].x = 0.f;
- else if(n == p->curve_num_nodes - 1)
- p->curve[n].x = 1.f;
+ cp->curve[n].x = 0.f;
+ else if(n == cp->curve_num_nodes - 1)
+ cp->curve[n].x = 1.f;
else
- p->curve[n].x = CLAMP(x, p->curve[n - 1].x + DT_IOP_SATCURVE_MIN_X_DISTANCE, p->curve[n + 1].x - DT_IOP_SATCURVE_MIN_X_DISTANCE);
- p->curve[n].y = CLAMP(1.f - (event->y - gy0) / h, 0.f, 1.f);
+ cp->curve[n].x = CLAMP(x,
+ cp->curve[n - 1].x + DT_IOP_SATCURVE_MIN_X_DISTANCE,
+ cp->curve[n + 1].x - DT_IOP_SATCURVE_MIN_X_DISTANCE);
+
+ cp->curve[n].y = CLAMP(1.f - (event->y - gy0) / h, 0.f, 1.f);
gtk_widget_queue_draw(widget);
return TRUE;
}
@@ -914,21 +1160,20 @@ static gboolean area_release(GtkWidget *widget, GdkEventButton *event, dt_iop_mo
return TRUE;
}
-// returns the module's committed gamut_lut if already available (module
-// processed at least once); otherwise falls back to a neutral unity LUT
-// (gamut = 1 for all hues) so the picker cannot crash right after loading,
-// though it then shows unnormalized S values
static const float *_get_gamut_lut_for_picker(dt_iop_module_t *self)
{
static float unity_gamut_lut[LUT_ELEM];
static gboolean unity_inited = FALSE;
+
if(!unity_inited)
{
for(int i = 0; i < LUT_ELEM; i++) unity_gamut_lut[i] = 1.f;
unity_inited = TRUE;
}
- for(GList *nodes = self->dev->full.pipe ? self->dev->full.pipe->nodes : NULL; nodes; nodes = g_list_next(nodes))
+ for(GList *nodes = self->dev->full.pipe ? self->dev->full.pipe->nodes : NULL;
+ nodes;
+ nodes = g_list_next(nodes))
{
dt_dev_pixelpipe_iop_t *piece = nodes->data;
if(piece->module == self && piece->data)
@@ -937,20 +1182,17 @@ static const float *_get_gamut_lut_for_picker(dt_iop_module_t *self)
if(d->lut_inited && d->gamut_lut) return d->gamut_lut;
}
}
+
return unity_gamut_lut;
}
-// called automatically after a color pick while g->colorpicker is active.
-// self->picked_color/-min/-max are already in pipeline RGB (default_colorspace
-// is IOP_CS_RGB), so they go straight through pixel_s_in_norm() using the
-// same metric as the curve and histogram
void color_picker_apply(dt_iop_module_t *self, GtkWidget *widget, dt_dev_pixelpipe_t *pipe)
{
dt_iop_satcurve_gui_data_t *g = self->gui_data;
if(!g) return;
const dt_iop_order_iccprofile_info_t *work_profile =
- dt_ioppr_get_iop_work_profile_info(self, self->dev->iop);
+ dt_ioppr_get_iop_work_profile_info(self, self->dev->iop);
if(!work_profile) return;
dt_colormatrix_t inputmatrix = {{ 0.0f }};
@@ -962,34 +1204,42 @@ void color_picker_apply(dt_iop_module_t *self, GtkWidget *widget, dt_dev_pixelpi
const float L_white = Y_to_dt_UCS_L_star(1.f);
const dt_aligned_pixel_t mean_rgb = { self->picked_color[0], self->picked_color[1], self->picked_color[2], 0.f };
- const dt_aligned_pixel_t min_rgb = { self->picked_color_min[0], self->picked_color_min[1], self->picked_color_min[2], 0.f };
- const dt_aligned_pixel_t max_rgb = { self->picked_color_max[0], self->picked_color_max[1], self->picked_color_max[2], 0.f };
-
- dt_iop_satcurve_data_t picker_data = { 0 };
- picker_data.formula = ((const dt_iop_satcurve_params_t *)self->params)->formula;
- g->picked_s = pixel_s_in_norm(&picker_data, mean_rgb, inputmatrix_trans, gamut_lut, L_white, NULL);
- g->picked_s_min = pixel_s_in_norm(&picker_data, min_rgb, inputmatrix_trans, gamut_lut, L_white, NULL);
- g->picked_s_max = pixel_s_in_norm(&picker_data, max_rgb, inputmatrix_trans, gamut_lut, L_white, NULL);
+ const dt_aligned_pixel_t min_rgb = { self->picked_color_min[0], self->picked_color_min[1], self->picked_color_min[2], 0.f };
+ const dt_aligned_pixel_t max_rgb = { self->picked_color_max[0], self->picked_color_max[1], self->picked_color_max[2], 0.f };
+
+ const dt_iop_satcurve_formula_t formula = ((const dt_iop_satcurve_params_t *)self->params)->formula;
+
+ g->picked_s = pixel_s_in_norm(formula, mean_rgb, inputmatrix_trans, gamut_lut, L_white, NULL);
+ g->picked_s_min = pixel_s_in_norm(formula, min_rgb, inputmatrix_trans, gamut_lut, L_white, NULL);
+ g->picked_s_max = pixel_s_in_norm(formula, max_rgb, inputmatrix_trans, gamut_lut, L_white, NULL);
g->picker_valid = TRUE;
gtk_widget_queue_draw(GTK_WIDGET(g->area));
}
-// reset the picker state when the module loses GUI focus, so no stale
-// overlay lingers in another module
void gui_focus(dt_iop_module_t *self, gboolean in)
{
- if(!in)
- dt_iop_color_picker_reset(self, FALSE);
+ if(!in) dt_iop_color_picker_reset(self, FALSE);
+}
+
+static void _channel_tabs_switch_callback(GtkNotebook *notebook,
+ GtkWidget *page,
+ guint page_num,
+ dt_iop_module_t *self)
+{
+ DT_GUARD_GUI_UPDATE();
+
+ dt_iop_satcurve_gui_data_t *g = self->gui_data;
+ if(!g) return;
+
+ g->active_channel = (page_num == 1)
+ ? DT_IOP_SATCURVE_CHANNEL_BRILLIANCE
+ : DT_IOP_SATCURVE_CHANNEL_SATURATION;
+ g->selected = -1;
+
+ gtk_widget_queue_draw(GTK_WIDGET(g->area));
}
-// S_in_norm is defined differently per formula (JzAzBz vs darktable UCS), so
-// a histogram computed under the previous formula is stale and must not
-// linger on screen after switching. The bauhaus binding for the formula
-// combobox already updates self->params and adds a history item, but not
-// necessarily a *forced* one (see the explicit TRUE argument used for curve
-// node edits below) - force it here as well so process() always re-runs
-// and _update_sat_histogram() refreshes the histogram in either direction.
void gui_changed(dt_iop_module_t *self, GtkWidget *widget, void *previous)
{
dt_iop_satcurve_gui_data_t *g = self->gui_data;
@@ -1006,13 +1256,22 @@ void gui_update(dt_iop_module_t *self)
if(!g) return;
if(g->formula) dt_bauhaus_combobox_set(g->formula, p->formula);
+
+ if(g->notebook)
+ gtk_notebook_set_current_page(GTK_NOTEBOOK(g->notebook),
+ g->active_channel == DT_IOP_SATCURVE_CHANNEL_BRILLIANCE ? 1 : 0);
+
if(g->area) gtk_widget_queue_draw(GTK_WIDGET(g->area));
}
void gui_cleanup(dt_iop_module_t *self)
{
dt_iop_satcurve_gui_data_t *g = self->gui_data;
- dt_draw_curve_destroy(g->curve);
+ if(!g) return;
+
+ for(int ch = 0; ch < DT_IOP_SATCURVE_CHANNELS; ch++)
+ if(g->channel[ch].curve) dt_draw_curve_destroy(g->channel[ch].curve);
+
dt_pthread_mutex_destroy(&g->histogram_lock);
}
@@ -1020,12 +1279,22 @@ void gui_init(dt_iop_module_t *self)
{
dt_iop_satcurve_gui_data_t *g = IOP_GUI_ALLOC(satcurve);
const dt_iop_satcurve_params_t *p = self->default_params;
+
g->selected = -1;
- g->curve_type = p->curve_type;
- g->curve_num_nodes = p->curve_num_nodes;
- g->curve = dt_draw_curve_new(0.f, 1.f, p->curve_type);
- for(int i = 0; i < p->curve_num_nodes; i++)
- dt_draw_curve_add_point(g->curve, p->curve[i].x, p->curve[i].y);
+ g->dragging = FALSE;
+ g->active_channel = DT_IOP_SATCURVE_CHANNEL_SATURATION;
+
+ for(int ch = 0; ch < DT_IOP_SATCURVE_CHANNELS; ch++)
+ {
+ g->channel[ch].curve_type = p->channel[ch].curve_type;
+ g->channel[ch].curve_num_nodes = p->channel[ch].curve_num_nodes;
+ g->channel[ch].curve = dt_draw_curve_new(0.f, 1.f, p->channel[ch].curve_type);
+
+ for(int i = 0; i < p->channel[ch].curve_num_nodes; i++)
+ dt_draw_curve_add_point(g->channel[ch].curve,
+ p->channel[ch].curve[i].x,
+ p->channel[ch].curve[i].y);
+ }
memset(g->histogram, 0, sizeof(g->histogram));
g->histogram_max = 1e-6f;
@@ -1036,28 +1305,46 @@ void gui_init(dt_iop_module_t *self)
self->widget = dt_gui_vbox();
+ g->notebook = GTK_NOTEBOOK(gtk_notebook_new());
+ gtk_notebook_set_show_border(g->notebook, FALSE);
+ gtk_notebook_set_scrollable(g->notebook, FALSE);
+ gtk_notebook_popup_disable(g->notebook);
+
+ GtkWidget *page_sat = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
+ GtkWidget *page_bri = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
+ gtk_widget_set_size_request(page_sat, -1, 0);
+ gtk_widget_set_size_request(page_bri, -1, 0);
+
+GtkWidget *tab_sat = gtk_label_new(_("saturation"));
+GtkWidget *tab_bri = gtk_label_new(_("brilliance"));
+
+gtk_widget_set_size_request(tab_sat, DT_PIXEL_APPLY_DPI(130), -1);
+gtk_widget_set_size_request(tab_bri, DT_PIXEL_APPLY_DPI(130), -1);
+
+gtk_notebook_append_page(g->notebook, page_sat, tab_sat);
+gtk_notebook_append_page(g->notebook, page_bri, tab_bri);
+ gtk_notebook_set_current_page(g->notebook, 0);
+
+ g_signal_connect(G_OBJECT(g->notebook), "switch-page",
+ G_CALLBACK(_channel_tabs_switch_callback), self);
+
+ dt_gui_box_add(self->widget, GTK_WIDGET(g->notebook));
+
g->area = GTK_DRAWING_AREA(dt_ui_resize_wrap(NULL, 0, "plugins/darkroom/satcurve/graph_height"));
gtk_widget_set_size_request(GTK_WIDGET(g->area), -1, DT_PIXEL_APPLY_DPI(180));
- gtk_widget_add_events(GTK_WIDGET(g->area), GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | GDK_POINTER_MOTION_MASK);
+ gtk_widget_add_events(GTK_WIDGET(g->area),
+ GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | GDK_POINTER_MOTION_MASK);
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), self);
g_signal_connect(G_OBJECT(g->area), "button-release-event", G_CALLBACK(area_release), self);
g_signal_connect(G_OBJECT(g->area), "motion-notify-event", G_CALLBACK(area_motion), self);
dt_gui_box_add(self->widget, GTK_WIDGET(g->area));
- // Row below the draw area: picker button on the left, formula combobox on
- // the right. dt_bauhaus_combobox_from_params() auto-packs the widget into
- // self->widget, so it is detached here and re-packed into the shared hbox
- // (pack_start/pack_end keep both widgets at their natural width with a
- // flexible gap in between).
- // dt_color_picker_new_with_cst() is used instead of dt_color_picker_new()
- // because the latter attaches the picker icon to an existing bauhaus
- // widget rather than creating a standalone button.
g->formula = dt_bauhaus_combobox_from_params(self, "formula");
dt_bauhaus_combobox_add(g->formula, _("JzAzBz"));
dt_bauhaus_combobox_add(g->formula, _("darktable UCS"));
gtk_widget_set_tooltip_text(g->formula,
- _("choose the perceptual saturation definition used by the curve"));
+ _("choose the perceptual saturation definition used by both curves"));
g_object_ref(g->formula);
gtk_container_remove(GTK_CONTAINER(self->widget), g->formula);
@@ -1067,6 +1354,7 @@ void gui_init(dt_iop_module_t *self)
g->colorpicker = dt_color_picker_new_with_cst(self, DT_COLOR_PICKER_POINT, NULL, IOP_CS_RGB);
gtk_widget_set_tooltip_text(g->colorpicker, _("pick saturation from image"));
+
gtk_box_pack_start(GTK_BOX(controls_hbox), g->colorpicker, FALSE, FALSE, 0);
gtk_box_pack_end(GTK_BOX(controls_hbox), g->formula, FALSE, FALSE, 0);
g_object_unref(g->formula);
@@ -1079,12 +1367,21 @@ void init(dt_iop_module_t *self)
self->params = calloc(1, sizeof(dt_iop_satcurve_params_t));
self->default_params = calloc(1, sizeof(dt_iop_satcurve_params_t));
self->params_size = sizeof(dt_iop_satcurve_params_t);
- reset_curve(self->params);
- reset_curve(self->default_params);
+
+ reset_params(self->params);
+ reset_params(self->default_params);
+}
+
+void cleanup(dt_iop_module_t *self)
+{
+ free(self->params);
+ self->params = NULL;
+ free(self->default_params);
+ self->default_params = NULL;
}
// clang-format off
// modelines: These editor modelines have been set for all relevant files by tools/update_modelines.py
// vim: shiftwidth=2 expandtab tabstop=2 cindent
// kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified;
-// clang-format on
+// clang-format on
\ No newline at end of file
From deb528ebb010a3a4eb629fcaf6827a151cabc21a Mon Sep 17 00:00:00 2001
From: Martin Straeten <39386816+MStraeten@users.noreply.github.com>
Date: Mon, 20 Jul 2026 22:42:48 +0200
Subject: [PATCH 04/12] performance optimization
---
data/kernels/satcurve.cl | 58 ++++++++++++++++++-
src/iop/satcurvergb.c | 120 +++++++++++++++++++++++++--------------
2 files changed, 135 insertions(+), 43 deletions(-)
diff --git a/data/kernels/satcurve.cl b/data/kernels/satcurve.cl
index 3abe25a56bfd..3d2c85c3d924 100755
--- a/data/kernels/satcurve.cl
+++ b/data/kernels/satcurve.cl
@@ -104,6 +104,62 @@ static inline float satcurve_ucs_gamut_saturation_cl(const float J, const float
return fmax(HSB_gamut_boundary.y, FLT_MIN);
}
+// s_in_norm only (no full pixel transform) -- used by the histogram
+// reduction kernel below. Mirrors the s_in_norm computation embedded in each
+// branch of satcurvergb() below and pixel_s_in_norm() on the CPU side.
+static inline float satcurve_s_in_norm_cl(const float4 rgb_in,
+ constant const float *const matrix_in,
+ global const float *const gamut_lut,
+ const int formula,
+ const float L_white)
+{
+ const float4 rgb = fmax(rgb_in, 0.f);
+ const float4 xyz = matrix_product_float4(rgb, matrix_in);
+
+ if(formula == DT_IOP_SATCURVE_DTUCS)
+ {
+ const float4 xyY = dt_D65_XYZ_to_xyY(xyz);
+ const float4 JCH = xyY_to_dt_UCS_JCH(xyY, L_white);
+ const float4 HCB = dt_UCS_JCH_to_HCB(JCH);
+ const float gamut_s = satcurve_ucs_gamut_saturation_cl(JCH.x, JCH.z, L_white, gamut_lut);
+ const float saturation = HCB.z > 0.f ? HCB.y / HCB.z : 0.f;
+ return saturation / gamut_s;
+ }
+ else
+ {
+ const float4 jab = XYZ_to_JzAzBz(xyz);
+ const float Jz = fmax(jab.x, 0.f);
+ const float Cz = hypot(jab.y, jab.z);
+ const float h = atan2(jab.z, jab.y);
+ const float gamut = fmax(satcurve_lookup_gamut_cl(gamut_lut, h), FLT_MIN);
+ return (Jz > 0.f ? Cz / Jz : 0.f) / gamut;
+ }
+}
+
+#define DT_IOP_SATCURVE_HIST_RES 256
+
+// on-device histogram reduction: accumulates per-pixel s_in_norm into
+// hist_bins so only DT_IOP_SATCURVE_HIST_RES ints need to cross the PCIe
+// bus, instead of copying the entire image back to the host. hist_bins must
+// be zeroed by the caller before this kernel runs.
+kernel void
+satcurve_histogram(read_only image2d_t in, const int width, const int height,
+ constant const float *const matrix_in,
+ global const float *const gamut_lut,
+ const int formula, const float L_white,
+ global int *const hist_bins)
+{
+ const int x = get_global_id(0);
+ const int y = get_global_id(1);
+ if(x >= width || y >= height) return;
+
+ const float4 rgb_in = Areadpixel(in, x, y);
+ const float s_in_norm = satcurve_s_in_norm_cl(rgb_in, matrix_in, gamut_lut, formula, L_white);
+
+ const int bin = clamp((int)(s_in_norm * (DT_IOP_SATCURVE_HIST_RES - 1)), 0, DT_IOP_SATCURVE_HIST_RES - 1);
+ atomic_inc(hist_bins + bin);
+}
+
kernel void
satcurvergb(read_only image2d_t in, write_only image2d_t out,
const int width, const int height,
@@ -116,7 +172,7 @@ satcurvergb(read_only image2d_t in, write_only image2d_t out,
const int y = get_global_id(1);
if(x >= width || y >= height) return;
- float4 pix_in = readpixel(in, x, y);
+ float4 pix_in = Areadpixel(in, x, y);
float4 rgb = fmax(pix_in, 0.f);
float4 xyz = matrix_product_float4(rgb, matrix_in);
diff --git a/src/iop/satcurvergb.c b/src/iop/satcurvergb.c
index 5b8822c60b34..d7fc82c49eca 100644
--- a/src/iop/satcurvergb.c
+++ b/src/iop/satcurvergb.c
@@ -132,6 +132,7 @@ typedef struct dt_iop_satcurve_gui_data_t
typedef struct dt_iop_satcurve_global_data_t
{
int kernel_satcurvergb;
+ int kernel_satcurve_histogram;
} dt_iop_satcurve_global_data_t;
typedef struct dt_iop_satcurve_factors_t
@@ -357,6 +358,30 @@ static inline dt_iop_satcurve_factors_t eval_curve_factors(const dt_iop_satcurve
};
}
+// applies log1p compression and publishes bin counts to the GUI histogram;
+// shared tail for both the CPU path (_update_sat_histogram) and the GPU path
+// (process_cl), which only differ in how they arrive at per-bin pixel counts
+static void _commit_sat_histogram(dt_iop_module_t *self, const int *const bins)
+{
+ dt_iop_satcurve_gui_data_t *g = self->gui_data;
+ if(!g) return;
+
+ float local_hist[DT_IOP_SATCURVE_HIST_RES];
+ float max_val = 0.f;
+ for(int i = 0; i < DT_IOP_SATCURVE_HIST_RES; i++)
+ {
+ local_hist[i] = log1pf((float)bins[i]);
+ max_val = MAX(max_val, local_hist[i]);
+ }
+
+ dt_pthread_mutex_lock(&g->histogram_lock);
+ memcpy(g->histogram, local_hist, sizeof(local_hist));
+ g->histogram_max = MAX(max_val, 1e-6f);
+ dt_pthread_mutex_unlock(&g->histogram_lock);
+
+ dt_control_queue_redraw_widget(GTK_WIDGET(g->area));
+}
+
static void _update_sat_histogram(dt_iop_module_t *self,
const dt_iop_satcurve_data_t *d,
const dt_colormatrix_t inputmatrix_trans,
@@ -366,7 +391,7 @@ static void _update_sat_histogram(dt_iop_module_t *self,
dt_iop_satcurve_gui_data_t *g = self->gui_data;
if(!g) return;
- float local_hist[DT_IOP_SATCURVE_HIST_RES] = { 0.f };
+ int local_hist[DT_IOP_SATCURVE_HIST_RES] = { 0 };
const float L_white = Y_to_dt_UCS_L_star(1.f);
DT_OMP_FOR(reduction(+ : local_hist[:DT_IOP_SATCURVE_HIST_RES]))
@@ -375,24 +400,13 @@ static void _update_sat_histogram(dt_iop_module_t *self,
const float s_in_norm = pixel_s_in_norm(d->formula, in + 4 * k, inputmatrix_trans, d->gamut_lut, L_white, NULL);
const int bin = CLAMP((int)(s_in_norm * (DT_IOP_SATCURVE_HIST_RES - 1)),
0, DT_IOP_SATCURVE_HIST_RES - 1);
- local_hist[bin] += 1.f;
+ local_hist[bin]++;
}
- float max_val = 0.f;
- for(int i = 0; i < DT_IOP_SATCURVE_HIST_RES; i++)
- {
- local_hist[i] = log1pf(local_hist[i]);
- max_val = MAX(max_val, local_hist[i]);
- }
-
- dt_pthread_mutex_lock(&g->histogram_lock);
- memcpy(g->histogram, local_hist, sizeof(local_hist));
- g->histogram_max = MAX(max_val, 1e-6f);
- dt_pthread_mutex_unlock(&g->histogram_lock);
-
- dt_control_queue_redraw_widget(GTK_WIDGET(g->area));
+ _commit_sat_histogram(self, local_hist);
}
+
static inline void apply_sat_and_brilliance_ucs(const dt_iop_satcurve_data_t *d,
dt_aligned_pixel_t xyz,
const float L_white)
@@ -562,29 +576,16 @@ int process_cl(dt_iop_module_t *self, dt_dev_pixelpipe_iop_t *piece,
const int width = roi_in->width;
const int height = roi_in->height;
- if(self->dev->gui_attached && dt_pipe_is_full(piece->pipe) && dt_iop_has_focus(self)
- && piece->pipe == self->dev->full.pipe)
- {
- dt_colormatrix_t hist_input_matrix = {{ 0.0f }};
- dt_colormatrix_t hist_input_matrix_trans;
- dt_colormatrix_mul(hist_input_matrix, XYZ_D50_to_D65_CAT16, work_profile->matrix_in);
- dt_colormatrix_transpose(hist_input_matrix_trans, hist_input_matrix);
-
- float *host_in = dt_alloc_align_float((size_t)4 * width * height);
- if(host_in)
- {
- cl_int hist_err = dt_opencl_copy_image_to_host(devid, host_in, dev_in, width, height, sizeof(float) * 4);
- if(hist_err == CL_SUCCESS)
- _update_sat_histogram(self, d, hist_input_matrix_trans, host_in, (size_t)width * height);
- dt_free_align(host_in);
- }
- }
+ const gboolean want_histogram =
+ self->dev->gui_attached && dt_pipe_is_full(piece->pipe) && dt_iop_has_focus(self)
+ && piece->pipe == self->dev->full.pipe;
cl_mem input_matrix_cl = NULL;
cl_mem output_matrix_cl = NULL;
cl_mem sat_lut_cl = NULL;
cl_mem bri_lut_cl = NULL;
cl_mem gamut_lut_cl = NULL;
+ cl_mem hist_bins_cl = NULL;
dt_colormatrix_t input_matrix = {{ 0.0f }};
dt_colormatrix_t output_matrix = {{ 0.0f }};
@@ -611,6 +612,8 @@ int process_cl(dt_iop_module_t *self, dt_dev_pixelpipe_iop_t *piece,
const float L_white = Y_to_dt_UCS_L_star(1.f);
+ // pipeline-critical kernel goes first: the histogram below is pure UI
+ // feedback and must never delay the actual image processing on the queue
err = dt_opencl_enqueue_kernel_2d_args(
devid, gd->kernel_satcurvergb, width, height,
CLARG(dev_in), CLARG(dev_out),
@@ -619,12 +622,43 @@ int process_cl(dt_iop_module_t *self, dt_dev_pixelpipe_iop_t *piece,
CLARG(sat_lut_cl), CLARG(bri_lut_cl), CLARG(gamut_lut_cl),
CLARG(d->formula), CLARG(L_white));
+ if(err != CL_SUCCESS) goto error;
+
+ // GPU-side histogram reduction: only DT_IOP_SATCURVE_HIST_RES ints cross
+ // the PCIe bus, instead of the full width*height*4 floats of the old
+ // copy-to-host approach. Reuses input_matrix_cl/gamut_lut_cl already
+ // uploaded above -- no second matrix upload needed.
+ if(want_histogram)
+ {
+ int hist_bins_host[DT_IOP_SATCURVE_HIST_RES] = { 0 };
+ const size_t hist_bins_size = sizeof(int) * DT_IOP_SATCURVE_HIST_RES;
+
+ hist_bins_cl = dt_opencl_alloc_device_buffer(devid, hist_bins_size);
+ if(hist_bins_cl
+ && dt_opencl_write_buffer_to_device(devid, hist_bins_host, hist_bins_cl, 0,
+ hist_bins_size, TRUE) == CL_SUCCESS)
+ {
+ const cl_int hist_err = dt_opencl_enqueue_kernel_2d_args(
+ devid, gd->kernel_satcurve_histogram, width, height,
+ CLARG(dev_in), CLARG(width), CLARG(height),
+ CLARG(input_matrix_cl), CLARG(gamut_lut_cl),
+ CLARG(d->formula), CLARG(L_white),
+ CLARG(hist_bins_cl));
+
+ if(hist_err == CL_SUCCESS
+ && dt_opencl_read_buffer_from_device(devid, hist_bins_host, hist_bins_cl, 0,
+ hist_bins_size, TRUE) == CL_SUCCESS)
+ _commit_sat_histogram(self, hist_bins_host);
+ }
+ }
+
error:
dt_opencl_release_mem_object(input_matrix_cl);
dt_opencl_release_mem_object(output_matrix_cl);
dt_opencl_release_mem_object(sat_lut_cl);
dt_opencl_release_mem_object(bri_lut_cl);
dt_opencl_release_mem_object(gamut_lut_cl);
+ dt_opencl_release_mem_object(hist_bins_cl);
return err;
}
@@ -634,12 +668,14 @@ void init_global(dt_iop_module_so_t *self)
dt_iop_satcurve_global_data_t *gd = malloc(sizeof(dt_iop_satcurve_global_data_t));
self->data = gd;
gd->kernel_satcurvergb = dt_opencl_create_kernel(program, "satcurvergb");
+ gd->kernel_satcurve_histogram = dt_opencl_create_kernel(program, "satcurve_histogram");
}
void cleanup_global(dt_iop_module_so_t *self)
{
const dt_iop_satcurve_global_data_t *gd = self->data;
dt_opencl_free_kernel(gd->kernel_satcurvergb);
+ dt_opencl_free_kernel(gd->kernel_satcurve_histogram);
free(self->data);
self->data = NULL;
}
@@ -914,12 +950,12 @@ static void _draw_sat_picker(cairo_t *cr, dt_iop_module_t *self, const int w, co
cairo_save(cr);
- cairo_set_source_rgba(cr, 0.5, 0.7, 0.5, 0.25);
- cairo_rectangle(cr, x_min, 0, fmaxf(x_max - x_min, 0.f), h);
+ cairo_set_source_rgba(cr, 1.0, 1.0, 1.0, 0.20);
+ cairo_rectangle(cr, x_min, 0, MAX(1.f, x_max - x_min), h);
cairo_fill(cr);
- cairo_set_source_rgba(cr, 0.5, 0.9, 0.5, 0.9);
- cairo_set_line_width(cr, DT_PIXEL_APPLY_DPI(2.));
+ cairo_set_source_rgba(cr, 1.0, 0.6, 0.2, 0.9);
+ cairo_set_line_width(cr, DT_PIXEL_APPLY_DPI(1.2));
cairo_move_to(cr, x_mean, 0);
cairo_line_to(cr, x_mean, h);
cairo_stroke(cr);
@@ -1315,14 +1351,14 @@ void gui_init(dt_iop_module_t *self)
gtk_widget_set_size_request(page_sat, -1, 0);
gtk_widget_set_size_request(page_bri, -1, 0);
-GtkWidget *tab_sat = gtk_label_new(_("saturation"));
-GtkWidget *tab_bri = gtk_label_new(_("brilliance"));
+ GtkWidget *tab_sat = gtk_label_new(_("saturation"));
+ GtkWidget *tab_bri = gtk_label_new(_("brilliance"));
-gtk_widget_set_size_request(tab_sat, DT_PIXEL_APPLY_DPI(130), -1);
-gtk_widget_set_size_request(tab_bri, DT_PIXEL_APPLY_DPI(130), -1);
+ gtk_widget_set_size_request(tab_sat, DT_PIXEL_APPLY_DPI(130), -1);
+ gtk_widget_set_size_request(tab_bri, DT_PIXEL_APPLY_DPI(130), -1);
-gtk_notebook_append_page(g->notebook, page_sat, tab_sat);
-gtk_notebook_append_page(g->notebook, page_bri, tab_bri);
+ gtk_notebook_append_page(g->notebook, page_sat, tab_sat);
+ gtk_notebook_append_page(g->notebook, page_bri, tab_bri);
gtk_notebook_set_current_page(g->notebook, 0);
g_signal_connect(G_OBJECT(g->notebook), "switch-page",
From 750f2d8f5074e3d265e686cda495a6a949f5b391 Mon Sep 17 00:00:00 2001
From: Martin Straeten <39386816+MStraeten@users.noreply.github.com>
Date: Mon, 20 Jul 2026 23:18:34 +0200
Subject: [PATCH 05/12] scroll to change
---
src/iop/satcurvergb.c | 24 +++++++++++++++++++++++-
1 file changed, 23 insertions(+), 1 deletion(-)
diff --git a/src/iop/satcurvergb.c b/src/iop/satcurvergb.c
index d7fc82c49eca..e5f47e6b5c78 100644
--- a/src/iop/satcurvergb.c
+++ b/src/iop/satcurvergb.c
@@ -1154,6 +1154,26 @@ static gboolean area_button(GtkWidget *widget, GdkEventButton *event, dt_iop_mod
return FALSE;
}
+static gboolean area_scroll(GtkWidget *widget, GdkEventScroll *event,
+ dt_iop_module_t *self)
+{
+ dt_iop_satcurve_gui_data_t *g = self->gui_data;
+ dt_iop_satcurve_channel_params_t *cp = get_active_channel_params(self);
+
+ if(g->selected < 0) return FALSE; // nur wenn ein Knoten selektiert ist
+
+ int delta_y = 0;
+ if(dt_gui_get_scroll_unit_delta(event, &delta_y))
+ {
+ const float step = 0.02f * (event->state & GDK_CONTROL_MASK ? 5.0f : 1.0f);
+ const int n = g->selected;
+ cp->curve[n].y = CLAMP(cp->curve[n].y - delta_y * step, 0.f, 1.f);
+
+ dt_dev_add_history_item(darktable.develop, self, TRUE);
+ gtk_widget_queue_draw(widget);
+ }
+ return TRUE;
+}
static gboolean area_motion(GtkWidget *widget, GdkEventMotion *event, dt_iop_module_t *self)
{
@@ -1369,11 +1389,13 @@ void gui_init(dt_iop_module_t *self)
g->area = GTK_DRAWING_AREA(dt_ui_resize_wrap(NULL, 0, "plugins/darkroom/satcurve/graph_height"));
gtk_widget_set_size_request(GTK_WIDGET(g->area), -1, DT_PIXEL_APPLY_DPI(180));
gtk_widget_add_events(GTK_WIDGET(g->area),
- GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | GDK_POINTER_MOTION_MASK);
+ GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK
+ | GDK_POINTER_MOTION_MASK | GDK_SCROLL_MASK);
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), self);
g_signal_connect(G_OBJECT(g->area), "button-release-event", G_CALLBACK(area_release), self);
g_signal_connect(G_OBJECT(g->area), "motion-notify-event", G_CALLBACK(area_motion), self);
+ g_signal_connect(G_OBJECT(g->area), "scroll-event", G_CALLBACK(area_scroll), self);
dt_gui_box_add(self->widget, GTK_WIDGET(g->area));
g->formula = dt_bauhaus_combobox_from_params(self, "formula");
From 0ff49c9d69cd4a9b84364e281f43249aaf5372d9 Mon Sep 17 00:00:00 2001
From: Martin Straeten <39386816+MStraeten@users.noreply.github.com>
Date: Wed, 22 Jul 2026 19:20:52 +0200
Subject: [PATCH 06/12] add saturation mask
---
data/kernels/satcurve.cl | 22 +++
src/iop/satcurvergb.c | 379 +++++++++++++++++++++++++--------------
2 files changed, 268 insertions(+), 133 deletions(-)
diff --git a/data/kernels/satcurve.cl b/data/kernels/satcurve.cl
index 3d2c85c3d924..d9b47b56897a 100755
--- a/data/kernels/satcurve.cl
+++ b/data/kernels/satcurve.cl
@@ -252,5 +252,27 @@ satcurvergb(read_only image2d_t in, write_only image2d_t out,
pixout = fmax(pixout, 0.f);
pixout.w = pix_in.w;
+ write_imagef(out, (int2)(x, y), pixout);
+}
+// Visualisierung der Sättigungsmaske für die GUI-Anzeige: schreibt
+// sqrt(s_in_norm) als Graustufe in alle 3 RGB-Kanäle, Alpha unverändert.
+// Mirrors display_saturation_mask() auf der CPU-Seite.
+kernel void
+satcurve_mask(read_only image2d_t in, write_only image2d_t out,
+ const int width, const int height,
+ constant const float *const matrix_in,
+ global const float *const gamut_lut,
+ const int formula, const float L_white)
+{
+ const int x = get_global_id(0);
+ const int y = get_global_id(1);
+ if(x >= width || y >= height) return;
+
+ const float4 pix_in = Areadpixel(in, x, y);
+ const float s_in_norm = satcurve_s_in_norm_cl(pix_in, matrix_in, gamut_lut, formula, L_white);
+
+ const float intensity = sqrt(clamp(s_in_norm, 0.f, 1.f));
+ float4 pixout = { intensity, intensity, intensity, pix_in.w };
+
write_imagef(out, (int2)(x, y), pixout);
}
\ No newline at end of file
diff --git a/src/iop/satcurvergb.c b/src/iop/satcurvergb.c
index e5f47e6b5c78..bcf9f43094e2 100644
--- a/src/iop/satcurvergb.c
+++ b/src/iop/satcurvergb.c
@@ -1,16 +1,16 @@
/*
- This file is part of darktable,
- Copyright (C) 2026 darktable developers.
-
- darktable is free software: you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- darktable is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
+ This file is part of darktable,
+ Copyright (C) 2026 darktable developers.
+
+ darktable is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ darktable is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
*/
// our includes go first:
@@ -25,6 +25,7 @@
#include "develop/imageop.h"
#include "develop/imageop_gui.h"
#include "develop/openmp_maths.h"
+#include "dtgtk/button.h"
#include "dtgtk/drawingarea.h"
#include "gui/color_picker_proxy.h"
#include "gui/draw.h"
@@ -112,6 +113,7 @@ typedef struct dt_iop_satcurve_gui_data_t
GtkDrawingArea *area;
GtkWidget *colorpicker;
GtkWidget *formula;
+ GtkWidget *show_saturation_mask; // NEU
GtkNotebook *notebook;
dt_iop_satcurve_gui_channel_t channel[DT_IOP_SATCURVE_CHANNELS];
@@ -127,12 +129,15 @@ typedef struct dt_iop_satcurve_gui_data_t
float picked_s;
float picked_s_min;
float picked_s_max;
+
+ gboolean mask_display; // NEU
} dt_iop_satcurve_gui_data_t;
typedef struct dt_iop_satcurve_global_data_t
{
int kernel_satcurvergb;
int kernel_satcurve_histogram;
+ int kernel_satcurve_mask; // NEU
} dt_iop_satcurve_global_data_t;
typedef struct dt_iop_satcurve_factors_t
@@ -154,10 +159,10 @@ const char *aliases()
const char **description(dt_iop_module_t *self)
{
return dt_iop_set_description(self, _("remap saturation and brilliance with curves"),
- _("corrective or creative"),
- _("linear, RGB, scene-referred"),
- _("linear, RGB, scene-referred"),
- _("linear, RGB, scene-referred"));
+ _("corrective or creative"),
+ _("linear, RGB, scene-referred"),
+ _("linear, RGB, scene-referred"),
+ _("linear, RGB, scene-referred"));
}
int flags()
@@ -171,8 +176,8 @@ int default_group()
}
dt_iop_colorspace_type_t default_colorspace(dt_iop_module_t *self,
- dt_dev_pixelpipe_t *pipe,
- dt_dev_pixelpipe_iop_t *piece)
+ dt_dev_pixelpipe_t *pipe,
+ dt_dev_pixelpipe_iop_t *piece)
{
return IOP_CS_RGB;
}
@@ -248,9 +253,9 @@ static inline float clip_jz_chroma(const float Jz, const float Cz, const float c
}
static inline float pixel_s_in_norm_jzazbz(const float *const restrict rgb_in,
- const dt_colormatrix_t inputmatrix_trans,
- const float *const restrict gamut_lut,
- float *const restrict h_out)
+ const dt_colormatrix_t inputmatrix_trans,
+ const float *const restrict gamut_lut,
+ float *const restrict h_out)
{
dt_aligned_pixel_t rgb, xyz, jab;
copy_pixel(rgb, rgb_in);
@@ -273,14 +278,14 @@ static inline float pixel_s_in_norm_jzazbz(const float *const restrict rgb_in,
// model used throughout darktable_ucs_22_helpers. Single source of truth for this
// computation -- do not re-derive it inline at call sites.
static inline float satcurve_ucs_gamut_saturation(const float J, const float h,
- const float L_white,
- const float *const restrict gamut_lut)
+ const float L_white,
+ const float *const restrict gamut_lut)
{
const float max_colorfulness = MAX(satcurve_lookup_gamut(gamut_lut, h), FLT_MIN);
const float max_chroma = 15.932993652962535f
- * powf(J / L_white, 0.6523997524738018f)
- * powf(max_colorfulness, 0.6007557017508491f)
- / L_white;
+ * powf(J / L_white, 0.6523997524738018f)
+ * powf(max_colorfulness, 0.6007557017508491f)
+ / L_white;
const dt_aligned_pixel_t JCH_gamut_boundary = { J, max_chroma, h, 0.f };
dt_aligned_pixel_t HSB_gamut_boundary;
@@ -290,10 +295,10 @@ static inline float satcurve_ucs_gamut_saturation(const float J, const float h,
}
static inline float pixel_s_in_norm_ucs(const float *const restrict rgb_in,
- const dt_colormatrix_t inputmatrix_trans,
- const float *const restrict gamut_lut,
- const float L_white,
- float *const restrict h_out)
+ const dt_colormatrix_t inputmatrix_trans,
+ const float *const restrict gamut_lut,
+ const float L_white,
+ float *const restrict h_out)
{
dt_aligned_pixel_t rgb, xyz, xyY, JCH, HCB;
@@ -314,11 +319,11 @@ static inline float pixel_s_in_norm_ucs(const float *const restrict rgb_in,
}
static inline float pixel_s_in_norm(const dt_iop_satcurve_formula_t formula,
- const float *const restrict rgb_in,
- const dt_colormatrix_t inputmatrix_trans,
- const float *const restrict gamut_lut,
- const float L_white,
- float *const restrict h_out)
+ const float *const restrict rgb_in,
+ const dt_colormatrix_t inputmatrix_trans,
+ const float *const restrict gamut_lut,
+ const float L_white,
+ float *const restrict h_out)
{
if(formula == DT_IOP_SATCURVE_DTUCS)
return pixel_s_in_norm_ucs(rgb_in, inputmatrix_trans, gamut_lut, L_white, h_out);
@@ -342,12 +347,12 @@ static inline dt_iop_satcurve_gui_channel_t *get_active_gui_channel(dt_iop_modul
static inline gboolean channel_is_neutral(const dt_iop_satcurve_channel_data_t *c)
{
return c->curve_num_nodes == 2
- && fabsf(c->lut[0] - .5f) < 1e-6f
- && fabsf(c->lut[DT_IOP_SATCURVE_RES - 1] - .5f) < 1e-6f;
+ && fabsf(c->lut[0] - .5f) < 1e-6f
+ && fabsf(c->lut[DT_IOP_SATCURVE_RES - 1] - .5f) < 1e-6f;
}
static inline dt_iop_satcurve_factors_t eval_curve_factors(const dt_iop_satcurve_data_t *d,
- const float s_in_norm)
+ const float s_in_norm)
{
const float sat_c = CLAMP(lookup_lut(d->channel[DT_IOP_SATCURVE_CHANNEL_SATURATION].lut, s_in_norm), 0.f, 1.f);
const float bri_c = CLAMP(lookup_lut(d->channel[DT_IOP_SATCURVE_CHANNEL_BRILLIANCE].lut, s_in_norm), 0.f, 1.f);
@@ -383,10 +388,10 @@ static void _commit_sat_histogram(dt_iop_module_t *self, const int *const bins)
}
static void _update_sat_histogram(dt_iop_module_t *self,
- const dt_iop_satcurve_data_t *d,
- const dt_colormatrix_t inputmatrix_trans,
- const float *const restrict in,
- const size_t npixels)
+ const dt_iop_satcurve_data_t *d,
+ const dt_colormatrix_t inputmatrix_trans,
+ const float *const restrict in,
+ const size_t npixels)
{
dt_iop_satcurve_gui_data_t *g = self->gui_data;
if(!g) return;
@@ -399,17 +404,16 @@ static void _update_sat_histogram(dt_iop_module_t *self,
{
const float s_in_norm = pixel_s_in_norm(d->formula, in + 4 * k, inputmatrix_trans, d->gamut_lut, L_white, NULL);
const int bin = CLAMP((int)(s_in_norm * (DT_IOP_SATCURVE_HIST_RES - 1)),
- 0, DT_IOP_SATCURVE_HIST_RES - 1);
+ 0, DT_IOP_SATCURVE_HIST_RES - 1);
local_hist[bin]++;
}
_commit_sat_histogram(self, local_hist);
}
-
static inline void apply_sat_and_brilliance_ucs(const dt_iop_satcurve_data_t *d,
- dt_aligned_pixel_t xyz,
- const float L_white)
+ dt_aligned_pixel_t xyz,
+ const float L_white)
{
dt_aligned_pixel_t xyY, JCH, HCB, HSB;
@@ -453,7 +457,7 @@ static inline void apply_sat_and_brilliance_ucs(const dt_iop_satcurve_data_t *d,
}
static inline void apply_sat_and_brilliance_jzazbz(const dt_iop_satcurve_data_t *d,
- dt_aligned_pixel_t xyz)
+ dt_aligned_pixel_t xyz)
{
dt_aligned_pixel_t jab;
dt_XYZ_2_JzAzBz(xyz, jab);
@@ -485,20 +489,47 @@ static inline void apply_sat_and_brilliance_jzazbz(const dt_iop_satcurve_data_t
dt_JzAzBz_2_XYZ(jab, xyz);
}
+// NEU: berechnet pro Pixel die normierte Sättigung in einen separaten Buffer,
+// analog zu compute_luminance_mask() in toneequal.c
+static inline void compute_saturation_mask(const dt_iop_satcurve_data_t *d,
+ const dt_colormatrix_t inputmatrix_trans,
+ const float L_white,
+ const float *const restrict in,
+ float *const restrict mask,
+ const size_t npixels)
+{
+ DT_OMP_FOR()
+ for(size_t k = 0; k < npixels; k++)
+ {
+ mask[k] = pixel_s_in_norm(d->formula, in + 4 * k, inputmatrix_trans,
+ d->gamut_lut, L_white, NULL);
+ }
+}
+
+// NEU: Graustufen-Visualisierung der Sättigungsmaske; sqrt-"Gamma" für bessere
+// Sichtbarkeit niedriger Werte, analog display_luminance_mask() in toneequal.c
+static inline void display_saturation_mask(const float *const restrict in,
+ const float *const restrict mask,
+ float *const restrict out,
+ const size_t npixels)
+{
+ DT_OMP_FOR()
+ for(size_t k = 0; k < npixels; k++)
+ {
+ const float intensity = sqrtf(CLAMP(mask[k], 0.f, 1.f));
+ out[4 * k + 0] = intensity;
+ out[4 * k + 1] = intensity;
+ out[4 * k + 2] = intensity;
+ out[4 * k + 3] = in[4 * k + 3];
+ }
+}
+
void process(dt_iop_module_t *self, dt_dev_pixelpipe_iop_t *piece,
const void *const ivoid, void *const ovoid,
const dt_iop_roi_t *const roi_in, const dt_iop_roi_t *const roi_out)
{
dt_iop_satcurve_data_t *d = piece->data;
-
- const gboolean neutral_sat = channel_is_neutral(&d->channel[DT_IOP_SATCURVE_CHANNEL_SATURATION]);
- const gboolean neutral_bri = channel_is_neutral(&d->channel[DT_IOP_SATCURVE_CHANNEL_BRILLIANCE]);
-
- if(neutral_sat && neutral_bri)
- {
- dt_iop_image_copy_by_size(ovoid, ivoid, roi_out->width, roi_out->height, piece->colors);
- return;
- }
+ dt_iop_satcurve_gui_data_t *g = self->gui_data;
const dt_iop_order_iccprofile_info_t *work_profile = dt_ioppr_get_pipe_current_profile_info(self, piece->pipe);
if(!work_profile || piece->colors != 4)
@@ -520,12 +551,34 @@ void process(dt_iop_module_t *self, dt_dev_pixelpipe_iop_t *piece,
const float *const restrict in = DT_IS_ALIGNED((const float *)ivoid);
float *const restrict out = DT_IS_ALIGNED((float *)ovoid);
const size_t npixels = (size_t)roi_out->width * roi_out->height;
+ const float L_white = Y_to_dt_UCS_L_star(1.f);
if(self->dev->gui_attached && dt_pipe_is_full(piece->pipe) && dt_iop_has_focus(self)
&& piece->pipe == self->dev->full.pipe)
_update_sat_histogram(self, d, inputmatrix_trans, in, npixels);
- const float L_white = Y_to_dt_UCS_L_star(1.f);
+ // NEU: Maskenanzeige-Zweig -- zeigt die normierte Sättigung als Graustufenbild
+ if(self->dev->gui_attached && dt_pipe_is_full(piece->pipe) && g && g->mask_display)
+ {
+ float *const restrict mask = dt_alloc_align_float(npixels);
+ if(mask)
+ {
+ compute_saturation_mask(d, inputmatrix_trans, L_white, in, mask, npixels);
+ display_saturation_mask(in, mask, out, npixels);
+ dt_free_align(mask);
+ piece->pipe->mask_display = DT_DEV_PIXELPIPE_DISPLAY_PASSTHRU;
+ return;
+ }
+ }
+
+ const gboolean neutral_sat = channel_is_neutral(&d->channel[DT_IOP_SATCURVE_CHANNEL_SATURATION]);
+ const gboolean neutral_bri = channel_is_neutral(&d->channel[DT_IOP_SATCURVE_CHANNEL_BRILLIANCE]);
+
+ if(neutral_sat && neutral_bri)
+ {
+ dt_iop_image_copy_by_size(ovoid, ivoid, roi_out->width, roi_out->height, piece->colors);
+ return;
+ }
DT_OMP_FOR()
for(size_t k = 0; k < npixels; k++)
@@ -545,7 +598,6 @@ void process(dt_iop_module_t *self, dt_dev_pixelpipe_iop_t *piece,
pixout[3] = in[4 * k + 3];
copy_pixel_nontemporal(out + 4 * k, pixout);
}
-
dt_omploop_sfence();
}
@@ -555,30 +607,22 @@ int process_cl(dt_iop_module_t *self, dt_dev_pixelpipe_iop_t *piece,
const dt_iop_roi_t *const roi_in, const dt_iop_roi_t *const roi_out)
{
dt_iop_satcurve_data_t *d = piece->data;
+ dt_iop_satcurve_gui_data_t *g = self->gui_data;
const dt_iop_satcurve_global_data_t *gd = self->global_data;
cl_int err = DT_OPENCL_DEFAULT_ERROR;
if(piece->colors != 4) return err;
- const gboolean neutral_sat = channel_is_neutral(&d->channel[DT_IOP_SATCURVE_CHANNEL_SATURATION]);
- const gboolean neutral_bri = channel_is_neutral(&d->channel[DT_IOP_SATCURVE_CHANNEL_BRILLIANCE]);
- if(neutral_sat && neutral_bri)
- return dt_opencl_enqueue_copy_image(piece->pipe->devid, dev_in, dev_out,
- (size_t[]){ 0, 0, 0 },
- (size_t[]){ 0, 0, 0 },
- (size_t[]){ roi_in->width, roi_in->height, 1 });
-
const dt_iop_order_iccprofile_info_t *const work_profile =
- dt_ioppr_get_pipe_current_profile_info(self, piece->pipe);
+ dt_ioppr_get_pipe_current_profile_info(self, piece->pipe);
if(work_profile == NULL) return err;
const int devid = piece->pipe->devid;
const int width = roi_in->width;
const int height = roi_in->height;
- const gboolean want_histogram =
- self->dev->gui_attached && dt_pipe_is_full(piece->pipe) && dt_iop_has_focus(self)
- && piece->pipe == self->dev->full.pipe;
+ const gboolean want_mask =
+ self->dev->gui_attached && dt_pipe_is_full(piece->pipe) && g && g->mask_display;
cl_mem input_matrix_cl = NULL;
cl_mem output_matrix_cl = NULL;
@@ -593,6 +637,41 @@ int process_cl(dt_iop_module_t *self, dt_dev_pixelpipe_iop_t *piece,
dt_colormatrix_mul(output_matrix, work_profile->matrix_out, XYZ_D65_to_D50_CAT16);
input_matrix_cl = dt_opencl_copy_host_to_device_constant(devid, 12 * sizeof(float), input_matrix);
+ gamut_lut_cl = dt_opencl_copy_host_to_device_constant(devid, LUT_ELEM * sizeof(float), d->gamut_lut);
+
+ if(input_matrix_cl == NULL || gamut_lut_cl == NULL)
+ {
+ err = CL_MEM_OBJECT_ALLOCATION_FAILURE;
+ goto error;
+ }
+
+ const float L_white = Y_to_dt_UCS_L_star(1.f);
+
+ // NEU: Maskenanzeige-Pfad -- eigener, einfacherer Kernel statt satcurvergb
+ if(want_mask)
+ {
+ err = dt_opencl_enqueue_kernel_2d_args(
+ devid, gd->kernel_satcurve_mask, width, height,
+ CLARG(dev_in), CLARG(dev_out),
+ CLARG(width), CLARG(height),
+ CLARG(input_matrix_cl), CLARG(gamut_lut_cl),
+ CLARG(d->formula), CLARG(L_white));
+
+ if(err == CL_SUCCESS) piece->pipe->mask_display = DT_DEV_PIXELPIPE_DISPLAY_PASSTHRU;
+ goto error;
+ }
+
+ const gboolean neutral_sat = channel_is_neutral(&d->channel[DT_IOP_SATCURVE_CHANNEL_SATURATION]);
+ const gboolean neutral_bri = channel_is_neutral(&d->channel[DT_IOP_SATCURVE_CHANNEL_BRILLIANCE]);
+ if(neutral_sat && neutral_bri)
+ {
+ err = dt_opencl_enqueue_copy_image(piece->pipe->devid, dev_in, dev_out,
+ (size_t[]){ 0, 0, 0 },
+ (size_t[]){ 0, 0, 0 },
+ (size_t[]){ roi_in->width, roi_in->height, 1 });
+ goto error;
+ }
+
output_matrix_cl = dt_opencl_copy_host_to_device_constant(devid, 12 * sizeof(float), output_matrix);
sat_lut_cl = dt_opencl_copy_host_to_device_constant(
devid, DT_IOP_SATCURVE_RES * sizeof(float),
@@ -600,17 +679,16 @@ int process_cl(dt_iop_module_t *self, dt_dev_pixelpipe_iop_t *piece,
bri_lut_cl = dt_opencl_copy_host_to_device_constant(
devid, DT_IOP_SATCURVE_RES * sizeof(float),
d->channel[DT_IOP_SATCURVE_CHANNEL_BRILLIANCE].lut);
- gamut_lut_cl = dt_opencl_copy_host_to_device_constant(
- devid, LUT_ELEM * sizeof(float), d->gamut_lut);
- if(input_matrix_cl == NULL || output_matrix_cl == NULL
- || sat_lut_cl == NULL || bri_lut_cl == NULL || gamut_lut_cl == NULL)
+ if(output_matrix_cl == NULL || sat_lut_cl == NULL || bri_lut_cl == NULL)
{
err = CL_MEM_OBJECT_ALLOCATION_FAILURE;
goto error;
}
- const float L_white = Y_to_dt_UCS_L_star(1.f);
+ const gboolean want_histogram =
+ self->dev->gui_attached && dt_pipe_is_full(piece->pipe) && dt_iop_has_focus(self)
+ && piece->pipe == self->dev->full.pipe;
// pipeline-critical kernel goes first: the histogram below is pure UI
// feedback and must never delay the actual image processing on the queue
@@ -636,7 +714,7 @@ int process_cl(dt_iop_module_t *self, dt_dev_pixelpipe_iop_t *piece,
hist_bins_cl = dt_opencl_alloc_device_buffer(devid, hist_bins_size);
if(hist_bins_cl
&& dt_opencl_write_buffer_to_device(devid, hist_bins_host, hist_bins_cl, 0,
- hist_bins_size, TRUE) == CL_SUCCESS)
+ hist_bins_size, TRUE) == CL_SUCCESS)
{
const cl_int hist_err = dt_opencl_enqueue_kernel_2d_args(
devid, gd->kernel_satcurve_histogram, width, height,
@@ -647,7 +725,7 @@ int process_cl(dt_iop_module_t *self, dt_dev_pixelpipe_iop_t *piece,
if(hist_err == CL_SUCCESS
&& dt_opencl_read_buffer_from_device(devid, hist_bins_host, hist_bins_cl, 0,
- hist_bins_size, TRUE) == CL_SUCCESS)
+ hist_bins_size, TRUE) == CL_SUCCESS)
_commit_sat_histogram(self, hist_bins_host);
}
}
@@ -669,6 +747,7 @@ void init_global(dt_iop_module_so_t *self)
self->data = gd;
gd->kernel_satcurvergb = dt_opencl_create_kernel(program, "satcurvergb");
gd->kernel_satcurve_histogram = dt_opencl_create_kernel(program, "satcurve_histogram");
+ gd->kernel_satcurve_mask = dt_opencl_create_kernel(program, "satcurve_mask"); // NEU
}
void cleanup_global(dt_iop_module_so_t *self)
@@ -676,6 +755,7 @@ void cleanup_global(dt_iop_module_so_t *self)
const dt_iop_satcurve_global_data_t *gd = self->data;
dt_opencl_free_kernel(gd->kernel_satcurvergb);
dt_opencl_free_kernel(gd->kernel_satcurve_histogram);
+ dt_opencl_free_kernel(gd->kernel_satcurve_mask); // NEU
free(self->data);
self->data = NULL;
}
@@ -716,7 +796,7 @@ void cleanup_pipe(dt_iop_module_t *self, dt_dev_pixelpipe_t *pipe, dt_dev_pixelp
}
static void build_jzazbz_gamut_lut(dt_iop_satcurve_data_t *d,
- const dt_iop_order_iccprofile_info_t *work_profile)
+ const dt_iop_order_iccprofile_info_t *work_profile)
{
dt_colormatrix_t inputmatrix = {{ 0.0f }};
dt_colormatrix_mul(inputmatrix, XYZ_D50_to_D65_CAT16, work_profile->matrix_in);
@@ -753,15 +833,15 @@ static void build_jzazbz_gamut_lut(dt_iop_satcurve_data_t *d,
d->gamut_lut[0] = (sampler[LUT_ELEM - 2] + sampler[LUT_ELEM - 1] + sampler[0] + sampler[1] + sampler[2]) / 5.f;
d->gamut_lut[1] = (sampler[LUT_ELEM - 1] + sampler[0] + sampler[1] + sampler[2] + sampler[3]) / 5.f;
d->gamut_lut[LUT_ELEM - 2] = (sampler[LUT_ELEM - 4] + sampler[LUT_ELEM - 3] + sampler[LUT_ELEM - 2]
- + sampler[LUT_ELEM - 1] + sampler[0]) / 5.f;
+ + sampler[LUT_ELEM - 1] + sampler[0]) / 5.f;
d->gamut_lut[LUT_ELEM - 1] = (sampler[LUT_ELEM - 3] + sampler[LUT_ELEM - 2] + sampler[LUT_ELEM - 1]
- + sampler[0] + sampler[1]) / 5.f;
+ + sampler[0] + sampler[1]) / 5.f;
dt_free_align(sampler);
}
static void build_gamut_lut(dt_iop_satcurve_data_t *d,
- const dt_iop_order_iccprofile_info_t *work_profile)
+ const dt_iop_order_iccprofile_info_t *work_profile)
{
if(d->formula == DT_IOP_SATCURVE_DTUCS)
{
@@ -776,7 +856,7 @@ static void build_gamut_lut(dt_iop_satcurve_data_t *d,
}
static void sync_channel_curve(dt_iop_satcurve_channel_data_t *dst,
- const dt_iop_satcurve_channel_params_t *src)
+ const dt_iop_satcurve_channel_params_t *src)
{
if(dst->curve_type != src->curve_type || dst->curve_num_nodes != src->curve_num_nodes)
{
@@ -798,7 +878,7 @@ static void sync_channel_curve(dt_iop_satcurve_channel_data_t *dst,
}
void commit_params(dt_iop_module_t *self, dt_iop_params_t *p1, dt_dev_pixelpipe_t *pipe,
- dt_dev_pixelpipe_iop_t *piece)
+ dt_dev_pixelpipe_iop_t *piece)
{
dt_iop_satcurve_data_t *d = piece->data;
const dt_iop_satcurve_params_t *p = (const dt_iop_satcurve_params_t *)p1;
@@ -813,7 +893,7 @@ void commit_params(dt_iop_module_t *self, dt_iop_params_t *p1, dt_dev_pixelpipe_
sync_channel_curve(&d->channel[ch], &p->channel[ch]);
const dt_iop_order_iccprofile_info_t *work_profile =
- dt_ioppr_get_pipe_current_profile_info(self, piece->pipe);
+ dt_ioppr_get_pipe_current_profile_info(self, piece->pipe);
if(work_profile && (!d->lut_inited || work_profile != d->work_profile))
{
@@ -824,11 +904,11 @@ void commit_params(dt_iop_module_t *self, dt_iop_params_t *p1, dt_dev_pixelpipe_
}
int legacy_params(dt_iop_module_t *self,
- const void *const old_params,
- const int old_version,
- void **new_params,
- int32_t *new_params_size,
- int *new_version)
+ const void *const old_params,
+ const int old_version,
+ void **new_params,
+ int32_t *new_params_size,
+ int *new_version)
{
if(old_version == 1)
{
@@ -849,7 +929,6 @@ int legacy_params(dt_iop_module_t *self,
*new_version = 2;
return 0;
}
-
return 1;
}
@@ -859,33 +938,33 @@ void init_presets(dt_iop_module_so_t *self)
reset_params(&p);
dt_gui_presets_add_generic(_("neutral"), self->op, self->version(),
- &p, sizeof(p), TRUE, DEVELOP_BLEND_CS_RGB_SCENE);
+ &p, sizeof(p), TRUE, DEVELOP_BLEND_CS_RGB_SCENE);
reset_params(&p);
p.channel[DT_IOP_SATCURVE_CHANNEL_SATURATION].curve[0].y = .60f;
p.channel[DT_IOP_SATCURVE_CHANNEL_SATURATION].curve[1].y = .55f;
dt_gui_presets_add_generic(_("gentle saturation"), self->op, self->version(),
- &p, sizeof(p), TRUE, DEVELOP_BLEND_CS_RGB_SCENE);
+ &p, sizeof(p), TRUE, DEVELOP_BLEND_CS_RGB_SCENE);
reset_params(&p);
p.channel[DT_IOP_SATCURVE_CHANNEL_SATURATION].curve_num_nodes = 3;
p.channel[DT_IOP_SATCURVE_CHANNEL_SATURATION].curve[1] = (dt_iop_satcurve_node_t){ .45f, .62f };
p.channel[DT_IOP_SATCURVE_CHANNEL_SATURATION].curve[2] = (dt_iop_satcurve_node_t){ 1.f, .42f };
dt_gui_presets_add_generic(_("protect saturated colors"), self->op, self->version(),
- &p, sizeof(p), TRUE, DEVELOP_BLEND_CS_RGB_SCENE);
+ &p, sizeof(p), TRUE, DEVELOP_BLEND_CS_RGB_SCENE);
reset_params(&p);
p.channel[DT_IOP_SATCURVE_CHANNEL_BRILLIANCE].curve[0].y = .56f;
p.channel[DT_IOP_SATCURVE_CHANNEL_BRILLIANCE].curve[1].y = .54f;
dt_gui_presets_add_generic(_("gentle brilliance"), self->op, self->version(),
- &p, sizeof(p), TRUE, DEVELOP_BLEND_CS_RGB_SCENE);
+ &p, sizeof(p), TRUE, DEVELOP_BLEND_CS_RGB_SCENE);
reset_params(&p);
p.channel[DT_IOP_SATCURVE_CHANNEL_BRILLIANCE].curve_num_nodes = 3;
p.channel[DT_IOP_SATCURVE_CHANNEL_BRILLIANCE].curve[1] = (dt_iop_satcurve_node_t){ .40f, .58f };
p.channel[DT_IOP_SATCURVE_CHANNEL_BRILLIANCE].curve[2] = (dt_iop_satcurve_node_t){ 1.f, .48f };
dt_gui_presets_add_generic(_("bright mids, protect extremes"), self->op, self->version(),
- &p, sizeof(p), TRUE, DEVELOP_BLEND_CS_RGB_SCENE);
+ &p, sizeof(p), TRUE, DEVELOP_BLEND_CS_RGB_SCENE);
}
static inline int add_node_to_channel(dt_iop_satcurve_channel_params_t *c, const float x, const float y)
@@ -911,7 +990,7 @@ static void _get_graph_geometry(const GtkAllocation *a, float *x0, float *y0, fl
*y0 = DT_IOP_SATCURVE_INSET;
*w = MAX(1, a->width - 2 * DT_IOP_SATCURVE_INSET);
*h = MAX(1, a->height - 2 * DT_IOP_SATCURVE_INSET - DT_IOP_SATCURVE_GRADIENT_GAP
- - DT_IOP_SATCURVE_GRADIENT_SIZE);
+ - DT_IOP_SATCURVE_GRADIENT_SIZE);
}
static void _draw_sat_histogram(cairo_t *cr, dt_iop_satcurve_gui_data_t *g, const int w, const int h)
@@ -964,12 +1043,12 @@ static void _draw_sat_picker(cairo_t *cr, dt_iop_module_t *self, const int w, co
}
static void _draw_channel_curve(cairo_t *cr,
- const dt_iop_satcurve_channel_params_t *cp,
- const dt_iop_satcurve_gui_channel_t *gc,
- const int selected,
- const gboolean active,
- const int w, const int h,
- const double r, const double g, const double b)
+ const dt_iop_satcurve_channel_params_t *cp,
+ const dt_iop_satcurve_gui_channel_t *gc,
+ const int selected,
+ const gboolean active,
+ const int w, const int h,
+ const double r, const double g, const double b)
{
cairo_save(cr);
@@ -1000,7 +1079,7 @@ static void _draw_channel_curve(cairo_t *cr,
}
static void _sync_gui_curve(dt_iop_satcurve_gui_channel_t *gc,
- const dt_iop_satcurve_channel_params_t *cp)
+ const dt_iop_satcurve_channel_params_t *cp)
{
if(gc->curve_type != cp->curve_type || gc->curve_num_nodes != cp->curve_num_nodes)
{
@@ -1061,20 +1140,20 @@ static gboolean area_draw(GtkWidget *widget, cairo_t *cr, dt_iop_module_t *self)
cairo_stroke(cr);
_draw_channel_curve(cr,
- &p->channel[DT_IOP_SATCURVE_CHANNEL_SATURATION],
- &g->channel[DT_IOP_SATCURVE_CHANNEL_SATURATION],
- g->active_channel == DT_IOP_SATCURVE_CHANNEL_SATURATION ? g->selected : -1,
- g->active_channel == DT_IOP_SATCURVE_CHANNEL_SATURATION,
- (int)gw, (int)gh,
- .90, .90, .90);
+ &p->channel[DT_IOP_SATCURVE_CHANNEL_SATURATION],
+ &g->channel[DT_IOP_SATCURVE_CHANNEL_SATURATION],
+ g->active_channel == DT_IOP_SATCURVE_CHANNEL_SATURATION ? g->selected : -1,
+ g->active_channel == DT_IOP_SATCURVE_CHANNEL_SATURATION,
+ (int)gw, (int)gh,
+ .90, .90, .90);
_draw_channel_curve(cr,
- &p->channel[DT_IOP_SATCURVE_CHANNEL_BRILLIANCE],
- &g->channel[DT_IOP_SATCURVE_CHANNEL_BRILLIANCE],
- g->active_channel == DT_IOP_SATCURVE_CHANNEL_BRILLIANCE ? g->selected : -1,
- g->active_channel == DT_IOP_SATCURVE_CHANNEL_BRILLIANCE,
- (int)gw, (int)gh,
- 1.00, .65, .20);
+ &p->channel[DT_IOP_SATCURVE_CHANNEL_BRILLIANCE],
+ &g->channel[DT_IOP_SATCURVE_CHANNEL_BRILLIANCE],
+ g->active_channel == DT_IOP_SATCURVE_CHANNEL_BRILLIANCE ? g->selected : -1,
+ g->active_channel == DT_IOP_SATCURVE_CHANNEL_BRILLIANCE,
+ (int)gw, (int)gh,
+ 1.00, .65, .20);
cairo_restore(cr);
@@ -1135,7 +1214,6 @@ static gboolean area_button(GtkWidget *widget, GdkEventButton *event, dt_iop_mod
for(int i = hit; i < cp->curve_num_nodes - 1; i++) cp->curve[i] = cp->curve[i + 1];
cp->curve_num_nodes--;
}
-
g->selected = -1;
dt_dev_add_history_item(darktable.develop, self, TRUE);
gtk_widget_queue_draw(widget);
@@ -1154,6 +1232,7 @@ static gboolean area_button(GtkWidget *widget, GdkEventButton *event, dt_iop_mod
return FALSE;
}
+
static gboolean area_scroll(GtkWidget *widget, GdkEventScroll *event,
dt_iop_module_t *self)
{
@@ -1172,6 +1251,7 @@ static gboolean area_scroll(GtkWidget *widget, GdkEventScroll *event,
dt_dev_add_history_item(darktable.develop, self, TRUE);
gtk_widget_queue_draw(widget);
}
+
return TRUE;
}
@@ -1197,8 +1277,8 @@ static gboolean area_motion(GtkWidget *widget, GdkEventMotion *event, dt_iop_mod
cp->curve[n].x = 1.f;
else
cp->curve[n].x = CLAMP(x,
- cp->curve[n - 1].x + DT_IOP_SATCURVE_MIN_X_DISTANCE,
- cp->curve[n + 1].x - DT_IOP_SATCURVE_MIN_X_DISTANCE);
+ cp->curve[n - 1].x + DT_IOP_SATCURVE_MIN_X_DISTANCE,
+ cp->curve[n + 1].x - DT_IOP_SATCURVE_MIN_X_DISTANCE);
cp->curve[n].y = CLAMP(1.f - (event->y - gy0) / h, 0.f, 1.f);
gtk_widget_queue_draw(widget);
@@ -1248,7 +1328,7 @@ void color_picker_apply(dt_iop_module_t *self, GtkWidget *widget, dt_dev_pixelpi
if(!g) return;
const dt_iop_order_iccprofile_info_t *work_profile =
- dt_ioppr_get_iop_work_profile_info(self, self->dev->iop);
+ dt_ioppr_get_iop_work_profile_info(self, self->dev->iop);
if(!work_profile) return;
dt_colormatrix_t inputmatrix = {{ 0.0f }};
@@ -1273,15 +1353,39 @@ void color_picker_apply(dt_iop_module_t *self, GtkWidget *widget, dt_dev_pixelpi
gtk_widget_queue_draw(GTK_WIDGET(g->area));
}
+// NEU: Callback für den Sättigungsmasken-Toggle-Button, analog
+// show_luminance_mask_callback() in toneequal.c
+static void show_saturation_mask_callback(GtkToggleButton *button, dt_iop_module_t *self)
+{
+ if(darktable.gui->reset) return;
+ dt_iop_satcurve_gui_data_t *g = self->gui_data;
+
+ dt_iop_request_focus(self);
+ g->mask_display = gtk_toggle_button_get_active(button);
+ dt_dev_reprocess_center(self->dev);
+}
+
void gui_focus(dt_iop_module_t *self, gboolean in)
{
- if(!in) dt_iop_color_picker_reset(self, FALSE);
+ dt_iop_satcurve_gui_data_t *g = self->gui_data;
+ if(!in)
+ {
+ dt_iop_color_picker_reset(self, FALSE);
+
+ // NEU: Maskenanzeige beim Verlassen des Moduls zurücksetzen
+ if(g && g->mask_display)
+ {
+ g->mask_display = FALSE;
+ if(g->show_saturation_mask)
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->show_saturation_mask), FALSE);
+ }
+ }
}
static void _channel_tabs_switch_callback(GtkNotebook *notebook,
- GtkWidget *page,
- guint page_num,
- dt_iop_module_t *self)
+ GtkWidget *page,
+ guint page_num,
+ dt_iop_module_t *self)
{
DT_GUARD_GUI_UPDATE();
@@ -1289,8 +1393,8 @@ static void _channel_tabs_switch_callback(GtkNotebook *notebook,
if(!g) return;
g->active_channel = (page_num == 1)
- ? DT_IOP_SATCURVE_CHANNEL_BRILLIANCE
- : DT_IOP_SATCURVE_CHANNEL_SATURATION;
+ ? DT_IOP_SATCURVE_CHANNEL_BRILLIANCE
+ : DT_IOP_SATCURVE_CHANNEL_SATURATION;
g->selected = -1;
gtk_widget_queue_draw(GTK_WIDGET(g->area));
@@ -1315,7 +1419,7 @@ void gui_update(dt_iop_module_t *self)
if(g->notebook)
gtk_notebook_set_current_page(GTK_NOTEBOOK(g->notebook),
- g->active_channel == DT_IOP_SATCURVE_CHANNEL_BRILLIANCE ? 1 : 0);
+ g->active_channel == DT_IOP_SATCURVE_CHANNEL_BRILLIANCE ? 1 : 0);
if(g->area) gtk_widget_queue_draw(GTK_WIDGET(g->area));
}
@@ -1339,6 +1443,7 @@ void gui_init(dt_iop_module_t *self)
g->selected = -1;
g->dragging = FALSE;
g->active_channel = DT_IOP_SATCURVE_CHANNEL_SATURATION;
+ g->mask_display = FALSE; // NEU
for(int ch = 0; ch < DT_IOP_SATCURVE_CHANNELS; ch++)
{
@@ -1348,8 +1453,8 @@ void gui_init(dt_iop_module_t *self)
for(int i = 0; i < p->channel[ch].curve_num_nodes; i++)
dt_draw_curve_add_point(g->channel[ch].curve,
- p->channel[ch].curve[i].x,
- p->channel[ch].curve[i].y);
+ p->channel[ch].curve[i].x,
+ p->channel[ch].curve[i].y);
}
memset(g->histogram, 0, sizeof(g->histogram));
@@ -1382,27 +1487,27 @@ void gui_init(dt_iop_module_t *self)
gtk_notebook_set_current_page(g->notebook, 0);
g_signal_connect(G_OBJECT(g->notebook), "switch-page",
- G_CALLBACK(_channel_tabs_switch_callback), self);
+ G_CALLBACK(_channel_tabs_switch_callback), self);
dt_gui_box_add(self->widget, GTK_WIDGET(g->notebook));
g->area = GTK_DRAWING_AREA(dt_ui_resize_wrap(NULL, 0, "plugins/darkroom/satcurve/graph_height"));
gtk_widget_set_size_request(GTK_WIDGET(g->area), -1, DT_PIXEL_APPLY_DPI(180));
gtk_widget_add_events(GTK_WIDGET(g->area),
- GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK
- | GDK_POINTER_MOTION_MASK | GDK_SCROLL_MASK);
+ GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK
+ | GDK_POINTER_MOTION_MASK | GDK_SCROLL_MASK);
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), self);
g_signal_connect(G_OBJECT(g->area), "button-release-event", G_CALLBACK(area_release), self);
g_signal_connect(G_OBJECT(g->area), "motion-notify-event", G_CALLBACK(area_motion), self);
- g_signal_connect(G_OBJECT(g->area), "scroll-event", G_CALLBACK(area_scroll), self);
+ g_signal_connect(G_OBJECT(g->area), "scroll-event", G_CALLBACK(area_scroll), self);
dt_gui_box_add(self->widget, GTK_WIDGET(g->area));
g->formula = dt_bauhaus_combobox_from_params(self, "formula");
dt_bauhaus_combobox_add(g->formula, _("JzAzBz"));
dt_bauhaus_combobox_add(g->formula, _("darktable UCS"));
gtk_widget_set_tooltip_text(g->formula,
- _("choose the perceptual saturation definition used by both curves"));
+ _("choose the perceptual saturation definition used by both curves"));
g_object_ref(g->formula);
gtk_container_remove(GTK_CONTAINER(self->widget), g->formula);
@@ -1413,7 +1518,14 @@ void gui_init(dt_iop_module_t *self)
g->colorpicker = dt_color_picker_new_with_cst(self, DT_COLOR_PICKER_POINT, NULL, IOP_CS_RGB);
gtk_widget_set_tooltip_text(g->colorpicker, _("pick saturation from image"));
+ // NEU: Toggle-Button für die Sättigungsmaskenanzeige
+ g->show_saturation_mask = dtgtk_togglebutton_new(dtgtk_cairo_paint_showmask, 0, NULL);
+ gtk_widget_set_tooltip_text(g->show_saturation_mask, _("display saturation mask"));
+ g_signal_connect(G_OBJECT(g->show_saturation_mask), "toggled",
+ G_CALLBACK(show_saturation_mask_callback), self);
+
gtk_box_pack_start(GTK_BOX(controls_hbox), g->colorpicker, FALSE, FALSE, 0);
+ gtk_box_pack_start(GTK_BOX(controls_hbox), g->show_saturation_mask, FALSE, FALSE, 0); // NEU
gtk_box_pack_end(GTK_BOX(controls_hbox), g->formula, FALSE, FALSE, 0);
g_object_unref(g->formula);
@@ -1425,6 +1537,7 @@ void init(dt_iop_module_t *self)
self->params = calloc(1, sizeof(dt_iop_satcurve_params_t));
self->default_params = calloc(1, sizeof(dt_iop_satcurve_params_t));
self->params_size = sizeof(dt_iop_satcurve_params_t);
+ self->gui_data = NULL;
reset_params(self->params);
reset_params(self->default_params);
From 19abf26371bf2d35998deebe0ca74f7aa9e20f1e Mon Sep 17 00:00:00 2001
From: Martin Straeten <39386816+MStraeten@users.noreply.github.com>
Date: Fri, 24 Jul 2026 22:22:14 +0200
Subject: [PATCH 07/12] added fast guided filter ported from toneequal plus
opencl path for it
---
data/kernels/fast_guided_filter.cl | 220 ++++++
data/kernels/programs.conf | 3 +-
data/kernels/satcurve.cl | 45 ++
src/iop/satcurvergb.c | 1032 ++++++++++++++++++++--------
4 files changed, 1009 insertions(+), 291 deletions(-)
create mode 100755 data/kernels/fast_guided_filter.cl
mode change 100644 => 100755 src/iop/satcurvergb.c
diff --git a/data/kernels/fast_guided_filter.cl b/data/kernels/fast_guided_filter.cl
new file mode 100755
index 000000000000..aa4ef383b655
--- /dev/null
+++ b/data/kernels/fast_guided_filter.cl
@@ -0,0 +1,220 @@
+/*
+ This file is part of darktable,
+ Copyright (C) 2019-2026 darktable developers.
+
+ darktable is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ darktable is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with darktable. If not, see .
+*/
+
+#include "common.h"
+
+/*
+ * OpenCL port of the "fast scalar guided filter" from common/fast_guided_filter.h
+ * (fast_surface_blur() and friends). This mirrors the CPU algorithm exactly:
+ *
+ * 1. downsample the single-channel image by a factor of 4 (bilinear)
+ * 2. per iteration:
+ * - quantize the (downsampled) image into a guide buffer g
+ * - box-blur g, p (=image), g*g, g*p over a window of 2*radius+1
+ * - solve the linear regression p ~= a*g + b per pixel
+ * - box-blur a, b over the same window (spatial smoothing of params)
+ * - if not the last iteration: image = a*image + b
+ * 3. upsample a, b back to full resolution (bilinear)
+ * 4. final blend: image = a*image + b
+ *
+ * All buffers are single-channel (CL_R / float) images, addressed the same
+ * way as the existing guided_filter_box_mean_x/_y kernels in guided_filter.cl,
+ * which this file reuses verbatim for the box-blur passes (see the host-side
+ * orchestration that must call them once per buffer, per direction).
+ *
+ * NOTE: this file only provides the per-pixel kernels. The host-side C code
+ * that allocates the intermediate buffers and launches these kernels (plus
+ * the existing guided_filter_box_mean_x/_y from guided_filter.cl) in the
+ * right order, once per iteration, still needs to be written -- mirroring
+ * what common/guided_filter.c does for the RGB-guided variant.
+ */
+
+#define FGF_MIN_FLOAT 0x1p-16f // exp2(-16.0f), matches MIN_FLOAT in fast_guided_filter.h
+
+
+// generic bilinear resample of a single-channel image; used both to
+// downsample the guide/signal by a factor of ~4 and to upsample the
+// solved (a, b) parameter buffers back to full resolution.
+kernel void
+fastguided_resample(read_only image2d_t in,
+ const int width_in, const int height_in,
+ write_only image2d_t out,
+ const int width_out, const int height_out)
+{
+ const int x = get_global_id(0);
+ const int y = get_global_id(1);
+ if(x >= width_out || y >= height_out) return;
+
+ const float x_out = (float)x / (float)width_out;
+ const float y_out = (float)y / (float)height_out;
+
+ const float x_in = x_out * (float)width_in;
+ const float y_in = y_out * (float)height_in;
+
+ int x_prev = (int)floor(x_in);
+ int x_next = x_prev + 1;
+ int y_prev = (int)floor(y_in);
+ int y_next = y_prev + 1;
+
+ x_prev = clamp(x_prev, 0, width_in - 1);
+ x_next = clamp(x_next, 0, width_in - 1);
+ y_prev = clamp(y_prev, 0, height_in - 1);
+ y_next = clamp(y_next, 0, height_in - 1);
+
+ const float Dy_next = (float)y_next - y_in;
+ const float Dy_prev = 1.f - Dy_next;
+ const float Dx_next = (float)x_next - x_in;
+ const float Dx_prev = 1.f - Dx_next;
+
+ const float Q_NW = Areadsingle(in, x_prev, y_prev);
+ const float Q_NE = Areadsingle(in, x_next, y_prev);
+ const float Q_SE = Areadsingle(in, x_next, y_next);
+ const float Q_SW = Areadsingle(in, x_prev, y_next);
+
+ const float result = Dy_prev * (Q_SW * Dx_next + Q_SE * Dx_prev)
+ + Dy_next * (Q_NW * Dx_next + Q_NE * Dx_prev);
+
+ write_imagef(out, (int2)(x, y), result);
+}
+
+
+// posterize `in` in log2 space; sampling == 0 is a no-op copy, sampling == 1
+// is the fast per-stop track, anything else uses the general formula.
+// Mirrors quantize() in fast_guided_filter.h.
+kernel void
+fastguided_quantize(read_only image2d_t in, write_only image2d_t out,
+ const int width, const int height,
+ const float sampling, const float clip_min, const float clip_max)
+{
+ const int x = get_global_id(0);
+ const int y = get_global_id(1);
+ if(x >= width || y >= height) return;
+
+ const float value = Areadsingle(in, x, y);
+ float result;
+
+ if(sampling == 0.0f)
+ result = value;
+ else if(sampling == 1.0f)
+ result = clamp(exp2(floor(log2(value))), clip_min, clip_max);
+ else
+ result = clamp(exp2(floor(log2(value) / sampling) * sampling), clip_min, clip_max);
+
+ write_imagef(out, (int2)(x, y), result);
+}
+
+
+// per-pixel g*g and g*p, ready for box-blurring. Mirrors the packing step in
+// variance_analyse() (the guide/mask/guide^2/guide*mask struct), split into
+// two single-channel outputs since the box-blur passes are single-channel.
+kernel void
+fastguided_covar_products(read_only image2d_t guide, read_only image2d_t signal,
+ write_only image2d_t guide_sq, write_only image2d_t guide_signal,
+ const int width, const int height)
+{
+ const int x = get_global_id(0);
+ const int y = get_global_id(1);
+ if(x >= width || y >= height) return;
+
+ const float g = Areadsingle(guide, x, y);
+ const float p = Areadsingle(signal, x, y);
+
+ write_imagef(guide_sq, (int2)(x, y), g * g);
+ write_imagef(guide_signal, (int2)(x, y), g * p);
+}
+
+
+// solve the linear blending parameters a, b such that p ~= a*g + b, from the
+// box-blurred means of g, p, g*g and g*p. Mirrors the blending step at the
+// end of variance_analyse().
+kernel void
+fastguided_solve_ab(read_only image2d_t mean_g, read_only image2d_t mean_p,
+ read_only image2d_t mean_gg, read_only image2d_t mean_gp,
+ write_only image2d_t a_out, write_only image2d_t b_out,
+ const int width, const int height,
+ const float feathering)
+{
+ const int x = get_global_id(0);
+ const int y = get_global_id(1);
+ if(x >= width || y >= height) return;
+
+ const float g = Areadsingle(mean_g, x, y);
+ const float p = Areadsingle(mean_p, x, y);
+ const float gg = Areadsingle(mean_gg, x, y);
+ const float gp = Areadsingle(mean_gp, x, y);
+
+ const float d = fmax((gg - g * g) + feathering, 1e-15f);
+ const float a = (gp - g * p) / d;
+ const float b = p - a * g;
+
+ write_imagef(a_out, (int2)(x, y), a);
+ write_imagef(b_out, (int2)(x, y), b);
+}
+
+
+// image = max(image * a + b, MIN_FLOAT), in-place capable (no neighbour
+// access). Used both for the intermediate per-iteration update of the
+// downsampled image and for the final full-resolution blend. Mirrors
+// apply_linear_blending() (DT_GF_BLENDING_LINEAR case only -- the geomean
+// blending mode is not ported here since satcurvergb only ever requests
+// DT_GF_BLENDING_LINEAR).
+kernel void
+fastguided_apply_blend(read_only image2d_t image_in, write_only image2d_t image_out,
+ read_only image2d_t a_img, read_only image2d_t b_img,
+ const int width, const int height)
+{
+ const int x = get_global_id(0);
+ const int y = get_global_id(1);
+ if(x >= width || y >= height) return;
+
+ const float value = Areadsingle(image_in, x, y);
+ const float a = Areadsingle(a_img, x, y);
+ const float b = Areadsingle(b_img, x, y);
+
+ write_imagef(image_out, (int2)(x, y), fmax(value * a + b, FGF_MIN_FLOAT));
+}
+
+
+// final step specific to satcurvergb: blend the already-corrected RGB pixel
+// back towards the untouched input RGB, using the filtered scalar mask as
+// per-pixel strength. Mirrors the `if(gf_mask)` branch inside the CPU
+// process() loop: pixout[c] = rgb[c] + w * (pixout[c] - rgb[c]).
+kernel void
+satcurve_apply_guided_mask(read_only image2d_t rgb_in, read_only image2d_t corrected,
+ read_only image2d_t mask, write_only image2d_t out,
+ const int width, const int height)
+{
+ const int x = get_global_id(0);
+ const int y = get_global_id(1);
+ if(x >= width || y >= height) return;
+
+ const float4 rgb = Areadpixel(rgb_in, x, y);
+ const float4 pixout = Areadpixel(corrected, x, y);
+ const float w = clamp(Areadsingle(mask, x, y), 0.f, 1.f);
+
+ float4 result = rgb + w * (pixout - rgb);
+ result.w = pixout.w;
+
+ write_imagef(out, (int2)(x, y), result);
+}
+
+// clang-format off
+// modelines: These editor modelines have been set for all relevant files by tools/update_modelines.py
+// vim: shiftwidth=2 expandtab tabstop=2 cindent
+// kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified;
+// clang-format on
diff --git a/data/kernels/programs.conf b/data/kernels/programs.conf
index 16b3addde72d..4d24747de157 100644
--- a/data/kernels/programs.conf
+++ b/data/kernels/programs.conf
@@ -42,4 +42,5 @@ capture.cl 38
agx.cl 39
colorharmonizer.cl 40
overlay.cl 41
-satcurve.cl 42
\ No newline at end of file
+satcurve.cl 42
+fast_guided_filter.cl 43
diff --git a/data/kernels/satcurve.cl b/data/kernels/satcurve.cl
index d9b47b56897a..b778b43f44ad 100755
--- a/data/kernels/satcurve.cl
+++ b/data/kernels/satcurve.cl
@@ -274,5 +274,50 @@ satcurve_mask(read_only image2d_t in, write_only image2d_t out,
const float intensity = sqrt(clamp(s_in_norm, 0.f, 1.f));
float4 pixout = { intensity, intensity, intensity, pix_in.w };
+ write_imagef(out, (int2)(x, y), pixout);
+}
+
+// scalar (single-channel) saturation mask, feeding the guided filter pipeline
+// in fast_guided_filter_scalar.cl. Mirrors compute_saturation_mask() on the
+// CPU side: MAX(s_in_norm, 0.f), written into a single-channel buffer instead
+// of the 3-channel visualisation that satcurve_mask above produces.
+kernel void
+satcurve_scalar_mask(read_only image2d_t in, write_only image2d_t out,
+ const int width, const int height,
+ constant const float *const matrix_in,
+ global const float *const gamut_lut,
+ const int formula, const float L_white)
+{
+ const int x = get_global_id(0);
+ const int y = get_global_id(1);
+ if(x >= width || y >= height) return;
+
+ const float4 pix_in = Areadpixel(in, x, y);
+ const float s_in_norm = satcurve_s_in_norm_cl(pix_in, matrix_in, gamut_lut, formula, L_white);
+
+ write_imagef(out, (int2)(x, y), fmax(s_in_norm, 0.f));
+}
+
+// broadcasts a single-channel (guided-filtered) mask into an RGB visualisation,
+// alpha taken from the original input. Used for the mask_display branch when
+// the guided filter is enabled, so the preview shows the mask that is
+// actually used to modulate the correction. Mirrors display_saturation_mask()
+// on the CPU side, but reading a pre-computed scalar buffer instead of
+// recomputing s_in_norm.
+kernel void
+satcurve_mask_from_scalar(read_only image2d_t scalar_mask, read_only image2d_t in,
+ write_only image2d_t out,
+ const int width, const int height)
+{
+ const int x = get_global_id(0);
+ const int y = get_global_id(1);
+ if(x >= width || y >= height) return;
+
+ const float mask_value = Areadsingle(scalar_mask, x, y);
+ const float4 pix_in = Areadpixel(in, x, y);
+
+ const float intensity = sqrt(clamp(mask_value, 0.f, 1.f));
+ float4 pixout = { intensity, intensity, intensity, pix_in.w };
+
write_imagef(out, (int2)(x, y), pixout);
}
\ No newline at end of file
diff --git a/src/iop/satcurvergb.c b/src/iop/satcurvergb.c
old mode 100644
new mode 100755
index bcf9f43094e2..67684e4bfdf2
--- a/src/iop/satcurvergb.c
+++ b/src/iop/satcurvergb.c
@@ -19,12 +19,15 @@
#include "common/color_picker.h"
#include "common/darktable_ucs_22_helpers.h"
#include "common/dtpthread.h"
+#include "common/fast_guided_filter.h"
+#include "common/guided_filter.h"
#include "common/gamut_mapping.h"
#include "common/imagebuf.h"
#include "common/math.h"
#include "develop/imageop.h"
#include "develop/imageop_gui.h"
#include "develop/openmp_maths.h"
+#include "develop/tiling.h"
#include "dtgtk/button.h"
#include "dtgtk/drawingarea.h"
#include "gui/color_picker_proxy.h"
@@ -43,7 +46,7 @@
#define DT_IOP_SATCURVE_GAMUT_STEPS 92
#define DT_IOP_SATCURVE_CHANNELS 2
-DT_MODULE_INTROSPECTION(2, dt_iop_satcurve_params_t)
+DT_MODULE_INTROSPECTION(1, dt_iop_satcurve_params_t)
typedef enum dt_iop_satcurve_formula_t
{
@@ -73,15 +76,14 @@ typedef struct dt_iop_satcurve_params_t
{
dt_iop_satcurve_channel_params_t channel[DT_IOP_SATCURVE_CHANNELS];
dt_iop_satcurve_formula_t formula; // $DEFAULT: 1 $DESCRIPTION: "saturation formula"
-} dt_iop_satcurve_params_t;
-typedef struct dt_iop_satcurve_params_v1_t
-{
- dt_iop_satcurve_node_t curve[DT_IOP_SATCURVE_MAXNODES];
- int curve_num_nodes;
- int curve_type;
- dt_iop_satcurve_formula_t formula;
-} dt_iop_satcurve_params_v1_t;
+ // fast scalar guided filter controls
+ gboolean use_guided_filter; // $DEFAULT: FALSE $DESCRIPTION: "use guided filter"
+ float gf_radius; // $MIN: 0.5 $MAX: 200.0 $DEFAULT: 10.0 $DESCRIPTION: "filter radius"
+ float gf_feathering; // $MIN: 0.1 $MAX: 50.0 $DEFAULT: 1.0 $DESCRIPTION: "edge feathering"
+ int gf_iterations; // $MIN: 1 $MAX: 10 $DEFAULT: 1 $DESCRIPTION: "iterations"
+ float gf_quantization; // $MIN: 0.0 $MAX: 2.0 $DEFAULT: 0.0 $DESCRIPTION: "mask quantization"
+} dt_iop_satcurve_params_t;
typedef struct dt_iop_satcurve_channel_data_t
{
@@ -98,6 +100,12 @@ typedef struct dt_iop_satcurve_data_t
float *gamut_lut;
gboolean lut_inited;
const struct dt_iop_order_iccprofile_info_t *work_profile;
+
+ gboolean use_guided_filter;
+ float gf_radius;
+ float gf_feathering;
+ int gf_iterations;
+ float gf_quantization;
} dt_iop_satcurve_data_t;
typedef struct dt_iop_satcurve_gui_channel_t
@@ -113,9 +121,16 @@ typedef struct dt_iop_satcurve_gui_data_t
GtkDrawingArea *area;
GtkWidget *colorpicker;
GtkWidget *formula;
- GtkWidget *show_saturation_mask; // NEU
+ GtkWidget *show_saturation_mask;
GtkNotebook *notebook;
+ GtkWidget *use_guided_filter;
+ GtkWidget *gf_radius;
+ GtkWidget *gf_feathering;
+ GtkWidget *gf_iterations;
+ GtkWidget *gf_quantization;
+ dt_gui_collapsible_section_t gf_section;
+
dt_iop_satcurve_gui_channel_t channel[DT_IOP_SATCURVE_CHANNELS];
dt_iop_satcurve_channel_t active_channel;
int selected;
@@ -130,14 +145,22 @@ typedef struct dt_iop_satcurve_gui_data_t
float picked_s_min;
float picked_s_max;
- gboolean mask_display; // NEU
+ gboolean mask_display; // NEU
} dt_iop_satcurve_gui_data_t;
typedef struct dt_iop_satcurve_global_data_t
{
int kernel_satcurvergb;
int kernel_satcurve_histogram;
- int kernel_satcurve_mask; // NEU
+ int kernel_satcurve_mask; // NEU
+ int kernel_satcurve_scalar_mask;
+ int kernel_satcurve_mask_from_scalar;
+ int kernel_satcurve_apply_guided_mask;
+ int kernel_fgf_resample;
+ int kernel_fgf_quantize;
+ int kernel_fgf_covar_products;
+ int kernel_fgf_solve_ab;
+ int kernel_fgf_apply_blend;
} dt_iop_satcurve_global_data_t;
typedef struct dt_iop_satcurve_factors_t
@@ -159,10 +182,10 @@ const char *aliases()
const char **description(dt_iop_module_t *self)
{
return dt_iop_set_description(self, _("remap saturation and brilliance with curves"),
- _("corrective or creative"),
- _("linear, RGB, scene-referred"),
- _("linear, RGB, scene-referred"),
- _("linear, RGB, scene-referred"));
+ _("corrective or creative"),
+ _("linear, RGB, scene-referred"),
+ _("linear, RGB, scene-referred"),
+ _("linear, RGB, scene-referred"));
}
int flags()
@@ -176,8 +199,8 @@ int default_group()
}
dt_iop_colorspace_type_t default_colorspace(dt_iop_module_t *self,
- dt_dev_pixelpipe_t *pipe,
- dt_dev_pixelpipe_iop_t *piece)
+ dt_dev_pixelpipe_t *pipe,
+ dt_dev_pixelpipe_iop_t *piece)
{
return IOP_CS_RGB;
}
@@ -186,8 +209,8 @@ static inline void reset_channel_curve(dt_iop_satcurve_channel_params_t *c)
{
c->curve_num_nodes = 2;
c->curve_type = MONOTONE_HERMITE;
- c->curve[0] = (dt_iop_satcurve_node_t){ 0.f, .5f };
- c->curve[1] = (dt_iop_satcurve_node_t){ 1.f, .5f };
+ c->curve[0] = (dt_iop_satcurve_node_t){0.f, .5f};
+ c->curve[1] = (dt_iop_satcurve_node_t){1.f, .5f};
}
static inline void reset_params(dt_iop_satcurve_params_t *p)
@@ -195,6 +218,12 @@ static inline void reset_params(dt_iop_satcurve_params_t *p)
reset_channel_curve(&p->channel[DT_IOP_SATCURVE_CHANNEL_SATURATION]);
reset_channel_curve(&p->channel[DT_IOP_SATCURVE_CHANNEL_BRILLIANCE]);
p->formula = DT_IOP_SATCURVE_DTUCS;
+
+ p->use_guided_filter = FALSE;
+ p->gf_radius = 10.0f;
+ p->gf_feathering = 1.0f;
+ p->gf_iterations = 1;
+ p->gf_quantization = 0.0f;
}
static inline float lookup_lut(const float *lut, const float x)
@@ -220,10 +249,12 @@ static inline float satcurve_lookup_gamut(const float *const gamut_lut, const fl
static inline float satcurve_soft_clip(const float x, const float knee, const float maximum)
{
- if(x <= knee) return x;
+ if (x <= knee)
+ return x;
const float range = maximum - knee;
- if(range <= 0.f) return maximum;
+ if (range <= 0.f)
+ return maximum;
return knee + range * (1.f - expf(-(x - knee) / range));
}
@@ -236,26 +267,28 @@ static inline float clip_jz_chroma(const float Jz, const float Cz, const float c
Iz = MAX(Iz, 0.f);
static const dt_colormatrix_t AI_trans = {
- { 1.f, 1.f, 1.f, 0.f },
- { .1386050432715393f, -.1386050432715393f, -.0960192420263190f, 0.f },
- { .0580473161561189f, -.0580473161561189f, -.8118918960560390f, 0.f }
- };
+ {1.f, 1.f, 1.f, 0.f},
+ {.1386050432715393f, -.1386050432715393f, -.0960192420263190f, 0.f},
+ {.0580473161561189f, -.0580473161561189f, -.8118918960560390f, 0.f}};
- dt_aligned_pixel_t izab = { Iz, Cz * ch, Cz * sh, 0.f }, lms;
+ dt_aligned_pixel_t izab = {Iz, Cz * ch, Cz * sh, 0.f}, lms;
dt_apply_transposed_color_matrix(izab, AI_trans, lms);
float max_c = Cz;
- if(lms[0] < 0.f) max_c = MIN(max_c, -Iz / (AI_trans[1][0] * ch + AI_trans[2][0] * sh));
- if(lms[1] < 0.f) max_c = MIN(max_c, -Iz / (AI_trans[1][1] * ch + AI_trans[2][1] * sh));
- if(lms[2] < 0.f) max_c = MIN(max_c, -Iz / (AI_trans[1][2] * ch + AI_trans[2][2] * sh));
+ if (lms[0] < 0.f)
+ max_c = MIN(max_c, -Iz / (AI_trans[1][0] * ch + AI_trans[2][0] * sh));
+ if (lms[1] < 0.f)
+ max_c = MIN(max_c, -Iz / (AI_trans[1][1] * ch + AI_trans[2][1] * sh));
+ if (lms[2] < 0.f)
+ max_c = MIN(max_c, -Iz / (AI_trans[1][2] * ch + AI_trans[2][2] * sh));
return MAX(max_c, 0.f);
}
static inline float pixel_s_in_norm_jzazbz(const float *const restrict rgb_in,
- const dt_colormatrix_t inputmatrix_trans,
- const float *const restrict gamut_lut,
- float *const restrict h_out)
+ const dt_colormatrix_t inputmatrix_trans,
+ const float *const restrict gamut_lut,
+ float *const restrict h_out)
{
dt_aligned_pixel_t rgb, xyz, jab;
copy_pixel(rgb, rgb_in);
@@ -269,7 +302,8 @@ static inline float pixel_s_in_norm_jzazbz(const float *const restrict rgb_in,
const float gamut = MAX(satcurve_lookup_gamut(gamut_lut, h), FLT_MIN);
const float s_in = Jz > 0.f ? Cz / Jz : 0.f;
- if(h_out) *h_out = h;
+ if (h_out)
+ *h_out = h;
return s_in / gamut;
}
@@ -278,16 +312,13 @@ static inline float pixel_s_in_norm_jzazbz(const float *const restrict rgb_in,
// model used throughout darktable_ucs_22_helpers. Single source of truth for this
// computation -- do not re-derive it inline at call sites.
static inline float satcurve_ucs_gamut_saturation(const float J, const float h,
- const float L_white,
- const float *const restrict gamut_lut)
+ const float L_white,
+ const float *const restrict gamut_lut)
{
const float max_colorfulness = MAX(satcurve_lookup_gamut(gamut_lut, h), FLT_MIN);
- const float max_chroma = 15.932993652962535f
- * powf(J / L_white, 0.6523997524738018f)
- * powf(max_colorfulness, 0.6007557017508491f)
- / L_white;
+ const float max_chroma = 15.932993652962535f * powf(J / L_white, 0.6523997524738018f) * powf(max_colorfulness, 0.6007557017508491f) / L_white;
- const dt_aligned_pixel_t JCH_gamut_boundary = { J, max_chroma, h, 0.f };
+ const dt_aligned_pixel_t JCH_gamut_boundary = {J, max_chroma, h, 0.f};
dt_aligned_pixel_t HSB_gamut_boundary;
dt_UCS_JCH_to_HSB(JCH_gamut_boundary, HSB_gamut_boundary);
@@ -295,10 +326,10 @@ static inline float satcurve_ucs_gamut_saturation(const float J, const float h,
}
static inline float pixel_s_in_norm_ucs(const float *const restrict rgb_in,
- const dt_colormatrix_t inputmatrix_trans,
- const float *const restrict gamut_lut,
- const float L_white,
- float *const restrict h_out)
+ const dt_colormatrix_t inputmatrix_trans,
+ const float *const restrict gamut_lut,
+ const float L_white,
+ float *const restrict h_out)
{
dt_aligned_pixel_t rgb, xyz, xyY, JCH, HCB;
@@ -312,20 +343,21 @@ static inline float pixel_s_in_norm_ucs(const float *const restrict rgb_in,
const float gamut_s = satcurve_ucs_gamut_saturation(JCH[0], JCH[2], L_white, gamut_lut);
- if(h_out) *h_out = JCH[2];
+ if (h_out)
+ *h_out = JCH[2];
const float saturation = HCB[2] > 0.f ? HCB[1] / HCB[2] : 0.f;
return saturation / gamut_s;
}
static inline float pixel_s_in_norm(const dt_iop_satcurve_formula_t formula,
- const float *const restrict rgb_in,
- const dt_colormatrix_t inputmatrix_trans,
- const float *const restrict gamut_lut,
- const float L_white,
- float *const restrict h_out)
+ const float *const restrict rgb_in,
+ const dt_colormatrix_t inputmatrix_trans,
+ const float *const restrict gamut_lut,
+ const float L_white,
+ float *const restrict h_out)
{
- if(formula == DT_IOP_SATCURVE_DTUCS)
+ if (formula == DT_IOP_SATCURVE_DTUCS)
return pixel_s_in_norm_ucs(rgb_in, inputmatrix_trans, gamut_lut, L_white, h_out);
return pixel_s_in_norm_jzazbz(rgb_in, inputmatrix_trans, gamut_lut, h_out);
@@ -346,21 +378,18 @@ static inline dt_iop_satcurve_gui_channel_t *get_active_gui_channel(dt_iop_modul
static inline gboolean channel_is_neutral(const dt_iop_satcurve_channel_data_t *c)
{
- return c->curve_num_nodes == 2
- && fabsf(c->lut[0] - .5f) < 1e-6f
- && fabsf(c->lut[DT_IOP_SATCURVE_RES - 1] - .5f) < 1e-6f;
+ return c->curve_num_nodes == 2 && fabsf(c->lut[0] - .5f) < 1e-6f && fabsf(c->lut[DT_IOP_SATCURVE_RES - 1] - .5f) < 1e-6f;
}
static inline dt_iop_satcurve_factors_t eval_curve_factors(const dt_iop_satcurve_data_t *d,
- const float s_in_norm)
+ const float s_in_norm)
{
const float sat_c = CLAMP(lookup_lut(d->channel[DT_IOP_SATCURVE_CHANNEL_SATURATION].lut, s_in_norm), 0.f, 1.f);
const float bri_c = CLAMP(lookup_lut(d->channel[DT_IOP_SATCURVE_CHANNEL_BRILLIANCE].lut, s_in_norm), 0.f, 1.f);
return (dt_iop_satcurve_factors_t){
- .sat_factor = curve_to_factor(sat_c),
- .bri_factor = curve_to_factor(bri_c)
- };
+ .sat_factor = curve_to_factor(sat_c),
+ .bri_factor = curve_to_factor(bri_c)};
}
// applies log1p compression and publishes bin counts to the GUI histogram;
@@ -369,11 +398,12 @@ static inline dt_iop_satcurve_factors_t eval_curve_factors(const dt_iop_satcurve
static void _commit_sat_histogram(dt_iop_module_t *self, const int *const bins)
{
dt_iop_satcurve_gui_data_t *g = self->gui_data;
- if(!g) return;
+ if (!g)
+ return;
float local_hist[DT_IOP_SATCURVE_HIST_RES];
float max_val = 0.f;
- for(int i = 0; i < DT_IOP_SATCURVE_HIST_RES; i++)
+ for (int i = 0; i < DT_IOP_SATCURVE_HIST_RES; i++)
{
local_hist[i] = log1pf((float)bins[i]);
max_val = MAX(max_val, local_hist[i]);
@@ -388,23 +418,24 @@ static void _commit_sat_histogram(dt_iop_module_t *self, const int *const bins)
}
static void _update_sat_histogram(dt_iop_module_t *self,
- const dt_iop_satcurve_data_t *d,
- const dt_colormatrix_t inputmatrix_trans,
- const float *const restrict in,
- const size_t npixels)
+ const dt_iop_satcurve_data_t *d,
+ const dt_colormatrix_t inputmatrix_trans,
+ const float *const restrict in,
+ const size_t npixels)
{
dt_iop_satcurve_gui_data_t *g = self->gui_data;
- if(!g) return;
+ if (!g)
+ return;
- int local_hist[DT_IOP_SATCURVE_HIST_RES] = { 0 };
+ int local_hist[DT_IOP_SATCURVE_HIST_RES] = {0};
const float L_white = Y_to_dt_UCS_L_star(1.f);
DT_OMP_FOR(reduction(+ : local_hist[:DT_IOP_SATCURVE_HIST_RES]))
- for(size_t k = 0; k < npixels; k++)
+ for (size_t k = 0; k < npixels; k++)
{
const float s_in_norm = pixel_s_in_norm(d->formula, in + 4 * k, inputmatrix_trans, d->gamut_lut, L_white, NULL);
const int bin = CLAMP((int)(s_in_norm * (DT_IOP_SATCURVE_HIST_RES - 1)),
- 0, DT_IOP_SATCURVE_HIST_RES - 1);
+ 0, DT_IOP_SATCURVE_HIST_RES - 1);
local_hist[bin]++;
}
@@ -412,8 +443,8 @@ static void _update_sat_histogram(dt_iop_module_t *self,
}
static inline void apply_sat_and_brilliance_ucs(const dt_iop_satcurve_data_t *d,
- dt_aligned_pixel_t xyz,
- const float L_white)
+ dt_aligned_pixel_t xyz,
+ const float L_white)
{
dt_aligned_pixel_t xyY, JCH, HCB, HSB;
@@ -457,7 +488,7 @@ static inline void apply_sat_and_brilliance_ucs(const dt_iop_satcurve_data_t *d,
}
static inline void apply_sat_and_brilliance_jzazbz(const dt_iop_satcurve_data_t *d,
- dt_aligned_pixel_t xyz)
+ dt_aligned_pixel_t xyz)
{
dt_aligned_pixel_t jab;
dt_XYZ_2_JzAzBz(xyz, jab);
@@ -492,29 +523,29 @@ static inline void apply_sat_and_brilliance_jzazbz(const dt_iop_satcurve_data_t
// NEU: berechnet pro Pixel die normierte Sättigung in einen separaten Buffer,
// analog zu compute_luminance_mask() in toneequal.c
static inline void compute_saturation_mask(const dt_iop_satcurve_data_t *d,
- const dt_colormatrix_t inputmatrix_trans,
- const float L_white,
- const float *const restrict in,
- float *const restrict mask,
- const size_t npixels)
+ const dt_colormatrix_t inputmatrix_trans,
+ const float L_white,
+ const float *const restrict in,
+ float *const restrict mask,
+ const size_t npixels)
{
DT_OMP_FOR()
- for(size_t k = 0; k < npixels; k++)
+ for (size_t k = 0; k < npixels; k++)
{
mask[k] = pixel_s_in_norm(d->formula, in + 4 * k, inputmatrix_trans,
- d->gamut_lut, L_white, NULL);
+ d->gamut_lut, L_white, NULL);
}
}
// NEU: Graustufen-Visualisierung der Sättigungsmaske; sqrt-"Gamma" für bessere
// Sichtbarkeit niedriger Werte, analog display_luminance_mask() in toneequal.c
static inline void display_saturation_mask(const float *const restrict in,
- const float *const restrict mask,
- float *const restrict out,
- const size_t npixels)
+ const float *const restrict mask,
+ float *const restrict out,
+ const size_t npixels)
{
DT_OMP_FOR()
- for(size_t k = 0; k < npixels; k++)
+ for (size_t k = 0; k < npixels; k++)
{
const float intensity = sqrtf(CLAMP(mask[k], 0.f, 1.f));
out[4 * k + 0] = intensity;
@@ -524,6 +555,30 @@ static inline void display_saturation_mask(const float *const restrict in,
}
}
+// Applies the fast scalar guided filter (edge-aware surface blur) in-place on a
+// saturation mask, using the guided-filter parameters cached in d. No-op if
+// use_guided_filter is off or the parameters are degenerate.
+static inline void apply_guided_filter_to_mask(const dt_iop_satcurve_data_t *d,
+ float *const restrict mask,
+ const int width,
+ const int height)
+{
+ if (!d->use_guided_filter || d->gf_radius < 0.5f || d->gf_iterations <= 0)
+ return;
+
+ fast_surface_blur(mask,
+ width,
+ height,
+ (int)d->gf_radius,
+ d->gf_feathering,
+ d->gf_iterations,
+ DT_GF_BLENDING_LINEAR,
+ 1.0f,
+ d->gf_quantization,
+ exp2f(-14.0f),
+ 4.0f);
+}
+
void process(dt_iop_module_t *self, dt_dev_pixelpipe_iop_t *piece,
const void *const ivoid, void *const ovoid,
const dt_iop_roi_t *const roi_in, const dt_iop_roi_t *const roi_out)
@@ -532,14 +587,14 @@ void process(dt_iop_module_t *self, dt_dev_pixelpipe_iop_t *piece,
dt_iop_satcurve_gui_data_t *g = self->gui_data;
const dt_iop_order_iccprofile_info_t *work_profile = dt_ioppr_get_pipe_current_profile_info(self, piece->pipe);
- if(!work_profile || piece->colors != 4)
+ if (!work_profile || piece->colors != 4)
{
dt_iop_image_copy_by_size(ovoid, ivoid, roi_out->width, roi_out->height, piece->colors);
return;
}
- dt_colormatrix_t inputmatrix = {{ 0.0f }};
- dt_colormatrix_t outputmatrix = {{ 0.0f }};
+ dt_colormatrix_t inputmatrix = {{0.0f}};
+ dt_colormatrix_t outputmatrix = {{0.0f}};
dt_colormatrix_t inputmatrix_trans;
dt_colormatrix_t outputmatrix_trans;
@@ -553,20 +608,25 @@ void process(dt_iop_module_t *self, dt_dev_pixelpipe_iop_t *piece,
const size_t npixels = (size_t)roi_out->width * roi_out->height;
const float L_white = Y_to_dt_UCS_L_star(1.f);
- if(self->dev->gui_attached && dt_pipe_is_full(piece->pipe) && dt_iop_has_focus(self)
- && piece->pipe == self->dev->full.pipe)
+ if (self->dev->gui_attached && dt_pipe_is_full(piece->pipe) && dt_iop_has_focus(self) && piece->pipe == self->dev->full.pipe)
_update_sat_histogram(self, d, inputmatrix_trans, in, npixels);
// NEU: Maskenanzeige-Zweig -- zeigt die normierte Sättigung als Graustufenbild
- if(self->dev->gui_attached && dt_pipe_is_full(piece->pipe) && g && g->mask_display)
+ if (self->dev->gui_attached && dt_pipe_is_full(piece->pipe) && g && g->mask_display)
{
float *const restrict mask = dt_alloc_align_float(npixels);
- if(mask)
+ if (mask)
{
compute_saturation_mask(d, inputmatrix_trans, L_white, in, mask, npixels);
+
+ // if the guided filter is enabled, show the filtered mask that is
+ // actually used to modulate the correction, not the raw saturation
+ apply_guided_filter_to_mask(d, mask, roi_out->width, roi_out->height);
+
display_saturation_mask(in, mask, out, npixels);
dt_free_align(mask);
piece->pipe->mask_display = DT_DEV_PIXELPIPE_DISPLAY_PASSTHRU;
+
return;
}
}
@@ -574,21 +634,40 @@ void process(dt_iop_module_t *self, dt_dev_pixelpipe_iop_t *piece,
const gboolean neutral_sat = channel_is_neutral(&d->channel[DT_IOP_SATCURVE_CHANNEL_SATURATION]);
const gboolean neutral_bri = channel_is_neutral(&d->channel[DT_IOP_SATCURVE_CHANNEL_BRILLIANCE]);
- if(neutral_sat && neutral_bri)
+ if (neutral_sat && neutral_bri)
{
dt_iop_image_copy_by_size(ovoid, ivoid, roi_out->width, roi_out->height, piece->colors);
return;
}
+ // Guided-filter strength mask, derived from normalized input saturation and
+ // edge-aware blurred, so that the correction fades smoothly across saturation
+ // edges instead of being applied uniformly or with hard transitions.
+ float *restrict gf_mask = NULL;
+
+ if (d->use_guided_filter && d->gf_radius >= 0.5f && d->gf_iterations > 0)
+ {
+ gf_mask = dt_alloc_align_float(npixels);
+ if (gf_mask)
+ {
+ compute_saturation_mask(d, inputmatrix_trans, L_white, in, gf_mask, npixels);
+ apply_guided_filter_to_mask(d, gf_mask, roi_out->width, roi_out->height);
+ }
+ else
+ {
+ dt_control_log(_("satcurve: guided filter failed to allocate memory, disabling it for this run"));
+ }
+ }
+
DT_OMP_FOR()
- for(size_t k = 0; k < npixels; k++)
+ for (size_t k = 0; k < npixels; k++)
{
dt_aligned_pixel_t rgb, xyz, pixout;
copy_pixel(rgb, in + 4 * k);
dt_vector_clipneg(rgb);
dt_apply_transposed_color_matrix(rgb, inputmatrix_trans, xyz);
- if(d->formula == DT_IOP_SATCURVE_DTUCS)
+ if (d->formula == DT_IOP_SATCURVE_DTUCS)
apply_sat_and_brilliance_ucs(d, xyz, L_white);
else
apply_sat_and_brilliance_jzazbz(d, xyz);
@@ -596,12 +675,251 @@ void process(dt_iop_module_t *self, dt_dev_pixelpipe_iop_t *piece,
dt_apply_transposed_color_matrix(xyz, outputmatrix_trans, pixout);
dt_vector_clipneg(pixout);
pixout[3] = in[4 * k + 3];
+
+ if (gf_mask)
+ {
+ // blend the corrected pixel back towards the untouched input, using the
+ // blurred saturation mask as per-pixel strength (1 = full correction)
+ const float w = CLAMP(gf_mask[k], 0.f, 1.f);
+ for (int c = 0; c < 3; c++)
+ pixout[c] = rgb[c] + w * (pixout[c] - rgb[c]);
+ }
+
copy_pixel_nontemporal(out + 4 * k, pixout);
}
dt_omploop_sfence();
+
+ if (gf_mask)
+ dt_free_align(gf_mask);
}
#if HAVE_OPENCL
+
+// box-blur a single-channel device buffer over a window of 2*radius+1,
+// reusing the generic box-mean kernels from common/guided_filter.c (they are
+// guide-agnostic single-channel passes, not RGB-specific -- see
+// _cl_box_mean() in guided_filter.c, which this mirrors exactly).
+static cl_int _cl_fgf_box_mean(const int devid, const int width, const int height,
+ const int radius, cl_mem in, cl_mem out, cl_mem temp)
+{
+ cl_int err = dt_opencl_enqueue_kernel_1d_args(
+ devid, darktable.opencl->guided_filter->kernel_guided_filter_box_mean_x, height,
+ CLARG(width), CLARG(height), CLARG(in), CLARG(temp), CLARG(radius));
+ if (err != CL_SUCCESS)
+ return err;
+
+ return dt_opencl_enqueue_kernel_1d_args(
+ devid, darktable.opencl->guided_filter->kernel_guided_filter_box_mean_y, width,
+ CLARG(width), CLARG(height), CLARG(temp), CLARG(out), CLARG(radius));
+}
+
+// GPU port of fast_surface_blur() from common/fast_guided_filter.h, operating
+// on a full-resolution single-channel scalar mask. mask_in and mask_out must
+// be distinct buffers (OpenCL images cannot safely be read and written by the
+// same kernel invocation). Returns a non-CL_SUCCESS error on any failure,
+// without touching mask_out, so the caller's own error handling (which in
+// process_cl ultimately triggers darktable's per-module CPU fallback) applies
+// unchanged.
+//
+// CAVEAT: unlike guided_filter.c's guided_filter_cl(), this does not tile the
+// computation to stay within GPU memory limits for very large images -- see
+// _guided_filter_cl_impl()'s tiling logic for the pattern that would need to
+// be replicated here for full parity.
+static cl_int _satcurve_guided_filter_cl(const dt_iop_satcurve_global_data_t *gd,
+ const int devid,
+ cl_mem mask_in, cl_mem mask_out,
+ const int width, const int height,
+ const int radius, const float feathering,
+ const int iterations, const float quantization)
+{
+ const float scaling = 4.0f;
+ const int ds_radius = (radius < 4) ? 1 : (int)(radius / scaling);
+ const int ds_width = (int)((float)width / scaling);
+ const int ds_height = (int)((float)height / scaling);
+
+ if (ds_width < 1 || ds_height < 1)
+ return DT_OPENCL_DEFAULT_ERROR;
+
+ cl_mem ds_image = dt_opencl_alloc_device(devid, ds_width, ds_height, sizeof(float));
+ cl_mem ds_blend_tmp = dt_opencl_alloc_device(devid, ds_width, ds_height, sizeof(float));
+ cl_mem ds_g = dt_opencl_alloc_device(devid, ds_width, ds_height, sizeof(float));
+ cl_mem ds_gg = dt_opencl_alloc_device(devid, ds_width, ds_height, sizeof(float));
+ cl_mem ds_gp = dt_opencl_alloc_device(devid, ds_width, ds_height, sizeof(float));
+ cl_mem mean_g = dt_opencl_alloc_device(devid, ds_width, ds_height, sizeof(float));
+ cl_mem mean_p = dt_opencl_alloc_device(devid, ds_width, ds_height, sizeof(float));
+ cl_mem mean_gg = dt_opencl_alloc_device(devid, ds_width, ds_height, sizeof(float));
+ cl_mem mean_gp = dt_opencl_alloc_device(devid, ds_width, ds_height, sizeof(float));
+ cl_mem a = dt_opencl_alloc_device(devid, ds_width, ds_height, sizeof(float));
+ cl_mem b = dt_opencl_alloc_device(devid, ds_width, ds_height, sizeof(float));
+ cl_mem temp_ds = dt_opencl_alloc_device(devid, ds_width, ds_height, sizeof(float));
+ cl_mem a_full = dt_opencl_alloc_device(devid, width, height, sizeof(float));
+ cl_mem b_full = dt_opencl_alloc_device(devid, width, height, sizeof(float));
+
+ cl_int err = CL_MEM_OBJECT_ALLOCATION_FAILURE;
+ if (!ds_image || !ds_blend_tmp || !ds_g || !ds_gg || !ds_gp || !mean_g || !mean_p || !mean_gg || !mean_gp || !a || !b || !temp_ds || !a_full || !b_full)
+ goto cleanup;
+
+ err = dt_opencl_enqueue_kernel_2d_args(
+ devid, gd->kernel_fgf_resample, ds_width, ds_height,
+ CLARG(mask_in), CLARG(width), CLARG(height),
+ CLARG(ds_image), CLARG(ds_width), CLARG(ds_height));
+ if (err != CL_SUCCESS)
+ goto cleanup;
+
+ for (int i = 0; i < iterations; i++)
+ {
+ const float qmin = exp2f(-14.0f);
+ const float qmax = 4.0f;
+
+ err = dt_opencl_enqueue_kernel_2d_args(
+ devid, gd->kernel_fgf_quantize, ds_width, ds_height,
+ CLARG(ds_image), CLARG(ds_g), CLARG(ds_width), CLARG(ds_height),
+ CLARG(quantization), CLARG(qmin), CLARG(qmax));
+ if (err != CL_SUCCESS)
+ goto cleanup;
+
+ err = dt_opencl_enqueue_kernel_2d_args(
+ devid, gd->kernel_fgf_covar_products, ds_width, ds_height,
+ CLARG(ds_g), CLARG(ds_image), CLARG(ds_gg), CLARG(ds_gp),
+ CLARG(ds_width), CLARG(ds_height));
+ if (err != CL_SUCCESS)
+ goto cleanup;
+
+ if ((err = _cl_fgf_box_mean(devid, ds_width, ds_height, ds_radius, ds_g, mean_g, temp_ds)) != CL_SUCCESS)
+ goto cleanup;
+ if ((err = _cl_fgf_box_mean(devid, ds_width, ds_height, ds_radius, ds_image, mean_p, temp_ds)) != CL_SUCCESS)
+ goto cleanup;
+ if ((err = _cl_fgf_box_mean(devid, ds_width, ds_height, ds_radius, ds_gg, mean_gg, temp_ds)) != CL_SUCCESS)
+ goto cleanup;
+ if ((err = _cl_fgf_box_mean(devid, ds_width, ds_height, ds_radius, ds_gp, mean_gp, temp_ds)) != CL_SUCCESS)
+ goto cleanup;
+
+ err = dt_opencl_enqueue_kernel_2d_args(
+ devid, gd->kernel_fgf_solve_ab, ds_width, ds_height,
+ CLARG(mean_g), CLARG(mean_p), CLARG(mean_gg), CLARG(mean_gp),
+ CLARG(a), CLARG(b), CLARG(ds_width), CLARG(ds_height), CLARG(feathering));
+ if (err != CL_SUCCESS)
+ goto cleanup;
+
+ if ((err = _cl_fgf_box_mean(devid, ds_width, ds_height, ds_radius, a, a, temp_ds)) != CL_SUCCESS)
+ goto cleanup;
+ if ((err = _cl_fgf_box_mean(devid, ds_width, ds_height, ds_radius, b, b, temp_ds)) != CL_SUCCESS)
+ goto cleanup;
+
+ if (i != iterations - 1)
+ {
+ err = dt_opencl_enqueue_kernel_2d_args(
+ devid, gd->kernel_fgf_apply_blend, ds_width, ds_height,
+ CLARG(ds_image), CLARG(ds_blend_tmp), CLARG(a), CLARG(b),
+ CLARG(ds_width), CLARG(ds_height));
+ if (err != CL_SUCCESS)
+ goto cleanup;
+
+ const cl_mem swap = ds_image;
+ ds_image = ds_blend_tmp;
+ ds_blend_tmp = swap;
+ }
+ }
+
+ err = dt_opencl_enqueue_kernel_2d_args(
+ devid, gd->kernel_fgf_resample, width, height,
+ CLARG(a), CLARG(ds_width), CLARG(ds_height),
+ CLARG(a_full), CLARG(width), CLARG(height));
+ if (err != CL_SUCCESS)
+ goto cleanup;
+
+ err = dt_opencl_enqueue_kernel_2d_args(
+ devid, gd->kernel_fgf_resample, width, height,
+ CLARG(b), CLARG(ds_width), CLARG(ds_height),
+ CLARG(b_full), CLARG(width), CLARG(height));
+ if (err != CL_SUCCESS)
+ goto cleanup;
+
+ err = dt_opencl_enqueue_kernel_2d_args(
+ devid, gd->kernel_fgf_apply_blend, width, height,
+ CLARG(mask_in), CLARG(mask_out), CLARG(a_full), CLARG(b_full),
+ CLARG(width), CLARG(height));
+
+cleanup:
+ dt_opencl_release_mem_object(ds_image);
+ dt_opencl_release_mem_object(ds_blend_tmp);
+ dt_opencl_release_mem_object(ds_g);
+ dt_opencl_release_mem_object(ds_gg);
+ dt_opencl_release_mem_object(ds_gp);
+ dt_opencl_release_mem_object(mean_g);
+ dt_opencl_release_mem_object(mean_p);
+ dt_opencl_release_mem_object(mean_gg);
+ dt_opencl_release_mem_object(mean_gp);
+ dt_opencl_release_mem_object(a);
+ dt_opencl_release_mem_object(b);
+ dt_opencl_release_mem_object(temp_ds);
+ dt_opencl_release_mem_object(a_full);
+ dt_opencl_release_mem_object(b_full);
+ return err;
+}
+
+void tiling_callback(dt_iop_module_t *self,
+ dt_dev_pixelpipe_iop_t *piece,
+ const dt_iop_roi_t *roi_in,
+ const dt_iop_roi_t *roi_out,
+ struct dt_develop_tiling_t *tiling)
+{
+ dt_iop_satcurve_data_t *d = piece->data;
+
+ const float ioratio = (float)roi_out->width * roi_out->height / ((float)roi_in->width * roi_in->height);
+
+ const gboolean gf_active = d->use_guided_filter && d->gf_radius >= 0.5f && d->gf_iterations > 0;
+
+ // default pixel-to-pixel case: input + output
+ tiling->factor = 1.0f + ioratio;
+ tiling->factor_cl = tiling->factor;
+ tiling->maxbuf = 1.0f;
+ tiling->maxbuf_cl = 1.0f;
+ tiling->overhead = 0;
+ tiling->overlap = 0;
+ tiling->align = 1;
+
+ if (!gf_active)
+ return;
+
+ // Additional OpenCL allocations in process_cl():
+ // corrected_cl : full-res RGBA -> 1.00
+ // mask_scalar_cl : full-res float -> 0.25
+ // mask_filtered_cl: full-res float -> 0.25
+ //
+ // Additional allocations in _satcurve_guided_filter_cl():
+ // ds_image, ds_blend_tmp, ds_g, ds_gg, ds_gp,
+ // mean_g, mean_p, mean_gg, mean_gp, a, b, temp_ds : ds float, 12x -> 12/16 = 0.75
+ //
+ // a_full, b_full : full-res float -> 0.25 + 0.25
+ //
+ // total guided-filter extras: 1.00 + 0.25 + 0.25 + 0.75 + 0.25 + 0.25 = 2.75
+ tiling->factor_cl += 2.75f;
+
+ // largest single extra allocation is corrected_cl
+ tiling->maxbuf_cl = MAX(tiling->maxbuf_cl, 1.0f);
+
+ // Guided-filter neighbourhood requirement: each iteration re-runs the
+ // box-blur chain (quantize -> covar -> mean -> solve -> mean(a,b) ->
+ // blend) on the *current* intermediate image, so the effective support
+ // of the filter grows roughly linearly with gf_iterations, not just with
+ // gf_radius. A single-iteration overlap of ceil(gf_radius) is only
+ // correct for gf_iterations == 1; for more iterations, information from
+ // outside the tile can propagate inward by an extra ~radius per
+ // iteration. Scale the overlap accordingly, and clamp to something
+ // sane so pathological radius/iteration combos don't request an
+ // absurd amount of overlap.
+ const int base_overlap = (int)ceilf(d->gf_radius);
+ const int scaled_overlap = base_overlap * d->gf_iterations;
+
+ // upsampling from the downsampled (factor 4) working resolution can add
+ // up to ~1 extra full-res pixel of bilinear blur at tile edges per
+ // resample pass; fold in a small fixed margin to stay safe.
+ const int resample_margin = 2;
+
+ tiling->overlap = MAX(tiling->overlap, scaled_overlap + resample_margin);
+}
+
int process_cl(dt_iop_module_t *self, dt_dev_pixelpipe_iop_t *piece,
cl_mem dev_in, cl_mem dev_out,
const dt_iop_roi_t *const roi_in, const dt_iop_roi_t *const roi_out)
@@ -611,16 +929,20 @@ int process_cl(dt_iop_module_t *self, dt_dev_pixelpipe_iop_t *piece,
const dt_iop_satcurve_global_data_t *gd = self->global_data;
cl_int err = DT_OPENCL_DEFAULT_ERROR;
- if(piece->colors != 4) return err;
+ if (piece->colors != 4)
+ return err;
const dt_iop_order_iccprofile_info_t *const work_profile =
dt_ioppr_get_pipe_current_profile_info(self, piece->pipe);
- if(work_profile == NULL) return err;
+ if (work_profile == NULL)
+ return err;
const int devid = piece->pipe->devid;
const int width = roi_in->width;
const int height = roi_in->height;
+ const gboolean gf_active = d->use_guided_filter && d->gf_radius >= 0.5f && d->gf_iterations > 0;
+
const gboolean want_mask =
self->dev->gui_attached && dt_pipe_is_full(piece->pipe) && g && g->mask_display;
@@ -630,16 +952,19 @@ int process_cl(dt_iop_module_t *self, dt_dev_pixelpipe_iop_t *piece,
cl_mem bri_lut_cl = NULL;
cl_mem gamut_lut_cl = NULL;
cl_mem hist_bins_cl = NULL;
+ cl_mem mask_scalar_cl = NULL;
+ cl_mem mask_filtered_cl = NULL;
+ cl_mem corrected_cl = NULL;
- dt_colormatrix_t input_matrix = {{ 0.0f }};
- dt_colormatrix_t output_matrix = {{ 0.0f }};
+ dt_colormatrix_t input_matrix = {{0.0f}};
+ dt_colormatrix_t output_matrix = {{0.0f}};
dt_colormatrix_mul(input_matrix, XYZ_D50_to_D65_CAT16, work_profile->matrix_in);
dt_colormatrix_mul(output_matrix, work_profile->matrix_out, XYZ_D65_to_D50_CAT16);
input_matrix_cl = dt_opencl_copy_host_to_device_constant(devid, 12 * sizeof(float), input_matrix);
gamut_lut_cl = dt_opencl_copy_host_to_device_constant(devid, LUT_ELEM * sizeof(float), d->gamut_lut);
- if(input_matrix_cl == NULL || gamut_lut_cl == NULL)
+ if (input_matrix_cl == NULL || gamut_lut_cl == NULL)
{
err = CL_MEM_OBJECT_ALLOCATION_FAILURE;
goto error;
@@ -648,27 +973,64 @@ int process_cl(dt_iop_module_t *self, dt_dev_pixelpipe_iop_t *piece,
const float L_white = Y_to_dt_UCS_L_star(1.f);
// NEU: Maskenanzeige-Pfad -- eigener, einfacherer Kernel statt satcurvergb
- if(want_mask)
+ if (want_mask)
{
+ if (!gf_active)
+ {
+ err = dt_opencl_enqueue_kernel_2d_args(
+ devid, gd->kernel_satcurve_mask, width, height,
+ CLARG(dev_in), CLARG(dev_out),
+ CLARG(width), CLARG(height),
+ CLARG(input_matrix_cl), CLARG(gamut_lut_cl),
+ CLARG(d->formula), CLARG(L_white));
+
+ if (err == CL_SUCCESS)
+ piece->pipe->mask_display = DT_DEV_PIXELPIPE_DISPLAY_PASSTHRU;
+ goto error;
+ }
+
+ // guided filter enabled: show the actually-filtered mask, not the raw one
+ mask_scalar_cl = dt_opencl_alloc_device(devid, width, height, sizeof(float));
+ mask_filtered_cl = dt_opencl_alloc_device(devid, width, height, sizeof(float));
+ if (mask_scalar_cl == NULL || mask_filtered_cl == NULL)
+ {
+ err = CL_MEM_OBJECT_ALLOCATION_FAILURE;
+ goto error;
+ }
+
err = dt_opencl_enqueue_kernel_2d_args(
- devid, gd->kernel_satcurve_mask, width, height,
- CLARG(dev_in), CLARG(dev_out),
+ devid, gd->kernel_satcurve_scalar_mask, width, height,
+ CLARG(dev_in), CLARG(mask_scalar_cl),
CLARG(width), CLARG(height),
CLARG(input_matrix_cl), CLARG(gamut_lut_cl),
CLARG(d->formula), CLARG(L_white));
+ if (err != CL_SUCCESS)
+ goto error;
+
+ err = _satcurve_guided_filter_cl(gd, devid, mask_scalar_cl, mask_filtered_cl,
+ width, height, (int)d->gf_radius, d->gf_feathering,
+ d->gf_iterations, d->gf_quantization);
+ if (err != CL_SUCCESS)
+ goto error;
+
+ err = dt_opencl_enqueue_kernel_2d_args(
+ devid, gd->kernel_satcurve_mask_from_scalar, width, height,
+ CLARG(mask_filtered_cl), CLARG(dev_in), CLARG(dev_out),
+ CLARG(width), CLARG(height));
- if(err == CL_SUCCESS) piece->pipe->mask_display = DT_DEV_PIXELPIPE_DISPLAY_PASSTHRU;
+ if (err == CL_SUCCESS)
+ piece->pipe->mask_display = DT_DEV_PIXELPIPE_DISPLAY_PASSTHRU;
goto error;
}
const gboolean neutral_sat = channel_is_neutral(&d->channel[DT_IOP_SATCURVE_CHANNEL_SATURATION]);
const gboolean neutral_bri = channel_is_neutral(&d->channel[DT_IOP_SATCURVE_CHANNEL_BRILLIANCE]);
- if(neutral_sat && neutral_bri)
+ if (neutral_sat && neutral_bri)
{
err = dt_opencl_enqueue_copy_image(piece->pipe->devid, dev_in, dev_out,
- (size_t[]){ 0, 0, 0 },
- (size_t[]){ 0, 0, 0 },
- (size_t[]){ roi_in->width, roi_in->height, 1 });
+ (size_t[]){0, 0, 0},
+ (size_t[]){0, 0, 0},
+ (size_t[]){roi_in->width, roi_in->height, 1});
goto error;
}
@@ -680,41 +1042,80 @@ int process_cl(dt_iop_module_t *self, dt_dev_pixelpipe_iop_t *piece,
devid, DT_IOP_SATCURVE_RES * sizeof(float),
d->channel[DT_IOP_SATCURVE_CHANNEL_BRILLIANCE].lut);
- if(output_matrix_cl == NULL || sat_lut_cl == NULL || bri_lut_cl == NULL)
+ if (output_matrix_cl == NULL || sat_lut_cl == NULL || bri_lut_cl == NULL)
{
err = CL_MEM_OBJECT_ALLOCATION_FAILURE;
goto error;
}
const gboolean want_histogram =
- self->dev->gui_attached && dt_pipe_is_full(piece->pipe) && dt_iop_has_focus(self)
- && piece->pipe == self->dev->full.pipe;
+ self->dev->gui_attached && dt_pipe_is_full(piece->pipe) && dt_iop_has_focus(self) && piece->pipe == self->dev->full.pipe;
+
+ cl_mem correction_target = dev_out;
+
+ if (gf_active)
+ {
+ corrected_cl = dt_opencl_alloc_device(devid, width, height, sizeof(float) * 4);
+ mask_scalar_cl = dt_opencl_alloc_device(devid, width, height, sizeof(float));
+ mask_filtered_cl = dt_opencl_alloc_device(devid, width, height, sizeof(float));
+ if (corrected_cl == NULL || mask_scalar_cl == NULL || mask_filtered_cl == NULL)
+ {
+ err = CL_MEM_OBJECT_ALLOCATION_FAILURE;
+ goto error;
+ }
+ correction_target = corrected_cl;
+ }
// pipeline-critical kernel goes first: the histogram below is pure UI
// feedback and must never delay the actual image processing on the queue
err = dt_opencl_enqueue_kernel_2d_args(
devid, gd->kernel_satcurvergb, width, height,
- CLARG(dev_in), CLARG(dev_out),
+ CLARG(dev_in), CLARG(correction_target),
CLARG(width), CLARG(height),
CLARG(input_matrix_cl), CLARG(output_matrix_cl),
CLARG(sat_lut_cl), CLARG(bri_lut_cl), CLARG(gamut_lut_cl),
CLARG(d->formula), CLARG(L_white));
- if(err != CL_SUCCESS) goto error;
+ if (err != CL_SUCCESS)
+ goto error;
+
+ if (gf_active)
+ {
+ err = dt_opencl_enqueue_kernel_2d_args(
+ devid, gd->kernel_satcurve_scalar_mask, width, height,
+ CLARG(dev_in), CLARG(mask_scalar_cl),
+ CLARG(width), CLARG(height),
+ CLARG(input_matrix_cl), CLARG(gamut_lut_cl),
+ CLARG(d->formula), CLARG(L_white));
+ if (err != CL_SUCCESS)
+ goto error;
+
+ err = _satcurve_guided_filter_cl(gd, devid, mask_scalar_cl, mask_filtered_cl,
+ width, height, (int)d->gf_radius, d->gf_feathering,
+ d->gf_iterations, d->gf_quantization);
+ if (err != CL_SUCCESS)
+ goto error;
+
+ err = dt_opencl_enqueue_kernel_2d_args(
+ devid, gd->kernel_satcurve_apply_guided_mask, width, height,
+ CLARG(dev_in), CLARG(corrected_cl), CLARG(mask_filtered_cl), CLARG(dev_out),
+ CLARG(width), CLARG(height));
+ if (err != CL_SUCCESS)
+ goto error;
+ }
// GPU-side histogram reduction: only DT_IOP_SATCURVE_HIST_RES ints cross
// the PCIe bus, instead of the full width*height*4 floats of the old
// copy-to-host approach. Reuses input_matrix_cl/gamut_lut_cl already
// uploaded above -- no second matrix upload needed.
- if(want_histogram)
+ if (want_histogram)
{
- int hist_bins_host[DT_IOP_SATCURVE_HIST_RES] = { 0 };
+ int hist_bins_host[DT_IOP_SATCURVE_HIST_RES] = {0};
const size_t hist_bins_size = sizeof(int) * DT_IOP_SATCURVE_HIST_RES;
hist_bins_cl = dt_opencl_alloc_device_buffer(devid, hist_bins_size);
- if(hist_bins_cl
- && dt_opencl_write_buffer_to_device(devid, hist_bins_host, hist_bins_cl, 0,
- hist_bins_size, TRUE) == CL_SUCCESS)
+ if (hist_bins_cl && dt_opencl_write_buffer_to_device(devid, hist_bins_host, hist_bins_cl, 0,
+ hist_bins_size, TRUE) == CL_SUCCESS)
{
const cl_int hist_err = dt_opencl_enqueue_kernel_2d_args(
devid, gd->kernel_satcurve_histogram, width, height,
@@ -723,9 +1124,8 @@ int process_cl(dt_iop_module_t *self, dt_dev_pixelpipe_iop_t *piece,
CLARG(d->formula), CLARG(L_white),
CLARG(hist_bins_cl));
- if(hist_err == CL_SUCCESS
- && dt_opencl_read_buffer_from_device(devid, hist_bins_host, hist_bins_cl, 0,
- hist_bins_size, TRUE) == CL_SUCCESS)
+ if (hist_err == CL_SUCCESS && dt_opencl_read_buffer_from_device(devid, hist_bins_host, hist_bins_cl, 0,
+ hist_bins_size, TRUE) == CL_SUCCESS)
_commit_sat_histogram(self, hist_bins_host);
}
}
@@ -737,17 +1137,30 @@ int process_cl(dt_iop_module_t *self, dt_dev_pixelpipe_iop_t *piece,
dt_opencl_release_mem_object(bri_lut_cl);
dt_opencl_release_mem_object(gamut_lut_cl);
dt_opencl_release_mem_object(hist_bins_cl);
+ dt_opencl_release_mem_object(mask_scalar_cl);
+ dt_opencl_release_mem_object(mask_filtered_cl);
+ dt_opencl_release_mem_object(corrected_cl);
return err;
}
void init_global(dt_iop_module_so_t *self)
{
- const int program = 42; // satcurve.cl in programs.conf
+ const int program = 42; // satcurve.cl in programs.conf
+ const int program_fgf = 43; // fast_guided_filter_scalar.cl in programs.conf
+
dt_iop_satcurve_global_data_t *gd = malloc(sizeof(dt_iop_satcurve_global_data_t));
self->data = gd;
gd->kernel_satcurvergb = dt_opencl_create_kernel(program, "satcurvergb");
gd->kernel_satcurve_histogram = dt_opencl_create_kernel(program, "satcurve_histogram");
gd->kernel_satcurve_mask = dt_opencl_create_kernel(program, "satcurve_mask"); // NEU
+ gd->kernel_satcurve_scalar_mask = dt_opencl_create_kernel(program, "satcurve_scalar_mask");
+ gd->kernel_satcurve_mask_from_scalar = dt_opencl_create_kernel(program, "satcurve_mask_from_scalar");
+ gd->kernel_satcurve_apply_guided_mask = dt_opencl_create_kernel(program_fgf, "satcurve_apply_guided_mask");
+ gd->kernel_fgf_resample = dt_opencl_create_kernel(program_fgf, "fastguided_resample");
+ gd->kernel_fgf_quantize = dt_opencl_create_kernel(program_fgf, "fastguided_quantize");
+ gd->kernel_fgf_covar_products = dt_opencl_create_kernel(program_fgf, "fastguided_covar_products");
+ gd->kernel_fgf_solve_ab = dt_opencl_create_kernel(program_fgf, "fastguided_solve_ab");
+ gd->kernel_fgf_apply_blend = dt_opencl_create_kernel(program_fgf, "fastguided_apply_blend");
}
void cleanup_global(dt_iop_module_so_t *self)
@@ -756,6 +1169,14 @@ void cleanup_global(dt_iop_module_so_t *self)
dt_opencl_free_kernel(gd->kernel_satcurvergb);
dt_opencl_free_kernel(gd->kernel_satcurve_histogram);
dt_opencl_free_kernel(gd->kernel_satcurve_mask); // NEU
+ dt_opencl_free_kernel(gd->kernel_satcurve_scalar_mask);
+ dt_opencl_free_kernel(gd->kernel_satcurve_mask_from_scalar);
+ dt_opencl_free_kernel(gd->kernel_satcurve_apply_guided_mask);
+ dt_opencl_free_kernel(gd->kernel_fgf_resample);
+ dt_opencl_free_kernel(gd->kernel_fgf_quantize);
+ dt_opencl_free_kernel(gd->kernel_fgf_covar_products);
+ dt_opencl_free_kernel(gd->kernel_fgf_solve_ab);
+ dt_opencl_free_kernel(gd->kernel_fgf_apply_blend);
free(self->data);
self->data = NULL;
}
@@ -765,7 +1186,7 @@ void init_pipe(dt_iop_module_t *self, dt_dev_pixelpipe_t *pipe, dt_dev_pixelpipe
{
dt_iop_satcurve_data_t *d = dt_calloc_align_type(dt_iop_satcurve_data_t, 1);
- for(int ch = 0; ch < DT_IOP_SATCURVE_CHANNELS; ch++)
+ for (int ch = 0; ch < DT_IOP_SATCURVE_CHANNELS; ch++)
{
d->channel[ch].lut = dt_alloc_align_float(DT_IOP_SATCURVE_RES);
d->channel[ch].curve = dt_draw_curve_new(0.f, 1.f, MONOTONE_HERMITE);
@@ -782,11 +1203,13 @@ void init_pipe(dt_iop_module_t *self, dt_dev_pixelpipe_t *pipe, dt_dev_pixelpipe
void cleanup_pipe(dt_iop_module_t *self, dt_dev_pixelpipe_t *pipe, dt_dev_pixelpipe_iop_t *piece)
{
dt_iop_satcurve_data_t *d = piece->data;
- if(!d) return;
+ if (!d)
+ return;
- for(int ch = 0; ch < DT_IOP_SATCURVE_CHANNELS; ch++)
+ for (int ch = 0; ch < DT_IOP_SATCURVE_CHANNELS; ch++)
{
- if(d->channel[ch].curve) dt_draw_curve_destroy(d->channel[ch].curve);
+ if (d->channel[ch].curve)
+ dt_draw_curve_destroy(d->channel[ch].curve);
dt_free_align(d->channel[ch].lut);
}
@@ -796,9 +1219,9 @@ void cleanup_pipe(dt_iop_module_t *self, dt_dev_pixelpipe_t *pipe, dt_dev_pixelp
}
static void build_jzazbz_gamut_lut(dt_iop_satcurve_data_t *d,
- const dt_iop_order_iccprofile_info_t *work_profile)
+ const dt_iop_order_iccprofile_info_t *work_profile)
{
- dt_colormatrix_t inputmatrix = {{ 0.0f }};
+ dt_colormatrix_t inputmatrix = {{0.0f}};
dt_colormatrix_mul(inputmatrix, XYZ_D50_to_D65_CAT16, work_profile->matrix_in);
dt_colormatrix_t inputmatrix_trans;
@@ -807,45 +1230,42 @@ static void build_jzazbz_gamut_lut(dt_iop_satcurve_data_t *d,
float *sampler = dt_calloc_align_float(LUT_ELEM);
DT_OMP_FOR(reduction(max : sampler[:LUT_ELEM]) collapse(3))
- for(int r = 0; r < DT_IOP_SATCURVE_GAMUT_STEPS; r++)
- for(int g = 0; g < DT_IOP_SATCURVE_GAMUT_STEPS; g++)
- for(int b = 0; b < DT_IOP_SATCURVE_GAMUT_STEPS; b++)
+ for (int r = 0; r < DT_IOP_SATCURVE_GAMUT_STEPS; r++)
+ for (int g = 0; g < DT_IOP_SATCURVE_GAMUT_STEPS; g++)
+ for (int b = 0; b < DT_IOP_SATCURVE_GAMUT_STEPS; b++)
{
const dt_aligned_pixel_t rgb = {
- r / (float)(DT_IOP_SATCURVE_GAMUT_STEPS - 1),
- g / (float)(DT_IOP_SATCURVE_GAMUT_STEPS - 1),
- b / (float)(DT_IOP_SATCURVE_GAMUT_STEPS - 1),
- 0.f
- };
+ r / (float)(DT_IOP_SATCURVE_GAMUT_STEPS - 1),
+ g / (float)(DT_IOP_SATCURVE_GAMUT_STEPS - 1),
+ b / (float)(DT_IOP_SATCURVE_GAMUT_STEPS - 1),
+ 0.f};
dt_aligned_pixel_t xyz, jab;
dt_apply_transposed_color_matrix(rgb, inputmatrix_trans, xyz);
dt_XYZ_2_JzAzBz(xyz, jab);
- const float sat = jab[0] > 0.f ? dt_fast_hypotf(jab[1], jab[2]) / jab[0] : 0.f;
+ const float sat = jab[0] > 1e-6f ? dt_fast_hypotf(jab[1], jab[2]) / jab[0] : 0.f;
int index = roundf((LUT_ELEM - 1) * (atan2f(jab[2], jab[1]) + M_PI_F) / DT_2PI_F);
index = index < 0 ? LUT_ELEM - 1 : (index >= LUT_ELEM ? 0 : index);
sampler[index] = MAX(sampler[index], sat);
}
- for(size_t i = 2; i < LUT_ELEM - 2; i++)
+ for (size_t i = 2; i < LUT_ELEM - 2; i++)
d->gamut_lut[i] = (sampler[i - 2] + sampler[i - 1] + sampler[i] + sampler[i + 1] + sampler[i + 2]) / 5.f;
d->gamut_lut[0] = (sampler[LUT_ELEM - 2] + sampler[LUT_ELEM - 1] + sampler[0] + sampler[1] + sampler[2]) / 5.f;
d->gamut_lut[1] = (sampler[LUT_ELEM - 1] + sampler[0] + sampler[1] + sampler[2] + sampler[3]) / 5.f;
- d->gamut_lut[LUT_ELEM - 2] = (sampler[LUT_ELEM - 4] + sampler[LUT_ELEM - 3] + sampler[LUT_ELEM - 2]
- + sampler[LUT_ELEM - 1] + sampler[0]) / 5.f;
- d->gamut_lut[LUT_ELEM - 1] = (sampler[LUT_ELEM - 3] + sampler[LUT_ELEM - 2] + sampler[LUT_ELEM - 1]
- + sampler[0] + sampler[1]) / 5.f;
+ d->gamut_lut[LUT_ELEM - 2] = (sampler[LUT_ELEM - 4] + sampler[LUT_ELEM - 3] + sampler[LUT_ELEM - 2] + sampler[LUT_ELEM - 1] + sampler[0]) / 5.f;
+ d->gamut_lut[LUT_ELEM - 1] = (sampler[LUT_ELEM - 3] + sampler[LUT_ELEM - 2] + sampler[LUT_ELEM - 1] + sampler[0] + sampler[1]) / 5.f;
dt_free_align(sampler);
}
static void build_gamut_lut(dt_iop_satcurve_data_t *d,
- const dt_iop_order_iccprofile_info_t *work_profile)
+ const dt_iop_order_iccprofile_info_t *work_profile)
{
- if(d->formula == DT_IOP_SATCURVE_DTUCS)
+ if (d->formula == DT_IOP_SATCURVE_DTUCS)
{
- dt_colormatrix_t inputmatrix = {{ 0.0f }};
+ dt_colormatrix_t inputmatrix = {{0.0f}};
dt_colormatrix_mul(inputmatrix, XYZ_D50_to_D65_CAT16, work_profile->matrix_in);
dt_UCS_22_build_gamut_LUT(inputmatrix, d->gamut_lut);
}
@@ -856,21 +1276,22 @@ static void build_gamut_lut(dt_iop_satcurve_data_t *d,
}
static void sync_channel_curve(dt_iop_satcurve_channel_data_t *dst,
- const dt_iop_satcurve_channel_params_t *src)
+ const dt_iop_satcurve_channel_params_t *src)
{
- if(dst->curve_type != src->curve_type || dst->curve_num_nodes != src->curve_num_nodes)
+ if (dst->curve_type != src->curve_type || dst->curve_num_nodes != src->curve_num_nodes)
{
- if(dst->curve) dt_draw_curve_destroy(dst->curve);
+ if (dst->curve)
+ dt_draw_curve_destroy(dst->curve);
dst->curve = dt_draw_curve_new(0.f, 1.f, src->curve_type);
dst->curve_type = src->curve_type;
dst->curve_num_nodes = src->curve_num_nodes;
- for(int i = 0; i < src->curve_num_nodes; i++)
+ for (int i = 0; i < src->curve_num_nodes; i++)
dt_draw_curve_add_point(dst->curve, src->curve[i].x, src->curve[i].y);
}
else
{
- for(int i = 0; i < src->curve_num_nodes; i++)
+ for (int i = 0; i < src->curve_num_nodes; i++)
dt_draw_curve_set_point(dst->curve, i, src->curve[i].x, src->curve[i].y);
}
@@ -878,24 +1299,30 @@ static void sync_channel_curve(dt_iop_satcurve_channel_data_t *dst,
}
void commit_params(dt_iop_module_t *self, dt_iop_params_t *p1, dt_dev_pixelpipe_t *pipe,
- dt_dev_pixelpipe_iop_t *piece)
+ dt_dev_pixelpipe_iop_t *piece)
{
dt_iop_satcurve_data_t *d = piece->data;
const dt_iop_satcurve_params_t *p = (const dt_iop_satcurve_params_t *)p1;
- if(d->formula != p->formula)
+ if (d->formula != p->formula)
{
d->formula = p->formula;
d->lut_inited = FALSE;
}
- for(int ch = 0; ch < DT_IOP_SATCURVE_CHANNELS; ch++)
+ for (int ch = 0; ch < DT_IOP_SATCURVE_CHANNELS; ch++)
sync_channel_curve(&d->channel[ch], &p->channel[ch]);
+ d->use_guided_filter = p->use_guided_filter;
+ d->gf_radius = p->gf_radius;
+ d->gf_feathering = p->gf_feathering;
+ d->gf_iterations = p->gf_iterations;
+ d->gf_quantization = p->gf_quantization;
+
const dt_iop_order_iccprofile_info_t *work_profile =
dt_ioppr_get_pipe_current_profile_info(self, piece->pipe);
- if(work_profile && (!d->lut_inited || work_profile != d->work_profile))
+ if (work_profile && (!d->lut_inited || work_profile != d->work_profile))
{
build_gamut_lut(d, work_profile);
d->work_profile = work_profile;
@@ -903,83 +1330,56 @@ void commit_params(dt_iop_module_t *self, dt_iop_params_t *p1, dt_dev_pixelpipe_
}
}
-int legacy_params(dt_iop_module_t *self,
- const void *const old_params,
- const int old_version,
- void **new_params,
- int32_t *new_params_size,
- int *new_version)
-{
- if(old_version == 1)
- {
- const dt_iop_satcurve_params_v1_t *o = old_params;
- dt_iop_satcurve_params_t *n = calloc(1, sizeof(dt_iop_satcurve_params_t));
-
- reset_params(n);
-
- n->formula = o->formula;
- n->channel[DT_IOP_SATCURVE_CHANNEL_SATURATION].curve_num_nodes = o->curve_num_nodes;
- n->channel[DT_IOP_SATCURVE_CHANNEL_SATURATION].curve_type = o->curve_type;
-
- for(int i = 0; i < o->curve_num_nodes; i++)
- n->channel[DT_IOP_SATCURVE_CHANNEL_SATURATION].curve[i] = o->curve[i];
-
- *new_params = n;
- *new_params_size = sizeof(dt_iop_satcurve_params_t);
- *new_version = 2;
- return 0;
- }
- return 1;
-}
-
void init_presets(dt_iop_module_so_t *self)
{
- dt_iop_satcurve_params_t p = { 0 };
+ dt_iop_satcurve_params_t p = {0};
reset_params(&p);
dt_gui_presets_add_generic(_("neutral"), self->op, self->version(),
- &p, sizeof(p), TRUE, DEVELOP_BLEND_CS_RGB_SCENE);
+ &p, sizeof(p), TRUE, DEVELOP_BLEND_CS_RGB_SCENE);
reset_params(&p);
p.channel[DT_IOP_SATCURVE_CHANNEL_SATURATION].curve[0].y = .60f;
p.channel[DT_IOP_SATCURVE_CHANNEL_SATURATION].curve[1].y = .55f;
dt_gui_presets_add_generic(_("gentle saturation"), self->op, self->version(),
- &p, sizeof(p), TRUE, DEVELOP_BLEND_CS_RGB_SCENE);
+ &p, sizeof(p), TRUE, DEVELOP_BLEND_CS_RGB_SCENE);
reset_params(&p);
p.channel[DT_IOP_SATCURVE_CHANNEL_SATURATION].curve_num_nodes = 3;
- p.channel[DT_IOP_SATCURVE_CHANNEL_SATURATION].curve[1] = (dt_iop_satcurve_node_t){ .45f, .62f };
- p.channel[DT_IOP_SATCURVE_CHANNEL_SATURATION].curve[2] = (dt_iop_satcurve_node_t){ 1.f, .42f };
+ p.channel[DT_IOP_SATCURVE_CHANNEL_SATURATION].curve[1] = (dt_iop_satcurve_node_t){.45f, .62f};
+ p.channel[DT_IOP_SATCURVE_CHANNEL_SATURATION].curve[2] = (dt_iop_satcurve_node_t){1.f, .42f};
dt_gui_presets_add_generic(_("protect saturated colors"), self->op, self->version(),
- &p, sizeof(p), TRUE, DEVELOP_BLEND_CS_RGB_SCENE);
+ &p, sizeof(p), TRUE, DEVELOP_BLEND_CS_RGB_SCENE);
reset_params(&p);
p.channel[DT_IOP_SATCURVE_CHANNEL_BRILLIANCE].curve[0].y = .56f;
p.channel[DT_IOP_SATCURVE_CHANNEL_BRILLIANCE].curve[1].y = .54f;
dt_gui_presets_add_generic(_("gentle brilliance"), self->op, self->version(),
- &p, sizeof(p), TRUE, DEVELOP_BLEND_CS_RGB_SCENE);
+ &p, sizeof(p), TRUE, DEVELOP_BLEND_CS_RGB_SCENE);
reset_params(&p);
p.channel[DT_IOP_SATCURVE_CHANNEL_BRILLIANCE].curve_num_nodes = 3;
- p.channel[DT_IOP_SATCURVE_CHANNEL_BRILLIANCE].curve[1] = (dt_iop_satcurve_node_t){ .40f, .58f };
- p.channel[DT_IOP_SATCURVE_CHANNEL_BRILLIANCE].curve[2] = (dt_iop_satcurve_node_t){ 1.f, .48f };
+ p.channel[DT_IOP_SATCURVE_CHANNEL_BRILLIANCE].curve[1] = (dt_iop_satcurve_node_t){.40f, .58f};
+ p.channel[DT_IOP_SATCURVE_CHANNEL_BRILLIANCE].curve[2] = (dt_iop_satcurve_node_t){1.f, .48f};
dt_gui_presets_add_generic(_("bright mids, protect extremes"), self->op, self->version(),
- &p, sizeof(p), TRUE, DEVELOP_BLEND_CS_RGB_SCENE);
+ &p, sizeof(p), TRUE, DEVELOP_BLEND_CS_RGB_SCENE);
}
static inline int add_node_to_channel(dt_iop_satcurve_channel_params_t *c, const float x, const float y)
{
- if(c->curve_num_nodes >= DT_IOP_SATCURVE_MAXNODES) return -1;
+ if (c->curve_num_nodes >= DT_IOP_SATCURVE_MAXNODES)
+ return -1;
int at = 0;
- while(at < c->curve_num_nodes && c->curve[at].x < x) at++;
+ while (at < c->curve_num_nodes && c->curve[at].x < x)
+ at++;
- if((at && x - c->curve[at - 1].x < DT_IOP_SATCURVE_MIN_X_DISTANCE)
- || (at < c->curve_num_nodes && c->curve[at].x - x < DT_IOP_SATCURVE_MIN_X_DISTANCE))
+ if ((at && x - c->curve[at - 1].x < DT_IOP_SATCURVE_MIN_X_DISTANCE) || (at < c->curve_num_nodes && c->curve[at].x - x < DT_IOP_SATCURVE_MIN_X_DISTANCE))
return -1;
- for(int i = c->curve_num_nodes; i > at; i--) c->curve[i] = c->curve[i - 1];
- c->curve[at] = (dt_iop_satcurve_node_t){ x, y };
+ for (int i = c->curve_num_nodes; i > at; i--)
+ c->curve[i] = c->curve[i - 1];
+ c->curve[at] = (dt_iop_satcurve_node_t){x, y};
c->curve_num_nodes++;
return at;
}
@@ -989,8 +1389,7 @@ static void _get_graph_geometry(const GtkAllocation *a, float *x0, float *y0, fl
*x0 = DT_IOP_SATCURVE_INSET;
*y0 = DT_IOP_SATCURVE_INSET;
*w = MAX(1, a->width - 2 * DT_IOP_SATCURVE_INSET);
- *h = MAX(1, a->height - 2 * DT_IOP_SATCURVE_INSET - DT_IOP_SATCURVE_GRADIENT_GAP
- - DT_IOP_SATCURVE_GRADIENT_SIZE);
+ *h = MAX(1, a->height - 2 * DT_IOP_SATCURVE_INSET - DT_IOP_SATCURVE_GRADIENT_GAP - DT_IOP_SATCURVE_GRADIENT_SIZE);
}
static void _draw_sat_histogram(cairo_t *cr, dt_iop_satcurve_gui_data_t *g, const int w, const int h)
@@ -1002,7 +1401,7 @@ static void _draw_sat_histogram(cairo_t *cr, dt_iop_satcurve_gui_data_t *g, cons
cairo_set_source_rgba(cr, .8, .8, .8, .30);
cairo_move_to(cr, 0, h);
- for(int i = 0; i < DT_IOP_SATCURVE_HIST_RES; i++)
+ for (int i = 0; i < DT_IOP_SATCURVE_HIST_RES; i++)
{
const float x = w * i / (float)(DT_IOP_SATCURVE_HIST_RES - 1);
const float y = h * (1.f - g->histogram[i] / hist_max);
@@ -1020,8 +1419,10 @@ static void _draw_sat_histogram(cairo_t *cr, dt_iop_satcurve_gui_data_t *g, cons
static void _draw_sat_picker(cairo_t *cr, dt_iop_module_t *self, const int w, const int h)
{
dt_iop_satcurve_gui_data_t *g = self->gui_data;
- if(!g->picker_valid) return;
- if(self->request_color_pick != DT_REQUEST_COLORPICK_MODULE) return;
+ if (!g->picker_valid)
+ return;
+ if (self->request_color_pick != DT_REQUEST_COLORPICK_MODULE)
+ return;
const float x_mean = CLAMP(g->picked_s, 0.f, 1.f) * w;
const float x_min = CLAMP(g->picked_s_min, 0.f, 1.f) * w;
@@ -1043,19 +1444,19 @@ static void _draw_sat_picker(cairo_t *cr, dt_iop_module_t *self, const int w, co
}
static void _draw_channel_curve(cairo_t *cr,
- const dt_iop_satcurve_channel_params_t *cp,
- const dt_iop_satcurve_gui_channel_t *gc,
- const int selected,
- const gboolean active,
- const int w, const int h,
- const double r, const double g, const double b)
+ const dt_iop_satcurve_channel_params_t *cp,
+ const dt_iop_satcurve_gui_channel_t *gc,
+ const int selected,
+ const gboolean active,
+ const int w, const int h,
+ const double r, const double g, const double b)
{
cairo_save(cr);
cairo_set_source_rgba(cr, r, g, b, active ? .95 : .50);
cairo_set_line_width(cr, DT_PIXEL_APPLY_DPI(active ? 2.0 : 1.2));
cairo_move_to(cr, 0, h * (1.f - gc->draw_ys[0]));
- for(int i = 1; i < DT_IOP_SATCURVE_RES; i++)
+ for (int i = 1; i < DT_IOP_SATCURVE_RES; i++)
{
const float x = w * i / (float)(DT_IOP_SATCURVE_RES - 1);
const float y = h * (1.f - gc->draw_ys[i]);
@@ -1063,10 +1464,10 @@ static void _draw_channel_curve(cairo_t *cr,
}
cairo_stroke(cr);
- if(active)
+ if (active)
{
cairo_set_line_width(cr, DT_PIXEL_APPLY_DPI(1.0));
- for(int i = 0; i < cp->curve_num_nodes; i++)
+ for (int i = 0; i < cp->curve_num_nodes; i++)
{
const float px = w * cp->curve[i].x;
const float py = h * (1.f - cp->curve[i].y);
@@ -1079,21 +1480,22 @@ static void _draw_channel_curve(cairo_t *cr,
}
static void _sync_gui_curve(dt_iop_satcurve_gui_channel_t *gc,
- const dt_iop_satcurve_channel_params_t *cp)
+ const dt_iop_satcurve_channel_params_t *cp)
{
- if(gc->curve_type != cp->curve_type || gc->curve_num_nodes != cp->curve_num_nodes)
+ if (gc->curve_type != cp->curve_type || gc->curve_num_nodes != cp->curve_num_nodes)
{
- if(gc->curve) dt_draw_curve_destroy(gc->curve);
+ if (gc->curve)
+ dt_draw_curve_destroy(gc->curve);
gc->curve = dt_draw_curve_new(0.f, 1.f, cp->curve_type);
gc->curve_type = cp->curve_type;
gc->curve_num_nodes = cp->curve_num_nodes;
- for(int i = 0; i < cp->curve_num_nodes; i++)
+ for (int i = 0; i < cp->curve_num_nodes; i++)
dt_draw_curve_add_point(gc->curve, cp->curve[i].x, cp->curve[i].y);
}
else
{
- for(int i = 0; i < cp->curve_num_nodes; i++)
+ for (int i = 0; i < cp->curve_num_nodes; i++)
dt_draw_curve_set_point(gc->curve, i, cp->curve[i].x, cp->curve[i].y);
}
@@ -1117,7 +1519,7 @@ static gboolean area_draw(GtkWidget *widget, cairo_t *cr, dt_iop_module_t *self)
cairo_save(cr);
cairo_translate(cr, gx0, gy0);
- for(int ch = 0; ch < DT_IOP_SATCURVE_CHANNELS; ch++)
+ for (int ch = 0; ch < DT_IOP_SATCURVE_CHANNELS; ch++)
_sync_gui_curve(&g->channel[ch], &p->channel[ch]);
_draw_sat_histogram(cr, g, (int)gw, (int)gh);
@@ -1125,13 +1527,13 @@ static gboolean area_draw(GtkWidget *widget, cairo_t *cr, dt_iop_module_t *self)
cairo_set_source_rgba(cr, 1, 1, 1, .08);
cairo_set_line_width(cr, 1.0);
- for(int i = 0; i <= 4; i++)
+ for (int i = 0; i <= 4; i++)
{
const float y = gh * i / 4.f;
cairo_move_to(cr, 0, y);
cairo_line_to(cr, gw, y);
}
- for(int i = 0; i <= 4; i++)
+ for (int i = 0; i <= 4; i++)
{
const float x = gw * i / 4.f;
cairo_move_to(cr, x, 0);
@@ -1140,20 +1542,20 @@ static gboolean area_draw(GtkWidget *widget, cairo_t *cr, dt_iop_module_t *self)
cairo_stroke(cr);
_draw_channel_curve(cr,
- &p->channel[DT_IOP_SATCURVE_CHANNEL_SATURATION],
- &g->channel[DT_IOP_SATCURVE_CHANNEL_SATURATION],
- g->active_channel == DT_IOP_SATCURVE_CHANNEL_SATURATION ? g->selected : -1,
- g->active_channel == DT_IOP_SATCURVE_CHANNEL_SATURATION,
- (int)gw, (int)gh,
- .90, .90, .90);
+ &p->channel[DT_IOP_SATCURVE_CHANNEL_SATURATION],
+ &g->channel[DT_IOP_SATCURVE_CHANNEL_SATURATION],
+ g->active_channel == DT_IOP_SATCURVE_CHANNEL_SATURATION ? g->selected : -1,
+ g->active_channel == DT_IOP_SATCURVE_CHANNEL_SATURATION,
+ (int)gw, (int)gh,
+ .90, .90, .90);
_draw_channel_curve(cr,
- &p->channel[DT_IOP_SATCURVE_CHANNEL_BRILLIANCE],
- &g->channel[DT_IOP_SATCURVE_CHANNEL_BRILLIANCE],
- g->active_channel == DT_IOP_SATCURVE_CHANNEL_BRILLIANCE ? g->selected : -1,
- g->active_channel == DT_IOP_SATCURVE_CHANNEL_BRILLIANCE,
- (int)gw, (int)gh,
- 1.00, .65, .20);
+ &p->channel[DT_IOP_SATCURVE_CHANNEL_BRILLIANCE],
+ &g->channel[DT_IOP_SATCURVE_CHANNEL_BRILLIANCE],
+ g->active_channel == DT_IOP_SATCURVE_CHANNEL_BRILLIANCE ? g->selected : -1,
+ g->active_channel == DT_IOP_SATCURVE_CHANNEL_BRILLIANCE,
+ (int)gw, (int)gh,
+ 1.00, .65, .20);
cairo_restore(cr);
@@ -1183,18 +1585,18 @@ static gboolean area_button(GtkWidget *widget, GdkEventButton *event, dt_iop_mod
const float y = CLAMP(1.f - (event->y - gy0) / h, 0.f, 1.f);
int hit = -1;
- for(int i = 0; i < cp->curve_num_nodes; i++)
+ for (int i = 0; i < cp->curve_num_nodes; i++)
{
const float dx = x - cp->curve[i].x;
const float dy = y - cp->curve[i].y;
- if(dx * dx + dy * dy < .0025f)
+ if (dx * dx + dy * dy < .0025f)
{
hit = i;
break;
}
}
- if(event->type == GDK_2BUTTON_PRESS && event->button == GDK_BUTTON_PRIMARY)
+ if (event->type == GDK_2BUTTON_PRESS && event->button == GDK_BUTTON_PRIMARY)
{
reset_channel_curve(cp);
g->selected = -1;
@@ -1203,15 +1605,16 @@ static gboolean area_button(GtkWidget *widget, GdkEventButton *event, dt_iop_mod
return TRUE;
}
- if(event->button == GDK_BUTTON_SECONDARY && hit >= 0)
+ if (event->button == GDK_BUTTON_SECONDARY && hit >= 0)
{
- if((event->state & GDK_CONTROL_MASK) || cp->curve_num_nodes <= 2)
+ if ((event->state & GDK_CONTROL_MASK) || cp->curve_num_nodes <= 2)
{
cp->curve[hit].y = .5f;
}
else
{
- for(int i = hit; i < cp->curve_num_nodes - 1; i++) cp->curve[i] = cp->curve[i + 1];
+ for (int i = hit; i < cp->curve_num_nodes - 1; i++)
+ cp->curve[i] = cp->curve[i + 1];
cp->curve_num_nodes--;
}
g->selected = -1;
@@ -1220,9 +1623,9 @@ static gboolean area_button(GtkWidget *widget, GdkEventButton *event, dt_iop_mod
return TRUE;
}
- if(event->button == GDK_BUTTON_PRIMARY)
+ if (event->button == GDK_BUTTON_PRIMARY)
{
- if(hit < 0 && cp->curve_num_nodes < DT_IOP_SATCURVE_MAXNODES)
+ if (hit < 0 && cp->curve_num_nodes < DT_IOP_SATCURVE_MAXNODES)
hit = add_node_to_channel(cp, x, CLAMP(dt_draw_curve_calc_value(gc->curve, x), 0.f, 1.f));
g->selected = hit;
@@ -1234,15 +1637,16 @@ static gboolean area_button(GtkWidget *widget, GdkEventButton *event, dt_iop_mod
}
static gboolean area_scroll(GtkWidget *widget, GdkEventScroll *event,
- dt_iop_module_t *self)
+ dt_iop_module_t *self)
{
dt_iop_satcurve_gui_data_t *g = self->gui_data;
dt_iop_satcurve_channel_params_t *cp = get_active_channel_params(self);
- if(g->selected < 0) return FALSE; // nur wenn ein Knoten selektiert ist
+ if (g->selected < 0)
+ return FALSE; // nur wenn ein Knoten selektiert ist
int delta_y = 0;
- if(dt_gui_get_scroll_unit_delta(event, &delta_y))
+ if (dt_gui_get_scroll_unit_delta(event, &delta_y))
{
const float step = 0.02f * (event->state & GDK_CONTROL_MASK ? 5.0f : 1.0f);
const int n = g->selected;
@@ -1260,7 +1664,8 @@ static gboolean area_motion(GtkWidget *widget, GdkEventMotion *event, dt_iop_mod
dt_iop_satcurve_gui_data_t *g = self->gui_data;
dt_iop_satcurve_channel_params_t *cp = get_active_channel_params(self);
- if(!g->dragging || g->selected < 0) return FALSE;
+ if (!g->dragging || g->selected < 0)
+ return FALSE;
GtkAllocation a;
gtk_widget_get_allocation(widget, &a);
@@ -1271,14 +1676,14 @@ static gboolean area_motion(GtkWidget *widget, GdkEventMotion *event, dt_iop_mod
const float x = CLAMP((event->x - gx0) / w, 0.f, 1.f);
const int n = g->selected;
- if(n == 0)
+ if (n == 0)
cp->curve[n].x = 0.f;
- else if(n == cp->curve_num_nodes - 1)
+ else if (n == cp->curve_num_nodes - 1)
cp->curve[n].x = 1.f;
else
cp->curve[n].x = CLAMP(x,
- cp->curve[n - 1].x + DT_IOP_SATCURVE_MIN_X_DISTANCE,
- cp->curve[n + 1].x - DT_IOP_SATCURVE_MIN_X_DISTANCE);
+ cp->curve[n - 1].x + DT_IOP_SATCURVE_MIN_X_DISTANCE,
+ cp->curve[n + 1].x - DT_IOP_SATCURVE_MIN_X_DISTANCE);
cp->curve[n].y = CLAMP(1.f - (event->y - gy0) / h, 0.f, 1.f);
gtk_widget_queue_draw(widget);
@@ -1288,7 +1693,7 @@ static gboolean area_motion(GtkWidget *widget, GdkEventMotion *event, dt_iop_mod
static gboolean area_release(GtkWidget *widget, GdkEventButton *event, dt_iop_module_t *self)
{
dt_iop_satcurve_gui_data_t *g = self->gui_data;
- if(g->dragging)
+ if (g->dragging)
{
g->dragging = FALSE;
dt_dev_add_history_item(darktable.develop, self, TRUE);
@@ -1301,21 +1706,23 @@ static const float *_get_gamut_lut_for_picker(dt_iop_module_t *self)
static float unity_gamut_lut[LUT_ELEM];
static gboolean unity_inited = FALSE;
- if(!unity_inited)
+ if (!unity_inited)
{
- for(int i = 0; i < LUT_ELEM; i++) unity_gamut_lut[i] = 1.f;
+ for (int i = 0; i < LUT_ELEM; i++)
+ unity_gamut_lut[i] = 1.f;
unity_inited = TRUE;
}
- for(GList *nodes = self->dev->full.pipe ? self->dev->full.pipe->nodes : NULL;
- nodes;
- nodes = g_list_next(nodes))
+ for (GList *nodes = self->dev->full.pipe ? self->dev->full.pipe->nodes : NULL;
+ nodes;
+ nodes = g_list_next(nodes))
{
dt_dev_pixelpipe_iop_t *piece = nodes->data;
- if(piece->module == self && piece->data)
+ if (piece->module == self && piece->data)
{
const dt_iop_satcurve_data_t *d = piece->data;
- if(d->lut_inited && d->gamut_lut) return d->gamut_lut;
+ if (d->lut_inited && d->gamut_lut)
+ return d->gamut_lut;
}
}
@@ -1325,13 +1732,15 @@ static const float *_get_gamut_lut_for_picker(dt_iop_module_t *self)
void color_picker_apply(dt_iop_module_t *self, GtkWidget *widget, dt_dev_pixelpipe_t *pipe)
{
dt_iop_satcurve_gui_data_t *g = self->gui_data;
- if(!g) return;
+ if (!g)
+ return;
const dt_iop_order_iccprofile_info_t *work_profile =
dt_ioppr_get_iop_work_profile_info(self, self->dev->iop);
- if(!work_profile) return;
+ if (!work_profile)
+ return;
- dt_colormatrix_t inputmatrix = {{ 0.0f }};
+ dt_colormatrix_t inputmatrix = {{0.0f}};
dt_colormatrix_t inputmatrix_trans;
dt_colormatrix_mul(inputmatrix, XYZ_D50_to_D65_CAT16, work_profile->matrix_in);
dt_colormatrix_transpose(inputmatrix_trans, inputmatrix);
@@ -1339,9 +1748,9 @@ void color_picker_apply(dt_iop_module_t *self, GtkWidget *widget, dt_dev_pixelpi
const float *const gamut_lut = _get_gamut_lut_for_picker(self);
const float L_white = Y_to_dt_UCS_L_star(1.f);
- const dt_aligned_pixel_t mean_rgb = { self->picked_color[0], self->picked_color[1], self->picked_color[2], 0.f };
- const dt_aligned_pixel_t min_rgb = { self->picked_color_min[0], self->picked_color_min[1], self->picked_color_min[2], 0.f };
- const dt_aligned_pixel_t max_rgb = { self->picked_color_max[0], self->picked_color_max[1], self->picked_color_max[2], 0.f };
+ const dt_aligned_pixel_t mean_rgb = {self->picked_color[0], self->picked_color[1], self->picked_color[2], 0.f};
+ const dt_aligned_pixel_t min_rgb = {self->picked_color_min[0], self->picked_color_min[1], self->picked_color_min[2], 0.f};
+ const dt_aligned_pixel_t max_rgb = {self->picked_color_max[0], self->picked_color_max[1], self->picked_color_max[2], 0.f};
const dt_iop_satcurve_formula_t formula = ((const dt_iop_satcurve_params_t *)self->params)->formula;
@@ -1357,7 +1766,8 @@ void color_picker_apply(dt_iop_module_t *self, GtkWidget *widget, dt_dev_pixelpi
// show_luminance_mask_callback() in toneequal.c
static void show_saturation_mask_callback(GtkToggleButton *button, dt_iop_module_t *self)
{
- if(darktable.gui->reset) return;
+ if (darktable.gui->reset)
+ return;
dt_iop_satcurve_gui_data_t *g = self->gui_data;
dt_iop_request_focus(self);
@@ -1368,33 +1778,34 @@ static void show_saturation_mask_callback(GtkToggleButton *button, dt_iop_module
void gui_focus(dt_iop_module_t *self, gboolean in)
{
dt_iop_satcurve_gui_data_t *g = self->gui_data;
- if(!in)
+ if (!in)
{
dt_iop_color_picker_reset(self, FALSE);
// NEU: Maskenanzeige beim Verlassen des Moduls zurücksetzen
- if(g && g->mask_display)
+ if (g && g->mask_display)
{
g->mask_display = FALSE;
- if(g->show_saturation_mask)
+ if (g->show_saturation_mask)
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->show_saturation_mask), FALSE);
}
}
}
static void _channel_tabs_switch_callback(GtkNotebook *notebook,
- GtkWidget *page,
- guint page_num,
- dt_iop_module_t *self)
+ GtkWidget *page,
+ guint page_num,
+ dt_iop_module_t *self)
{
DT_GUARD_GUI_UPDATE();
dt_iop_satcurve_gui_data_t *g = self->gui_data;
- if(!g) return;
+ if (!g)
+ return;
g->active_channel = (page_num == 1)
- ? DT_IOP_SATCURVE_CHANNEL_BRILLIANCE
- : DT_IOP_SATCURVE_CHANNEL_SATURATION;
+ ? DT_IOP_SATCURVE_CHANNEL_BRILLIANCE
+ : DT_IOP_SATCURVE_CHANNEL_SATURATION;
g->selected = -1;
gtk_widget_queue_draw(GTK_WIDGET(g->area));
@@ -1403,9 +1814,10 @@ static void _channel_tabs_switch_callback(GtkNotebook *notebook,
void gui_changed(dt_iop_module_t *self, GtkWidget *widget, void *previous)
{
dt_iop_satcurve_gui_data_t *g = self->gui_data;
- if(!g) return;
+ if (!g)
+ return;
- if(widget == g->formula)
+ if (widget == g->formula)
dt_dev_add_history_item(darktable.develop, self, TRUE);
}
@@ -1413,24 +1825,29 @@ void gui_update(dt_iop_module_t *self)
{
dt_iop_satcurve_gui_data_t *g = self->gui_data;
const dt_iop_satcurve_params_t *p = self->params;
- if(!g) return;
+ if (!g)
+ return;
- if(g->formula) dt_bauhaus_combobox_set(g->formula, p->formula);
+ if (g->formula)
+ dt_bauhaus_combobox_set(g->formula, p->formula);
- if(g->notebook)
+ if (g->notebook)
gtk_notebook_set_current_page(GTK_NOTEBOOK(g->notebook),
- g->active_channel == DT_IOP_SATCURVE_CHANNEL_BRILLIANCE ? 1 : 0);
+ g->active_channel == DT_IOP_SATCURVE_CHANNEL_BRILLIANCE ? 1 : 0);
- if(g->area) gtk_widget_queue_draw(GTK_WIDGET(g->area));
+ if (g->area)
+ gtk_widget_queue_draw(GTK_WIDGET(g->area));
}
void gui_cleanup(dt_iop_module_t *self)
{
dt_iop_satcurve_gui_data_t *g = self->gui_data;
- if(!g) return;
+ if (!g)
+ return;
- for(int ch = 0; ch < DT_IOP_SATCURVE_CHANNELS; ch++)
- if(g->channel[ch].curve) dt_draw_curve_destroy(g->channel[ch].curve);
+ for (int ch = 0; ch < DT_IOP_SATCURVE_CHANNELS; ch++)
+ if (g->channel[ch].curve)
+ dt_draw_curve_destroy(g->channel[ch].curve);
dt_pthread_mutex_destroy(&g->histogram_lock);
}
@@ -1445,16 +1862,16 @@ void gui_init(dt_iop_module_t *self)
g->active_channel = DT_IOP_SATCURVE_CHANNEL_SATURATION;
g->mask_display = FALSE; // NEU
- for(int ch = 0; ch < DT_IOP_SATCURVE_CHANNELS; ch++)
+ for (int ch = 0; ch < DT_IOP_SATCURVE_CHANNELS; ch++)
{
g->channel[ch].curve_type = p->channel[ch].curve_type;
g->channel[ch].curve_num_nodes = p->channel[ch].curve_num_nodes;
g->channel[ch].curve = dt_draw_curve_new(0.f, 1.f, p->channel[ch].curve_type);
- for(int i = 0; i < p->channel[ch].curve_num_nodes; i++)
+ for (int i = 0; i < p->channel[ch].curve_num_nodes; i++)
dt_draw_curve_add_point(g->channel[ch].curve,
- p->channel[ch].curve[i].x,
- p->channel[ch].curve[i].y);
+ p->channel[ch].curve[i].x,
+ p->channel[ch].curve[i].y);
}
memset(g->histogram, 0, sizeof(g->histogram));
@@ -1487,15 +1904,14 @@ void gui_init(dt_iop_module_t *self)
gtk_notebook_set_current_page(g->notebook, 0);
g_signal_connect(G_OBJECT(g->notebook), "switch-page",
- G_CALLBACK(_channel_tabs_switch_callback), self);
+ G_CALLBACK(_channel_tabs_switch_callback), self);
dt_gui_box_add(self->widget, GTK_WIDGET(g->notebook));
g->area = GTK_DRAWING_AREA(dt_ui_resize_wrap(NULL, 0, "plugins/darkroom/satcurve/graph_height"));
gtk_widget_set_size_request(GTK_WIDGET(g->area), -1, DT_PIXEL_APPLY_DPI(180));
gtk_widget_add_events(GTK_WIDGET(g->area),
- GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK
- | GDK_POINTER_MOTION_MASK | GDK_SCROLL_MASK);
+ GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | GDK_POINTER_MOTION_MASK | GDK_SCROLL_MASK);
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), self);
g_signal_connect(G_OBJECT(g->area), "button-release-event", G_CALLBACK(area_release), self);
@@ -1507,7 +1923,7 @@ void gui_init(dt_iop_module_t *self)
dt_bauhaus_combobox_add(g->formula, _("JzAzBz"));
dt_bauhaus_combobox_add(g->formula, _("darktable UCS"));
gtk_widget_set_tooltip_text(g->formula,
- _("choose the perceptual saturation definition used by both curves"));
+ _("choose the perceptual saturation definition used by both curves"));
g_object_ref(g->formula);
gtk_container_remove(GTK_CONTAINER(self->widget), g->formula);
@@ -1522,7 +1938,7 @@ void gui_init(dt_iop_module_t *self)
g->show_saturation_mask = dtgtk_togglebutton_new(dtgtk_cairo_paint_showmask, 0, NULL);
gtk_widget_set_tooltip_text(g->show_saturation_mask, _("display saturation mask"));
g_signal_connect(G_OBJECT(g->show_saturation_mask), "toggled",
- G_CALLBACK(show_saturation_mask_callback), self);
+ G_CALLBACK(show_saturation_mask_callback), self);
gtk_box_pack_start(GTK_BOX(controls_hbox), g->colorpicker, FALSE, FALSE, 0);
gtk_box_pack_start(GTK_BOX(controls_hbox), g->show_saturation_mask, FALSE, FALSE, 0); // NEU
@@ -1530,6 +1946,42 @@ void gui_init(dt_iop_module_t *self)
g_object_unref(g->formula);
dt_gui_box_add(self->widget, controls_hbox);
+
+ GtkWidget *gf_box = dt_gui_vbox();
+ dt_gui_new_collapsible_section(&g->gf_section,
+ "plugins/darkroom/satcurve/expand_guided_filter",
+ _("guided filter"),
+ GTK_BOX(gf_box), DT_ACTION(self));
+
+ dt_iop_module_t *gf_section =
+ DT_IOP_SECTION_FOR_PARAMS(self, NULL, g->gf_section.container);
+
+ g->use_guided_filter = dt_bauhaus_toggle_from_params(gf_section, "use_guided_filter");
+ gtk_widget_set_tooltip_text(g->use_guided_filter,
+ _("spatially modulate the strength of the curves with an edge-aware "
+ "guided filter, based on local saturation, to reduce halos"));
+
+ g->gf_radius = dt_bauhaus_slider_from_params(gf_section, "gf_radius");
+ dt_bauhaus_slider_set_format(g->gf_radius, _("px"));
+ gtk_widget_set_tooltip_text(g->gf_radius, _("size of the neighbourhood used to guide the filter"));
+
+ g->gf_feathering = dt_bauhaus_slider_from_params(gf_section, "gf_feathering");
+ gtk_widget_set_tooltip_text(g->gf_feathering,
+ _("precision of the feathering: higher values produce softer, "
+ "less edge-sensitive transitions"));
+
+ g->gf_iterations = dt_bauhaus_slider_from_params(gf_section, "gf_iterations");
+ gtk_widget_set_tooltip_text(g->gf_iterations,
+ _("number of times the guided filter is applied recursively; "
+ "increases smoothing but costs more time"));
+
+ g->gf_quantization = dt_bauhaus_slider_from_params(gf_section, "gf_quantization");
+ dt_bauhaus_slider_set_format(g->gf_quantization, _("EV"));
+ gtk_widget_set_tooltip_text(g->gf_quantization,
+ _("posterize the guiding mask in log2 space to produce smoother, "
+ "more contoured transitions; 0 disables quantization"));
+
+ dt_gui_box_add(self->widget, gf_box);
}
void init(dt_iop_module_t *self)
From 15a9476e157ac546029f81d22f4d51c414ad6ad3 Mon Sep 17 00:00:00 2001
From: Martin Straeten <39386816+MStraeten@users.noreply.github.com>
Date: Fri, 24 Jul 2026 23:02:19 +0200
Subject: [PATCH 08/12] adapted to changed develop.h
---
src/iop/satcurvergb.c | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/iop/satcurvergb.c b/src/iop/satcurvergb.c
index 67684e4bfdf2..6396643618eb 100755
--- a/src/iop/satcurvergb.c
+++ b/src/iop/satcurvergb.c
@@ -1772,7 +1772,7 @@ static void show_saturation_mask_callback(GtkToggleButton *button, dt_iop_module
dt_iop_request_focus(self);
g->mask_display = gtk_toggle_button_get_active(button);
- dt_dev_reprocess_center(self->dev);
+ dt_dev_reprocess_center(self->dev, self->iop_order);
}
void gui_focus(dt_iop_module_t *self, gboolean in)
From 18a158860ce1fade99d7fef4db4a30c539c54c18 Mon Sep 17 00:00:00 2001
From: Martin Straeten <39386816+MStraeten@users.noreply.github.com>
Date: Sat, 8 Aug 2026 00:13:07 +0200
Subject: [PATCH 09/12] update copyright note
---
data/kernels/fast_guided_filter.cl | 2 +-
data/kernels/satcurve.cl | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/data/kernels/fast_guided_filter.cl b/data/kernels/fast_guided_filter.cl
index aa4ef383b655..3e2bb42b36cd 100755
--- a/data/kernels/fast_guided_filter.cl
+++ b/data/kernels/fast_guided_filter.cl
@@ -1,6 +1,6 @@
/*
This file is part of darktable,
- Copyright (C) 2019-2026 darktable developers.
+ Copyright (C) 2026 darktable developers.
darktable is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
diff --git a/data/kernels/satcurve.cl b/data/kernels/satcurve.cl
index b778b43f44ad..0b321e986f79 100755
--- a/data/kernels/satcurve.cl
+++ b/data/kernels/satcurve.cl
@@ -1,6 +1,6 @@
/*
This file is part of darktable,
- Copyright (C) 2009-2026 darktable developers.
+ Copyright (C) 2026 darktable developers.
darktable is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
From 0796c70ecf6128cd49dac63f6684b16c3e058720 Mon Sep 17 00:00:00 2001
From: Martin Straeten <39386816+MStraeten@users.noreply.github.com>
Date: Sat, 8 Aug 2026 00:31:54 +0200
Subject: [PATCH 10/12] add dt_dev_add_history_item to area_motion
---
src/iop/satcurvergb.c | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/iop/satcurvergb.c b/src/iop/satcurvergb.c
index 6396643618eb..8504c91dd458 100755
--- a/src/iop/satcurvergb.c
+++ b/src/iop/satcurvergb.c
@@ -1686,6 +1686,7 @@ static gboolean area_motion(GtkWidget *widget, GdkEventMotion *event, dt_iop_mod
cp->curve[n + 1].x - DT_IOP_SATCURVE_MIN_X_DISTANCE);
cp->curve[n].y = CLAMP(1.f - (event->y - gy0) / h, 0.f, 1.f);
+ dt_dev_add_history_item(darktable.develop, self, TRUE);
gtk_widget_queue_draw(widget);
return TRUE;
}
From 41af41a074726e7389c04d7df181d22003646cb0 Mon Sep 17 00:00:00 2001
From: Martin Straeten <39386816+MStraeten@users.noreply.github.com>
Date: Sat, 8 Aug 2026 00:50:58 +0200
Subject: [PATCH 11/12] add satcurve to all + scene referred module group
---
src/libs/modulegroups.c | 2 ++
1 file changed, 2 insertions(+)
diff --git a/src/libs/modulegroups.c b/src/libs/modulegroups.c
index bba427dd2a6e..049e2246faa2 100644
--- a/src/libs/modulegroups.c
+++ b/src/libs/modulegroups.c
@@ -1604,6 +1604,7 @@ void init_presets(dt_lib_module_t *self)
AM("monochrome");
AM("profile");
AM("primaries");
+ AM("satcurvergb");
AM("gamma");
AM("velvia");
@@ -1766,6 +1767,7 @@ void init_presets(dt_lib_module_t *self)
AM("colorequal");
AM("colorharmonizer");
AM("primaries");
+ AM("satcurvergb");
SMG(C_("modulegroup", "correct"), "correct");
AM("cacorrect");
From 3883ec366cd1e4f8781f001ef866901484501e92 Mon Sep 17 00:00:00 2001
From: Martin Straeten <39386816+MStraeten@users.noreply.github.com>
Date: Sat, 8 Aug 2026 19:46:18 +0200
Subject: [PATCH 12/12] replaced german comments + rework
show_saturation_mask_callback
---
src/iop/satcurvergb.c | 50 +++++++++++++++++++++++++++----------------
1 file changed, 31 insertions(+), 19 deletions(-)
diff --git a/src/iop/satcurvergb.c b/src/iop/satcurvergb.c
index 8504c91dd458..1608a507afc7 100755
--- a/src/iop/satcurvergb.c
+++ b/src/iop/satcurvergb.c
@@ -145,14 +145,14 @@ typedef struct dt_iop_satcurve_gui_data_t
float picked_s_min;
float picked_s_max;
- gboolean mask_display; // NEU
+ gboolean mask_display;
} dt_iop_satcurve_gui_data_t;
typedef struct dt_iop_satcurve_global_data_t
{
int kernel_satcurvergb;
int kernel_satcurve_histogram;
- int kernel_satcurve_mask; // NEU
+ int kernel_satcurve_mask;
int kernel_satcurve_scalar_mask;
int kernel_satcurve_mask_from_scalar;
int kernel_satcurve_apply_guided_mask;
@@ -520,8 +520,8 @@ static inline void apply_sat_and_brilliance_jzazbz(const dt_iop_satcurve_data_t
dt_JzAzBz_2_XYZ(jab, xyz);
}
-// NEU: berechnet pro Pixel die normierte Sättigung in einen separaten Buffer,
-// analog zu compute_luminance_mask() in toneequal.c
+// Compute the normalized saturation for each pixel into a separate buffer,
+// analogous to compute_luminance_mask() in toneequal.c.
static inline void compute_saturation_mask(const dt_iop_satcurve_data_t *d,
const dt_colormatrix_t inputmatrix_trans,
const float L_white,
@@ -537,8 +537,8 @@ static inline void compute_saturation_mask(const dt_iop_satcurve_data_t *d,
}
}
-// NEU: Graustufen-Visualisierung der Sättigungsmaske; sqrt-"Gamma" für bessere
-// Sichtbarkeit niedriger Werte, analog display_luminance_mask() in toneequal.c
+// Render the saturation mask as a grayscale preview. Using a sqrt-like gamma
+// makes low values easier to see, analogous to display_luminance_mask() in toneequal.c.
static inline void display_saturation_mask(const float *const restrict in,
const float *const restrict mask,
float *const restrict out,
@@ -611,7 +611,7 @@ void process(dt_iop_module_t *self, dt_dev_pixelpipe_iop_t *piece,
if (self->dev->gui_attached && dt_pipe_is_full(piece->pipe) && dt_iop_has_focus(self) && piece->pipe == self->dev->full.pipe)
_update_sat_histogram(self, d, inputmatrix_trans, in, npixels);
- // NEU: Maskenanzeige-Zweig -- zeigt die normierte Sättigung als Graustufenbild
+ // Display a grayscale preview of the normalized saturation mask.
if (self->dev->gui_attached && dt_pipe_is_full(piece->pipe) && g && g->mask_display)
{
float *const restrict mask = dt_alloc_align_float(npixels);
@@ -972,7 +972,7 @@ int process_cl(dt_iop_module_t *self, dt_dev_pixelpipe_iop_t *piece,
const float L_white = Y_to_dt_UCS_L_star(1.f);
- // NEU: Maskenanzeige-Pfad -- eigener, einfacherer Kernel statt satcurvergb
+ // Mask-preview path: use a simpler kernel than the main satcurvergb path.
if (want_mask)
{
if (!gf_active)
@@ -1152,7 +1152,7 @@ void init_global(dt_iop_module_so_t *self)
self->data = gd;
gd->kernel_satcurvergb = dt_opencl_create_kernel(program, "satcurvergb");
gd->kernel_satcurve_histogram = dt_opencl_create_kernel(program, "satcurve_histogram");
- gd->kernel_satcurve_mask = dt_opencl_create_kernel(program, "satcurve_mask"); // NEU
+ gd->kernel_satcurve_mask = dt_opencl_create_kernel(program, "satcurve_mask");
gd->kernel_satcurve_scalar_mask = dt_opencl_create_kernel(program, "satcurve_scalar_mask");
gd->kernel_satcurve_mask_from_scalar = dt_opencl_create_kernel(program, "satcurve_mask_from_scalar");
gd->kernel_satcurve_apply_guided_mask = dt_opencl_create_kernel(program_fgf, "satcurve_apply_guided_mask");
@@ -1168,7 +1168,7 @@ void cleanup_global(dt_iop_module_so_t *self)
const dt_iop_satcurve_global_data_t *gd = self->data;
dt_opencl_free_kernel(gd->kernel_satcurvergb);
dt_opencl_free_kernel(gd->kernel_satcurve_histogram);
- dt_opencl_free_kernel(gd->kernel_satcurve_mask); // NEU
+ dt_opencl_free_kernel(gd->kernel_satcurve_mask);
dt_opencl_free_kernel(gd->kernel_satcurve_scalar_mask);
dt_opencl_free_kernel(gd->kernel_satcurve_mask_from_scalar);
dt_opencl_free_kernel(gd->kernel_satcurve_apply_guided_mask);
@@ -1763,17 +1763,29 @@ void color_picker_apply(dt_iop_module_t *self, GtkWidget *widget, dt_dev_pixelpi
gtk_widget_queue_draw(GTK_WIDGET(g->area));
}
-// NEU: Callback für den Sättigungsmasken-Toggle-Button, analog
-// show_luminance_mask_callback() in toneequal.c
+// Toggle the saturation-mask preview, following the standard pattern used by
+// other mask preview controls in darktable.
static void show_saturation_mask_callback(GtkToggleButton *button, dt_iop_module_t *self)
{
- if (darktable.gui->reset)
- return;
+ DT_GUARD_GUI_UPDATE();
+
dt_iop_satcurve_gui_data_t *g = self->gui_data;
+ if (!g)
+ return;
dt_iop_request_focus(self);
+
+ if (self->request_mask_display)
+ {
+ dt_control_log(_("cannot display saturation masks while the blending mask is displayed"));
+ gtk_toggle_button_set_active(button, FALSE);
+ g->mask_display = FALSE;
+ return;
+ }
+
g->mask_display = gtk_toggle_button_get_active(button);
- dt_dev_reprocess_center(self->dev, self->iop_order);
+ dt_iop_refresh_center(self);
+ dt_iop_color_picker_reset(self, TRUE);
}
void gui_focus(dt_iop_module_t *self, gboolean in)
@@ -1783,7 +1795,7 @@ void gui_focus(dt_iop_module_t *self, gboolean in)
{
dt_iop_color_picker_reset(self, FALSE);
- // NEU: Maskenanzeige beim Verlassen des Moduls zurücksetzen
+ // Reset the mask preview when leaving the module.
if (g && g->mask_display)
{
g->mask_display = FALSE;
@@ -1861,7 +1873,7 @@ void gui_init(dt_iop_module_t *self)
g->selected = -1;
g->dragging = FALSE;
g->active_channel = DT_IOP_SATCURVE_CHANNEL_SATURATION;
- g->mask_display = FALSE; // NEU
+ g->mask_display = FALSE;
for (int ch = 0; ch < DT_IOP_SATCURVE_CHANNELS; ch++)
{
@@ -1935,14 +1947,14 @@ void gui_init(dt_iop_module_t *self)
g->colorpicker = dt_color_picker_new_with_cst(self, DT_COLOR_PICKER_POINT, NULL, IOP_CS_RGB);
gtk_widget_set_tooltip_text(g->colorpicker, _("pick saturation from image"));
- // NEU: Toggle-Button für die Sättigungsmaskenanzeige
+ // Toggle button for the saturation-mask preview.
g->show_saturation_mask = dtgtk_togglebutton_new(dtgtk_cairo_paint_showmask, 0, NULL);
gtk_widget_set_tooltip_text(g->show_saturation_mask, _("display saturation mask"));
g_signal_connect(G_OBJECT(g->show_saturation_mask), "toggled",
G_CALLBACK(show_saturation_mask_callback), self);
gtk_box_pack_start(GTK_BOX(controls_hbox), g->colorpicker, FALSE, FALSE, 0);
- gtk_box_pack_start(GTK_BOX(controls_hbox), g->show_saturation_mask, FALSE, FALSE, 0); // NEU
+ gtk_box_pack_start(GTK_BOX(controls_hbox), g->show_saturation_mask, FALSE, FALSE, 0);
gtk_box_pack_end(GTK_BOX(controls_hbox), g->formula, FALSE, FALSE, 0);
g_object_unref(g->formula);