src/iop/colorbalancergb.c
Rama master · réplica de lectura del repositorio original.
/*
This file is part of darktable,
Copyright (C) 2020-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/>.
*/
// our includes go first:
#include "bauhaus/bauhaus.h"
#include "common/exif.h"
#include "common/dttypes.h"
#include "common/chromatic_adaptation.h"
#include "common/darktable_ucs_22_helpers.h"
#include "common/gamut_mapping.h"
#include "common/opencl.h"
#include "develop/blend.h"
#include "develop/imageop.h"
#include "develop/imageop_math.h"
#include "develop/openmp_maths.h"
#include "develop/imageop_gui.h"
#include "dtgtk/drawingarea.h"
#include "dtgtk/gradientslider.h"
#include "gui/accelerators.h"
#include "gui/draw.h"
#include "gui/gtk.h"
#include "gui/presets.h"
#include "gui/color_picker_proxy.h"
#include "iop/iop_api.h"
//#include <gtk/gtk.h>
#include <stdlib.h>
#define STEPS 92 // so we test 92×92×92 combinations of RGB in [0; 1] to build the gamut LUT
// Filmlight Yrg puts red at 330°, while usual HSL wheels put it at 360/0°
// so shift in GUI only it to not confuse people. User params are always degrees,
// pixel params are always radians.
#define ANGLE_SHIFT -30.f
#define CONVENTIONAL_DEG_TO_YRG_RAD(x) (deg2radf(x + ANGLE_SHIFT))
#define YRG_RAD_TO_CONVENTIONAL_DEG(x) (rad2degf(x) - ANGLE_SHIFT)
DT_MODULE_INTROSPECTION(5, dt_iop_colorbalancergb_params_t)
typedef enum dt_iop_colorbalancrgb_saturation_t
{
DT_COLORBALANCE_SATURATION_JZAZBZ = 0, // $DESCRIPTION: "JzAzBz (2021)"
DT_COLORBALANCE_SATURATION_DTUCS = 1 // $DESCRIPTION: "darktable UCS (2022)"
} dt_iop_colorbalancrgb_saturation_t;
typedef struct dt_iop_colorbalancergb_params_t
{
/* params of v1 */
float shadows_Y; // $MIN: -1.0 $MAX: 1.0 $DEFAULT: 0.0 $DESCRIPTION: "luminance"
float shadows_C; // $MIN: 0.0 $MAX: 1.0 $DEFAULT: 0.0 $DESCRIPTION: "chroma"
float shadows_H; // $MIN: 0.0 $MAX: 360.0 $DEFAULT: 0.0 $DESCRIPTION: "hue"
float midtones_Y; // $MIN: -1.0 $MAX: 1.0 $DEFAULT: 0.0 $DESCRIPTION: "luminance"
float midtones_C; // $MIN: 0.0 $MAX: 1.0 $DEFAULT: 0.0 $DESCRIPTION: "chroma"
float midtones_H; // $MIN: 0.0 $MAX: 360.0 $DEFAULT: 0.0 $DESCRIPTION: "hue"
float highlights_Y; // $MIN: -1.0 $MAX: 1.0 $DEFAULT: 0.0 $DESCRIPTION: "luminance"
float highlights_C; // $MIN: 0.0 $MAX: 1.0 $DEFAULT: 0.0 $DESCRIPTION: "chroma"
float highlights_H; // $MIN: 0.0 $MAX: 360.0 $DEFAULT: 0.0 $DESCRIPTION: "hue"
float global_Y; // $MIN: -1.0 $MAX: 1.0 $DEFAULT: 0.0 $DESCRIPTION: "luminance"
float global_C; // $MIN: 0.0 $MAX: 1.0 $DEFAULT: 0.0 $DESCRIPTION: "chroma"
float global_H; // $MIN: 0.0 $MAX: 360.0 $DEFAULT: 0.0 $DESCRIPTION: "hue"
float shadows_weight; // $MIN: 0.0 $MAX: 3.0 $DEFAULT: 1.0 $DESCRIPTION: "shadows fall-off"
float white_fulcrum; // $MIN: -16.0 $MAX: 16.0 $DEFAULT: 0.0 $DESCRIPTION: "white fulcrum"
float highlights_weight; // $MIN: 0.0 $MAX: 3.0 $DEFAULT: 1.0 $DESCRIPTION: "highlights fall-off"
float chroma_shadows; // $MIN: -1.0 $MAX: 1.0 $DEFAULT: 0.0 $DESCRIPTION: "shadows"
float chroma_highlights; // $MIN: -1.0 $MAX: 1.0 $DEFAULT: 0.0 $DESCRIPTION: "highlights"
float chroma_global; // $MIN: -1.0 $MAX: 1.0 $DEFAULT: 0.0 $DESCRIPTION: "global chroma"
float chroma_midtones; // $MIN: -1.0 $MAX: 1.0 $DEFAULT: 0.0 $DESCRIPTION: "mid-tones"
float saturation_global; // $MIN: -1.0 $MAX: 1.0 $DEFAULT: 0.0 $DESCRIPTION: "global saturation"
float saturation_highlights; // $MIN: -1.0 $MAX: 1.0 $DEFAULT: 0.0 $DESCRIPTION: "highlights"
float saturation_midtones; // $MIN: -1.0 $MAX: 1.0 $DEFAULT: 0.0 $DESCRIPTION: "mid-tones"
float saturation_shadows; // $MIN: -1.0 $MAX: 1.0 $DEFAULT: 0.0 $DESCRIPTION: "shadows"
float hue_angle; // $MIN: -180. $MAX: 180. $DEFAULT: 0.0 $DESCRIPTION: "hue shift"
/* params of v2 */
float brilliance_global; // $MIN: -1.0 $MAX: 1.0 $DEFAULT: 0.0 $DESCRIPTION: "global brilliance"
float brilliance_highlights; // $MIN: -1.0 $MAX: 1.0 $DEFAULT: 0.0 $DESCRIPTION: "highlights"
float brilliance_midtones; // $MIN: -1.0 $MAX: 1.0 $DEFAULT: 0.0 $DESCRIPTION: "mid-tones"
float brilliance_shadows; // $MIN: -1.0 $MAX: 1.0 $DEFAULT: 0.0 $DESCRIPTION: "shadows"
/* params of v3 */
float mask_grey_fulcrum; // $MIN: 0.0 $MAX: 1.0 $DEFAULT: 0.1845 $DESCRIPTION: "mask middle-gray fulcrum"
/* params of v4 */
float vibrance; // $MIN: -1.0 $MAX: 1.0 $DEFAULT: 0 $DESCRIPTION: "global vibrance"
float grey_fulcrum; // $MIN: 0.0 $MAX: 1.0 $DEFAULT: 0.1845 $DESCRIPTION: "contrast gray fulcrum"
float contrast; // $MIN: -1.0 $MAX: 1.0 $DEFAULT: 0. $DESCRIPTION: "contrast"
/* params of v5 */
dt_iop_colorbalancrgb_saturation_t saturation_formula; // $DEFAULT: 1 $DESCRIPTION: "saturation formula"
/* add future params after this so the legacy params import can use a blind memcpy */
} dt_iop_colorbalancergb_params_t;
typedef enum dt_iop_colorbalancergb_mask_data_t
{
MASK_SHADOWS = 0,
MASK_MIDTONES = 1,
MASK_HIGHLIGHTS = 2,
MASK_NONE
} dt_iop_colorbalancergb_mask_data_t;
typedef struct dt_iop_colorbalancergb_gui_data_t
{
GtkWidget *shadows_H, *midtones_H, *highlights_H, *global_H;
GtkWidget *shadows_C, *midtones_C, *highlights_C, *global_C;
GtkWidget *shadows_Y, *midtones_Y, *highlights_Y, *global_Y;
GtkWidget *shadows_weight, *mask_grey_fulcrum, *highlights_weight;
GtkWidget *chroma_highlights, *chroma_global, *chroma_shadows, *chroma_midtones, *vibrance;
GtkWidget *contrast, *grey_fulcrum, *white_fulcrum;
GtkWidget *saturation_global, *saturation_highlights, *saturation_midtones, *saturation_shadows;
GtkWidget *brilliance_global, *brilliance_highlights, *brilliance_midtones, *brilliance_shadows;
GtkWidget *saturation_formula;
GtkWidget *hue_angle;
GtkDrawingArea *area;
GtkNotebook *notebook;
GtkWidget *checker_color_1_picker, *checker_color_2_picker, *checker_size;
gboolean mask_display;
dt_iop_colorbalancergb_mask_data_t mask_type;
const dt_iop_order_iccprofile_info_t *sliders_output_profile;
} dt_iop_colorbalancergb_gui_data_t;
typedef struct dt_iop_colorbalancergb_data_t
{
float global[4];
float shadows[4];
float highlights[4];
float midtones[4];
float midtones_Y;
float chroma_global, chroma[4], vibrance, contrast;
float saturation_global, saturation[4];
float brilliance_global, brilliance[4];
float hue_angle;
float shadows_weight, highlights_weight, midtones_weight, mask_grey_fulcrum;
float white_fulcrum, grey_fulcrum;
float *gamut_LUT;
float max_chroma;
dt_aligned_pixel_t checker_color_1, checker_color_2;
dt_iop_colorbalancrgb_saturation_t saturation_formula;
size_t checker_size;
gboolean lut_inited;
struct dt_iop_order_iccprofile_info_t *work_profile;
} dt_iop_colorbalancergb_data_t;
typedef struct dt_iop_colorbalance_global_data_t
{
int kernel_colorbalance_rgb;
} dt_iop_colorbalancergb_global_data_t;
const char *name()
{
return _("color balance rgb");
}
const char *aliases()
{
return _("offset power slope|cdl|color grading|contrast|chroma_highlights|hue|vibrance|saturation");
}
const char **description(dt_iop_module_t *self)
{
return dt_iop_set_description(self, _("color grading tools using alpha masks to separate\n"
"shadows, mid-tones and highlights"),
_("corrective or creative"),
_("linear, RGB, scene-referred"),
_("non-linear, RGB"),
_("non-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;
}
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)
{
typedef struct dt_iop_colorbalancergb_params_v5_t
{
/* params of v1 */
float shadows_Y;
float shadows_C;
float shadows_H;
float midtones_Y;
float midtones_C;
float midtones_H;
float highlights_Y;
float highlights_C;
float highlights_H;
float global_Y;
float global_C;
float global_H;
float shadows_weight;
float white_fulcrum;
float highlights_weight;
float chroma_shadows;
float chroma_highlights;
float chroma_global;
float chroma_midtones;
float saturation_global;
float saturation_highlights;
float saturation_midtones;
float saturation_shadows;
float hue_angle;
/* params of v2 */
float brilliance_global;
float brilliance_highlights;
float brilliance_midtones;
float brilliance_shadows;
/* params of v3 */
float mask_grey_fulcrum;
/* params of v4 */
float vibrance;
float grey_fulcrum;
float contrast;
/* params of v5 */
dt_iop_colorbalancrgb_saturation_t saturation_formula;
/* add future params after this so the legacy params import can use a blind memcpy */
} dt_iop_colorbalancergb_params_v5_t;
const dt_iop_colorbalancergb_params_v5_t default_v5 =
{ 0.0f, 0.0f, 0.0f, 0.0f, 0.0f,
0.0f, 0.0f, 0.0f, 0.0f, 0.0f,
0.0f, 0.0f, 1.0f, 0.0f, 1.0f,
0.0f, 0.0f, 0.0f, 0.0f, 0.0f,
0.0f, 0.0f, 0.0f, 0.0f, 0.0f,
0.0f, 0.0f, 0.0f, 0.1845f, 0.0f,
0.0f, 0.0f, 1
};
if(old_version == 1)
{
typedef struct dt_iop_colorbalancergb_params_v1_t
{
float shadows_Y;
float shadows_C;
float shadows_H;
float midtones_Y;
float midtones_C;
float midtones_H;
float highlights_Y;
float highlights_C;
float highlights_H;
float global_Y;
float global_C;
float global_H;
float shadows_weight;
float white_fulcrum;
float highlights_weight;
float chroma_shadows;
float chroma_highlights;
float chroma_global;
float chroma_midtones;
float saturation_global;
float saturation_highlights;
float saturation_midtones;
float saturation_shadows;
float hue_angle;
} dt_iop_colorbalancergb_params_v1_t;
const dt_iop_colorbalancergb_params_v1_t *o = old_params;
dt_iop_colorbalancergb_params_v5_t *n =
malloc(sizeof(dt_iop_colorbalancergb_params_v5_t));
// Init params with defaults
memcpy(n, &default_v5, sizeof(dt_iop_colorbalancergb_params_v5_t));
// Copy the common part of the params struct
memcpy(n, o, sizeof(dt_iop_colorbalancergb_params_v1_t));
n->saturation_global /= 100.f;
n->mask_grey_fulcrum = 0.1845f;
n->vibrance = 0.f;
n->grey_fulcrum = 0.1845f;
n->contrast = 0.f;
n->saturation_formula = DT_COLORBALANCE_SATURATION_JZAZBZ;
*new_params = n;
*new_params_size = sizeof(dt_iop_colorbalancergb_params_v5_t);
*new_version = 5;
return 0;
}
if(old_version == 2)
{
typedef struct dt_iop_colorbalancergb_params_v2_t
{
/* params of v1 */
float shadows_Y;
float shadows_C;
float shadows_H;
float midtones_Y;
float midtones_C;
float midtones_H;
float highlights_Y;
float highlights_C;
float highlights_H;
float global_Y;
float global_C;
float global_H;
float shadows_weight;
float white_fulcrum;
float highlights_weight;
float chroma_shadows;
float chroma_highlights;
float chroma_global;
float chroma_midtones;
float saturation_global;
float saturation_highlights;
float saturation_midtones;
float saturation_shadows;
float hue_angle;
/* params of v2 */
float brilliance_global;
float brilliance_highlights;
float brilliance_midtones;
float brilliance_shadows;
} dt_iop_colorbalancergb_params_v2_t;
const dt_iop_colorbalancergb_params_v2_t *o = old_params;
dt_iop_colorbalancergb_params_v5_t *n =
malloc(sizeof(dt_iop_colorbalancergb_params_v5_t));
// Init params with defaults
memcpy(n, &default_v5, sizeof(dt_iop_colorbalancergb_params_v5_t));
// Copy the common part of the params struct
memcpy(n, o, sizeof(dt_iop_colorbalancergb_params_v2_t));
n->mask_grey_fulcrum = 0.1845f;
n->vibrance = 0.f;
n->grey_fulcrum = 0.1845f;
n->contrast = 0.f;
n->saturation_formula = DT_COLORBALANCE_SATURATION_JZAZBZ;
*new_params = n;
*new_params_size = sizeof(dt_iop_colorbalancergb_params_v5_t);
*new_version = 5;
return 0;
}
if(old_version == 3)
{
typedef struct dt_iop_colorbalancergb_params_v3_t
{
/* params of v1 */
float shadows_Y;
float shadows_C;
float shadows_H;
float midtones_Y;
float midtones_C;
float midtones_H;
float highlights_Y;
float highlights_C;
float highlights_H;
float global_Y;
float global_C;
float global_H;
float shadows_weight;
float white_fulcrum;
float highlights_weight;
float chroma_shadows;
float chroma_highlights;
float chroma_global;
float chroma_midtones;
float saturation_global;
float saturation_highlights;
float saturation_midtones;
float saturation_shadows;
float hue_angle;
/* params of v2 */
float brilliance_global;
float brilliance_highlights;
float brilliance_midtones;
float brilliance_shadows;
/* params of v3 */
float mask_grey_fulcrum;
} dt_iop_colorbalancergb_params_v3_t;
const dt_iop_colorbalancergb_params_v3_t *o = old_params;
dt_iop_colorbalancergb_params_v5_t *n =
malloc(sizeof(dt_iop_colorbalancergb_params_v5_t));
// Init params with defaults
memcpy(n, &default_v5, sizeof(dt_iop_colorbalancergb_params_v5_t));
// Copy the common part of the params struct
memcpy(n, o, sizeof(dt_iop_colorbalancergb_params_v3_t));
n->vibrance = 0.f;
n->grey_fulcrum = 0.1845f;
n->contrast = 0.f;
n->saturation_formula = DT_COLORBALANCE_SATURATION_JZAZBZ;
*new_params = n;
*new_params_size = sizeof(dt_iop_colorbalancergb_params_v5_t);
*new_version = 5;
return 0;
}
if(old_version == 4)
{
typedef struct dt_iop_colorbalancergb_params_v4_t
{
/* params of v1 */
float shadows_Y;
float shadows_C;
float shadows_H;
float midtones_Y;
float midtones_C;
float midtones_H;
float highlights_Y;
float highlights_C;
float highlights_H;
float global_Y;
float global_C;
float global_H;
float shadows_weight;
float white_fulcrum;
float highlights_weight;
float chroma_shadows;
float chroma_highlights;
float chroma_global;
float chroma_midtones;
float saturation_global;
float saturation_highlights;
float saturation_midtones;
float saturation_shadows;
float hue_angle;
/* params of v2 */
float brilliance_global;
float brilliance_highlights;
float brilliance_midtones;
float brilliance_shadows;
/* params of v3 */
float mask_grey_fulcrum;
/* params of v4 */
float vibrance;
float grey_fulcrum;
float contrast;
} dt_iop_colorbalancergb_params_v4_t;
const dt_iop_colorbalancergb_params_v4_t *o = old_params;
dt_iop_colorbalancergb_params_v5_t *n =
malloc(sizeof(dt_iop_colorbalancergb_params_v5_t));
// Init params with defaults
memcpy(n, &default_v5, sizeof(dt_iop_colorbalancergb_params_v5_t));
// Copy the common part of the params struct
memcpy(n, o, sizeof(dt_iop_colorbalancergb_params_v4_t));
n->saturation_formula = DT_COLORBALANCE_SATURATION_JZAZBZ;
*new_params = n;
*new_params_size = sizeof(dt_iop_colorbalancergb_params_v5_t);
*new_version = 5;
return 0;
}
return 1;
}
void init_presets(dt_iop_module_so_t *self)
{
// Note : all the elements of the params structure are scalar floats,
// so we can just init them all to 0.f in batch
// Then, only 4 params have to be manually inited to non-zero values
dt_iop_colorbalancergb_params_t p = { 0.f };
p.shadows_weight = 1.f; // DEFAULT: 1.0 DESCRIPTION: "shadows fall-off"
p.highlights_weight = 1.f; // DEFAULT: 1.0 DESCRIPTION: "highlights fall-off"
p.mask_grey_fulcrum = 0.1845f; // DEFAULT: 0.1845 DESCRIPTION: "mask middle-gray fulcrum"
p.grey_fulcrum = 0.1845f; // DEFAULT: 0.1845 DESCRIPTION: "contrast gray fulcrum"
p.saturation_formula = DT_COLORBALANCE_SATURATION_JZAZBZ;
// preset
p.chroma_global = 0.2f;
p.saturation_shadows = 0.1f;
p.saturation_midtones = 0.05f;
p.saturation_highlights = -0.05f;
dt_gui_presets_add_generic(_("basic colorfulness | legacy"), self->op, self->version(), &p, sizeof(p), TRUE, DEVELOP_BLEND_CS_RGB_SCENE);
p.saturation_formula = DT_COLORBALANCE_SATURATION_DTUCS;
p.chroma_global = 0.f;
p.saturation_global = 0.2f;
p.saturation_shadows = 0.30f;
p.saturation_midtones = 0.f;
p.saturation_highlights = -0.5f;
dt_gui_presets_add_generic(_("basic colorfulness | natural skin"), self->op, self->version(), &p, sizeof(p), TRUE, DEVELOP_BLEND_CS_RGB_SCENE);
p.saturation_global = 0.2f;
p.saturation_shadows = 0.5f;
p.saturation_midtones = 0.f;
p.saturation_highlights = -0.25f;
dt_gui_presets_add_generic(_("basic colorfulness | vibrant colors"), self->op, self->version(), &p, sizeof(p), TRUE, DEVELOP_BLEND_CS_RGB_SCENE);
p.saturation_global = 0.2f;
p.saturation_shadows = 0.25f;
p.saturation_midtones = 0.f;
p.saturation_highlights = -0.25f;
dt_gui_presets_add_generic(_("basic colorfulness | standard"), self->op, self->version(), &p, sizeof(p), TRUE, DEVELOP_BLEND_CS_RGB_SCENE);
}
DT_OMP_DECLARE_SIMD(aligned(output, output_comp: 16) uniform(shadows_weight, midtones_weight, highlights_weight))
static inline void opacity_masks(const float x,
const float shadows_weight, const float highlights_weight,
const float midtones_weight, const float mask_grey_fulcrum,
dt_aligned_pixel_t output, dt_aligned_pixel_t output_comp)
{
const float x_offset = (x - mask_grey_fulcrum);
const float x_offset_norm = x_offset / mask_grey_fulcrum;
const float alpha = 1.f / (1.f + expf(x_offset_norm * shadows_weight)); // opacity of shadows
const float beta = 1.f / (1.f + expf(-x_offset_norm * highlights_weight)); // opacity of highlights
const float alpha_comp = 1.f - alpha;
const float beta_comp = 1.f - beta;
const float gamma = expf(-sqf(x_offset) * midtones_weight / 4.f) * sqf(alpha_comp) * sqf(beta_comp) * 8.f; // opacity of midtones
const float gamma_comp = 1.f - gamma;
output[0] = alpha;
output[1] = gamma;
output[2] = beta;
output[3] = 0.f;
if(output_comp)
{
output_comp[0] = alpha_comp;
output_comp[1] = gamma_comp;
output_comp[2] = beta_comp;
output_comp[3] = 0.f;
}
}
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_colorbalancergb_data_t *d = piece->data;
dt_iop_colorbalancergb_gui_data_t *g = self->gui_data;
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; // no point
// work profile can't be fetched in commit_params since it is not yet initialised
// work_profile->matrix_in === RGB_to_XYZ
// work_profile->matrix_out === XYZ_to_RGB
// Premultiply the input matrices
/* What we do here is equivalent to :
// go to CIE 1931 XYZ 2° D50
dot_product(RGB, RGB_to_XYZ, XYZ_D50); // matrice product
// chroma adapt D50 to D65
XYZ_D50_to_65(XYZ_D50, XYZ_D65); // matrice product
// go to CIE 2006 LMS
XYZ_to_LMS(XYZ_D65, LMS); // matrice product
* so we pre-multiply the 3 conversion matrices and operate only one matrix product
*/
dt_colormatrix_t input_matrix = { { 0.0f } };
dt_colormatrix_t output_matrix = { { 0.0f } };
dt_colormatrix_mul(output_matrix, XYZ_D50_to_D65_CAT16, work_profile->matrix_in); // output_matrix used as temp buffer
dt_colormatrix_mul(input_matrix, XYZ_D65_to_LMS_2006_D65, output_matrix);
dt_colormatrix_t input_matrix_trans;
dt_colormatrix_transpose(input_matrix_trans, input_matrix);
// Premultiply the output matrix
/* What we do here is equivalent to :
XYZ_D65_to_50(XYZ_D65, XYZ_D50); // matrix product
dot_product(XYZ_D50, XYZ_to_RGB, pix_out); // matrix product
*/
dt_colormatrix_mul(output_matrix, work_profile->matrix_out, XYZ_D65_to_D50_CAT16);
dt_colormatrix_t output_matrix_trans;
dt_colormatrix_transpose(output_matrix_trans, output_matrix);
const float *const restrict in = DT_IS_ALIGNED(((const float *const restrict)ivoid));
float *const restrict out = DT_IS_ALIGNED(((float *const restrict)ovoid));
const float *const restrict gamut_LUT = DT_IS_ALIGNED(((const float *const restrict)d->gamut_LUT));
const float *const restrict global = DT_IS_ALIGNED_PIXEL((const float *const restrict)d->global);
const float *const restrict highlights = DT_IS_ALIGNED_PIXEL((const float *const restrict)d->highlights);
const float *const restrict shadows = DT_IS_ALIGNED_PIXEL((const float *const restrict)d->shadows);
const float *const restrict midtones = DT_IS_ALIGNED_PIXEL((const float *const restrict)d->midtones);
const float *const restrict chroma = DT_IS_ALIGNED_PIXEL((const float *const restrict)d->chroma);
const float *const restrict saturation = DT_IS_ALIGNED_PIXEL((const float *const restrict)d->saturation);
const float *const restrict brilliance = DT_IS_ALIGNED_PIXEL((const float *const restrict)d->brilliance);
gboolean mask_display = FALSE;
dt_iop_colorbalancergb_mask_data_t mask_type = MASK_NONE;
if(self->dev->gui_attached && g && dt_pipe_is_full(piece->pipe))
{
dt_iop_gui_enter_critical_section(self);
mask_display = g->mask_display;
mask_type = g->mask_type;
dt_iop_gui_leave_critical_section(self);
}
// pixel size of the checker background
const size_t checker_1 = mask_display ? DT_PIXEL_APPLY_DPI(d->checker_size) : 0;
const size_t checker_2 = 2 * checker_1;
const float L_white = Y_to_dt_UCS_L_star(d->white_fulcrum);
const float DT_ALIGNED_ARRAY hue_rotation_matrix[2][2] = {
{ cosf(d->hue_angle), -sinf(d->hue_angle) },
{ sinf(d->hue_angle), cosf(d->hue_angle) },
};
const size_t npixels = (size_t)roi_out->height * roi_out->width;
const size_t out_width = roi_out->width;
DT_OMP_FOR()
for(size_t k = 0; k < 4 * npixels; k += 4)
{
// clip pipeline RGB
dt_aligned_pixel_t RGB;
copy_pixel(RGB, in + k);
dt_vector_clipneg(RGB);
// go to CIE 2006 LMS D65
dt_aligned_pixel_t LMS;
dt_apply_transposed_color_matrix(RGB, input_matrix_trans, LMS);
/* The previous line is equivalent to :
// go to CIE 1931 XYZ 2° D50
dot_product(RGB, RGB_to_XYZ, XYZ_D50); // matrice product
// chroma adapt D50 to D65
XYZ_D50_to_65(XYZ_D50, XYZ_D65); // matrice product
// go to CIE 2006 LMS
XYZ_to_LMS(XYZ_D65, LMS); // matrice product
*/
// go to Filmlight Yrg
dt_aligned_pixel_t Yrg = { 0.f };
LMS_to_Yrg(LMS, Yrg);
// go to Ych
dt_aligned_pixel_t Ych = { 0.f };
Yrg_to_Ych(Yrg, Ych);
// Sanitize input : no negative luminance
Ych[0] = MAX(Ych[0], 0.f);
// Opacities for luma masks
dt_aligned_pixel_t opacities;
dt_aligned_pixel_t opacities_comp;
opacity_masks(powf(Ych[0], 0.4101205819200422f), // center middle grey in 50 %
d->shadows_weight, d->highlights_weight, d->midtones_weight,
d->mask_grey_fulcrum, opacities, opacities_comp);
// Hue shift - do it now because we need the gamut limit at output hue right after
// The hue rotation is implemented as a matrix multiplication.
const float cos_h = Ych[2];
const float sin_h = Ych[3];
Ych[2] = hue_rotation_matrix[0][0] * cos_h + hue_rotation_matrix[0][1] * sin_h;
Ych[3] = hue_rotation_matrix[1][0] * cos_h + hue_rotation_matrix[1][1] * sin_h;
// Linear chroma : distance to achromatic at constant luminance in scene-referred
const float chroma_boost = d->chroma_global + scalar_product(opacities, chroma);
const float vibrance = d->vibrance * (1.0f - powf(Ych[1], fabsf(d->vibrance)));
const float chroma_factor = MAX(1.f + chroma_boost + vibrance, 0.f);
Ych[1] *= chroma_factor;
// clip chroma at constant hue and Y if needed
gamut_check_Yrg(Ych);
// go to Yrg for real
Ych_to_Yrg(Ych, Yrg);
// Go to LMS
Yrg_to_LMS(Yrg, LMS);
// Go to Filmlight RGB
LMS_to_gradingRGB(LMS, RGB);
// Color balance
for_four_channels(c, aligned(RGB, global))
{
// global : offset
RGB[c] += global[c];
}
for_four_channels(c, aligned(RGB, opacities, opacities_comp, shadows, midtones, highlights:16))
{
// highlights, shadows : 2 slopes with masking
RGB[c] *= opacities_comp[2] * (opacities_comp[0] + opacities[0] * shadows[c]) + opacities[2] * highlights[c];
// factorization of : (RGB[c] * (1.f - alpha) + RGB[c] * d->shadows[c] * alpha) * (1.f - beta) + RGB[c] * d->highlights[c] * beta;
}
dt_aligned_pixel_t sign;
for_each_channel(c)
sign[c] = (RGB[c] < 0.f) ? -1.f : 1.f;
dt_aligned_pixel_t abs_RGB;
for_each_channel(c)
abs_RGB[c] = fabsf(RGB[c]);
dt_aligned_pixel_t scaled_RGB;
for_each_channel(c)
scaled_RGB[c] = abs_RGB[c] /d->white_fulcrum;
dt_vector_powf(scaled_RGB, midtones, RGB);
for_each_channel(c)
RGB[c] = RGB[c] * sign[c] * d->white_fulcrum;
// for the non-linear ops we need to go in Yrg again because RGB doesn't preserve color
gradingRGB_to_LMS(RGB, LMS);
LMS_to_Yrg(LMS, Yrg);
// Y midtones power (gamma)
Yrg[0] = powf(MAX(Yrg[0] / d->white_fulcrum, 0.f), d->midtones_Y) * d->white_fulcrum;
// Y fulcrumed contrast
Yrg[0] = d->grey_fulcrum * powf(Yrg[0] / d->grey_fulcrum, d->contrast);
Yrg_to_LMS(Yrg, LMS);
dt_aligned_pixel_t XYZ_D65 = { 0.f };
LMS_to_XYZ(LMS, XYZ_D65);
// Perceptual color adjustments
if(d->saturation_formula == DT_COLORBALANCE_SATURATION_JZAZBZ)
{
dt_aligned_pixel_t Jab = { 0.f };
dt_XYZ_2_JzAzBz(XYZ_D65, Jab);
// Convert to JCh
float JC[2] = { Jab[0], dt_fast_hypotf(Jab[1], Jab[2]) }; // brightness/chroma vector
const float h = atan2f(Jab[2], Jab[1]); // hue : (a, b) angle
// Project JC onto S, the saturation eigenvector, with orthogonal vector O.
// Note : O should be = (C * cosf(T) - J * sinf(T)) = 0 since S is the eigenvector,
// so we add the chroma projected along the orthogonal axis to get some control value
const float T = atan2f(JC[1], JC[0]); // angle of the eigenvector over the hue plane
const float sin_T = sinf(T);
const float cos_T = cosf(T);
const float DT_ALIGNED_PIXEL M_rot_dir[2][2] = { { cos_T, sin_T },
{ -sin_T, cos_T } };
const float DT_ALIGNED_PIXEL M_rot_inv[2][2] = { { cos_T, -sin_T },
{ sin_T, cos_T } };
float SO[2];
// brilliance & Saturation : mix of chroma and luminance
const float boosts[2] = { 1.f + d->brilliance_global + scalar_product(opacities, brilliance), // move in S direction
d->saturation_global + scalar_product(opacities, saturation) }; // move in O direction
SO[0] = JC[0] * M_rot_dir[0][0] + JC[1] * M_rot_dir[0][1];
SO[1] = SO[0] * MIN(MAX(T * boosts[1], -T), M_PI_2f - T);
SO[0] = MAX(SO[0] * boosts[0], 0.f);
// Project back to JCh, that is rotate back of -T angle
JC[0] = MAX(SO[0] * M_rot_inv[0][0] + SO[1] * M_rot_inv[0][1], 0.f);
JC[1] = MAX(SO[0] * M_rot_inv[1][0] + SO[1] * M_rot_inv[1][1], 0.f);
// Gamut mapping
const float out_max_sat_h = lookup_gamut(gamut_LUT, h);
// if JC[0] == 0.f, the saturation / luminance ratio is infinite - assign the largest practical value we have
const float sat = (JC[0] > 0.f) ? soft_clip(JC[1] / JC[0], 0.8f * out_max_sat_h, out_max_sat_h)
: out_max_sat_h;
const float max_C_at_sat = JC[0] * sat;
// if sat == 0.f, the chroma is zero - assign the original luminance because there's no need to gamut map
const float max_J_at_sat = (sat > 0.f) ? JC[1] / sat : JC[0];
JC[0] = (JC[0] + max_J_at_sat) / 2.f;
JC[1] = (JC[1] + max_C_at_sat) / 2.f;
// Gamut-clip in Jch at constant hue and lightness,
// e.g. find the max chroma available at current hue that doesn't
// yield negative L'M'S' values, which will need to be clipped during conversion
const float cos_H = cosf(h);
const float sin_H = sinf(h);
const float d0 = 1.6295499532821566e-11f;
const float dd = -0.56f;
float Iz = JC[0] + d0;
Iz /= (1.f + dd - dd * Iz);
Iz = MAX(Iz, 0.f);
static const dt_colormatrix_t AI_trans
= { { 1.0f, 1.0f, 1.0f, 0.0f },
{ 0.1386050432715393f, -0.1386050432715393f, -0.0960192420263190f, 0.0f },
{ 0.0580473161561189f, -0.0580473161561189f, -0.8118918960560390f, 0.0f } };
// Do a test conversion to L'M'S'
const dt_aligned_pixel_t IzAzBz = { Iz, JC[1] * cos_H, JC[1] * sin_H, 0.f };
dt_apply_transposed_color_matrix(IzAzBz, AI_trans, LMS);
// Clip chroma
float max_C = JC[1];
if(LMS[0] < 0.f)
max_C = MIN(-Iz / (AI_trans[1][0] * cos_H + AI_trans[2][0] * sin_H), max_C);
if(LMS[1] < 0.f)
max_C = MIN(-Iz / (AI_trans[1][1] * cos_H + AI_trans[2][1] * sin_H), max_C);
if(LMS[2] < 0.f)
max_C = MIN(-Iz / (AI_trans[1][2] * cos_H + AI_trans[2][2] * sin_H), max_C);
// Project back to JzAzBz for real
Jab[0] = JC[0];
Jab[1] = max_C * cos_H;
Jab[2] = max_C * sin_H;
dt_JzAzBz_2_XYZ(Jab, XYZ_D65);
}
else
{
dt_aligned_pixel_t xyY, JCH, HCB;
dt_D65_XYZ_to_xyY(XYZ_D65, xyY);
xyY_to_dt_UCS_JCH(xyY, L_white, JCH);
dt_UCS_JCH_to_HCB(JCH, HCB);
const float radius = dt_fast_hypotf(HCB[1], HCB[2]);
const float sin_T = (radius > 0.f) ? HCB[1] / radius : 0.f;
const float cos_T = (radius > 0.f) ? HCB[2] / radius : 0.f;
const float DT_ALIGNED_PIXEL M_rot_inv[2][2] = { { cos_T, sin_T }, { -sin_T, cos_T } };
// This would be the full matrice of direct rotation if we didn't need only its last row
//const float DT_ALIGNED_PIXEL M_rot_dir[2][2] = { { cos_T, -sin_T }, { sin_T, cos_T } };
const float P = MAX(FLT_MIN, HCB[1]); // as HCB[1] is at least zero we don't fiddle with sign
const float W = sin_T * HCB[1] + cos_T * HCB[2];
float a = MAX(1.f + d->saturation_global + scalar_product(opacities, saturation), 0.f);
const float b = MAX(1.f + d->brilliance_global + scalar_product(opacities, brilliance), 0.f);
const float max_a = dt_fast_hypotf(P, W) / P;
a = soft_clip(a, 0.5f * max_a, max_a);
const float P_prime = (a - 1.f) * P;
const float W_prime = sqrtf(sqf(P) * (1.f - sqf(a)) + sqf(W)) * b;
HCB[1] = MAX(M_rot_inv[0][0] * P_prime + M_rot_inv[0][1] * W_prime, 0.f);
HCB[2] = MAX(M_rot_inv[1][0] * P_prime + M_rot_inv[1][1] * W_prime, 0.f);
dt_UCS_HCB_to_JCH(HCB, JCH);
// Gamut mapping
const float max_colorfulness = lookup_gamut(gamut_LUT, JCH[2]); // WARNING : this is M²
const float max_chroma = (15.932993652962535f * powf(JCH[0] * L_white, 0.6523997524738018f)
* powf(max_colorfulness, 0.6007557017508491f) / L_white);
const dt_aligned_pixel_t JCH_gamut_boundary = { JCH[0], max_chroma, JCH[2], 0.f };
dt_aligned_pixel_t HSB_gamut_boundary;
dt_UCS_JCH_to_HSB(JCH_gamut_boundary, HSB_gamut_boundary);
// Clip saturation at constant brightness
dt_aligned_pixel_t HSB = { HCB[0], (HCB[2] > 0.f) ? HCB[1] / HCB[2] : 0.f, HCB[2], 0.f };
HSB[1] = soft_clip(HSB[1], 0.8f * HSB_gamut_boundary[1], HSB_gamut_boundary[1]);
dt_UCS_HSB_to_JCH(HSB, JCH);
dt_UCS_JCH_to_xyY(JCH, L_white, xyY);
dt_xyY_to_XYZ(xyY, XYZ_D65);
}
// Project back to D50 pipeline RGB
dt_aligned_pixel_t pix_out;
dt_apply_transposed_color_matrix(XYZ_D65, output_matrix_trans, pix_out);
/* The previous line is equivalent to :
XYZ_D65_to_50(XYZ_D65, XYZ_D50); // matrix product
dot_product(XYZ_D50, XYZ_to_RGB, pix_out); // matrix product
*/
if(mask_display)
{
// draw checkerboard
dt_aligned_pixel_t color;
const size_t i = (k / 4) / out_width;
const size_t j = (k / 4) % out_width;
if(i % checker_1 < i % checker_2)
{
if(j % checker_1 < j % checker_2)
copy_pixel(color, d->checker_color_2);
else
copy_pixel(color, d->checker_color_1);
}
else
{
if(j % checker_1 < j % checker_2)
copy_pixel(color, d->checker_color_1);
else
copy_pixel(color, d->checker_color_2);
}
float opacity = opacities[mask_type];
const float opacity_comp = 1.0f - opacity;
dt_vector_clipneg(pix_out);
for_four_channels(c, aligned(pix_out, color:16))
pix_out[c] = opacity_comp * color[c] + opacity * pix_out[c];
pix_out[3] = 1.0f; // alpha is opaque, we need to preview it
}
else
{
dt_vector_clipneg(pix_out);
}
copy_pixel_nontemporal(out + k, pix_out);
}
dt_omploop_sfence(); // ensure all nontemporal writes complete before we use them
}
#if HAVE_OPENCL
int process_cl(dt_iop_module_t *self,
dt_dev_pixelpipe_iop_t *piece,
cl_mem dev_in, cl_mem dev_out,
const dt_iop_roi_t *const roi_in,
const dt_iop_roi_t *const roi_out)
{
const dt_iop_colorbalancergb_data_t *const d = piece->data;
const dt_iop_colorbalancergb_global_data_t *const gd = self->global_data;
const dt_iop_colorbalancergb_gui_data_t *g = self->gui_data;
cl_int err = DT_OPENCL_DEFAULT_ERROR;
if(piece->colors != 4)
{
dt_control_log(_("colorbalance works only on RGB input"));
return err;
}
const int devid = piece->pipe->devid;
const int width = roi_in->width;
const int height = roi_in->height;
// Get working color profile
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; // no point
cl_mem dev_profile_info = NULL;
cl_mem dev_profile_lut = NULL;
dt_colorspaces_iccprofile_info_cl_t *profile_info_cl;
cl_float *profile_lut_cl = NULL;
cl_mem input_matrix_cl = NULL;
cl_mem output_matrix_cl = NULL;
cl_mem gamut_LUT_cl = NULL;
cl_mem hue_rotation_matrix_cl = NULL;
err = dt_ioppr_build_iccprofile_params_cl(work_profile, devid, &profile_info_cl, &profile_lut_cl,
&dev_profile_info, &dev_profile_lut);
if(err != CL_SUCCESS) goto error;
// repack the matrices as flat AVX2-compliant matrice
// work profile can't be fetched in commit_params since it is not yet initialised
// work_profile->matrix_in === RGB_to_XYZ
// work_profile->matrix_out === XYZ_to_RGB
// Premultiply the input matrices
/* What we do here is equivalent to :
// go to CIE 1931 XYZ 2° D50
dot_product(RGB, RGB_to_XYZ, XYZ_D50); // matrice product
// chroma adapt D50 to D65
XYZ_D50_to_65(XYZ_D50, XYZ_D65); // matrice product
// go to CIE 2006 LMS
XYZ_to_LMS(XYZ_D65, LMS); // matrice product
* so we pre-multiply the 3 conversion matrices and operate only one matrix product
*/
dt_colormatrix_t input_matrix;
dt_colormatrix_t output_matrix;
dt_colormatrix_mul(output_matrix, XYZ_D50_to_D65_CAT16, work_profile->matrix_in); // output_matrix used as temp buffer
dt_colormatrix_mul(input_matrix, XYZ_D65_to_LMS_2006_D65, output_matrix);
// Premultiply the output matrix
/* What we do here is equivalent to :
XYZ_D65_to_50(XYZ_D65, XYZ_D50); // matrix product
dot_product(XYZ_D50, XYZ_to_RGB, pix_out); // matrix product
*/
dt_colormatrix_mul(output_matrix, work_profile->matrix_out, XYZ_D65_to_D50_CAT16);
input_matrix_cl = dt_opencl_copy_host_to_device_constant(devid, 12 * sizeof(float), input_matrix);
output_matrix_cl = dt_opencl_copy_host_to_device_constant(devid, 12 * sizeof(float), output_matrix);
gamut_LUT_cl = dt_opencl_copy_host_to_device_constant(devid, LUT_ELEM * sizeof(float), d->gamut_LUT);
// Size of the checker
gboolean mask_display = FALSE;
dt_iop_colorbalancergb_mask_data_t mask_type = MASK_NONE;
if(self->dev->gui_attached && g && dt_pipe_is_full(piece->pipe))
{
dt_iop_gui_enter_critical_section(self);
mask_display = g->mask_display;
mask_type = g->mask_type;
dt_iop_gui_leave_critical_section(self);
}
const int checker_1 = mask_display ? DT_PIXEL_APPLY_DPI(d->checker_size) : 0;
const int checker_2 = 2 * checker_1;
const float L_white = Y_to_dt_UCS_L_star(d->white_fulcrum);
float hue_rotation_matrix[4]
= { cosf(d->hue_angle), -sinf(d->hue_angle), sinf(d->hue_angle), cosf(d->hue_angle) };
hue_rotation_matrix_cl = dt_opencl_copy_host_to_device_constant(devid, 4 * sizeof(float), hue_rotation_matrix);
if(input_matrix_cl == NULL || output_matrix_cl == NULL || gamut_LUT_cl == NULL || hue_rotation_matrix_cl == NULL)
{
err = CL_MEM_OBJECT_ALLOCATION_FAILURE;
goto error;
}
err = dt_opencl_enqueue_kernel_2d_args(devid, gd->kernel_colorbalance_rgb, width, height,
CLARG(dev_in), CLARG(dev_out),
CLARG(width), CLARG(height), CLARG(dev_profile_info), CLARG(input_matrix_cl), CLARG(output_matrix_cl),
CLARG(gamut_LUT_cl), CLARG(d->shadows_weight), CLARG(d->highlights_weight), CLARG(d->midtones_weight),
CLARG(d->mask_grey_fulcrum), CLARG(d->hue_angle), CLARG(d->chroma_global), CLARG(d->chroma), CLARG(d->vibrance),
CLARG(d->global), CLARG(d->shadows), CLARG(d->highlights), CLARG(d->midtones), CLARG(d->white_fulcrum),
CLARG(d->midtones_Y), CLARG(d->grey_fulcrum), CLARG(d->contrast), CLARG(d->brilliance_global),
CLARG(d->brilliance), CLARG(d->saturation_global), CLARG(d->saturation), CLARG(mask_display), CLARG(mask_type),
CLARG(checker_1), CLARG(checker_2), CLARG(d->checker_color_1), CLARG(d->checker_color_2), CLARG(L_white),
CLARG(d->saturation_formula), CLARG(hue_rotation_matrix_cl));
error:
dt_ioppr_free_iccprofile_params_cl(&profile_info_cl, &profile_lut_cl, &dev_profile_info, &dev_profile_lut);
dt_opencl_release_mem_object(input_matrix_cl);
dt_opencl_release_mem_object(output_matrix_cl);
dt_opencl_release_mem_object(gamut_LUT_cl);
dt_opencl_release_mem_object(hue_rotation_matrix_cl);
return err;
}
void init_global(dt_iop_module_so_t *self)
{
const int program = 8; // extended.cl in programs.conf
dt_iop_colorbalancergb_global_data_t *gd = malloc(sizeof(dt_iop_colorbalancergb_global_data_t));
self->data = gd;
gd->kernel_colorbalance_rgb = dt_opencl_create_kernel(program, "colorbalancergb");
}
void cleanup_global(dt_iop_module_so_t *self)
{
const dt_iop_colorbalancergb_global_data_t *gd = self->data;
dt_opencl_free_kernel(gd->kernel_colorbalance_rgb);
free(self->data);
self->data = NULL;
}
#endif
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_colorbalancergb_data_t *d = piece->data;
const dt_iop_colorbalancergb_params_t *p = (dt_iop_colorbalancergb_params_t *)p1;
d->checker_color_1[0] = CLAMP(dt_conf_get_float("plugins/darkroom/colorbalancergb/checker1/red"), 0.f, 1.f);
d->checker_color_1[1] = CLAMP(dt_conf_get_float("plugins/darkroom/colorbalancergb/checker1/green"), 0.f, 1.f);
d->checker_color_1[2] = CLAMP(dt_conf_get_float("plugins/darkroom/colorbalancergb/checker1/blue"), 0.f, 1.f);
d->checker_color_1[3] = 1.f;
d->checker_color_2[0] = CLAMP(dt_conf_get_float("plugins/darkroom/colorbalancergb/checker2/red"), 0.f, 1.f);
d->checker_color_2[1] = CLAMP(dt_conf_get_float("plugins/darkroom/colorbalancergb/checker2/green"), 0.f, 1.f);
d->checker_color_2[2] = CLAMP(dt_conf_get_float("plugins/darkroom/colorbalancergb/checker2/blue"), 0.f, 1.f);
d->checker_color_2[3] = 1.f;
d->checker_size = MAX(dt_conf_get_int("plugins/darkroom/colorbalancergb/checker/size"), 2);
d->vibrance = p->vibrance;
d->contrast = 1.0f + p->contrast; // that limits the user param range to [-1, 1], but it seems enough
d->grey_fulcrum = p->grey_fulcrum;
d->chroma_global = p->chroma_global;
d->chroma[0] = p->chroma_shadows;
d->chroma[1] = p->chroma_midtones;
d->chroma[2] = p->chroma_highlights;
d->chroma[3] = 0.f;
d->saturation_global = p->saturation_global;
d->saturation[0] = p->saturation_shadows;
d->saturation[1] = p->saturation_midtones;
d->saturation[2] = p->saturation_highlights;
d->saturation[3] = 0.f;
d->brilliance_global = p->brilliance_global;
d->brilliance[0] = p->brilliance_shadows;
d->brilliance[1] = p->brilliance_midtones;
d->brilliance[2] = p->brilliance_highlights;
d->brilliance[3] = 0.f;
d->hue_angle = deg2radf(p->hue_angle);
// measure the grading RGB of a pure white
const dt_aligned_pixel_t Ych_norm = { 1.f, 0.f, 1.f, 0.f };
dt_aligned_pixel_t RGB_norm = { 0.f };
Ych_to_gradingRGB(Ych_norm, RGB_norm);
dt_aligned_pixel_t Ych;
// global
{
make_Ych(1.f, p->global_C, CONVENTIONAL_DEG_TO_YRG_RAD(p->global_H), Ych);
Ych_to_gradingRGB(Ych, d->global);
for(size_t c = 0; c < 4; c++) d->global[c] = (d->global[c] - RGB_norm[c]) + RGB_norm[c] * p->global_Y;
}
// shadows
{
make_Ych(1.f, p->shadows_C, CONVENTIONAL_DEG_TO_YRG_RAD(p->shadows_H), Ych);
Ych_to_gradingRGB(Ych, d->shadows);
for(size_t c = 0; c < 4; c++) d->shadows[c] = 1.f + (d->shadows[c] - RGB_norm[c]) + p->shadows_Y;
d->shadows_weight = 2.f + p->shadows_weight * 2.f;
}
// highlights
{
make_Ych(1.f, p->highlights_C, CONVENTIONAL_DEG_TO_YRG_RAD(p->highlights_H), Ych);
Ych_to_gradingRGB(Ych, d->highlights);
for(size_t c = 0; c < 4; c++) d->highlights[c] = 1.f + (d->highlights[c] - RGB_norm[c]) + p->highlights_Y;
d->highlights_weight = 2.f + p->highlights_weight * 2.f;
}
// midtones
{
make_Ych(1.f, p->midtones_C, CONVENTIONAL_DEG_TO_YRG_RAD(p->midtones_H), Ych);
Ych_to_gradingRGB(Ych, d->midtones);
for(size_t c = 0; c < 4; c++) d->midtones[c] = 1.f / (1.f + (d->midtones[c] - RGB_norm[c]));
d->midtones_Y = 1.f / (1.f + p->midtones_Y);
d->white_fulcrum = exp2f(p->white_fulcrum);
d->midtones_weight = sqf(d->shadows_weight) * sqf(d->highlights_weight) /
(sqf(d->shadows_weight) + sqf(d->highlights_weight));
d->mask_grey_fulcrum = powf(p->mask_grey_fulcrum, 0.4101205819200422f);
}
if(p->saturation_formula != d->saturation_formula) d->lut_inited = FALSE;
d->saturation_formula = p->saturation_formula;
// Check if the RGB working profile has changed in pipe
// WARNING: this function is not triggered upon working profile change,
// so the gamut boundaries are wrong until we change some param in this module
dt_iop_order_iccprofile_info_t *const work_profile = dt_ioppr_get_pipe_current_profile_info(self, piece->pipe);
if(work_profile == NULL) return;
if(work_profile != d->work_profile)
{
d->lut_inited = FALSE;
d->work_profile = work_profile;
}
// find the maximum chroma allowed by the current working gamut in conjunction to hue
// this will be used to prevent users to mess up their images by pushing chroma out of gamut
if(!d->lut_inited)
{
// Premultiply both matrices to go from D50 pipeline RGB to D65 XYZ in a single matrix dot product
// instead of D50 pipeline to D50 XYZ (work_profile->matrix_in) and then D50 XYZ to D65 XYZ
dt_colormatrix_t input_matrix;
dt_colormatrix_mul(input_matrix, XYZ_D50_to_D65_CAT16, work_profile->matrix_in);
float *gamut_LUT = d->gamut_LUT;
// make RGB values vary between [0; 1] in working space, convert to Ych and get the max(c(h)))
if(p->saturation_formula == DT_COLORBALANCE_SATURATION_JZAZBZ)
{
float *const restrict sampler = dt_calloc_align_float(LUT_ELEM);
DT_OMP_FOR(reduction(max: sampler[:LUT_ELEM]) collapse(3))
for(size_t r = 0; r < STEPS; r++)
for(size_t g = 0; g < STEPS; g++)
for(size_t b = 0; b < STEPS; b++)
{
const dt_aligned_pixel_t rgb = { (float)r / (float)(STEPS - 1), (float)g / (float)(STEPS - 1),
(float)b / (float)(STEPS - 1), 0.f };
dt_aligned_pixel_t XYZ = { 0.f };
float saturation = 0.f;
float hue = 0.f;
dot_product(rgb, input_matrix, XYZ); // Go from D50 pipeline RGB to D65 XYZ in one step
dt_aligned_pixel_t Jab, Jch;
dt_XYZ_2_JzAzBz(XYZ, Jab); // this one expects D65 XYZ
Jch[0] = Jab[0];
Jch[1] = dt_fast_hypotf(Jab[2], Jab[1]);
Jch[2] = atan2f(Jab[2], Jab[1]);
saturation = (Jch[0] > 0.f) ? Jch[1] / Jch[0] : 0.f;
hue = Jch[2];
int index = roundf((LUT_ELEM - 1) * (hue + M_PI_F) / DT_2PI_F);
index += (index < 0) ? LUT_ELEM : 0;
index -= (index >= LUT_ELEM) ? LUT_ELEM : 0;
sampler[index] = fmaxf(sampler[index], saturation);
}
// anti-aliasing on the LUT (simple 5-taps 1D box average)
for(size_t k = 2; k < LUT_ELEM - 2; k++)
d->gamut_LUT[k] = (sampler[k - 2] + sampler[k - 1] + sampler[k] + sampler[k + 1] + sampler[k + 2]) / 5.f;
// handle bounds
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 - 1] = (sampler[LUT_ELEM - 3] + sampler[LUT_ELEM - 2] + sampler[LUT_ELEM - 1] + sampler[0] + sampler[1]) / 5.f;
d->gamut_LUT[LUT_ELEM - 2] = (sampler[LUT_ELEM - 4] + sampler[LUT_ELEM - 3] + sampler[LUT_ELEM - 2] + sampler[LUT_ELEM - 1] + sampler[0]) / 5.f;
dt_free_align(sampler);
}
else if(p->saturation_formula == DT_COLORBALANCE_SATURATION_DTUCS)
dt_UCS_22_build_gamut_LUT(input_matrix, gamut_LUT);
d->lut_inited = TRUE;
}
}
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_colorbalancergb_data_t);
dt_iop_colorbalancergb_data_t *d = piece->data;
d->gamut_LUT = dt_alloc_align_float(LUT_ELEM);
d->lut_inited = FALSE;
d->work_profile = NULL;
}
void cleanup_pipe(dt_iop_module_t *self, dt_dev_pixelpipe_t *pipe, dt_dev_pixelpipe_iop_t *piece)
{
const dt_iop_colorbalancergb_data_t *d = piece->data;
if(d->gamut_LUT) dt_free_align(d->gamut_LUT);
dt_free_align(piece->data);
piece->data = NULL;
}
void pipe_RGB_to_Ych(dt_iop_module_t *self, dt_dev_pixelpipe_t *pipe, const dt_aligned_pixel_t RGB,
dt_aligned_pixel_t Ych)
{
const dt_iop_order_iccprofile_info_t *const work_profile = dt_ioppr_get_pipe_current_profile_info(self, pipe);
if(work_profile == NULL) return; // no point
dt_aligned_pixel_t XYZ_D50 = { 0.f };
dt_aligned_pixel_t XYZ_D65 = { 0.f };
dt_ioppr_rgb_matrix_to_xyz(RGB, XYZ_D50, work_profile->matrix_in_transposed, work_profile->lut_in,
work_profile->unbounded_coeffs_in, work_profile->lutsize,
work_profile->nonlinearlut);
XYZ_D50_to_D65(XYZ_D50, XYZ_D65);
XYZ_to_Ych(XYZ_D65, Ych);
}
void color_picker_apply(dt_iop_module_t *self, GtkWidget *picker,
dt_dev_pixelpipe_t *pipe)
{
const dt_iop_colorbalancergb_gui_data_t *g = self->gui_data;
dt_iop_colorbalancergb_params_t *p = self->params;
dt_aligned_pixel_t Ych = { 0.f };
dt_aligned_pixel_t max_Ych = { 0.f };
pipe_RGB_to_Ych(self, pipe, (const float *)self->picked_color, Ych);
pipe_RGB_to_Ych(self, pipe, (const float *)self->picked_color_max, max_Ych);
const float picked_hue = get_hue_angle_from_Ych(Ych);
const GdkModifierType state = dt_key_modifier_state();
const float hue = (state & GDK_CONTROL_MASK) ? YRG_RAD_TO_CONVENTIONAL_DEG(picked_hue) : YRG_RAD_TO_CONVENTIONAL_DEG(picked_hue) + 180.f; //take the current or opponent color
DT_ENTER_GUI_UPDATE();
if(picker == g->global_H)
{
p->global_H = hue;
p->global_C = Ych[1] * Ych[0];
dt_bauhaus_slider_set(g->global_H, p->global_H);
dt_bauhaus_slider_set(g->global_C, p->global_C);
}
else if(picker == g->shadows_H)
{
p->shadows_H = hue;
p->shadows_C = Ych[1] * Ych[0];
dt_bauhaus_slider_set(g->shadows_H, p->shadows_H);
dt_bauhaus_slider_set(g->shadows_C, p->shadows_C);
}
else if(picker == g->midtones_H)
{
p->midtones_H = hue;
p->midtones_C = Ych[1] * Ych[0];
dt_bauhaus_slider_set(g->midtones_H, p->midtones_H);
dt_bauhaus_slider_set(g->midtones_C, p->midtones_C);
}
else if(picker == g->highlights_H)
{
p->highlights_H = hue;
p->highlights_C = Ych[1] * Ych[0];
dt_bauhaus_slider_set(g->highlights_H, p->highlights_H);
dt_bauhaus_slider_set(g->highlights_C, p->highlights_C);
}
else if(picker == g->white_fulcrum)
{
p->white_fulcrum = log2f(max_Ych[0]);
dt_bauhaus_slider_set(g->white_fulcrum, p->white_fulcrum);
}
else if(picker == g->grey_fulcrum)
{
p->grey_fulcrum = Ych[0];
dt_bauhaus_slider_set(g->grey_fulcrum, p->grey_fulcrum);
}
else
dt_print(DT_DEBUG_ALWAYS, "[colorbalancergb] unknown color picker");
DT_LEAVE_GUI_UPDATE();
gui_changed(self, picker, NULL);
dt_dev_add_history_item(darktable.develop, self, TRUE);
}
static void _YchToRGB(dt_aligned_pixel_t *RGB_out, const float chroma, const float hue,
const dt_iop_order_iccprofile_info_t *output_profile,
const dt_colormatrix_t output_matrix_LMS_to_RGB)
{
dt_aligned_pixel_t RGB_linear = { 0.f };
dt_aligned_pixel_t Ych;
make_Ych(0.75f, chroma, hue, Ych);
dt_aligned_pixel_t XYZ_D65 = { 0.f };
dt_aligned_pixel_t XYZ_D50 = { 0.f };
Ych_to_XYZ(Ych, XYZ_D65);
XYZ_D65_to_D50(XYZ_D65, XYZ_D50);
dt_apply_transposed_color_matrix(XYZ_D50, output_profile->matrix_out_transposed, RGB_linear);
// normalize to the brightest value available at this hue and chroma
const float max_RGB = max3f(RGB_linear);
for_each_channel(c) RGB_linear[c] = MAX(RGB_linear[c] / max_RGB, 0.f);
// Apply nonlinear LUT if necessary
if(output_profile->nonlinearlut)
dt_ioppr_apply_trc(RGB_linear, *RGB_out, output_profile->lut_out, output_profile->unbounded_coeffs_out,
output_profile->lutsize);
else
memcpy(*RGB_out, RGB_linear, sizeof(RGB_linear));
}
static void paint_chroma_slider(const dt_iop_order_iccprofile_info_t *output_profile,
const dt_colormatrix_t output_matrix_LMS_to_RGB,
GtkWidget *w, const float hue)
{
const float x_min = 0;
const float x_max = 1;
const float x_range = x_max - x_min;
const float h = CONVENTIONAL_DEG_TO_YRG_RAD(hue);
const float cos_h = cosf(h);
const float sin_h = sinf(h);
// Find max available chroma at this hue without negative RGB
const float max_chroma = Ych_max_chroma_without_negatives(output_matrix_LMS_to_RGB, cos_h, sin_h);
// Varies x in range around current y param
for(int i = 0; i < DT_BAUHAUS_SLIDER_MAX_STOPS; i++)
{
const float stop = ((float)i / (float)(DT_BAUHAUS_SLIDER_MAX_STOPS - 1));
const float x = MIN(x_min + stop * x_range, max_chroma);
dt_aligned_pixel_t RGB;
_YchToRGB(&RGB, x, h, output_profile, output_matrix_LMS_to_RGB);
dt_bauhaus_slider_set_stop(w, stop, RGB[0], RGB[1], RGB[2]);
}
gtk_widget_queue_draw(w);
}
static void paint_hue_sliders(const dt_iop_order_iccprofile_info_t *output_profile,
const dt_colormatrix_t output_matrix_LMS_to_RGB,
const dt_iop_colorbalancergb_gui_data_t *const g)
{
for(int i = 0; i < DT_BAUHAUS_SLIDER_MAX_STOPS; i++)
{
const float stop = ((float)i / (float)(DT_BAUHAUS_SLIDER_MAX_STOPS - 1));
const float h = CONVENTIONAL_DEG_TO_YRG_RAD(stop * (360.f));
const float max_chroma = Ych_max_chroma_without_negatives(output_matrix_LMS_to_RGB, cosf(h), sinf(h));
dt_aligned_pixel_t RGB;
_YchToRGB(&RGB, MIN(0.2f, max_chroma), h, output_profile, output_matrix_LMS_to_RGB);
dt_bauhaus_slider_set_stop(g->global_H, stop, RGB[0], RGB[1], RGB[2]);
dt_bauhaus_slider_set_stop(g->shadows_H, stop, RGB[0], RGB[1], RGB[2]);
dt_bauhaus_slider_set_stop(g->highlights_H, stop, RGB[0], RGB[1], RGB[2]);
dt_bauhaus_slider_set_stop(g->midtones_H, stop, RGB[0], RGB[1], RGB[2]);
}
}
void gui_focus(dt_iop_module_t *self, gboolean in)
{
if(!in)
{
dt_iop_colorbalancergb_gui_data_t *g = self->gui_data;
const gboolean was_mask = g->mask_display;
g->mask_display = FALSE;
g->mask_type = MASK_NONE;
dt_bauhaus_widget_set_quad_active(GTK_WIDGET(g->shadows_weight), FALSE);
dt_bauhaus_widget_set_quad_active(GTK_WIDGET(g->mask_grey_fulcrum), FALSE);
dt_bauhaus_widget_set_quad_active(GTK_WIDGET(g->highlights_weight), FALSE);
if(was_mask)
dt_iop_refresh_center(self);
}
}
static void mask_callback(GtkWidget *togglebutton, dt_iop_module_t *self)
{
DT_GUARD_GUI_UPDATE();
dt_iop_request_focus(self);
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(self->off), TRUE);
dt_iop_colorbalancergb_gui_data_t *g = self->gui_data;
// if blend module is displaying mask do not display it here
const gboolean mask_display = self->request_mask_display
? FALSE
: dt_bauhaus_widget_get_quad_active(GTK_WIDGET(togglebutton));
if(self->request_mask_display)
dt_control_log(_("cannot display masks when the blending mask is displayed"));
dt_iop_gui_enter_critical_section(self);
g->mask_display = mask_display;
if(g->mask_display)
{
if(togglebutton == g->shadows_weight) g->mask_type = MASK_SHADOWS;
if(togglebutton == g->mask_grey_fulcrum) g->mask_type = MASK_MIDTONES;
if(togglebutton == g->highlights_weight) g->mask_type = MASK_HIGHLIGHTS;
}
else
{
g->mask_type = MASK_NONE;
}
const dt_iop_colorbalancergb_mask_data_t mask_type = g->mask_type;
dt_iop_gui_leave_critical_section(self);
dt_bauhaus_widget_set_quad_active(GTK_WIDGET(g->shadows_weight), mask_type == MASK_SHADOWS);
dt_bauhaus_widget_set_quad_active(GTK_WIDGET(g->mask_grey_fulcrum), mask_type == MASK_MIDTONES);
dt_bauhaus_widget_set_quad_active(GTK_WIDGET(g->highlights_weight), mask_type == MASK_HIGHLIGHTS);
dt_iop_refresh_center(self);
}
static gboolean dt_iop_tonecurve_draw(GtkWidget *widget, cairo_t *crf, const dt_iop_module_t *self)
{
const dt_iop_colorbalancergb_params_t *p = self->params;
const float shadows_weight = 2.f + p->shadows_weight * 2.f;
const float highlights_weight = 2.f + p->highlights_weight * 2.f;
// Cache the graph objects to avoid recomputing all the view at each redraw
GtkAllocation allocation;
gtk_widget_get_allocation(widget, &allocation);
GtkStyleContext *context = gtk_widget_get_style_context(widget);
cairo_surface_t *cst =
dt_cairo_image_surface_create(CAIRO_FORMAT_ARGB32, allocation.width, allocation.height);
PangoFontDescription *desc = dt_gui_get_font();
cairo_t *cr = cairo_create(cst);
PangoLayout *layout = pango_cairo_create_layout(cr);
const gint font_size = pango_font_description_get_size(desc);
pango_font_description_set_size(desc, 0.95 * font_size);
pango_layout_set_font_description(layout, desc);
pango_cairo_context_set_resolution(pango_layout_get_context(layout), darktable.gui->dpi);
char text[256];
// Get the text line height for spacing
PangoRectangle ink;
snprintf(text, sizeof(text), "X");
pango_layout_set_text(layout, text, -1);
pango_layout_get_pixel_extents(layout, &ink, NULL);
const float line_height = ink.height;
const float inset = DT_PIXEL_APPLY_DPI(4);
const float margin_top = inset;
const float margin_bottom = line_height + 2 * inset;
const float margin_left = line_height + inset;
const float margin_right = 0;
const float graph_width = allocation.width - margin_right - margin_left; // align the right border on sliders
const float graph_height = allocation.height - margin_bottom - margin_top; // give room to nodes
gtk_render_background(context, cr, 0, 0, allocation.width, allocation.height);
// draw x gradient as axis legend
cairo_pattern_t *grad;
grad = cairo_pattern_create_linear(margin_left, 0.0, graph_width, 0.0);
dt_cairo_perceptual_gradient(grad, 1.0);
cairo_set_line_width(cr, 0.0);
cairo_rectangle(cr, margin_left, graph_height + 2 * inset, graph_width, line_height);
cairo_set_source(cr, grad);
cairo_fill(cr);
cairo_pattern_destroy(grad);
// draw y gradient as axis legend
const int stride = cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, line_height);
unsigned char *data = malloc(stride * graph_height);
if(!data)
{
cairo_destroy(cr);
cairo_surface_destroy(cst);
return TRUE;
}
cairo_surface_t *surface = cairo_image_surface_create_for_data(data, CAIRO_FORMAT_ARGB32, (size_t)line_height, (size_t)graph_height, stride);
const size_t checker_1 = DT_PIXEL_APPLY_DPI(6);
const size_t checker_2 = 2 * checker_1;
DT_OMP_FOR(collapse(2))
for(size_t i = 0; i < (size_t)graph_height; i++)
for(size_t j = 0; j < (size_t)line_height; j++)
{
const size_t k = ((i * (size_t)line_height) + j) * 4;
unsigned char color;
const float alpha = (float)i / graph_height;
if(i % checker_1 < i % checker_2)
{
if(j % checker_1 < j % checker_2) color = 150;
else color = 100;
}
else
{
if(j % checker_1 < j % checker_2) color = 100;
else color = 150;
}
for(size_t c = 0; c < 4; ++c) data[k + c] = color * alpha;
data[k+3] = alpha * 255;
}
cairo_set_source_surface(cr, surface, 0, margin_top);
cairo_paint(cr);
free(data);
cairo_surface_destroy(surface);
// set the graph as the origin of the coordinates
cairo_translate(cr, margin_left, margin_top);
cairo_set_line_cap(cr, CAIRO_LINE_CAP_ROUND);
set_color(cr, darktable.bauhaus->graph_bg);
cairo_rectangle(cr, 0, 0, graph_width, graph_height);
cairo_fill_preserve(cr);
cairo_clip(cr);
// from https://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.583.3007&rep=rep1&type=pdf
const float midtones_weight
= sqf(shadows_weight) * sqf(highlights_weight) / (sqf(shadows_weight) + sqf(highlights_weight));
const float mask_grey_fulcrum = powf(p->mask_grey_fulcrum, 0.4101205819200422f);
float *LUT[3];
for(size_t c = 0; c < 3; c++) LUT[c] = dt_alloc_align_float(LUT_ELEM);
DT_OMP_FOR()
for(size_t k = 0 ; k < LUT_ELEM; k++)
{
const float Y = k / (float)(LUT_ELEM - 1);
dt_aligned_pixel_t output;
opacity_masks(Y, shadows_weight, highlights_weight, midtones_weight, mask_grey_fulcrum, output, NULL);
for(size_t c = 0; c < 3; c++) LUT[c][k] = output[c];
}
const GdkRGBA fg_color = darktable.bauhaus->graph_fg;
cairo_set_line_width(cr, DT_PIXEL_APPLY_DPI(2.));
for(size_t c = 0; c < 3; c++)
{
const GdkRGBA line_color = { fg_color.red * (1. - (2 - c) / 4.),
fg_color.green * (1. - (2 - c) / 4.),
fg_color.blue * (1. - (2 - c) / 4.),
fg_color.alpha };
set_color(cr, line_color);
cairo_move_to(cr, 0, (1.f - LUT[c][0]) * graph_height);
for(size_t k = 0; k < LUT_ELEM; k++)
{
const float x = (float)k / (float)(LUT_ELEM - 1) * graph_width;
const float y = (1.f - LUT[c][k]) * graph_height;
cairo_line_to(cr, x, y);
}
cairo_stroke(cr);
}
for(size_t c = 0; c < 3; c++) dt_free_align(LUT[c]);
cairo_restore(cr);
// restore font size
pango_font_description_set_size(desc, font_size);
pango_layout_set_font_description(layout, desc);
cairo_destroy(cr);
cairo_set_source_surface(crf, cst, 0, 0);
cairo_paint(crf);
cairo_surface_destroy(cst);
g_object_unref(layout);
pango_font_description_free(desc);
return FALSE;
}
static void checker_1_picker_callback(GtkColorButton *widget, const dt_iop_module_t *self)
{
DT_GUARD_GUI_UPDATE();
GdkRGBA color;
gtk_color_chooser_get_rgba(GTK_COLOR_CHOOSER(widget), &color);
dt_conf_set_float("plugins/darkroom/colorbalancergb/checker1/red", color.red);
dt_conf_set_float("plugins/darkroom/colorbalancergb/checker1/green", color.green);
dt_conf_set_float("plugins/darkroom/colorbalancergb/checker1/blue", color.blue);
dt_iop_refresh_center(self);
}
static void checker_2_picker_callback(GtkColorButton *widget, const dt_iop_module_t *self)
{
DT_GUARD_GUI_UPDATE();
GdkRGBA color;
gtk_color_chooser_get_rgba(GTK_COLOR_CHOOSER(widget), &color);
dt_conf_set_float("plugins/darkroom/colorbalancergb/checker2/red", color.red);
dt_conf_set_float("plugins/darkroom/colorbalancergb/checker2/green", color.green);
dt_conf_set_float("plugins/darkroom/colorbalancergb/checker2/blue", color.blue);
dt_iop_refresh_center(self);
}
static void checker_size_callback(GtkWidget *widget, const dt_iop_module_t *self)
{
DT_GUARD_GUI_UPDATE();
const size_t size = dt_bauhaus_slider_get(widget);
dt_conf_set_int("plugins/darkroom/colorbalancergb/checker/size", size);
dt_iop_refresh_center(self);
}
void gui_changed(dt_iop_module_t *self, GtkWidget *w, void *previous)
{
dt_iop_colorbalancergb_gui_data_t *g = self->gui_data;
const dt_iop_colorbalancergb_params_t *p = self->params;
// Prepare data for gamut mapping slider backgrounds
// Make our best effort to find display profile. If not found
// or it is not a matrix profile, fall back to sRGB.
const dt_iop_order_iccprofile_info_t *output_profile = NULL;
if(self->dev && self->dev->full.pipe)
output_profile = dt_ioppr_get_pipe_output_profile_info(self->dev->full.pipe);
if(!output_profile || !dt_is_valid_colormatrix(output_profile->matrix_out[0][0]))
{
output_profile = dt_ioppr_add_profile_info_to_list(self->dev, DT_COLORSPACE_SRGB, "",
DT_INTENT_RELATIVE_COLORIMETRIC);
}
// Prepare LMS 2006 -> RGB matrix for chroma clipping
dt_colormatrix_t input_matrix = { { 0.f } }; // this is actually unused but is generated by below function anyway
dt_colormatrix_t output_matrix = { { 0.f } };
prepare_RGB_Yrg_matrices(output_profile, input_matrix, output_matrix);
// Check if output profile is different than last time - will need to repaint
// slider backgrounds.
const gboolean output_profile_changed = output_profile != g->sliders_output_profile;
DT_ENTER_GUI_UPDATE();
if(output_profile_changed)
paint_hue_sliders(output_profile, output_matrix, g);
if(!w || w == g->global_H || output_profile_changed)
paint_chroma_slider(output_profile, output_matrix, g->global_C, p->global_H);
if(!w || w == g->shadows_H || output_profile_changed)
paint_chroma_slider(output_profile, output_matrix, g->shadows_C, p->shadows_H);
if(!w || w == g->midtones_H || output_profile_changed)
paint_chroma_slider(output_profile, output_matrix, g->midtones_C, p->midtones_H);
if(!w || w == g->highlights_H || output_profile_changed)
paint_chroma_slider(output_profile, output_matrix, g->highlights_C, p->highlights_H);
if(!w || w == g->shadows_weight || w == g->highlights_weight || w == g->mask_grey_fulcrum)
gtk_widget_queue_draw(GTK_WIDGET(g->area));
DT_LEAVE_GUI_UPDATE();
g->sliders_output_profile = output_profile;
}
void gui_update(dt_iop_module_t *self)
{
dt_iop_colorbalancergb_gui_data_t *g = self->gui_data;
const dt_iop_colorbalancergb_params_t *p = self->params;
dt_bauhaus_slider_set(g->hue_angle, p->hue_angle);
dt_bauhaus_slider_set(g->vibrance, p->vibrance);
dt_bauhaus_slider_set(g->contrast, p->contrast);
dt_bauhaus_slider_set(g->chroma_global, p->chroma_global);
dt_bauhaus_slider_set(g->chroma_highlights, p->chroma_highlights);
dt_bauhaus_slider_set(g->chroma_midtones, p->chroma_midtones);
dt_bauhaus_slider_set(g->chroma_shadows, p->chroma_shadows);
dt_bauhaus_slider_set(g->saturation_global, p->saturation_global);
dt_bauhaus_slider_set(g->saturation_highlights, p->saturation_highlights);
dt_bauhaus_slider_set(g->saturation_midtones, p->saturation_midtones);
dt_bauhaus_slider_set(g->saturation_shadows, p->saturation_shadows);
dt_bauhaus_slider_set(g->brilliance_global, p->brilliance_global);
dt_bauhaus_slider_set(g->brilliance_highlights, p->brilliance_highlights);
dt_bauhaus_slider_set(g->brilliance_midtones, p->brilliance_midtones);
dt_bauhaus_slider_set(g->brilliance_shadows, p->brilliance_shadows);
dt_bauhaus_slider_set(g->global_C, p->global_C);
dt_bauhaus_slider_set(g->global_H, p->global_H);
dt_bauhaus_slider_set(g->global_Y, p->global_Y);
dt_bauhaus_slider_set(g->shadows_C, p->shadows_C);
dt_bauhaus_slider_set(g->shadows_H, p->shadows_H);
dt_bauhaus_slider_set(g->shadows_Y, p->shadows_Y);
dt_bauhaus_slider_set(g->shadows_weight, p->shadows_weight);
dt_bauhaus_slider_set(g->midtones_C, p->midtones_C);
dt_bauhaus_slider_set(g->midtones_H, p->midtones_H);
dt_bauhaus_slider_set(g->midtones_Y, p->midtones_Y);
dt_bauhaus_slider_set(g->white_fulcrum, p->white_fulcrum);
dt_bauhaus_slider_set(g->highlights_C, p->highlights_C);
dt_bauhaus_slider_set(g->highlights_H, p->highlights_H);
dt_bauhaus_slider_set(g->highlights_Y, p->highlights_Y);
dt_bauhaus_slider_set(g->highlights_weight, p->highlights_weight);
dt_bauhaus_slider_set(g->mask_grey_fulcrum, p->mask_grey_fulcrum);
dt_bauhaus_slider_set(g->grey_fulcrum, p->grey_fulcrum);
dt_bauhaus_combobox_set(g->saturation_formula, p->saturation_formula);
gui_changed(self, NULL, NULL);
dt_iop_color_picker_reset(self, TRUE);
dt_iop_gui_enter_critical_section(self);
g->mask_display = FALSE;
g->mask_type = MASK_NONE;
dt_iop_gui_leave_critical_section(self);
dt_bauhaus_widget_set_quad_active(GTK_WIDGET(g->shadows_weight), FALSE);
dt_bauhaus_widget_set_quad_active(GTK_WIDGET(g->mask_grey_fulcrum), FALSE);
dt_bauhaus_widget_set_quad_active(GTK_WIDGET(g->highlights_weight), FALSE);
// Checkerboard mask preview preferences
GdkRGBA color;
color.alpha = 1.0f;
color.red = dt_conf_get_float("plugins/darkroom/colorbalancergb/checker1/red");
color.green = dt_conf_get_float("plugins/darkroom/colorbalancergb/checker1/green");
color.blue = dt_conf_get_float("plugins/darkroom/colorbalancergb/checker1/blue");
gtk_color_chooser_set_rgba(GTK_COLOR_CHOOSER(g->checker_color_1_picker), &color);
color.red = dt_conf_get_float("plugins/darkroom/colorbalancergb/checker2/red");
color.green = dt_conf_get_float("plugins/darkroom/colorbalancergb/checker2/green");
color.blue = dt_conf_get_float("plugins/darkroom/colorbalancergb/checker2/blue");
gtk_color_chooser_set_rgba(GTK_COLOR_CHOOSER(g->checker_color_2_picker), &color);
dt_bauhaus_slider_set(g->checker_size, dt_conf_get_int("plugins/darkroom/colorbalancergb/checker/size"));
}
void gui_reset(dt_iop_module_t *self)
{
//dt_iop_colorbalancergb_gui_data_t *g = self->gui_data;
dt_iop_color_picker_reset(self, TRUE);
}
void gui_init(dt_iop_module_t *self)
{
dt_iop_colorbalancergb_gui_data_t *g = IOP_GUI_ALLOC(colorbalancergb);
dt_iop_gui_enter_critical_section(self);
g->mask_display = FALSE;
dt_iop_gui_leave_critical_section(self);
dt_iop_module_t *sect = NULL;
// start building top level widget
static dt_action_def_t notebook_def = { };
g->notebook = dt_ui_notebook_new(¬ebook_def);
dt_action_define_iop(self, NULL, N_("page"), GTK_WIDGET(g->notebook), ¬ebook_def);
// Page master
self->widget = dt_ui_notebook_page(g->notebook, N_("master"), _("global grading"));
g->hue_angle = dt_bauhaus_slider_from_params(self, "hue_angle");
dt_bauhaus_slider_set_format(g->hue_angle, "°");
gtk_widget_set_tooltip_text(g->hue_angle, _("rotate all hues by an angle, at the same luminance"));
g->vibrance = dt_bauhaus_slider_from_params(self, "vibrance");
dt_bauhaus_slider_set_soft_range(g->vibrance, -0.5, 0.5);
dt_bauhaus_slider_set_digits(g->vibrance, 4);
dt_bauhaus_slider_set_format(g->vibrance, "%");
gtk_widget_set_tooltip_text(g->vibrance, _("increase colorfulness mostly on low-chroma colors"));
g->contrast = dt_bauhaus_slider_from_params(self, "contrast");
dt_bauhaus_slider_set_soft_range(g->contrast, -0.5, 0.5);
dt_bauhaus_slider_set_digits(g->contrast, 4);
dt_bauhaus_slider_set_format(g->contrast, "%");
gtk_widget_set_tooltip_text(g->contrast, _("increase the contrast at constant chromaticity"));
dt_gui_box_add(self->widget, dt_ui_section_label_new(C_("section", "linear chroma grading")));
sect = DT_IOP_SECTION_FOR_PARAMS(self, N_("chroma"));
g->chroma_global = dt_bauhaus_slider_from_params(self, "chroma_global");
dt_bauhaus_slider_set_soft_range(g->chroma_global, -0.5, 0.5);
dt_bauhaus_slider_set_digits(g->chroma_global, 4);
dt_bauhaus_slider_set_format(g->chroma_global, "%");
gtk_widget_set_tooltip_text(g->chroma_global, _("increase colorfulness at same luminance globally"));
g->chroma_shadows = dt_bauhaus_slider_from_params(sect, "chroma_shadows");
dt_bauhaus_slider_set_digits(g->chroma_shadows, 4);
dt_bauhaus_slider_set_format(g->chroma_shadows, "%");
gtk_widget_set_tooltip_text(g->chroma_shadows, _("increase colorfulness at same luminance mostly in shadows"));
g->chroma_midtones = dt_bauhaus_slider_from_params(sect, "chroma_midtones");
dt_bauhaus_slider_set_digits(g->chroma_midtones, 4);
dt_bauhaus_slider_set_format(g->chroma_midtones, "%");
gtk_widget_set_tooltip_text(g->chroma_midtones, _("increase colorfulness at same luminance mostly in mid-tones"));
g->chroma_highlights = dt_bauhaus_slider_from_params(sect, "chroma_highlights");
dt_bauhaus_slider_set_digits(g->chroma_highlights, 4);
dt_bauhaus_slider_set_format(g->chroma_highlights, "%");
gtk_widget_set_tooltip_text(g->chroma_highlights, _("increase colorfulness at same luminance mostly in highlights"));
dt_gui_box_add(self->widget, dt_ui_section_label_new(C_("section", "perceptual saturation grading")));
sect = DT_IOP_SECTION_FOR_PARAMS(self, N_("saturation"));
g->saturation_global = dt_bauhaus_slider_from_params(self, "saturation_global");
dt_bauhaus_slider_set_digits(g->saturation_global, 4);
dt_bauhaus_slider_set_format(g->saturation_global, "%");
gtk_widget_set_tooltip_text(g->saturation_global, _("add or remove saturation by an absolute amount"));
g->saturation_shadows = dt_bauhaus_slider_from_params(sect, "saturation_shadows");
dt_bauhaus_slider_set_digits(g->saturation_shadows, 4);
dt_bauhaus_slider_set_format(g->saturation_shadows, "%");
gtk_widget_set_tooltip_text(g->saturation_shadows, _("increase or decrease saturation proportionally to the original pixel saturation"));
g->saturation_midtones= dt_bauhaus_slider_from_params(sect, "saturation_midtones");
dt_bauhaus_slider_set_digits(g->saturation_midtones, 4);
dt_bauhaus_slider_set_format(g->saturation_midtones, "%");
gtk_widget_set_tooltip_text(g->saturation_midtones, _("increase or decrease saturation proportionally to the original pixel saturation"));
g->saturation_highlights = dt_bauhaus_slider_from_params(sect, "saturation_highlights");
dt_bauhaus_slider_set_digits(g->saturation_highlights, 4);
dt_bauhaus_slider_set_format(g->saturation_highlights, "%");
gtk_widget_set_tooltip_text(g->saturation_highlights, _("increase or decrease saturation proportionally to the original pixel saturation"));
dt_gui_box_add(self->widget, dt_ui_section_label_new(C_("section", "perceptual brilliance grading")));
sect = DT_IOP_SECTION_FOR_PARAMS(self, N_("brilliance"));
g->brilliance_global = dt_bauhaus_slider_from_params(self, "brilliance_global");
dt_bauhaus_slider_set_digits(g->brilliance_global, 4);
dt_bauhaus_slider_set_format(g->brilliance_global, "%");
gtk_widget_set_tooltip_text(g->brilliance_global, _("add or remove brilliance by an absolute amount"));
g->brilliance_shadows = dt_bauhaus_slider_from_params(sect, "brilliance_shadows");
dt_bauhaus_slider_set_digits(g->brilliance_shadows, 4);
dt_bauhaus_slider_set_format(g->brilliance_shadows, "%");
gtk_widget_set_tooltip_text(g->brilliance_shadows, _("increase or decrease brilliance proportionally to the original pixel brilliance"));
g->brilliance_midtones= dt_bauhaus_slider_from_params(sect, "brilliance_midtones");
dt_bauhaus_slider_set_digits(g->brilliance_midtones, 4);
dt_bauhaus_slider_set_format(g->brilliance_midtones, "%");
gtk_widget_set_tooltip_text(g->brilliance_midtones, _("increase or decrease brilliance proportionally to the original pixel brilliance"));
g->brilliance_highlights = dt_bauhaus_slider_from_params(sect, "brilliance_highlights");
dt_bauhaus_slider_set_digits(g->brilliance_highlights, 4);
dt_bauhaus_slider_set_format(g->brilliance_highlights, "%");
gtk_widget_set_tooltip_text(g->brilliance_highlights, _("increase or decrease brilliance proportionally to the original pixel brilliance"));
// Page 4-ways
self->widget = dt_ui_notebook_page(g->notebook, N_("4 ways"), _("selective color grading"));
const char *hue_picker_tooltip = _("pick opposite color from image\nctrl+click to pick selected color");
dt_gui_box_add(self->widget, dt_ui_section_label_new(C_("section", "global offset")));
sect = DT_IOP_SECTION_FOR_PARAMS(self, N_("offset"));
g->global_Y = dt_bauhaus_slider_from_params(sect, "global_Y");
dt_bauhaus_slider_set_soft_range(g->global_Y, -0.05, 0.05);
dt_bauhaus_slider_set_digits(g->global_Y, 4);
dt_bauhaus_slider_set_format(g->global_Y, "%");
gtk_widget_set_tooltip_text(g->global_Y, _("global luminance offset"));
g->global_H = dt_color_picker_new(self, DT_COLOR_PICKER_AREA, dt_bauhaus_slider_from_params(sect, "global_H"));
dt_bauhaus_slider_set_feedback(g->global_H, 0);
dt_bauhaus_slider_set_format(g->global_H, "°");
gtk_widget_set_tooltip_text(g->global_H, _("hue of the global color offset"));
dt_bauhaus_widget_set_quad_tooltip(g->global_H, hue_picker_tooltip);
g->global_C = dt_bauhaus_slider_from_params(sect, "global_C");
dt_bauhaus_slider_set_soft_range(g->global_C, 0., 0.01);
dt_bauhaus_slider_set_digits(g->global_C, 4);
dt_bauhaus_slider_set_format(g->global_C, "%");
gtk_widget_set_tooltip_text(g->global_C, _("chroma of the global color offset"));
dt_gui_box_add(self->widget, dt_ui_section_label_new(C_("section", "shadows lift")));
sect = DT_IOP_SECTION_FOR_PARAMS(self, N_("lift"));
g->shadows_Y = dt_bauhaus_slider_from_params(sect, "shadows_Y");
dt_bauhaus_slider_set_soft_range(g->shadows_Y, -1.0, 1.0);
dt_bauhaus_slider_set_digits(g->shadows_Y, 4);
dt_bauhaus_slider_set_format(g->shadows_Y, "%");
gtk_widget_set_tooltip_text(g->shadows_Y, _("luminance gain in shadows"));
g->shadows_H = dt_color_picker_new(self, DT_COLOR_PICKER_AREA, dt_bauhaus_slider_from_params(sect, "shadows_H"));
dt_bauhaus_slider_set_feedback(g->shadows_H, 0);
dt_bauhaus_slider_set_format(g->shadows_H, "°");
gtk_widget_set_tooltip_text(g->shadows_H, _("hue of the color gain in shadows"));
dt_bauhaus_widget_set_quad_tooltip(g->shadows_H, hue_picker_tooltip);
g->shadows_C = dt_bauhaus_slider_from_params(sect, "shadows_C");
dt_bauhaus_slider_set_soft_range(g->shadows_C, 0., 0.5);
dt_bauhaus_slider_set_digits(g->shadows_C, 4);
dt_bauhaus_slider_set_format(g->shadows_C, "%");
gtk_widget_set_tooltip_text(g->shadows_C, _("chroma of the color gain in shadows"));
dt_gui_box_add(self->widget, dt_ui_section_label_new(C_("section", "highlights gain")));
sect = DT_IOP_SECTION_FOR_PARAMS(self, N_("gain"));
g->highlights_Y = dt_bauhaus_slider_from_params(sect, "highlights_Y");
dt_bauhaus_slider_set_soft_range(g->highlights_Y, -0.5, 0.5);
dt_bauhaus_slider_set_digits(g->highlights_Y, 4);
dt_bauhaus_slider_set_format(g->highlights_Y, "%");
gtk_widget_set_tooltip_text(g->highlights_Y, _("luminance gain in highlights"));
g->highlights_H = dt_color_picker_new(self, DT_COLOR_PICKER_AREA, dt_bauhaus_slider_from_params(sect, "highlights_H"));
dt_bauhaus_slider_set_feedback(g->highlights_H, 0);
dt_bauhaus_slider_set_format(g->highlights_H, "°");
gtk_widget_set_tooltip_text(g->highlights_H, _("hue of the color gain in highlights"));
dt_bauhaus_widget_set_quad_tooltip(g->highlights_H, hue_picker_tooltip);
g->highlights_C = dt_bauhaus_slider_from_params(sect, "highlights_C");
dt_bauhaus_slider_set_soft_range(g->highlights_C, 0., 0.2);
dt_bauhaus_slider_set_digits(g->highlights_C, 4);
dt_bauhaus_slider_set_format(g->highlights_C, "%");
gtk_widget_set_tooltip_text(g->highlights_C, _("chroma of the color gain in highlights"));
dt_gui_box_add(self->widget, dt_ui_section_label_new(C_("section", "power")));
sect = DT_IOP_SECTION_FOR_PARAMS(self, N_("power"));
g->midtones_Y = dt_bauhaus_slider_from_params(sect, "midtones_Y");
dt_bauhaus_slider_set_soft_range(g->midtones_Y, -0.25, 0.25);
dt_bauhaus_slider_set_digits(g->midtones_Y, 4);
dt_bauhaus_slider_set_format(g->midtones_Y, "%");
gtk_widget_set_tooltip_text(g->midtones_Y, _("luminance exponent in mid-tones"));
g->midtones_H = dt_color_picker_new(self, DT_COLOR_PICKER_AREA, dt_bauhaus_slider_from_params(sect, "midtones_H"));
dt_bauhaus_slider_set_feedback(g->midtones_H, 0);
dt_bauhaus_slider_set_format(g->midtones_H, "°");
gtk_widget_set_tooltip_text(g->midtones_H, _("hue of the color exponent in mid-tones"));
dt_bauhaus_widget_set_quad_tooltip(g->midtones_H, hue_picker_tooltip);
g->midtones_C = dt_bauhaus_slider_from_params(sect, "midtones_C");
dt_bauhaus_slider_set_soft_range(g->midtones_C, 0., 0.1);
dt_bauhaus_slider_set_digits(g->midtones_C, 4);
dt_bauhaus_slider_set_format(g->midtones_C, "%");
gtk_widget_set_tooltip_text(g->midtones_C, _("chroma of the color exponent in mid-tones"));
// Page masks
self->widget = dt_ui_notebook_page(g->notebook, N_("masks"), _("isolate luminances"));
g->saturation_formula = dt_bauhaus_combobox_from_params(self, "saturation_formula");
gtk_widget_set_tooltip_text(g->saturation_formula,
_("choose in which uniform color space the saturation is computed"));
dt_gui_box_add(self->widget, dt_ui_section_label_new(C_("section", "luminance ranges")));
g->area = GTK_DRAWING_AREA(dt_ui_resize_wrap(NULL,
0,
"plugins/darkroom/colorbalancergb/graphheight"));
g_object_set_data(G_OBJECT(g->area), "iop-instance", self);
dt_action_define_iop(self, NULL, N_("graph"), GTK_WIDGET(g->area), NULL);
g_signal_connect(G_OBJECT(g->area), "draw", G_CALLBACK(dt_iop_tonecurve_draw), self);
dt_gui_box_add(self->widget, GTK_WIDGET(g->area));
g->shadows_weight = dt_bauhaus_slider_from_params(self, "shadows_weight");
dt_bauhaus_slider_set_digits(g->shadows_weight, 4);
dt_bauhaus_slider_set_format(g->shadows_weight, "%");
gtk_widget_set_tooltip_text(g->shadows_weight, _("weight of the shadows over the whole tonal range"));
dt_bauhaus_widget_set_quad(g->shadows_weight,
self,
dtgtk_cairo_paint_showmask,
TRUE,
mask_callback,
_("displays a shadows mask, overlaid as a checkerboard\n"
"the still-visible area of the image (not hidden by the mask) is the area\n"
"that will be affected by the shadows sliders in the other tabs"));
g->mask_grey_fulcrum = dt_bauhaus_slider_from_params(self, "mask_grey_fulcrum");
dt_bauhaus_slider_set_digits(g->mask_grey_fulcrum, 4);
dt_bauhaus_slider_set_format(g->mask_grey_fulcrum, "%");
gtk_widget_set_tooltip_text(g->mask_grey_fulcrum, _("position of the middle-gray reference for masking"));
dt_bauhaus_widget_set_quad(g->mask_grey_fulcrum,
self,
dtgtk_cairo_paint_showmask,
TRUE,
mask_callback,
_("displays a mid-tones mask, overlaid as a checkerboard\n"
"the still-visible area of the image (not hidden by the mask) is the area\n"
"that will be affected by the mid-tones sliders in the other tabs"));
g->highlights_weight = dt_bauhaus_slider_from_params(self, "highlights_weight");
dt_bauhaus_slider_set_digits(g->highlights_weight, 4);
dt_bauhaus_slider_set_format(g->highlights_weight, "%");
gtk_widget_set_tooltip_text(g->highlights_weight, _("weights of highlights over the whole tonal range"));
dt_bauhaus_widget_set_quad(g->highlights_weight,
self,
dtgtk_cairo_paint_showmask,
TRUE,
mask_callback,
_("displays a highlights mask, overlaid as a checkerboard\n"
"the still-visible area of the image (not hidden by the mask) is the area\n"
"that will be affected by the highlights sliders in the other tabs"));
dt_gui_box_add(self->widget, dt_ui_section_label_new(C_("section", "threshold")));
g->white_fulcrum = dt_color_picker_new(self, DT_COLOR_PICKER_AREA, dt_bauhaus_slider_from_params(self, "white_fulcrum"));
dt_bauhaus_slider_set_soft_range(g->white_fulcrum, -2., +2.);
dt_bauhaus_slider_set_format(g->white_fulcrum, _(" EV"));
gtk_widget_set_tooltip_text(g->white_fulcrum, _("peak white luminance value used to normalize the power function"));
g->grey_fulcrum = dt_color_picker_new(self, DT_COLOR_PICKER_AREA, dt_bauhaus_slider_from_params(self, "grey_fulcrum"));
dt_bauhaus_slider_set_soft_range(g->grey_fulcrum, 0.1, 0.5);
dt_bauhaus_slider_set_digits(g->grey_fulcrum, 4);
dt_bauhaus_slider_set_format(g->grey_fulcrum, "%");
gtk_widget_set_tooltip_text(g->grey_fulcrum, _("peak gray luminance value used to normalize the power function"));
dt_gui_box_add(self->widget, dt_ui_section_label_new(C_("section", "mask preview settings")));
g->checker_color_1_picker = gtk_color_button_new();
gtk_color_chooser_set_use_alpha(GTK_COLOR_CHOOSER(g->checker_color_1_picker), FALSE);
gtk_color_button_set_title(GTK_COLOR_BUTTON(g->checker_color_1_picker), _("select color of the checkerboard from a swatch"));
g_signal_connect(G_OBJECT(g->checker_color_1_picker), "color-set", G_CALLBACK(checker_1_picker_callback), self);
g->checker_color_2_picker = gtk_color_button_new();
gtk_color_chooser_set_use_alpha(GTK_COLOR_CHOOSER(g->checker_color_2_picker), FALSE);
gtk_color_button_set_title(GTK_COLOR_BUTTON(g->checker_color_2_picker), _("select color of the checkerboard from a swatch"));
g_signal_connect(G_OBJECT(g->checker_color_2_picker), "color-set", G_CALLBACK(checker_2_picker_callback), self);
g->checker_size = dt_bauhaus_slider_new_with_range(self, 2., 32., 0, 8., 0);
dt_bauhaus_slider_set_format(g->checker_size, _(" px"));
dt_bauhaus_widget_set_label(g->checker_size, NULL, _("checkerboard size"));
g_signal_connect(G_OBJECT(g->checker_size), "value-changed", G_CALLBACK(checker_size_callback), self);
dt_gui_box_add(self->widget,
dt_gui_hbox(dt_gui_expand(dt_ui_label_new(_("checkerboard color 1"))), g->checker_color_1_picker),
dt_gui_hbox(dt_gui_expand(dt_ui_label_new(_("checkerboard color 2"))), g->checker_color_2_picker),
g->checker_size);
// Init the conf keys if they don't exist
if(!dt_conf_key_exists("plugins/darkroom/colorbalancergb/checker1/red"))
dt_conf_set_float("plugins/darkroom/colorbalancergb/checker1/red", 1.0f);
if(!dt_conf_key_exists("plugins/darkroom/colorbalancergb/checker1/green"))
dt_conf_set_float("plugins/darkroom/colorbalancergb/checker1/green", 1.0f);
if(!dt_conf_key_exists("plugins/darkroom/colorbalancergb/checker1/blue"))
dt_conf_set_float("plugins/darkroom/colorbalancergb/checker1/blue", 1.0f);
if(!dt_conf_key_exists("plugins/darkroom/colorbalancergb/checker2/red"))
dt_conf_set_float("plugins/darkroom/colorbalancergb/checker2/red", 0.18f);
if(!dt_conf_key_exists("plugins/darkroom/colorbalancergb/checker2/green"))
dt_conf_set_float("plugins/darkroom/colorbalancergb/checker2/green", 0.18f);
if(!dt_conf_key_exists("plugins/darkroom/colorbalancergb/checker2/blue"))
dt_conf_set_float("plugins/darkroom/colorbalancergb/checker2/blue", 0.18f);
if(!dt_conf_key_exists("plugins/darkroom/colorbalancergb/checker/size"))
dt_conf_set_int("plugins/darkroom/colorbalancergb/checker/size", 8);
// paint backgrounds
for(int i = 0; i < DT_BAUHAUS_SLIDER_MAX_STOPS; i++)
{
const float stop = ((float)i / (float)(DT_BAUHAUS_SLIDER_MAX_STOPS - 1));
const float Y = 0.f + stop;
dt_bauhaus_slider_set_stop(g->global_Y, stop, Y, Y, Y);
dt_bauhaus_slider_set_stop(g->shadows_Y, stop, Y, Y, Y);
dt_bauhaus_slider_set_stop(g->highlights_Y, stop, Y, Y, Y);
dt_bauhaus_slider_set_stop(g->midtones_Y, stop, Y, Y, Y);
}
// main widget is the notebook
self->widget = GTK_WIDGET(g->notebook);
}
// 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