/*
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 .
*/
/*
DOCUMENTATION
This module implements a scene-referred local contrast enhancement algorithm,
designed to enhance local details while preserving edges and avoiding artifacts.
It builds upon the original proof-of-concept algorithm proposed by WileCoyote:
https://discuss.pixls.us/t/experiments-with-a-scene-referred-local-contrast-module-proof-of-concept/55402
And then further explored and optimized by Christian Bouhon
https://discuss.pixls.us/t/contrast-management-rgb-a-new-scene-referred-approach-poc/56004
Current status as implemented by Jandren:
- Local contrast in log space based on the eigf surface blur filter.
*/
/* Work around a compiler bug, see
https://github.com/darktable-org/darktable/issues/21801
*/
#if __GNUC__ == 16
#pragma GCC optimize ("no-ipa-cp")
#endif
#include "common/extra_optimizations.h"
#include
#include
#include
#include
#include "bauhaus/bauhaus.h"
#include "common/darktable.h"
#include "common/fast_guided_filter.h"
#include "common/eigf.h"
#include "common/luminance_mask.h"
#include "common/opencl.h"
#include "common/bilinear.h"
#include "common/gaussian.h"
#include "develop/tiling.h"
#include "control/conf.h"
#include "control/control.h"
#include "develop/blend.h"
#include "develop/develop.h"
#include "develop/imageop.h"
#include "develop/imageop_math.h"
#include "develop/imageop_gui.h"
#include "gui/accelerators.h"
#include "gui/draw.h"
#include "dtgtk/paint.h"
#include "dtgtk/togglebutton.h"
#include "dtgtk/expander.h"
#include "gui/gtk.h"
#include "gui/presets.h"
#include "iop/iop_api.h"
#include "common/iop_group.h"
#ifdef _OPENMP
#include
#endif
DT_MODULE_INTROSPECTION(2, dt_iop_contrastntexture_params_t)
typedef struct dt_iop_contrastntexture_params_t
{
float gain_coarse; // $MIN: -1.0 $MAX: 5.0 $DEFAULT: 0.0 $DESCRIPTION: "coarse details"
float gain_medium; // $MIN: -1.0 $MAX: 5.0 $DEFAULT: 0.0 $DESCRIPTION: "medium details"
float gain_fine; // $MIN: -1.0 $MAX: 5.0 $DEFAULT: 0.0 $DESCRIPTION: "fine details"
float detail_level; // $MIN: 1.0 $MAX: 15.0 $DEFAULT: 5.0 $DESCRIPTION: "base detail level"
float halo_control; // $MIN: -10.0 $MAX: 10.0 $DEFAULT: 0.0 $DESCRIPTION: "halo control"
float noise_bias; // $MIN: 0.0 $MAX: 1.0 $DEFAULT: 0.001 $DESCRIPTION: "noise bias"
float gain_shadows; // $MIN: -5.0 $MAX: 5.0 $DEFAULT: 0.0 $DESCRIPTION: "shadows"
float gain_highlights; // $MIN: -5.0 $MAX: 5.0 $DEFAULT: 0.0 $DESCRIPTION: "highlights"
} dt_iop_contrastntexture_params_t;
typedef enum dt_iop_contrastntexture_details_display_t
{
DT_LC_MASK_OFF = -1,
DT_LC_MASK_COARSE = 0,
DT_LC_MASK_MEDIUM = 1,
DT_LC_MASK_FINE = 2,
DT_LC_MASK_LAST = 3
} dt_iop_contrastntexture_details_display_t;
#define MAX_ITERATIONS 20
typedef struct dt_iop_contrastntexture_data_t
{
float gain_details[DT_LC_MASK_LAST];
float radius_details[MAX_ITERATIONS];
float scale_details[MAX_ITERATIONS];
float max_detail_level;
int max_used_level;
float feathering;
float noise_bias[MAX_ITERATIONS];
float slope_shadows;
float slope_highlights;
float midtones_width;
float midtones_polynomial[3];
} dt_iop_contrastntexture_data_t;
typedef struct dt_iop_contrastntexture_global_data_t
{
int kernel_luminance;
int kernel_details;
int kernel_apply;
int kernel_display_details;
int kernel_display_lowpass;
// the guided filter kernels of toneequal.cl, see _fast_eigf_surface_blur_cl()
int kernel_eigf_pack_2c;
int kernel_eigf_finish_2c;
int kernel_eigf_blend_no_mask;
} dt_iop_contrastntexture_global_data_t;
typedef struct dt_iop_contrastntexture_gui_data_t
{
// Flags
dt_iop_contrastntexture_details_display_t details_display;
// GTK widgets adjustments
GtkWidget *gain_details[DT_LC_MASK_LAST]; // coarse, medium, and fine
GtkWidget *gain_shadows;
GtkWidget *gain_highlights;
// GTK widgets filter settings
GtkWidget *detail_level;
GtkWidget *halo_control;
GtkWidget *noise_bias;
} dt_iop_contrastntexture_gui_data_t;
const char *name()
{
return _("contrast & texture");
}
const char *aliases()
{
return _("local contrast|texture|clarity|detail enhancement|highlights and shadows");
}
const char **description(dt_iop_module_t *self)
{
return dt_iop_set_description
(self, _("enhance local contrast by boosting details while preserving edges"),
_("creative"),
_("linear, RGB, scene-referred"),
_("linear, RGB"),
_("linear, RGB, scene-referred"));
}
int default_group()
{
return IOP_GROUP_EFFECTS;
}
int flags()
{
return IOP_FLAGS_INCLUDE_IN_STYLES | IOP_FLAGS_SUPPORTS_BLENDING;
}
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;
}
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)
{
typedef struct dt_iop_contrastntexture_params_v1_t
{
float gain_local_contrast;
float detail_level;
float edge_protection;
int filter_iterations;
float noise_bias;
} dt_iop_contrastntexture_params_v1_t;
dt_iop_contrastntexture_params_v1_t *o = (dt_iop_contrastntexture_params_v1_t *)old_params;
dt_iop_contrastntexture_params_t *n = malloc(sizeof(dt_iop_contrastntexture_params_t));
n->gain_coarse = o->gain_local_contrast - 1.0f;
n->gain_medium = o->gain_local_contrast - 1.0f;
n->gain_fine = o->gain_local_contrast - 1.0f;
n->detail_level = o->detail_level;
n->halo_control = o->edge_protection;
n->noise_bias = o->noise_bias;
n->gain_highlights = 0.0f;
n->gain_shadows = 0.0f;
*new_params = n;
*new_params_size = sizeof(dt_iop_contrastntexture_params_t);
*new_version = 2;
return 0;
}
return 1;
}
// Compute smoothed luminance mask using edge-aware filters
__DT_CLONE_TARGETS__
static inline void compute_luminance(const float *const restrict in,
float *const restrict luminance,
const dt_iop_roi_t *const roi_in,
const float noise_bias)
{
const size_t width = (size_t)roi_in->width;
const size_t height = (size_t)roi_in->height;
const size_t npixels = width * height;
// First compute pixel-wise luminance (no boost) and add noise bias
luminance_mask(in, luminance, width, height, DT_TONEEQ_NORM_2, 1.0f, 0.0f, 1.0f);
const float normalize_luminance = 1.0f / sqrtf(3.0f);
DT_OMP_FOR()
for(size_t k = 0; k < npixels; k++)
{
luminance[k] = normalize_luminance * luminance[k] + noise_bias;
}
}
// Compute smoothed luminance mask using edge-aware filters
__DT_CLONE_TARGETS__
static inline void compute_mask(float *const restrict smoothed_luminance,
const dt_iop_roi_t *const roi_in,
const dt_iop_contrastntexture_data_t *const d,
const int level)
{
const size_t width = (size_t)roi_in->width;
const size_t height = (size_t)roi_in->height;
fast_eigf_surface_blur(smoothed_luminance, width, height,
d->radius_details[level], d->feathering, 1,
DT_GF_BLENDING_LINEAR, 1.0f,
0.0f, NORM_MIN, 4.0f);
}
// Extract logarithmic high pass detail in log space (EV):
// How much brighter/darker is this pixel compared to the smooth version
__DT_CLONE_TARGETS__
static inline float extract_details(const float luminance_pixel,
const float luminance_smoothed,
const float noise_bias)
{
const float log_pixel = log2f(fmaxf(luminance_pixel, NORM_MIN));
const float log_smoothed = log2f(fmaxf(luminance_smoothed, NORM_MIN));
const float noise_power = noise_bias * noise_bias;
const float combined_power = luminance_smoothed * luminance_smoothed;
const float weiner_gain = fmaxf(combined_power - noise_power, 0.0f) / fmaxf(combined_power, NORM_MIN);
return weiner_gain * fmaxf(fminf(log_pixel - log_smoothed, 5.0f), -5.0f);
}
// Squared cosine/sine crossfade weight of a given band (coarse/medium/fine) at position t
static inline float band_weight(const dt_iop_contrastntexture_details_display_t band, const int level, const float max_level)
{
float t = (float)level / max_level - 0.5f;
t = fmaxf(fminf(t, 0.5f), -0.5f);
float weight = 0.0f;
switch(band)
{
case DT_LC_MASK_COARSE: weight = t < 0.0f ? sinf(M_PI_F * t) : 0.0f; break;
case DT_LC_MASK_MEDIUM: weight = cosf(M_PI_F * t); break;
case DT_LC_MASK_FINE: weight = t >= 0.0f ? sinf(M_PI_F * t) : 0.0f; break;
default: weight = 0.0f; break;
}
return weight * weight;
}
// Apply shadow and highlight enhancement
// Different slopes for shadows and highlights, with a smooth transition in the midtones
__DT_CLONE_TARGETS__
static inline float apply_shadows_highlights(const float luminance_lowpass,
const dt_iop_contrastntexture_data_t *const d)
{
const float slope_shadows = d->slope_shadows;
const float slope_highlights = d->slope_highlights;
const float midtones_width = d->midtones_width;
const float a0 = d->midtones_polynomial[0];
const float a1 = d->midtones_polynomial[1];
const float a2 = d->midtones_polynomial[2];
const float noise_bias = d->noise_bias[0]; // Applied on the base level
const float pivot_offset = log2f(noise_bias + 0.1845f);
float correction_ev = 0.0f;
// Low pass correction for shadows and highlights
const float normalized_ev = log2f(fmaxf(luminance_lowpass, NORM_MIN)) - pivot_offset;
if(normalized_ev <= -midtones_width)
correction_ev += slope_shadows * normalized_ev;
else if(normalized_ev >= midtones_width)
correction_ev += slope_highlights * normalized_ev;
else
{
const float shifted_ev = normalized_ev + midtones_width; // Shift to [0, 2*midtones_width] for polynomial evaluation
correction_ev += (a0 + a1 * shifted_ev + a2 * shifted_ev * shifted_ev);
}
return correction_ev - normalized_ev; // It is the difference from the identity line that matters for correction
}
/*
Display the detail mask (difference between pixel and smoothed luminance)
Output is a grayscale image normalized to [0, 1] where:
- 0.5 = no local detail (pixel matches neighborhood)
- < 0.5 = pixel darker than neighborhood
- > 0.5 = pixel brighter than neighborhood
*/
__DT_CLONE_TARGETS__
static inline void display_local_mask(const float *const restrict corrections,
float *const restrict out,
const dt_iop_roi_t *const roi_in)
{
const size_t npixels = roi_in->width * roi_in->height;
DT_OMP_FOR()
for(size_t k = 0; k < npixels; k++)
{
const float local_ev = corrections[k];
// Detail in log space, mapped to [0, 1] for display
// Detail range roughly [-2, +2] EV mapped to [0, 1]
const float intensity = local_ev / sqrtf(local_ev * local_ev + 1.0f) * 0.5f + 0.5f; // Smooth mapping to [0, 1]
// Set all RGB channels to the same intensity (grayscale)
for_each_channel(c)
{
out[4 * k + c] = intensity;
}
// Full opacity
out[4 * k + 3] = 1.0f;
}
}
/*
Display the final low pass filter result with a tint, i.e. the value fed into the
shadows (blue) and highlights (yellow) control, mapped to linear [0, 1].
*/
__DT_CLONE_TARGETS__
static inline void display_lowpass_mask(const float *const restrict luminance_lowpass,
float *const restrict out,
const dt_iop_roi_t *const roi_in,
const dt_iop_contrastntexture_data_t *const d)
{
const size_t npixels = roi_in->width * roi_in->height;
const float pivot_offset = d->noise_bias[0] + 0.1845f;
const dt_aligned_pixel_t blue = {0.0f, 0.0f, 1.0f};
const dt_aligned_pixel_t yellow = {1.0f, 1.0f, 0.0f};
DT_OMP_FOR()
for(size_t k = 0; k < npixels; k++)
{
// Low contrast sigmoid for the preview.
const float intensity = luminance_lowpass[k] / (pivot_offset + luminance_lowpass[k]);
float saturation = (intensity - 0.5f) * 2.0f;
saturation *= saturation;
for_each_channel(c)
{
if(intensity < 0.5f)
out[4 * k + c] = intensity * ((1.0f - saturation) + saturation * blue[c]);
else
out[4 * k + c] = intensity * ((1.0f - saturation) + saturation * yellow[c]);
}
out[4 * k + 3] = 1.0f;
}
}
void process(dt_iop_module_t *self,
dt_dev_pixelpipe_iop_t *piece,
const void *const restrict ivoid,
void *const restrict ovoid,
const dt_iop_roi_t *const roi_in,
const dt_iop_roi_t *const roi_out)
{
const dt_iop_contrastntexture_data_t *const d = piece->data;
dt_iop_contrastntexture_gui_data_t *const g = self->gui_data;
// Validate input format
if(!dt_iop_have_required_input_format(4, self, piece->colors,
ivoid, ovoid, roi_in, roi_out))
return;
const float *const restrict in = (float *const)ivoid;
float *const restrict out = (float *const)ovoid;
const size_t npixels = roi_in->width * roi_in->height;
float *restrict luminance_highpass = dt_alloc_align_float(npixels);
float *restrict luminance_lowpass = dt_alloc_align_float(npixels);
float *restrict corrections = dt_alloc_align_float(npixels);
if(!luminance_lowpass ||
!luminance_highpass ||
!corrections)
{
dt_control_log(_("contrast and texture failed to allocate memory, check your RAM settings"));
dt_free_align(luminance_highpass);
dt_free_align(luminance_lowpass);
dt_free_align(corrections);
return;
}
// The actual gain per level is computed here in process() instead of
// in commit params as it is affected by the display mask option.
float gain_per_level[MAX_ITERATIONS] = { 0.0f };
// Display output
dt_iop_contrastntexture_details_display_t details_display = DT_LC_MASK_OFF;
gboolean display_mask = FALSE;
if(self->dev->gui_attached && g && dt_pipe_is_full(piece->pipe))
{
dt_iop_gui_enter_critical_section(self);
details_display = g->details_display;
display_mask = g->details_display != DT_LC_MASK_OFF
&& dt_pipe_is_full(piece->pipe);
dt_iop_gui_leave_critical_section(self);
}
dt_print(DT_DEBUG_PIPE, "display_mask: %d, max_used_level: %d\n", display_mask, d->max_used_level);
if(display_mask)
{
piece->pipe->mask_display = DT_DEV_PIXELPIPE_DISPLAY_PASSTHRU;
// Preview the displayed band alone, at full strength, across all pyramid levels
// (the lowpass preview does not use band weights, the final luminance_lowpass suffices)
if(details_display != DT_LC_MASK_LAST)
{
for(int level = 0; level <= d->max_used_level; level++)
{
gain_per_level[level] = band_weight(details_display, level, d->max_detail_level);
}
}
}
else
{
// Smoothly interpolate the 3 sliders (coarse/medium/fine) across all pyramid
// levels using squared cosine/sine crossfade weights, so neighboring levels blend
// instead of jumping discretely between the 3 gains.
for(int level = 0; level <= d->max_used_level; level++)
{
for(int band = 0; band < DT_LC_MASK_LAST; band++)
gain_per_level[level] += d->gain_details[band] * band_weight(band, level, d->max_detail_level);
}
}
const float max_noise_bias = d->noise_bias[d->max_used_level];
compute_luminance(in, luminance_lowpass, roi_in, max_noise_bias);
memset(corrections, 0, npixels * sizeof(float));
for(int level = d->max_used_level; level >= 0; level--)
{
memcpy(luminance_highpass, luminance_lowpass, npixels * sizeof(float));
compute_mask(luminance_lowpass, roi_in, d, level);
DT_OMP_FOR()
for(size_t k = 0; k < npixels; k++)
{
// Details as the bandpass difference
corrections[k] += gain_per_level[level] * extract_details(luminance_highpass[k], luminance_lowpass[k], d->noise_bias[level]);
// Reduce the noise bias to the next level
if(level > 0)
luminance_lowpass[k] -= d->noise_bias[level] - d->noise_bias[level - 1];
}
}
if(display_mask)
{
if(details_display == DT_LC_MASK_LAST)
display_lowpass_mask(luminance_lowpass, out, roi_in, d);
else
display_local_mask(corrections, out, roi_in);
}
else
{
DT_OMP_FOR()
for(size_t k = 0; k < npixels; k++)
{
// Low pass correction for shadows and highlights
const float lowpass_correction = apply_shadows_highlights(luminance_lowpass[k], d);
// Apply correction in linear space
const float multiplier = exp2f(corrections[k] + lowpass_correction);;
for_each_channel(c)
out[4 * k + c] = in[4 * k + c] * multiplier;
out[4 * k + 3] = in[4 * k + 3];
}
}
dt_free_align(luminance_highpass);
dt_free_align(luminance_lowpass);
dt_free_align(corrections);
}
void modify_roi_in(dt_iop_module_t *self,
dt_dev_pixelpipe_iop_t *piece,
const dt_iop_roi_t *roi_out,
dt_iop_roi_t *roi_in)
{
dt_iop_contrastntexture_data_t *const d = piece->data;
// Get the scaled window radius for the box average
const float max_size = (float)((piece->iwidth > piece->iheight) ? piece->iwidth : piece->iheight);
for(int level = 0; level < MAX_ITERATIONS; level++)
{
const float base_diameter = max_size * roi_in->scale;
const float radius = 0.5f * fminf(d->scale_details[level], 0.5f) * base_diameter;
d->radius_details[level] = radius;
}
// Find the smallest level with non zero radius.
d->max_used_level = MAX_ITERATIONS - 1;
for(int level = MAX_ITERATIONS - 1; level >= 0; level--)
{
// If the radius is <2.0 pixels, the next level will be unused (<1.0)
if(d->radius_details[level] < 2.0f)
{
d->max_used_level = level;
}
}
}
void commit_params(dt_iop_module_t *self,
dt_iop_params_t *p1,
dt_dev_pixelpipe_t *pipe,
dt_dev_pixelpipe_iop_t *piece)
{
const dt_iop_contrastntexture_params_t *p = (dt_iop_contrastntexture_params_t *)p1;
dt_iop_contrastntexture_data_t *d = piece->data;
d->gain_details[DT_LC_MASK_COARSE] = p->gain_coarse;
d->gain_details[DT_LC_MASK_MEDIUM] = p->gain_medium;
d->gain_details[DT_LC_MASK_FINE] = p->gain_fine;
// Log slope of shadows and highlights, .i.e. the power the modify them with.
d->slope_shadows = powf(2.0f, -p->gain_shadows);
d->slope_highlights = powf(2.0f, p->gain_highlights);
// Quadratic polynomial for the midtones pivot transition.
// First order smooth with aligned slopes at [0, 2*width]
d->midtones_width = 0.3f;
d->midtones_polynomial[0] = -d->slope_shadows * d->midtones_width;
d->midtones_polynomial[1] = d->slope_shadows;
d->midtones_polynomial[2] = (d->slope_highlights - d->slope_shadows) / (4.0f * d->midtones_width);
const float max_piece_size = (float)((piece->iwidth > piece->iheight) ? piece->iwidth : piece->iheight);
const float max_image_size = max_piece_size * piece->iscale;
d->max_detail_level = log2f(max_image_size) - p->detail_level - 1.0f;
// UI contrast scale is inverse logarithmic with 0 as 100% of image width.
// Convert it to a linear scale for processing. Scales are separated by powers of 2 for each step in the UI.
// The noise bias is reduced for each level in the pyramid.
for(int level = 0; level < MAX_ITERATIONS; level++)
{
d->scale_details[level] = powf(2.0f, -p->detail_level - (float)level);
d->noise_bias[level] = p->noise_bias * powf(2.0f, fminf((float)level - d->max_detail_level, 0.0f));
}
// UI feathering is inverted (higher = stricter halo control).
const float default_feathering = 0.2f; // Base value based on Christian's experiments for a good balance of halo control and contrast boost at default settings.
d->feathering = default_feathering * powf(2.0f, -p->halo_control);
}
// The default implementation sizes piece->data by params_size, which is only correct while the data struct
// is no larger than the params struct. Ours is not: dt_iop_contrast_data_t carries contrast_scale, feathering and
// radius_local in place of the params' detail_level and edge_protection, and one field more besides, so noise_bias
// would land past the end of the allocation, written by commit_params and then read back by process().
void init_pipe(dt_iop_module_t *self,
dt_dev_pixelpipe_t *pipe,
dt_dev_pixelpipe_iop_t *piece)
{
piece->data = dt_calloc1_align_type(dt_iop_contrastntexture_data_t);
}
void cleanup_pipe(dt_iop_module_t *self,
dt_dev_pixelpipe_t *pipe,
dt_dev_pixelpipe_iop_t *piece)
{
dt_free_align(piece->data);
piece->data = NULL;
}
void 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,
dt_develop_tiling_t *tiling)
{
// in and out plus the three grey full size buffers of process(). On the CPU,
// fast_eigf_surface_blur() adds a full size mask and 4 channel averages; on
// the GPU, the averages stay downscaled
tiling->factor = 2.0f + 3.0f / 4.0f + 1.0f / 4.0f + 1.0f;
tiling->factor_cl = 2.0f + 3.0f / 4.0f + 1.0f / 4.0f;
tiling->maxbuf = 1.0f;
tiling->maxbuf_cl = 1.0f;
tiling->overhead = 0;
tiling->overlap = 0;
tiling->align = 1;
}
#ifdef HAVE_OPENCL
// fast_eigf_surface_blur() of common/eigf.h without quantization, which is how
// compute_mask() calls it. In place on a grey device buffer.
static cl_int _fast_eigf_surface_blur_cl(const int devid,
const dt_iop_contrastntexture_global_data_t *const gd,
cl_mem dev_image,
const int width,
const int height,
const float sigma,
const float feathering)
{
const float scaling = fmaxf(fminf(sigma, 4.0f), 1.0f);
const float ds_sigma = fmaxf(sigma / scaling, 1.0f);
const int ds_width = MAX(1, (int)((float)width / scaling));
const int ds_height = MAX(1, (int)((float)height / scaling));
const size_t ds_bsize = (size_t)ds_width * ds_height * sizeof(float);
const int blending = DT_GF_BLENDING_LINEAR;
cl_int err = CL_MEM_OBJECT_ALLOCATION_FAILURE;
cl_mem dev_ds_image = dt_opencl_alloc_device_buffer(devid, ds_bsize);
cl_mem dev_ds_av = dt_opencl_alloc_device_buffer(devid, 2 * ds_bsize);
if(!dev_ds_image || !dev_ds_av) goto error;
err = dt_interpolate_bilinear_cl(devid, dev_image, width, height,
dev_ds_image, ds_width, ds_height, 1);
if(err != CL_SUCCESS) goto error;
err = dt_opencl_enqueue_kernel_2d_args(devid, gd->kernel_eigf_pack_2c, ds_width, ds_height,
CLARG(dev_ds_image), CLARG(dev_ds_av), CLARG(ds_width), CLARG(ds_height));
if(err != CL_SUCCESS) goto error;
err = dt_gaussian_mean_blur_cl(devid, dev_ds_av, ds_width, ds_height, 2, ds_sigma);
if(err != CL_SUCCESS) goto error;
err = dt_opencl_enqueue_kernel_2d_args(devid, gd->kernel_eigf_finish_2c, ds_width, ds_height,
CLARG(dev_ds_av), CLARG(ds_width), CLARG(ds_height));
if(err != CL_SUCCESS) goto error;
err = dt_opencl_enqueue_kernel_2d_args(devid, gd->kernel_eigf_blend_no_mask, width, height,
CLARG(dev_image), CLARG(width), CLARG(height),
CLARG(dev_ds_av), CLARG(ds_width), CLARG(ds_height),
CLARG(blending), CLARG(feathering));
error:
dt_opencl_release_mem_object(dev_ds_av);
dt_opencl_release_mem_object(dev_ds_image);
return err;
}
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)
{
const dt_iop_contrastntexture_data_t *const d = piece->data;
const dt_iop_contrastntexture_global_data_t *const gd = self->global_data;
dt_iop_contrastntexture_gui_data_t *const g = self->gui_data;
if(piece->colors != 4) return DT_OPENCL_PROCESS_CL;
const int devid = piece->pipe->devid;
const int width = roi_in->width;
const int height = roi_in->height;
const size_t bsize = (size_t)width * height * sizeof(float);
// the gains per level and the mask display, as in process()
float gain_per_level[MAX_ITERATIONS] = { 0.0f };
dt_iop_contrastntexture_details_display_t details_display = DT_LC_MASK_OFF;
gboolean display_mask = FALSE;
if(self->dev->gui_attached && g && dt_pipe_is_full(piece->pipe))
{
dt_iop_gui_enter_critical_section(self);
details_display = g->details_display;
display_mask = g->details_display != DT_LC_MASK_OFF;
dt_iop_gui_leave_critical_section(self);
}
if(display_mask)
{
piece->pipe->mask_display = DT_DEV_PIXELPIPE_DISPLAY_PASSTHRU;
if(details_display != DT_LC_MASK_LAST)
for(int level = 0; level <= d->max_used_level; level++)
gain_per_level[level] = band_weight(details_display, level, d->max_detail_level);
}
else
{
for(int level = 0; level <= d->max_used_level; level++)
for(int band = 0; band < DT_LC_MASK_LAST; band++)
gain_per_level[level] += d->gain_details[band] * band_weight(band, level, d->max_detail_level);
}
cl_int err = CL_MEM_OBJECT_ALLOCATION_FAILURE;
cl_mem dev_highpass = dt_opencl_alloc_device_buffer(devid, bsize);
cl_mem dev_lowpass = dt_opencl_alloc_device_buffer(devid, bsize);
cl_mem dev_corrections = dt_opencl_alloc_device_buffer(devid, bsize);
if(!dev_highpass || !dev_lowpass || !dev_corrections) goto error;
const float max_noise_bias = d->noise_bias[d->max_used_level];
err = dt_opencl_enqueue_kernel_2d_args(devid, gd->kernel_luminance, width, height,
CLARG(dev_in), CLARG(dev_lowpass), CLARG(dev_highpass), CLARG(width), CLARG(height),
CLARG(max_noise_bias));
if(err != CL_SUCCESS) goto error;
for(int level = d->max_used_level; level >= 0; level--)
{
err = _fast_eigf_surface_blur_cl(devid, gd, dev_lowpass, width, height,
d->radius_details[level], d->feathering);
if(err != CL_SUCCESS) goto error;
const float gain = gain_per_level[level];
const float noise_bias = d->noise_bias[level];
const float next_bias_step = level > 0 ? d->noise_bias[level] - d->noise_bias[level - 1] : 0.0f;
// the first level starts the sum, so the corrections need no clearing
const int accumulate = level < d->max_used_level;
err = dt_opencl_enqueue_kernel_2d_args(devid, gd->kernel_details, width, height,
CLARG(dev_highpass), CLARG(dev_lowpass), CLARG(dev_corrections),
CLARG(width), CLARG(height), CLARG(gain), CLARG(noise_bias), CLARG(next_bias_step),
CLARG(accumulate));
if(err != CL_SUCCESS) goto error;
}
if(display_mask && details_display == DT_LC_MASK_LAST)
{
const float pivot_offset = d->noise_bias[0] + 0.1845f;
err = dt_opencl_enqueue_kernel_2d_args(devid, gd->kernel_display_lowpass, width, height,
CLARG(dev_out), CLARG(dev_lowpass), CLARG(width), CLARG(height), CLARG(pivot_offset));
}
else if(display_mask)
{
err = dt_opencl_enqueue_kernel_2d_args(devid, gd->kernel_display_details, width, height,
CLARG(dev_out), CLARG(dev_corrections), CLARG(width), CLARG(height));
}
else
{
const float polynomial[4] = { d->midtones_polynomial[0], d->midtones_polynomial[1],
d->midtones_polynomial[2], 0.0f };
const float pivot_offset = log2f(d->noise_bias[0] + 0.1845f);
err = dt_opencl_enqueue_kernel_2d_args(devid, gd->kernel_apply, width, height,
CLARG(dev_in), CLARG(dev_out), CLARG(dev_lowpass), CLARG(dev_corrections),
CLARG(width), CLARG(height), CLARG(d->slope_shadows), CLARG(d->slope_highlights),
CLARG(d->midtones_width), CLFLARRAY(4, polynomial), CLARG(pivot_offset));
}
error:
dt_opencl_release_mem_object(dev_corrections);
dt_opencl_release_mem_object(dev_lowpass);
dt_opencl_release_mem_object(dev_highpass);
return err;
}
#endif
void init_global(dt_iop_module_so_t *self)
{
const int program = 46; // contrastntexture.cl, from programs.conf
const int program_toneequal = 45; // toneequal.cl
dt_iop_contrastntexture_global_data_t *gd = malloc(sizeof(dt_iop_contrastntexture_global_data_t));
self->data = gd;
gd->kernel_luminance = dt_opencl_create_kernel(program, "contrastntexture_luminance");
gd->kernel_details = dt_opencl_create_kernel(program, "contrastntexture_details");
gd->kernel_apply = dt_opencl_create_kernel(program, "contrastntexture_apply");
gd->kernel_display_details = dt_opencl_create_kernel(program, "contrastntexture_display_details");
gd->kernel_display_lowpass = dt_opencl_create_kernel(program, "contrastntexture_display_lowpass");
gd->kernel_eigf_pack_2c = dt_opencl_create_kernel(program_toneequal, "toneequal_eigf_pack_2c");
gd->kernel_eigf_finish_2c = dt_opencl_create_kernel(program_toneequal, "toneequal_eigf_finish_2c");
gd->kernel_eigf_blend_no_mask = dt_opencl_create_kernel(program_toneequal, "toneequal_eigf_blend_no_mask");
}
void cleanup_global(dt_iop_module_so_t *self)
{
const dt_iop_contrastntexture_global_data_t *gd = self->data;
dt_opencl_free_kernel(gd->kernel_luminance);
dt_opencl_free_kernel(gd->kernel_details);
dt_opencl_free_kernel(gd->kernel_apply);
dt_opencl_free_kernel(gd->kernel_display_details);
dt_opencl_free_kernel(gd->kernel_display_lowpass);
dt_opencl_free_kernel(gd->kernel_eigf_pack_2c);
dt_opencl_free_kernel(gd->kernel_eigf_finish_2c);
dt_opencl_free_kernel(gd->kernel_eigf_blend_no_mask);
free(self->data);
self->data = NULL;
}
static void show_details_callback(GtkWidget *togglebutton, dt_iop_module_t *self)
{
DT_GUARD_GUI_UPDATE();
// early return if blend module is already displaying a mask
if(self->request_mask_display)
{
dt_control_log(_("cannot display masks when the blending mask is displayed"));
dt_bauhaus_widget_set_quad_active(GTK_WIDGET(togglebutton), FALSE);
return;
}
dt_iop_request_focus(self);
// Activate the module if it wasn't
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(self->off), TRUE);
dt_iop_contrastntexture_gui_data_t *g = self->gui_data;
const gboolean toggle_is_active = dt_bauhaus_widget_get_quad_active(GTK_WIDGET(togglebutton));
dt_iop_gui_enter_critical_section(self);
g->details_display = DT_LC_MASK_OFF;
if(toggle_is_active)
{
if(togglebutton == g->detail_level)
{
g->details_display = DT_LC_MASK_LAST;
}
else
{
for(int i = 0; i < DT_LC_MASK_LAST; i++)
{
if(togglebutton == g->gain_details[i])
{
g->details_display = i;
break;
}
}
}
}
dt_iop_gui_leave_critical_section(self);
for(int i = 0; i < DT_LC_MASK_LAST; i++)
{
dt_bauhaus_widget_set_quad_active(GTK_WIDGET(g->gain_details[i]), g->details_display == i);
}
dt_bauhaus_widget_set_quad_active(GTK_WIDGET(g->detail_level), g->details_display == DT_LC_MASK_LAST);
dt_iop_refresh_center(self);
}
void gui_focus(dt_iop_module_t *self, const gboolean in)
{
DT_GUARD_GUI_UPDATE();
if(!in)
{
dt_iop_contrastntexture_gui_data_t *g = self->gui_data;
const gboolean was_mask = g->details_display != DT_LC_MASK_OFF;
g->details_display = DT_LC_MASK_OFF;
for(int i = 0; i < DT_LC_MASK_LAST; i++)
dt_bauhaus_widget_set_quad_active(GTK_WIDGET(g->gain_details[i]), FALSE);
dt_bauhaus_widget_set_quad_active(GTK_WIDGET(g->detail_level), FALSE);
if(was_mask)
dt_iop_refresh_center(self);
}
}
void gui_init(dt_iop_module_t *self)
{
dt_iop_contrastntexture_gui_data_t *g = IOP_GUI_ALLOC(contrastntexture);
dt_iop_gui_enter_critical_section(self);
g->details_display = DT_LC_MASK_OFF;
dt_iop_gui_leave_critical_section(self);
// Main container
self->widget = dt_gui_vbox();
g->detail_level = dt_bauhaus_slider_from_params(self, "detail_level");
dt_bauhaus_slider_set_soft_range(g->detail_level, 1.0, 10.0);
gtk_widget_set_tooltip_text(g->detail_level,
_("adjust the detail level used for highlights, shadows, and coarse details.\n"
"higher = more contrast boost in finer details\n"
"lower = more contrast boost in coarser details\n"
"press the mask button to preview the low pass filter result used for shadows/highlights."));
dt_bauhaus_widget_set_quad(g->detail_level, self, dtgtk_cairo_paint_showmask, TRUE, show_details_callback,
_("preview the low pass filter result used for shadows(blue)/highlights(yellow)."));
// Highlights and shadows sliders
g->gain_highlights = dt_bauhaus_slider_from_params(self, "gain_highlights");
dt_bauhaus_slider_set_soft_range(g->gain_highlights, -2.0, 2.0);
gtk_widget_set_tooltip_text(g->gain_highlights,
_("adjust highlights at the base detail level size."));
g->gain_shadows = dt_bauhaus_slider_from_params(self, "gain_shadows");
dt_bauhaus_slider_set_soft_range(g->gain_shadows, -2.0, 2.0);
gtk_widget_set_tooltip_text(g->gain_shadows,
_("adjust shadows at the base detail level size."));
dt_gui_box_add(self->widget, dt_ui_section_label_new(_("local contrast")));
// Details boost sliders
const char *labels[DT_LC_MASK_LAST] = {"gain_coarse", "gain_medium", "gain_fine"};
for(int i = 0; i < DT_LC_MASK_LAST; i++)
{
g->gain_details[i] = dt_bauhaus_slider_from_params(self, labels[i]);
dt_bauhaus_slider_set_soft_range(g->gain_details[i], -1.0, 1.5);
dt_bauhaus_slider_set_digits(g->gain_details[i], 2);
dt_bauhaus_slider_set_format(g->gain_details[i], "%");
dt_bauhaus_slider_set_factor(g->gain_details[i], 100.0);
}
gtk_widget_set_tooltip_text(g->gain_details[DT_LC_MASK_COARSE],
_("adjust coarse, low frequency content.\n"
"press the mask button to preview the effect."));
dt_bauhaus_widget_set_quad(g->gain_details[DT_LC_MASK_COARSE], self, dtgtk_cairo_paint_showmask, TRUE, show_details_callback,
_("preview the size of the coarse details to adjust."));
gtk_widget_set_tooltip_text(g->gain_details[DT_LC_MASK_MEDIUM],
_("adjust medium, frequency content between coarse and fine.\n"
"press the mask button to preview the effect."));
dt_bauhaus_widget_set_quad(g->gain_details[DT_LC_MASK_MEDIUM], self, dtgtk_cairo_paint_showmask, TRUE, show_details_callback,
_("preview the size of the medium details to adjust."));
gtk_widget_set_tooltip_text(g->gain_details[DT_LC_MASK_FINE],
_("adjust fine, high frequency content.\n"
"press the mask button to preview the effect."));
dt_bauhaus_widget_set_quad(g->gain_details[DT_LC_MASK_FINE], self, dtgtk_cairo_paint_showmask, TRUE, show_details_callback,
_("preview the size of the fine details to adjust."));
// Filter settings section
dt_gui_box_add(self->widget, dt_ui_section_label_new(C_("section", "filter settings")));
g->halo_control = dt_bauhaus_slider_from_params(self, "halo_control");
dt_bauhaus_slider_set_soft_range(g->halo_control, -5.0, 5.0);
dt_bauhaus_slider_set_digits(g->halo_control, 2);
dt_bauhaus_slider_set_format(g->halo_control, "%");
dt_bauhaus_slider_set_factor(g->halo_control, 100.0);
gtk_widget_set_tooltip_text(g->halo_control, _("adjust the halo control of the filter.\n"
"higher = suppress halos at the expense of details around edges.\n"
"lower = allow more halos to get more local contrast and details."));
g->noise_bias = dt_bauhaus_slider_from_params(self, "noise_bias");
dt_bauhaus_slider_set_soft_range(g->noise_bias, 0.0, 0.2);
dt_bauhaus_slider_set_digits(g->noise_bias, 4);
dt_bauhaus_slider_set_step(g->noise_bias, 0.0001);
gtk_widget_set_tooltip_text(g->noise_bias, _("add bias to reduce shadow noise amplification.\n"
"only affects dark parts of the image."));
}
// 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