src/iop/satcurvergb.c
Rama master · réplica de lectura del repositorio original.
/*
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 <math.h>
#include <string.h>
#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/gdk_event_utils.h"
#include "common/guided_filter.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"
#include "gui/draw.h"
#include "gui/gtk.h"
#include "gui/accelerators.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)
#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
// squared normalized distance below which a curve node is considered grabbed by
// the pointer (radius ~0.05). Shared by the button-press and motion handlers so
// a node that is hovered is also the one a click picks up and drags.
#define DT_IOP_SATCURVE_NODE_GRAB_SQ 0.0025f
DT_MODULE_INTROSPECTION(1, dt_iop_satcurve_params_t)
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;
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"
// 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"
float gf_protect_from; // $MIN: 0.0 $MAX: 0.5 $DEFAULT: 0.00 $DESCRIPTION: "protect near-neutrals from"
float gf_protect_to; // $MIN: 0.0 $MAX: 0.5 $DEFAULT: 0.00 $DESCRIPTION: "protect near-neutrals to"
float noise_protection; // $MIN: 0.0 $MAX: 1.0 $DEFAULT: 0.0 $DESCRIPTION: "noise protection"
} dt_iop_satcurve_params_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;
gboolean use_guided_filter;
float gf_radius;
float gf_feathering;
float gf_protect_from;
float gf_protect_to;
float noise_protection;
} dt_iop_satcurve_data_t;
typedef struct dt_iop_satcurve_gui_channel_t
{
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;
GtkWidget *show_saturation_mask;
GtkNotebook *notebook;
GtkWidget *use_guided_filter;
GtkWidget *gf_radius;
GtkWidget *gf_feathering;
GtkWidget *gf_protect_center;
GtkWidget *gf_protect_width;
GtkWidget *noise_protection;
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;
gboolean dragging;
float histogram[DT_IOP_SATCURVE_HIST_RES];
float histogram_max;
dt_pthread_mutex_t histogram_lock;
gboolean picker_valid;
float picked_s;
float picked_s_min;
float picked_s_max;
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;
int kernel_satcurve_scalar_mask;
int kernel_satcurve_perceptual_guide;
int kernel_satcurve_filter_confidence;
int kernel_satcurve_mask_from_control;
} 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");
}
const char *aliases()
{
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 and brilliance with curves"),
_("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;
}
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};
}
// Center/width -> from/to. Width is clamped so that from/to stay within
// [0, 0.5] and from <= to holds (identical to the previous slider bounds).
static inline void protect_center_width_to_from_to(const float center,
const float width,
float *from,
float *to)
{
const float half = MAX(width, 0.f) * 0.5f;
*from = CLAMP(center - half, 0.f, 0.5f);
*to = CLAMP(center + half, 0.f, 0.5f);
if(*to < *from)
*to = *from;
}
// from/to -> center/width; used when loading existing params (presets,
// history, old .xmp files) to correctly initialize the new sliders.
static inline void protect_from_to_to_center_width(const float from,
const float to,
float *center,
float *width)
{
*center = 0.5f * (from + to);
*width = MAX(0.f, to - from);
}
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_protect_from = 0.00f;
p->gf_protect_to = 0.00f;
p->noise_protection = 0.0f;
}
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]);
}
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;
}
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));
}
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);
}
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;
}
// 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;
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 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 / 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)
{
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);
}
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 gboolean guided_filter_active(const dt_iop_satcurve_data_t *d)
{
return d->formula == DT_IOP_SATCURVE_DTUCS && d->use_guided_filter && d->gf_radius >= 0.5f;
}
static inline dt_iop_satcurve_factors_t eval_curve_factors(const dt_iop_satcurve_data_t *d,
const float s_in_sat,
const float s_in_bri)
{
const float sat_c = CLAMP(lookup_lut(d->channel[DT_IOP_SATCURVE_CHANNEL_SATURATION].lut, s_in_sat), 0.f, 1.f);
const float bri_c = CLAMP(lookup_lut(d->channel[DT_IOP_SATCURVE_CHANNEL_BRILLIANCE].lut, s_in_bri), 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 inline float smoothstep01(const float edge0, const float edge1, const float x);
// Combined neutral-/noise-protection weight for a single pixel. Replaces the
// separate full-image pass apply_neutral_protection(): the neutral weight
// (raw saturation vs. protect_from/to) is now computed inline in the main
// pixel loop instead of upfront over the whole image.
static inline float protection_weight(const float s_raw,
const float protect_from,
const float protect_to,
const float noise_confidence,
const float noise_protection)
{
const float neutral_w = smoothstep01(protect_from, protect_to, s_raw);
const float noise_w = 1.0f - CLAMP(noise_protection * noise_confidence, 0.0f, 1.0f);
return neutral_w * noise_w;
}
// Apply log1p compression to the histogram bins to compress the dynamic
// range for better visual representation in the GUI. Shared tail for both
// the CPU path (_update_sat_histogram) and the GPU path (process_cl).
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,
const float *const restrict in,
const size_t npixels)
{
dt_iop_satcurve_gui_data_t *g = self->gui_data;
if(!g)
return;
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++)
{
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]++;
}
_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,
const float s_in_sat,
const float s_in_bri,
const float noise_confidence)
{
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);
// Compute curve factors from the (possibly independently blended) per-channel inputs
dt_iop_satcurve_factors_t f = eval_curve_factors(d, s_in_sat, s_in_bri);
const float effect = 1.0f - CLAMP(d->noise_protection * noise_confidence, 0.0f, 1.0f);
f.sat_factor = 1.0f + effect * (f.sat_factor - 1.0f);
f.bri_factor = 1.0f + effect * (f.bri_factor - 1.0f);
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,
const float s_in_sat,
const float s_in_bri,
const float noise_confidence)
{
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);
// Compute curve factors from the (possibly independently blended) per-channel inputs
dt_iop_satcurve_factors_t f = eval_curve_factors(d, s_in_sat, s_in_bri);
const float effect = 1.0f - CLAMP(d->noise_protection * noise_confidence, 0.0f, 1.0f);
f.sat_factor = 1.0f + effect * (f.sat_factor - 1.0f);
f.bri_factor = 1.0f + effect * (f.bri_factor - 1.0f);
const float s_out = satcurve_soft_clip(MAX(s_in_sat * 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);
}
// Compute the normalized saturation for each pixel into a separate buffer,
// analogous to compute_luminance_mask() in toneequal.c.
static inline void prepare_scalar_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);
}
}
// Render the saturation mask as a color preview. Zero saturation is shown as
// white, full saturation as purple/magenta, matching the chroma gradient end
// color {0.5, 0.0, 0.5} used in blend_gui.c. A sqrt-like gamma makes low
// values easier to see, analogous to display_luminance_mask() in toneequal.c.
static inline void visualize_mask_preview(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++)
{
// white at t = 0, magenta {0.5, 0.0, 0.5} at t = 1
const float t = sqrtf(CLAMP(mask[k], 0.f, 1.f));
out[4 * k + 0] = 1.0f - 0.5f * t;
out[4 * k + 1] = 1.0f - t;
out[4 * k + 2] = 1.0f - 0.5f * t;
out[4 * k + 3] = in[4 * k + 3];
}
}
static inline float smoothstep01(const float edge0, const float edge1, const float x)
{
const float t = CLAMP((x - edge0) / MAX(edge1 - edge0, 1e-6f), 0.0f, 1.0f);
return t * t * (3.0f - 2.0f * t);
}
// Prepare the scalar curve coordinate, perceptual guide, and per-pixel
// confidence used by the guided-filter branch. This matches the stage split in
// the OpenCL implementation: scalar mask -> guide -> confidence -> control.
static inline void prepare_guided_filter_control(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 raw,
float *const restrict filtered,
float *const restrict noise_confidence,
const int width,
const int height,
const int radius)
{
const size_t npixels = (size_t)width * height;
float *const restrict guide = dt_alloc_align_float(npixels * 3);
if(!guide)
{
memcpy(filtered, raw, npixels * sizeof(float));
memset(noise_confidence, 0, npixels * sizeof(float));
return;
}
DT_OMP_FOR()
for(size_t k = 0; k < npixels; k++)
{
dt_aligned_pixel_t rgb, xyz;
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_JZAZBZ)
{
dt_aligned_pixel_t jab;
dt_XYZ_2_JzAzBz(xyz, jab);
guide[3 * k + 0] = 100.0f * jab[0];
guide[3 * k + 1] = 100.0f * jab[1];
guide[3 * k + 2] = 100.0f * jab[2];
}
else
{
dt_aligned_pixel_t xyY, JCH;
dt_D65_XYZ_to_xyY(xyz, xyY);
xyY_to_dt_UCS_JCH(xyY, L_white, JCH);
guide[3 * k + 0] = JCH[0] / MAX(L_white, 1e-6f);
guide[3 * k + 1] = JCH[1] * cosf(JCH[2]) / MAX(L_white, 1e-6f);
guide[3 * k + 2] = JCH[1] * sinf(JCH[2]) / MAX(L_white, 1e-6f);
}
}
guided_filter(guide, raw, filtered, width, height, 3, MAX(1, radius),
MAX(0.01f, d->gf_feathering * 0.01f), 1.0f, 0.0f, 1.0f);
DT_OMP_FOR()
for(int row = 0; row < height; row++)
{
for(int col = 0; col < width; col++)
{
const size_t k = (size_t)row * width + col;
float residual_energy = 0.0f;
float edge_energy = 0.0f;
int count = 0;
for(int dy = -1; dy <= 1; dy++)
{
for(int dx = -1; dx <= 1; dx++)
{
const int r = CLAMP(row + dy, 0, height - 1);
const int c = CLAMP(col + dx, 0, width - 1);
const size_t n = (size_t)r * width + c;
const float residual = raw[n] - filtered[n];
residual_energy += residual * residual;
for(int channel = 0; channel < 3; channel++)
{
const float delta = guide[3 * n + channel] - guide[3 * k + channel];
edge_energy += delta * delta;
}
count++;
}
}
residual_energy /= (float)count;
edge_energy /= (float)(count * 3);
const float noisy = smoothstep01(0.0005f, 0.01f, residual_energy);
const float edge_safe = 1.0f - smoothstep01(0.0005f, 0.01f, edge_energy);
const float confidence = noisy * edge_safe;
noise_confidence[k] = confidence;
}
}
dt_free_align(guide);
}
static inline void apply_guided_filter_control(const float *const restrict raw,
const float *const restrict filtered,
const float *const restrict confidence,
float *const restrict control,
const float protect_from,
const float protect_to,
const float noise_protection,
const size_t npixels)
{
DT_OMP_FOR()
for(size_t k = 0; k < npixels; k++)
{
const float weight = protection_weight(raw[k], protect_from, protect_to,
confidence[k], noise_protection);
control[k] = raw[k] + weight * (filtered[k] - raw[k]);
}
}
// Allocate three aligned float scratch buffers of npixels elements each. If
// any allocation fails, frees whatever succeeded and resets all three
// pointers to NULL. Used by both the mask-preview branch and the main
// processing branch of process() to allocate their guided-filter scratch
// buffers.
static gboolean alloc_scratch_floats(const size_t npixels, float **a, float **b, float **c)
{
*a = dt_alloc_align_float(npixels);
*b = dt_alloc_align_float(npixels);
*c = dt_alloc_align_float(npixels);
if(*a && *b && *c)
return TRUE;
dt_free_align(*a);
dt_free_align(*b);
dt_free_align(*c);
*a = *b = *c = NULL;
return FALSE;
}
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;
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)
{
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;
const float L_white = Y_to_dt_UCS_L_star(1.f);
if(self->dev->gui_attached && piece->pipe == self->dev->preview_pipe)
_update_sat_histogram(self, d, inputmatrix_trans, in, npixels);
// Display a grayscale preview of the normalized saturation mask.
gboolean mask_display = FALSE;
if(self->dev->gui_attached && dt_pipe_is_full(piece->pipe) && g)
{
dt_iop_gui_enter_critical_section(self);
mask_display = g->mask_display;
dt_iop_gui_leave_critical_section(self);
}
if(mask_display)
{
float *const restrict mask = dt_alloc_align_float(npixels);
if(mask)
{
prepare_scalar_mask(d, inputmatrix_trans, L_white, in, mask, npixels);
// Show final coordinate fed into curve.
if(guided_filter_active(d))
{
float *filtered, *confidence, *control;
if(alloc_scratch_floats(npixels, &filtered, &confidence, &control))
{
prepare_guided_filter_control(d, inputmatrix_trans, L_white, in, mask, filtered, confidence,
roi_out->width, roi_out->height,
(int)d->gf_radius);
apply_guided_filter_control(mask, filtered, confidence, control,
d->gf_protect_from, d->gf_protect_to,
0.0f, npixels);
memcpy(mask, control, npixels * sizeof(float));
}
dt_free_align(filtered);
dt_free_align(confidence);
dt_free_align(control);
}
visualize_mask_preview(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;
}
// Compute curve inputs from guided-filtered saturation or raw saturation.
float *filtered_mask = NULL;
float *noise_confidence = NULL;
float *raw_mask = NULL;
if(guided_filter_active(d))
{
if(alloc_scratch_floats(npixels, &raw_mask, &filtered_mask, &noise_confidence))
{
prepare_scalar_mask(d, inputmatrix_trans, L_white, in, raw_mask, npixels);
prepare_guided_filter_control(d, inputmatrix_trans, L_white, in, raw_mask, filtered_mask,
noise_confidence, roi_out->width, roi_out->height,
(int)d->gf_radius);
}
else
{
dt_control_log(_("saturation curve: guided filter failed to allocate memory, disabling it for this run"));
}
}
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);
// Guided filtering supplies same protected coordinate to both curves.
float s_in_sat, s_in_bri;
float noise_conf = 0.0f;
if(filtered_mask)
{
// noise_protection is deliberately NOT passed here (0.0f): the noise
// damping still acts on sat_factor/bri_factor inside
// apply_sat_and_brilliance_*(), not on the coordinate itself.
// protection_weight() effectively yields only the neutral component here.
const float weight = protection_weight(raw_mask[k], d->gf_protect_from, d->gf_protect_to,
noise_confidence[k], 0.0f);
const float protected_coord = raw_mask[k] + weight * (filtered_mask[k] - raw_mask[k]);
s_in_sat = s_in_bri = CLAMP(protected_coord, 0.f, 1.f);
noise_conf = noise_confidence[k];
}
else
{
s_in_sat = s_in_bri = pixel_s_in_norm(d->formula, in + 4 * k, inputmatrix_trans, d->gamut_lut, L_white, NULL);
}
if(d->formula == DT_IOP_SATCURVE_DTUCS)
apply_sat_and_brilliance_ucs(d, xyz, L_white, s_in_sat, s_in_bri, noise_conf);
else
apply_sat_and_brilliance_jzazbz(d, xyz, s_in_sat, s_in_bri, noise_conf);
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();
dt_free_align(raw_mask);
dt_free_align(filtered_mask);
dt_free_align(noise_confidence);
}
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 = guided_filter_active(d);
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;
// Four scalar full-resolution buffers plus one three-channel perceptual
// guide are allocated in addition to the normal input/output buffers.
tiling->factor_cl += 2.0f;
const float filter_radius = d->gf_radius;
const int base_overlap = (int)ceilf(filter_radius);
const int resample_margin = 2;
tiling->overlap = MAX(tiling->overlap, base_overlap + resample_margin);
}
#if HAVE_OPENCL
// Allocate the five guided-filter scratch buffers (scalar mask, filtered
// mask, perceptual guide, noise confidence, control). On partial allocation
// failure, releases whatever succeeded, resets all five pointers to NULL,
// logs once, and returns FALSE so the caller can fall back to the
// unfiltered mask instead of aborting. Used by both the mask-preview branch
// and the main processing branch of process_cl().
static gboolean alloc_gf_scratch_cl(const int devid, const int width, const int height,
cl_mem *mask_scalar_cl, cl_mem *mask_filtered_cl,
cl_mem *guide_cl, cl_mem *noise_confidence_cl,
cl_mem *mask_control_cl)
{
*mask_scalar_cl = dt_opencl_alloc_device(devid, width, height, sizeof(float));
*mask_filtered_cl = dt_opencl_alloc_device(devid, width, height, sizeof(float));
*guide_cl = dt_opencl_alloc_device(devid, width, height, 4 * sizeof(float));
*noise_confidence_cl = dt_opencl_alloc_device(devid, width, height, sizeof(float));
*mask_control_cl = dt_opencl_alloc_device(devid, width, height, sizeof(float));
if(*mask_scalar_cl && *mask_filtered_cl && *guide_cl && *noise_confidence_cl && *mask_control_cl)
return TRUE;
dt_control_log(_("saturation curve: guided filter failed to allocate memory, "
"disabling it for this run"));
dt_opencl_release_mem_object(*mask_scalar_cl);
dt_opencl_release_mem_object(*mask_filtered_cl);
dt_opencl_release_mem_object(*guide_cl);
dt_opencl_release_mem_object(*noise_confidence_cl);
dt_opencl_release_mem_object(*mask_control_cl);
*mask_scalar_cl = *mask_filtered_cl = *guide_cl = *noise_confidence_cl = *mask_control_cl = NULL;
return FALSE;
}
// Runs the four-stage guided-filter mask pipeline (scalar mask -> perceptual
// guide -> guided filter -> filter confidence) into the already-allocated
// scratch buffers. Shared by the mask-preview branch and the main processing
// branch of process_cl(); previously duplicated verbatim in both places.
static cl_int run_gf_pipeline_cl(const int devid,
const dt_iop_satcurve_global_data_t *gd,
const dt_iop_satcurve_data_t *d,
cl_mem dev_in,
cl_mem input_matrix_cl,
cl_mem gamut_lut_cl,
const float L_white,
const int width, const int height,
cl_mem mask_scalar_cl,
cl_mem guide_cl,
cl_mem mask_filtered_cl,
cl_mem mask_control_cl,
cl_mem noise_confidence_cl)
{
cl_int 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)
return err;
err = dt_opencl_enqueue_kernel_2d_args(
devid, gd->kernel_satcurve_perceptual_guide, width, height,
CLARG(dev_in), CLARG(guide_cl), CLARG(width), CLARG(height),
CLARG(input_matrix_cl), CLARG(L_white));
if(err != CL_SUCCESS)
return err;
err = guided_filter_cl(devid, guide_cl, mask_scalar_cl, mask_filtered_cl,
width, height, 3,
MAX(1, (int)d->gf_radius),
MAX(0.01f, d->gf_feathering * 0.01f), 1.0f, 0.0f, 1.0f);
if(err != CL_SUCCESS)
return err;
return dt_opencl_enqueue_kernel_2d_args(
devid, gd->kernel_satcurve_filter_confidence, width, height,
CLARG(mask_scalar_cl), CLARG(mask_filtered_cl), CLARG(guide_cl),
CLARG(mask_control_cl), CLARG(noise_confidence_cl), CLARG(width), CLARG(height),
CLARG(d->gf_protect_from), CLARG(d->gf_protect_to));
}
// Combines allocation + pipeline execution. Returns FALSE if the scratch
// buffers could not be allocated (caller should fall back to gf_active =
// FALSE, matching the previous inline behaviour in both branches of
// process_cl()). If allocation succeeds but a kernel/guided-filter stage
// fails, TRUE is returned and *err carries the OpenCL error for the caller's
// existing goto-error handling.
static gboolean prepare_gf_mask_cl(const int devid,
const dt_iop_satcurve_global_data_t *gd,
const dt_iop_satcurve_data_t *d,
cl_mem dev_in,
cl_mem input_matrix_cl,
cl_mem gamut_lut_cl,
const float L_white,
const int width, const int height,
cl_mem *mask_scalar_cl, cl_mem *mask_filtered_cl,
cl_mem *guide_cl, cl_mem *noise_confidence_cl,
cl_mem *mask_control_cl,
cl_int *err)
{
if(!alloc_gf_scratch_cl(devid, width, height, mask_scalar_cl, mask_filtered_cl,
guide_cl, noise_confidence_cl, mask_control_cl))
return FALSE;
*err = run_gf_pipeline_cl(devid, gd, d, dev_in, input_matrix_cl, gamut_lut_cl, L_white,
width, height, *mask_scalar_cl, *guide_cl, *mask_filtered_cl,
*mask_control_cl, *noise_confidence_cl);
return TRUE;
}
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;
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 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;
// not const anymore: may be downgraded to FALSE below if the guided-filter
// scratch buffers fail to allocate, mirroring the CPU fallback in process()
gboolean gf_active = guided_filter_active(d);
gboolean want_mask = FALSE;
if(self->dev->gui_attached && dt_pipe_is_full(piece->pipe) && g)
{
dt_iop_gui_enter_critical_section(self);
want_mask = g->mask_display;
dt_iop_gui_leave_critical_section(self);
}
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;
cl_mem mask_scalar_cl = NULL;
cl_mem mask_filtered_cl = NULL;
cl_mem guide_cl = NULL;
cl_mem noise_confidence_cl = NULL;
cl_mem mask_control_cl = NULL;
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)
{
err = CL_MEM_OBJECT_ALLOCATION_FAILURE;
goto error;
}
const float L_white = Y_to_dt_UCS_L_star(1.f);
// Histogram update: runs independently of want_mask/neutral, mirroring
// _update_sat_histogram() on the CPU path, which also runs before every
// early return.
const gboolean want_histogram =
self->dev->gui_attached && piece->pipe == self->dev->preview_pipe;
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);
}
dt_opencl_release_mem_object(hist_bins_cl);
hist_bins_cl = NULL;
}
// Mask preview mode mirrors CPU final coordinate.
if(want_mask)
{
if(gf_active)
{
cl_int gf_err = CL_SUCCESS;
if(!prepare_gf_mask_cl(devid, gd, d, dev_in, input_matrix_cl, gamut_lut_cl, L_white,
width, height, &mask_scalar_cl, &mask_filtered_cl, &guide_cl,
&noise_confidence_cl, &mask_control_cl, &gf_err))
gf_active = FALSE;
else if(gf_err != CL_SUCCESS)
{
err = gf_err;
goto error;
}
}
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));
}
else
{
// mask_control_cl holds final coordinate.
const float mask_noise_protection = 0.0f;
err = dt_opencl_enqueue_kernel_2d_args(
devid, gd->kernel_satcurve_mask_from_control, width, height,
CLARG(mask_scalar_cl), CLARG(mask_filtered_cl), CLARG(noise_confidence_cl),
CLARG(dev_in), CLARG(dev_out), CLARG(width), CLARG(height),
CLARG(d->gf_protect_from), CLARG(d->gf_protect_to), CLARG(mask_noise_protection));
}
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),
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);
if(output_matrix_cl == NULL || sat_lut_cl == NULL || bri_lut_cl == NULL)
{
err = CL_MEM_OBJECT_ALLOCATION_FAILURE;
goto error;
}
// 1. Prepare filtered saturation mask if guided filter is enabled
if(gf_active)
{
cl_int gf_err = CL_SUCCESS;
if(!prepare_gf_mask_cl(devid, gd, d, dev_in, input_matrix_cl, gamut_lut_cl, L_white,
width, height, &mask_scalar_cl, &mask_filtered_cl, &guide_cl,
&noise_confidence_cl, &mask_control_cl, &gf_err))
gf_active = FALSE;
else if(gf_err != CL_SUCCESS)
{
err = gf_err;
goto error;
}
}
const int use_mask = gf_active ? 1 : 0;
cl_mem sat_mask_arg = gf_active ? mask_control_cl : dev_in;
cl_mem bri_mask_arg = gf_active ? mask_control_cl : dev_in;
cl_mem noise_confidence_arg = gf_active ? noise_confidence_cl : dev_in;
// 2. Main processing pass: evaluate curves using the smoothed / blended masks
err = dt_opencl_enqueue_kernel_2d_args(
devid, gd->kernel_satcurvergb, width, height,
CLARG(dev_in), CLARG(dev_out),
CLARG(sat_mask_arg), CLARG(bri_mask_arg), CLARG(noise_confidence_arg), CLARG(use_mask),
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), CLARG(d->noise_protection));
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);
dt_opencl_release_mem_object(mask_scalar_cl);
dt_opencl_release_mem_object(mask_filtered_cl);
dt_opencl_release_mem_object(guide_cl);
dt_opencl_release_mem_object(noise_confidence_cl);
dt_opencl_release_mem_object(mask_control_cl);
return err;
}
void init_global(dt_iop_module_so_t *self)
{
const int program = 44; // 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");
gd->kernel_satcurve_histogram = dt_opencl_create_kernel(program, "satcurve_histogram");
gd->kernel_satcurve_mask = dt_opencl_create_kernel(program, "satcurve_mask");
gd->kernel_satcurve_scalar_mask = dt_opencl_create_kernel(program, "satcurve_prepare_scalar_mask");
gd->kernel_satcurve_perceptual_guide = dt_opencl_create_kernel(program, "satcurve_prepare_perceptual_guide");
gd->kernel_satcurve_filter_confidence = dt_opencl_create_kernel(program, "satcurve_prepare_filter_confidence");
gd->kernel_satcurve_mask_from_control = dt_opencl_create_kernel(program, "satcurve_mask_from_control");
}
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);
dt_opencl_free_kernel(gd->kernel_satcurve_scalar_mask);
dt_opencl_free_kernel(gd->kernel_satcurve_perceptual_guide);
dt_opencl_free_kernel(gd->kernel_satcurve_filter_confidence);
dt_opencl_free_kernel(gd->kernel_satcurve_mask_from_control);
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);
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->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;
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;
}
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 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++)
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);
}
}
// Synchronizes a dt_draw_curve_t (identified by its curve/curve_type/
// curve_num_nodes triple) with the given channel params, then recomputes
// its sampled values into out_values (length res). If the curve topology
// (type or node count) changed, the curve is destroyed and rebuilt from
// scratch; otherwise the existing curve's points are just updated in place
// (cheaper, and needed so the GUI curve keeps its identity while dragging).
// Shared by the pipe-data LUT sync (sync_channel_curve) and the GUI preview
// sync (_sync_gui_curve), which previously duplicated this verbatim.
static void _sync_curve_and_calc_values(dt_draw_curve_t **curve,
int *curve_type,
int *curve_num_nodes,
const dt_iop_satcurve_channel_params_t *src,
float *const restrict out_values,
const int res)
{
if(*curve_type != src->curve_type || *curve_num_nodes != src->curve_num_nodes)
{
if(*curve)
dt_draw_curve_destroy(*curve);
*curve = dt_draw_curve_new(0.f, 1.f, src->curve_type);
*curve_type = src->curve_type;
*curve_num_nodes = src->curve_num_nodes;
for(int i = 0; i < src->curve_num_nodes; i++)
dt_draw_curve_add_point(*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(*curve, i, src->curve[i].x, src->curve[i].y);
}
dt_draw_curve_calc_values(*curve, 0.f, 1.f, res, NULL, out_values);
}
static void sync_channel_curve(dt_iop_satcurve_channel_data_t *dst,
const dt_iop_satcurve_channel_params_t *src)
{
_sync_curve_and_calc_values(&dst->curve, &dst->curve_type, &dst->curve_num_nodes,
src, dst->lut, DT_IOP_SATCURVE_RES);
}
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;
}
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_protect_from = p->gf_protect_from;
d->gf_protect_to = p->gf_protect_to;
d->noise_protection = p->noise_protection;
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;
}
#ifdef HAVE_OPENCL
// As the OpenCL code uses atomic_inc() we must not use OpenCL in that case
piece->process_cl_ready = piece->process_cl_ready && !dt_opencl_avoid_atomics(pipe->devid);
#endif
}
void init_presets(dt_iop_module_so_t *self)
{
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_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 < 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 = 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;
}
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, .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);
}
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, 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, 1.0, 1.0, 1.0, 1.0);
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);
cairo_restore(cr);
}
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)
{
_sync_curve_and_calc_values(&gc->curve, &gc->curve_type, &gc->curve_num_nodes,
cp, gc->draw_ys, DT_IOP_SATCURVE_RES);
}
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);
cairo_set_source_rgb(cr, .15, .15, .15);
cairo_paint(cr);
cairo_save(cr);
cairo_translate(cr, gx0, gy0);
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);
_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++)
{
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);
_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);
// Saturation axis gradient: white at zero saturation, purple/magenta
// {0.5, 0.0, 0.5} at full saturation, matching the mask preview and the
// chroma gradient end color in blend_gui.c.
cairo_pattern_t *grad = cairo_pattern_create_linear(gx0, 0.0, gx0 + gw, 0.0);
cairo_pattern_add_color_stop_rgba(grad, 0.0, 1.0, 1.0, 1.0, 1.0);
cairo_pattern_add_color_stop_rgba(grad, 1.0, 0.5, 0.0, 0.5, 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);
return FALSE;
}
// Returns the index of the curve node nearest to (x, y) in normalized graph
// coordinates, within the shared grab radius, or -1 if none is close enough.
// Picking the nearest (rather than the first within range) avoids ambiguities
// when two nodes sit close together. This is what makes a node moveable on
// hover: the motion handler drags g->selected, the scroll handler nudges it,
// and the draw code renders it larger.
static inline int _node_hit_at(const dt_iop_satcurve_channel_params_t *cp,
const float x, const float y)
{
int nearest = -1;
float best = DT_IOP_SATCURVE_NODE_GRAB_SQ;
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;
const float d2 = dx * dx + dy * dy;
if(d2 < best)
{
best = d2;
nearest = i;
}
}
return nearest;
}
static void area_button_press(GtkGestureSingle *gesture,
gint n_press,
gdouble x,
gdouble y,
dt_iop_module_t *self)
{
DT_GUARD_GUI_UPDATE();
const int button = gtk_gesture_single_get_current_button(gesture);
if(button != GDK_BUTTON_PRIMARY && button != GDK_BUTTON_SECONDARY)
return;
dt_iop_satcurve_gui_data_t *g = self->gui_data;
dt_iop_satcurve_channel_params_t *cp = get_active_channel_params(self);
dt_iop_satcurve_gui_channel_t *gc = get_active_gui_channel(self);
GtkWidget *widget = dt_gui_get_widget(gesture);
GtkAllocation a;
gtk_widget_get_allocation(widget, &a);
// keep the existing resize-handle guard: pointer-position based, so it
// still applies regardless of whether the handle strip's own gesture
// claims the event first.
if(y >= a.height - DT_RESIZE_HANDLE_SIZE)
return;
float gx0, gy0, w, h;
_get_graph_geometry(&a, &gx0, &gy0, &w, &h);
const float nx = CLAMP((x - gx0) / w, 0.f, 1.f);
const float ny = CLAMP(1.f - (y - gy0) / h, 0.f, 1.f);
int hit = _node_hit_at(cp, nx, ny);
if(button == GDK_BUTTON_PRIMARY)
{
// a double-click resets the curve of the active channel
if(n_press >= 2)
{
reset_channel_curve(cp);
g->selected = -1;
dt_dev_add_history_item(darktable.develop, self, TRUE);
gtk_widget_queue_draw(widget);
}
else
{
if(hit < 0 && cp->curve_num_nodes < DT_IOP_SATCURVE_MAXNODES)
hit = add_node_to_channel(cp, nx, CLAMP(dt_draw_curve_calc_value(gc->curve, nx), 0.f, 1.f));
g->selected = hit;
g->dragging = hit >= 0;
}
return;
}
// right-click: reset the node to neutral, or delete it (Ctrl keeps it)
if(hit >= 0)
{
if(dt_modifier_is(dt_key_modifier_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];
cp->curve_num_nodes--;
}
g->selected = -1;
dt_dev_add_history_item(darktable.develop, self, TRUE);
gtk_widget_queue_draw(widget);
}
}
static void area_button_release(GtkGestureSingle *gesture,
gint n_press,
gdouble x,
gdouble y,
dt_iop_module_t *self)
{
DT_GUARD_GUI_UPDATE();
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);
}
}
static void area_scroll_notify(GtkEventControllerScroll *controller, gdouble dx, gdouble dy,
dt_iop_module_t *self)
{
DT_GUARD_GUI_UPDATE();
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 || dy == 0.0)
return;
const float step = 0.02f * (dt_modifier_is(dt_key_modifier_state(), GDK_CONTROL_MASK) ? 5.0f : 1.0f);
const int n = g->selected;
cp->curve[n].y = CLAMP(cp->curve[n].y - dy * step, 0.f, 1.f);
dt_dev_add_history_item(darktable.develop, self, TRUE);
gtk_widget_queue_draw(dt_gui_get_widget(controller));
}
static void area_motion_notify(GtkEventControllerMotion *controller, gdouble x, gdouble y,
dt_iop_module_t *self)
{
DT_GUARD_GUI_UPDATE();
dt_iop_satcurve_gui_data_t *g = self->gui_data;
dt_iop_satcurve_channel_params_t *cp = get_active_channel_params(self);
GtkWidget *widget = dt_gui_get_widget(controller);
GtkAllocation a;
gtk_widget_get_allocation(widget, &a);
float gx0, gy0, w, h;
_get_graph_geometry(&a, &gx0, &gy0, &w, &h);
// Dragging a node that was grabbed by a button press.
if(g->dragging && g->selected >= 0)
{
const float nx = CLAMP((x - gx0) / w, 0.f, 1.f);
const int n = g->selected;
if(n == 0)
cp->curve[n].x = 0.f;
else if(n == cp->curve_num_nodes - 1)
cp->curve[n].x = 1.f;
else
cp->curve[n].x = CLAMP(nx,
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 - (y - gy0) / h, 0.f, 1.f);
dt_dev_add_history_item(darktable.develop, self, FALSE);
gtk_widget_queue_draw(widget);
return;
}
// Otherwise, pick up the node under the pointer so it becomes moveable: a
// subsequent press drags it, the scroll wheel nudges it, and it is drawn
// highlighted. Hit-test only inside the graph rectangle -- clamping would
// otherwise grab an endpoint node from the inset/gradient margin.
const gboolean inside = x >= gx0 && x <= gx0 + w && y >= gy0 && y <= gy0 + h;
int hit = -1;
if(inside)
{
const float nx = (x - gx0) / w;
const float ny = 1.f - (y - gy0) / h;
hit = _node_hit_at(cp, nx, ny);
}
if(hit != g->selected)
{
g->selected = hit;
gtk_widget_queue_draw(widget);
if(g->selected >= 0)
gtk_widget_grab_focus(widget);
}
}
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;
}
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};
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));
}
// 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)
{
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);
if(g->mask_display && !self->enabled)
{
self->enabled = TRUE;
dt_dev_add_history_item(darktable.develop, self, TRUE);
}
dt_iop_refresh_center(self);
}
void gui_focus(dt_iop_module_t *self, gboolean in)
{
dt_iop_satcurve_gui_data_t *g = self->gui_data;
if(!in)
{
dt_iop_color_picker_reset(self, FALSE);
// Reset the mask preview when leaving the module.
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)
{
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));
}
static void _protect_center_width_changed(GtkWidget *widget, dt_iop_module_t *self)
{
DT_GUARD_GUI_UPDATE();
dt_iop_satcurve_gui_data_t *g = self->gui_data;
dt_iop_satcurve_params_t *p = self->params;
const float center = dt_bauhaus_slider_get(g->gf_protect_center);
const float width = dt_bauhaus_slider_get(g->gf_protect_width);
protect_center_width_to_from_to(center, width, &p->gf_protect_from, &p->gf_protect_to);
dt_dev_add_history_item(darktable.develop, self, TRUE);
}
void gui_changed(dt_iop_module_t *self, GtkWidget *widget, void *previous)
{
dt_iop_satcurve_gui_data_t *g = self->gui_data;
dt_iop_satcurve_params_t *p = self->params;
if(!g)
return;
// g->formula is a _from_params widget: the framework already commits
// the history item internally after this callback returns (Path A).
// No manual dt_dev_add_history_item() needed here.
if(!widget || widget == g->formula)
gtk_widget_queue_draw(GTK_WIDGET(g->area));
if(!widget || widget == g->use_guided_filter || widget == g->formula)
{
const gboolean guided_filter_available = p->formula == DT_IOP_SATCURVE_DTUCS;
gtk_widget_set_sensitive(g->use_guided_filter, guided_filter_available);
gtk_widget_set_sensitive(g->gf_radius, guided_filter_available && p->use_guided_filter);
gtk_widget_set_sensitive(g->gf_feathering, guided_filter_available && p->use_guided_filter);
gtk_widget_set_sensitive(g->gf_protect_center, guided_filter_available && p->use_guided_filter);
gtk_widget_set_sensitive(g->gf_protect_width, guided_filter_available && p->use_guided_filter);
gtk_widget_set_sensitive(g->noise_protection, guided_filter_available && p->use_guided_filter);
}
}
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);
dt_bauhaus_toggle_set(g->use_guided_filter, p->use_guided_filter);
if(g->notebook)
gtk_notebook_set_current_page(GTK_NOTEBOOK(g->notebook),
g->active_channel == DT_IOP_SATCURVE_CHANNEL_BRILLIANCE ? 1 : 0);
float center, width;
protect_from_to_to_center_width(p->gf_protect_from, p->gf_protect_to, ¢er, &width);
dt_bauhaus_slider_set(g->gf_protect_center, center);
dt_bauhaus_slider_set(g->gf_protect_width, width);
if(g->area)
gtk_widget_queue_draw(GTK_WIDGET(g->area));
gui_changed(self, NULL, NULL);
}
void gui_cleanup(dt_iop_module_t *self)
{
dt_iop_satcurve_gui_data_t *g = self->gui_data;
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);
}
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->dragging = FALSE;
g->active_channel = DT_IOP_SATCURVE_CHANNEL_SATURATION;
g->mask_display = FALSE;
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;
dt_pthread_mutex_init(&g->histogram_lock);
g->picker_valid = FALSE;
g->picked_s = g->picked_s_min = g->picked_s_max = 0.f;
self->widget = dt_gui_vbox();
static dt_action_def_t notebook_def = {};
g->notebook = dt_ui_notebook_new(¬ebook_def);
gtk_notebook_set_show_border(g->notebook, FALSE);
gtk_notebook_set_scrollable(g->notebook, FALSE);
gtk_notebook_popup_disable(g->notebook);
GtkWidget *page_sat = dt_ui_notebook_page(g->notebook, N_("saturation"), NULL);
GtkWidget *page_bri = dt_ui_notebook_page(g->notebook, N_("brilliance"), NULL);
gtk_widget_set_size_request(page_sat, -1, 0);
gtk_widget_set_size_request(page_bri, -1, 0);
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_action_define_iop(self, NULL, N_("page"), GTK_WIDGET(g->notebook), ¬ebook_def);
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));
g_signal_connect(G_OBJECT(g->area), "draw", G_CALLBACK(area_draw), self);
dt_gui_connect_click(GTK_WIDGET(g->area), area_button_press, area_button_release, self);
dt_gui_connect_motion(GTK_WIDGET(g->area), area_motion_notify, NULL, NULL, self);
dt_gui_connect_scroll(GTK_WIDGET(g->area), GTK_EVENT_CONTROLLER_SCROLL_BOTH_AXES | GTK_EVENT_CONTROLLER_SCROLL_DISCRETE,
area_scroll_notify, self);
dt_gui_box_add(self->widget, GTK_WIDGET(g->area));
GtkWidget *main_vbox = self->widget;
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"));
// 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);
// Temporarily set packing target to controls_hbox
self->widget = controls_hbox;
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"));
// Expand to fill remaining horizontal space and align the widget to the right end
gtk_widget_set_hexpand(g->formula, TRUE);
gtk_widget_set_halign(g->formula, GTK_ALIGN_END);
// Restore main container packing target
self->widget = main_vbox;
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"));
dt_bauhaus_slider_set_digits(g->gf_radius, 1);
gtk_widget_set_tooltip_text(g->gf_radius, _("size of the neighborhood used to guide the filter"));
g->gf_feathering = dt_bauhaus_slider_from_params(gf_section, "gf_feathering");
dt_bauhaus_slider_set_digits(g->gf_feathering, 1);
gtk_widget_set_tooltip_text(g->gf_feathering,
_("precision of the feathering: higher values produce softer, "
"less edge-sensitive transitions"));
g->gf_protect_center = dt_bauhaus_slider_new_with_range(
self, 0.0f, 0.5f, 0.f, 0.0f, 3);
dt_bauhaus_widget_set_label(g->gf_protect_center, NULL, _("protect near-neutrals"));
dt_bauhaus_slider_set_factor(g->gf_protect_center, 100.0f);
dt_bauhaus_slider_set_format(g->gf_protect_center, "%");
dt_bauhaus_slider_set_digits(g->gf_protect_center, 1);
gtk_widget_set_tooltip_text(g->gf_protect_center,
_("center of the saturation range protected from the guided filter"));
g_signal_connect(G_OBJECT(g->gf_protect_center), "value-changed",
G_CALLBACK(_protect_center_width_changed), self);
gtk_box_pack_start(GTK_BOX(gf_section->widget), g->gf_protect_center, TRUE, TRUE, 0);
g->gf_protect_width = dt_bauhaus_slider_new_with_range(
self, 0.0f, 0.3f, 0.f, 0.00f, 3);
dt_bauhaus_widget_set_label(g->gf_protect_width, NULL, _("protection transition width"));
dt_bauhaus_slider_set_format(g->gf_protect_width, "%");
dt_bauhaus_slider_set_digits(g->gf_protect_width, 0);
gtk_widget_set_tooltip_text(g->gf_protect_width,
_("width of the smooth transition around the protected center; wider "
"values give a gentler, less abrupt protection falloff"));
g_signal_connect(G_OBJECT(g->gf_protect_width), "value-changed",
G_CALLBACK(_protect_center_width_changed), self);
gtk_box_pack_start(GTK_BOX(gf_section->widget), g->gf_protect_width, TRUE, TRUE, 0);
g->noise_protection = dt_bauhaus_slider_from_params(gf_section, "noise_protection");
dt_bauhaus_slider_set_digits(g->noise_protection, 2);
dt_bauhaus_slider_set_format(g->noise_protection, "%");
gtk_widget_set_tooltip_text(g->noise_protection,
_("limit curve changes in noisy or uncertain regions"));
dt_gui_box_add(self->widget, gf_box);
}
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);
}
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