Ctrl K
ComunidadGuías…

src/iop/contrastntexture.c

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
/*
    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 <http://www.gnu.org/licenses/>.
*/

/*
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 <math.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

#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 <omp.h>
#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