/* http://www.youtube.com/watch?v=JVoUgR6bhBc */ /* This file is part of darktable, Copyright (C) 2013-2026 darktable developers. darktable is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. darktable is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with darktable. If not, see . */ // our includes go first: #include "bauhaus/bauhaus.h" #include "common/exif.h" #include "common/colorspaces_inline_conversions.h" #include "common/math.h" #include "common/opencl.h" #include "develop/blend.h" #include "develop/imageop.h" #include "develop/imageop_math.h" #include "develop/imageop_gui.h" #include "develop/openmp_maths.h" #include "dtgtk/gradientslider.h" #include "gui/accelerators.h" #include "gui/gtk.h" #include "gui/presets.h" #include "gui/color_picker_proxy.h" #include "iop/iop_api.h" #include // uncomment the following line to test auto-vectorized code on a system which supports SSE //#undef __SSE2__ // these are not in a state to be useful. but they look nice. too bad i couldn't map the enhanced mode with // negative values to the wheels :( //#define SHOW_COLOR_WHEELS DT_MODULE_INTROSPECTION(3, dt_iop_colorbalance_params_t) /* Meaning of the values: 0 --> 100% -1 --> 0% 1 --> 200% */ typedef enum dt_iop_colorbalance_mode_t { LIFT_GAMMA_GAIN = 0, // $DESCRIPTION: "lift, gamma, gain (ProPhoto RGB)" SLOPE_OFFSET_POWER = 1, // $DESCRIPTION: "slope, offset, power (ProPhoto RGB)" LEGACY = 2 // $DESCRIPTION: "lift, gamma, gain (sRGB)" } dt_iop_colorbalance_mode_t; typedef enum _colorbalance_channel_t { CHANNEL_FACTOR = 0, CHANNEL_RED, CHANNEL_GREEN, CHANNEL_BLUE, CHANNEL_SIZE } _colorbalance_channel_t; typedef enum _colorbalance_levels_t { LIFT = 0, GAMMA, GAIN, LEVELS } _colorbalance_levels_t; typedef enum _controls_t { HSL, RGBL, BOTH } _controls_t; typedef enum _colorbalance_patch_t { INVALID, USER_SELECTED, AUTO_SELECTED } _colorbalance_patch_t; typedef struct dt_iop_colorbalance_params_t { dt_iop_colorbalance_mode_t mode; // $DEFAULT: SLOPE_OFFSET_POWER float lift[CHANNEL_SIZE], gamma[CHANNEL_SIZE], gain[CHANNEL_SIZE]; // $MIN: 0.0 $MAX: 2.0 $DEFAULT: 1.0 float saturation; // $MIN: 0.0 $MAX: 2.0 $DEFAULT: 1.0 $DESCRIPTION: "input saturation" float contrast; // $MIN: 0.01 $MAX: 1.99 $DEFAULT: 1.0 float grey; // $MIN: 0.1 $MAX: 100.0 $DEFAULT: 18.0 $DESCRIPTION: "contrast fulcrum" float saturation_out; // $MIN: 0.0 $MAX: 2.0 $DEFAULT: 1.0 $DESCRIPTION: "output saturation" } dt_iop_colorbalance_params_t; typedef struct dt_iop_colorbalance_gui_data_t { GtkWidget *master_box; GtkWidget *main_label; GtkWidget *main_box; GtkWidget *blocks[3]; GtkWidget *optimizer_box; GtkWidget *mode; GtkWidget *controls; GtkWidget *hue_lift, *hue_gamma, *hue_gain; GtkWidget *sat_lift, *sat_gamma, *sat_gain; GtkWidget *lift_r, *lift_g, *lift_b, *lift_factor; GtkWidget *gamma_r, *gamma_g, *gamma_b, *gamma_factor; GtkWidget *gain_r, *gain_g, *gain_b, *gain_factor; GtkWidget *saturation, *contrast, *grey, *saturation_out; GtkWidget *auto_luma; GtkWidget *auto_color; float color_patches_lift[3]; float color_patches_gamma[3]; float color_patches_gain[3]; _colorbalance_patch_t color_patches_flags[LEVELS]; float luma_patches[LEVELS]; _colorbalance_patch_t luma_patches_flags[LEVELS]; } dt_iop_colorbalance_gui_data_t; typedef struct dt_iop_colorbalance_data_t { dt_iop_colorbalance_mode_t mode; float lift[CHANNEL_SIZE], gamma[CHANNEL_SIZE], gain[CHANNEL_SIZE]; float saturation, contrast, grey, saturation_out; } dt_iop_colorbalance_data_t; typedef struct dt_iop_colorbalance_global_data_t { int kernel_colorbalance; int kernel_colorbalance_cdl; int kernel_colorbalance_lgg; } dt_iop_colorbalance_global_data_t; const char *name() { return _("color balance"); } const char *aliases() { return _("lift gamma gain|cdl|color grading|contrast|saturation|hue"); } const char **description(dt_iop_module_t *self) { return dt_iop_set_description(self, _("shift colors selectively by luminance range"), _("corrective or creative"), _("linear, Lab, scene-referred"), _("non-linear, RGB"), _("non-linear, Lab, scene-referred")); } int flags() { return IOP_FLAGS_INCLUDE_IN_STYLES | IOP_FLAGS_SUPPORTS_BLENDING; } 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_LAB; } 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_colorbalance_params_v3_t { dt_iop_colorbalance_mode_t mode; float lift[CHANNEL_SIZE], gamma[CHANNEL_SIZE], gain[CHANNEL_SIZE]; float saturation; float contrast; float grey; float saturation_out; } dt_iop_colorbalance_params_v3_t; if(old_version == 1) { typedef struct dt_iop_colorbalance_params_v1_t { float lift[CHANNEL_SIZE], gamma[CHANNEL_SIZE], gain[CHANNEL_SIZE]; } dt_iop_colorbalance_params_v1_t; const dt_iop_colorbalance_params_v1_t *o = old_params; dt_iop_colorbalance_params_v3_t *n = malloc(sizeof(dt_iop_colorbalance_params_v3_t)); for(int i = 0; i < CHANNEL_SIZE; i++) { n->lift[i] = o->lift[i]; n->gamma[i] = o->gamma[i]; n->gain[i] = o->gain[i]; } n->mode = LEGACY; n->saturation = 1.0f; n->contrast = 1.0f; n->grey = 18.0f; n->saturation_out = 1.0f; *new_params = n; *new_params_size = sizeof(dt_iop_colorbalance_params_v3_t); *new_version = 3; return 0; } if(old_version == 2) { typedef struct dt_iop_colorbalance_params_v2_t { dt_iop_colorbalance_mode_t mode; float lift[CHANNEL_SIZE], gamma[CHANNEL_SIZE], gain[CHANNEL_SIZE]; float saturation, contrast, grey; } dt_iop_colorbalance_params_v2_t; const dt_iop_colorbalance_params_v2_t *o = old_params; dt_iop_colorbalance_params_v3_t *n = malloc(sizeof(dt_iop_colorbalance_params_v3_t)); for(int i = 0; i < CHANNEL_SIZE; i++) { n->lift[i] = o->lift[i]; n->gamma[i] = o->gamma[i]; n->gain[i] = o->gain[i]; } n->mode = o->mode; n->contrast = o->contrast; n->saturation = o->saturation; n->contrast = o->contrast; n->grey = o->grey; n->saturation_out = 1.0f; *new_params = n; *new_params_size = sizeof(dt_iop_colorbalance_params_v3_t); *new_version = 3; return 0; } return 1; } // taken from denoiseprofile.c static void add_preset(dt_iop_module_so_t *self, const char *name, const char *pi, const int version, const char *bpi, const int blendop_version) { int len, blen; uint8_t *p = dt_exif_xmp_decode(pi, strlen(pi), &len); uint8_t *bp = dt_exif_xmp_decode(bpi, strlen(bpi), &blen); if(blendop_version != dt_develop_blend_version()) { // update to current blendop params format void *bp_new = malloc(sizeof(dt_develop_blend_params_t)); if(dt_develop_blend_legacy_params_from_so(self, bp, blendop_version, bp_new, dt_develop_blend_version(), blen) == FALSE) { free(bp); bp = bp_new; blen = sizeof(dt_develop_blend_params_t); } else { free(bp); free(bp_new); bp = NULL; } } if(p && bp) { gchar *prefixed_name = g_strdup_printf(BUILTIN_PREFIX "%s", name); dt_gui_presets_add_with_blendop(prefixed_name, self->op, version, p, len, bp, TRUE); g_free(prefixed_name); } free(bp); free(p); } void init_presets(dt_iop_module_so_t *self) { // these blobs were exported as dtstyle and copied from there: add_preset(self, N_("split-toning teal-orange (2nd instance)"), "gz02eJxjZGBg8HhYZX99cYN9kkCDfdCOOnsGhgZ7ruvN9m8CK+yXFNTaz5w50z5PqBku9u9/PVjNv//9jqfP+NgDAHs0HIc=", 3, "gz05eJxjZWBgYGUAgRNODFDAzszAxMBQ5cwI4Tow4AUNdkBsD8E3gGwue9x8uB6q8s+c8bEF8Z9Y9Nnt2f3bbluCN03tg/EBIBckVg==", 8); add_preset(self, N_("split-toning teal-orange (1st instance)"), "gz02eJxjZACBBvugHXX2E3fU219f3GAP4n/TqLFvfd1oL8HZaH/2jI/9prn1cLHUtDSwGgaGCY7//tfbAwBRixpm", 3, "gz04eJxjZWBgYGUAgRNODFDApgwiq5wZIVyHD4E7bBnwggZ7CIYBRiBbBA8fXT1l/P5DX21i+pnA/Pfv8uw6OzzIMq9I5rgtSH//4wii1AMASbIlcw==", 8); add_preset(self, N_("generic film"), "gz02eJxjZACBBntN5gb7op/19u5AGsSX3dFgr+jYaL+vttb+0NcM+1Pnq+3XyFTZr/rYBJZPS0sD0hMcQDQA29kXSQ==", 3, "gz11eJxjYGBgkGAAgRNODGiAEV0AJ2iwh+CRxQcA5qIZBA==", 8); add_preset(self, N_("similar to Kodak Portra"), "gz02eJxjZACBBnsQfh3YYK8VU28P43s8rLKP6W+yP/Q1w36deyMYLymoBcsZGxcDaQGHs2d87AGnphWu", 3, "gz11eJxjYGBgkGAAgRNODGiAEV0AJ2iwh+CRxQcA5qIZBA==", 8); add_preset(self, N_("similar to Kodak Ektar"), "gz02eJxjZACBBvvrixvsrXIb7IN21NnD+CA2iOa6nmxvZFxsX15ebp+e1gaWNwbyGRgEHNLS0uwBE7wWhw==", 3, "gz11eJxjYGBgkGAAgRNODGiAEV0AJ2iwh+CRxQcA5qIZBA==", 8); add_preset(self, N_("similar to Kodachrome"), "gz02eJxjZACBBvvrixvsrXIb7IN21NnD+CA2iG59HWhvZFxsX15ebp+e1gaWT0tLA9ICDrNmRtoDACjOF7c=", 3, "gz11eJxjYGBgkGAAgRNODGiAEV0AJ2iwh+CRxQcA5qIZBA==", 8); } static const dt_aligned_pixel_t zero = { 0.0f, 0.0f, 0.0f, 0.0f }; static const dt_aligned_pixel_t one = { 1.0f, 1.0f, 1.0f, 1.0f }; DT_OMP_DECLARE_SIMD(simdlen(4)) static inline float CDL(float x, float slope, float offset, float power) { float out; out = slope * x + offset; // out = (out <= 0.0f) ? 0.0f : powf(out, power); out = powf(MAX(out, 0.0f), power); return out; } static inline void _apply_CDL(dt_aligned_pixel_t x, const dt_aligned_pixel_t slope, const dt_aligned_pixel_t offset, const dt_aligned_pixel_t power) { dt_aligned_pixel_t res; for_each_channel(c) res[c] = slope[c] * x[c] + offset[c]; dt_vector_max(res, res, zero); // clip away negatives dt_vector_powf(res, power, x); } static inline void _apply_fulcrum_contrast(dt_aligned_pixel_t rgb, const dt_aligned_pixel_t grey, const dt_aligned_pixel_t contrast_power) { dt_vector_max(rgb, rgb, zero); // clip away negatives for_each_channel(c) rgb[c] /= grey[c]; dt_vector_powf(rgb, contrast_power, rgb); for_each_channel(c) rgb[c] *= grey[c]; } static void _process_legacy(const dt_aligned_pixel_t in, dt_aligned_pixel_t out, const size_t npixels, const dt_aligned_pixel_t lift, const dt_aligned_pixel_t gamma_inv, const dt_aligned_pixel_t gain) { for(size_t k = 0; k < npixels; k++) { // transform the pixel to sRGB: // Lab -> XYZ dt_aligned_pixel_t XYZ = { 0.0f }; dt_Lab_to_XYZ(in + 4*k, XYZ); // XYZ -> sRGB dt_aligned_pixel_t rgb = { 0.0f }; dt_XYZ_to_sRGB(XYZ, rgb); // do the calculation in RGB space for_each_channel(c) { // lift gamma gain - apply lift and gain rgb[c] = ((( rgb[c] - one[c]) * lift[c]) + one[c]) * gain[c]; } dt_vector_max(rgb, rgb, zero); // clip away negatives // lift gamma gain - apply gamma dt_vector_powf(rgb, gamma_inv, rgb); // transform the result back to Lab // sRGB -> XYZ dt_sRGB_to_XYZ(rgb, XYZ); // XYZ -> Lab dt_aligned_pixel_t res; dt_XYZ_to_Lab(XYZ, res); copy_pixel_nontemporal(out + 4*k, res); } } #ifndef __SSE2__ static void _apply_lgg(dt_aligned_pixel_t rgb, const dt_aligned_pixel_t lift, const dt_aligned_pixel_t gamma_inv, const dt_aligned_pixel_t gain) { dt_vector_max(rgb, rgb, zero); // clip away negatives // RGB gamma correction static const dt_aligned_pixel_t power = { 1.0f/2.2f, 1.0f/2.2f, 1.0f/2.2f, 1.0f/2.2f }; dt_vector_powf(rgb, power, rgb); for_each_channel(c) { // lift gamma gain - apply lift and gain rgb[c] = ((( rgb[c] - one[c]) * lift[c]) + one[c]) * gain[c]; } dt_vector_max(rgb, rgb, zero); // clip away negatives dt_vector_powf(rgb, gamma_inv, rgb); } #endif static void _process_lgg(const dt_aligned_pixel_t in, dt_aligned_pixel_t out, const size_t npixels, const dt_aligned_pixel_t lift_, const dt_aligned_pixel_t gamma_inv_, const dt_aligned_pixel_t gain_, const float grey, const float saturation, const float saturation_out, const dt_aligned_pixel_t contrast_power) { const int run_saturation = fabsf(saturation - 1.0f) > 1e-6; const int run_saturation_out = fabsf(saturation_out - 1.0f) > 1e-6; const int run_contrast = fabsf(contrast_power[0] - 1.0f) > 1e-6; #ifdef __SSE2__ const __m128 mm_zero = _mm_setzero_ps(); const __m128 mm_one = _mm_set1_ps(1.0); const __m128 gamma_inv_RGB = _mm_set1_ps(1.0f/2.2f); const __m128 lift = _mm_load_ps(lift_); const __m128 gamma_inv = _mm_load_ps(gamma_inv_); const __m128 gain = _mm_load_ps(gain_); const __m128 contrast = _mm_load_ps(contrast_power); for(size_t k = 0; k < npixels; k++) { // transform the pixel to sRGB: // Lab -> XYZ __m128 XYZ = dt_Lab_to_XYZ_sse2(_mm_load_ps(in + 4*k)); // XYZ -> sRGB __m128 rgb = dt_XYZ_to_prophotoRGB_sse2(XYZ); __m128 luma; // adjust main saturation input if(run_saturation) { luma = _mm_set1_ps(XYZ[1]); // the Y channel is the relative luminance rgb = luma + saturation * (rgb - luma); } // RGB gamma adjustment rgb = _mm_pow_ps(_mm_max_ps(rgb, mm_zero), gamma_inv_RGB); // regular lift gamma gain rgb = ((rgb - mm_one) * lift + mm_one) * gain; rgb = _mm_max_ps(rgb, mm_zero); rgb = _mm_pow_ps(rgb, gamma_inv); // adjust main saturation output if(run_saturation_out) { XYZ = dt_prophotoRGB_to_XYZ_sse2(rgb); luma = _mm_set1_ps(XYZ[1]); // the Y channel is the relative luminance rgb = luma + saturation_out * (rgb - luma); } // fulcrum contrast if(run_contrast) { rgb = _mm_max_ps(rgb, mm_zero); rgb = _mm_pow_ps(rgb / grey, contrast) * grey; } // transform the result back to Lab // sRGB -> XYZ XYZ = dt_prophotoRGB_to_XYZ_sse2(rgb); // XYZ -> Lab _mm_stream_ps(out + 4*k, dt_XYZ_to_Lab_sse2(XYZ)); } #else if(!run_saturation && !run_saturation_out && !run_contrast) { // fully neutral settings, only apply the curves for(size_t k = 0; k < npixels; k++) { // transform the pixel to ProphotoRGB: // Lab -> XYZ -> RGB, return Y as luma dt_aligned_pixel_t rgb; (void)dt_Lab_to_prophotorgb(in + 4*k, rgb); _apply_lgg(rgb, lift_, gamma_inv_, gain_); // transform the result back to Lab // ProphotoRGB -> XYZ -> Lab dt_aligned_pixel_t res; dt_prophotorgb_to_Lab(rgb, res); copy_pixel_nontemporal(out + 4*k, res); } return; } const dt_aligned_pixel_t grey4 = { grey, grey, grey, grey }; const dt_aligned_pixel_t saturation4 = { saturation, saturation, saturation, saturation }; const dt_aligned_pixel_t saturation_out4 = { saturation_out, saturation_out, saturation_out, saturation_out }; for(size_t k = 0; k < npixels; k++) { // transform the pixel to ProphotoRGB: // Lab -> XYZ -> RGB, return Y as luma dt_aligned_pixel_t rgb; float luma = dt_Lab_to_prophotorgb(in + 4*k, rgb); if(run_saturation) { // main saturation input for_each_channel(c) rgb[c] = luma + saturation4[c] * (rgb[c] - luma); } _apply_lgg(rgb, lift_, gamma_inv_, gain_); // main saturation output if(run_saturation_out) { luma = dt_prophotorgb_to_XYZ_luma(rgb); for_each_channel(c) rgb[c] = luma + saturation_out4[c] * (rgb[c] - luma); } // fulcrum contrat if(run_contrast) { _apply_fulcrum_contrast(rgb, grey4, contrast_power); } // transform the result back to Lab // ProphotoRGB -> XYZ -> Lab dt_aligned_pixel_t res; dt_prophotorgb_to_Lab(rgb, res); copy_pixel_nontemporal(out + 4*k, res); } #endif /* __SSE2__ */ } static void _process_sop(const dt_aligned_pixel_t in, dt_aligned_pixel_t out, const size_t npixels, const dt_aligned_pixel_t lift, const dt_aligned_pixel_t gamma, const dt_aligned_pixel_t gain, const float grey, const float saturation, const float saturation_out, const float contrast_amt, const dt_aligned_pixel_t contrast_power) { const int run_saturation = fabsf(saturation - 1.0f) > 1e-6; const int run_saturation_out = fabsf(saturation_out - 1.0f) > 1e-6; const int run_contrast = fabsf(contrast_amt - 1.0f) > 1e-6; #ifdef __SSE2__ const __m128 mm_zero = _mm_setzero_ps(); const __m128 lift4 = _mm_load_ps(lift); const __m128 gamma4 = _mm_load_ps(gamma); const __m128 gain4 = _mm_load_ps(gain); const __m128 contrast = _mm_load_ps(contrast_power); for(size_t k = 0; k < npixels; k++) { // transform the pixel to sRGB: // Lab -> XYZ __m128 XYZ = dt_Lab_to_XYZ_sse2(_mm_load_ps(in + 4*k)); // XYZ -> sRGB __m128 rgb = dt_XYZ_to_prophotoRGB_sse2(XYZ); __m128 luma; // adjust main saturation if(run_saturation) { luma = _mm_set1_ps(XYZ[1]); // the Y channel is the relative luminance rgb = luma + saturation * (rgb - luma); } // slope offset rgb = rgb * gain4 + lift4; //power rgb = _mm_max_ps(rgb, mm_zero); rgb = _mm_pow_ps(rgb, gamma4); // adjust main saturation output if(run_saturation_out) { XYZ = dt_prophotoRGB_to_XYZ_sse2(rgb); luma = _mm_set1_ps(XYZ[1]); // the Y channel is the relative luminance rgb = luma + saturation_out * (rgb - luma); } // fulcrum contrast if(run_contrast) { rgb = _mm_max_ps(rgb, mm_zero); rgb = _mm_pow_ps(rgb / grey, contrast) * grey; } // transform the result back to Lab // sRGB -> XYZ XYZ = dt_prophotoRGB_to_XYZ_sse2(rgb); // XYZ -> Lab _mm_stream_ps(out + 4*k, dt_XYZ_to_Lab_sse2(XYZ)); } #else if(!run_saturation && !run_saturation_out && !run_contrast) { // fully neutral settings, only apply the curves for(size_t k = 0; k < npixels; k++) { // transform the pixel to RGB: // Lab -> XYZ -> RGB, return Y as luma dt_aligned_pixel_t rgb; (void)dt_Lab_to_prophotorgb(in + 4*k, rgb); _apply_CDL(rgb, gain, lift, gamma); // transform the result back to Lab // ProphotoRGB -> XYZ -> Lab dt_aligned_pixel_t res; dt_prophotorgb_to_Lab(rgb , res); copy_pixel_nontemporal(out + 4*k, res); } return; } const dt_aligned_pixel_t grey4 = { grey, grey, grey, grey }; const dt_aligned_pixel_t saturation4 = { saturation, saturation, saturation, saturation }; const dt_aligned_pixel_t saturation_out4 = { saturation_out, saturation_out, saturation_out, saturation_out }; for(size_t k = 0; k < npixels; k++) { // transform the pixel to RGB: // Lab -> XYZ -> RGB, return Y as luma dt_aligned_pixel_t rgb; float luma = dt_Lab_to_prophotorgb(in + 4*k, rgb); if(run_saturation) { // main saturation input for_each_channel(c) rgb[c] = luma + saturation4[c] * (rgb[c] - luma); } _apply_CDL(rgb, gain, lift, gamma); // main saturation output if(run_saturation_out) { luma = dt_prophotorgb_to_XYZ_luma(rgb); for_each_channel(c) rgb[c] = luma + saturation_out4[c] * (rgb[c] - luma); } // fulcrum contrast if(run_contrast) { _apply_fulcrum_contrast(rgb, grey4, contrast_power); } // transform the result back to Lab // ProphotoRGB -> XYZ -> Lab dt_aligned_pixel_t res; dt_prophotorgb_to_Lab(rgb , res); copy_pixel_nontemporal(out + 4*k, res); } #endif /* __SSE2__ */ } // see http://www.brucelindbloom.com/Eqn_RGB_XYZ_Matrix.html for the transformation matrices 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) { if(!dt_iop_have_required_input_format(4 /*we need full-color pixels*/, self, piece->colors, ivoid, ovoid, roi_in, roi_out)) return; dt_iop_colorbalance_data_t *d = piece->data; const float contrast = (d->contrast != 0.0f) ? 1.0f / d->contrast : 1000000.0f, grey = d->grey / 100.0f; const float saturation = d->saturation; const float saturation_out = d->saturation_out; const size_t npixels = (size_t)roi_out->width * roi_out->height; const float *const restrict in = ivoid; float *const restrict out = ovoid; const dt_aligned_pixel_t contrast_power = { contrast, contrast, contrast, contrast }; // these are RGB values! const dt_aligned_pixel_t lift = { 2.0f - (d->lift[CHANNEL_RED] * d->lift[CHANNEL_FACTOR]), 2.0f - (d->lift[CHANNEL_GREEN] * d->lift[CHANNEL_FACTOR]), 2.0f - (d->lift[CHANNEL_BLUE] * d->lift[CHANNEL_FACTOR]), 0.0f }; const dt_aligned_pixel_t lift_sop = { ( d->lift[CHANNEL_RED] + d->lift[CHANNEL_FACTOR] - 2.0f), ( d->lift[CHANNEL_GREEN] + d->lift[CHANNEL_FACTOR] - 2.0f), ( d->lift[CHANNEL_BLUE] + d->lift[CHANNEL_FACTOR] - 2.0f), 0.0f }; const dt_aligned_pixel_t gamma = { d->gamma[CHANNEL_RED] * d->gamma[CHANNEL_FACTOR], d->gamma[CHANNEL_GREEN] * d->gamma[CHANNEL_FACTOR], d->gamma[CHANNEL_BLUE] * d->gamma[CHANNEL_FACTOR], 1.0f }; const dt_aligned_pixel_t gamma_sop = { (2.0f - d->gamma[CHANNEL_RED]) * (2.0f - d->gamma[CHANNEL_FACTOR]), (2.0f - d->gamma[CHANNEL_GREEN]) * (2.0f - d->gamma[CHANNEL_FACTOR]), (2.0f - d->gamma[CHANNEL_BLUE]) * (2.0f - d->gamma[CHANNEL_FACTOR]), 1.0f }; const dt_aligned_pixel_t gamma_inv_legacy = { (gamma[0] != 0.0f) ? 1.0f / gamma[0] : 1000000.0f, (gamma[1] != 0.0f) ? 1.0f / gamma[1] : 1000000.0f, (gamma[2] != 0.0f) ? 1.0f / gamma[2] : 1000000.0f, 1.0f }; const dt_aligned_pixel_t gamma_inv_lgg = { 2.2f * ((gamma[0] != 0.0f) ? 1.0f / gamma[0] : 1000000.0f), 2.2f * ((gamma[1] != 0.0f) ? 1.0f / gamma[1] : 1000000.0f), 2.2f * ((gamma[2] != 0.0f) ? 1.0f / gamma[2] : 1000000.0f), 1.0f }; const dt_aligned_pixel_t gain = { d->gain[CHANNEL_RED] * d->gain[CHANNEL_FACTOR], d->gain[CHANNEL_GREEN] * d->gain[CHANNEL_FACTOR], d->gain[CHANNEL_BLUE] * d->gain[CHANNEL_FACTOR], 1.0f }; const int mode = d->mode; // figure out the number of pixels each thread needs to process, // rounded up to a multiple of the CPU's cache line size const size_t nthreads = dt_get_num_threads(); const size_t chunksize = dt_cacheline_chunks(npixels, nthreads); DT_OMP_FOR() for(size_t chunkstart = 0; chunkstart < npixels; chunkstart += chunksize) { size_t end = MIN(chunkstart + chunksize, npixels); switch(mode) { case LEGACY: _process_legacy(in + 4*chunkstart, out + 4*chunkstart, end-chunkstart, lift, gamma_inv_legacy, gain); break; case LIFT_GAMMA_GAIN: _process_lgg(in + 4*chunkstart, out + 4*chunkstart, end-chunkstart, lift, gamma_inv_lgg, gain, grey, saturation, saturation_out, contrast_power); break; case SLOPE_OFFSET_POWER: _process_sop(in + 4*chunkstart, out + 4*chunkstart, end-chunkstart, lift_sop, gamma_sop, gain, grey, saturation, saturation_out, contrast, contrast_power); break; } } dt_omploop_sfence(); } #ifdef HAVE_OPENCL int process_cl(dt_iop_module_t *self, dt_dev_pixelpipe_iop_t *piece, cl_mem dev_in, cl_mem dev_out, const dt_iop_roi_t *const roi_in, const dt_iop_roi_t *const roi_out) { dt_iop_colorbalance_data_t *d = piece->data; dt_iop_colorbalance_global_data_t *gd = self->global_data; cl_int err = DT_OPENCL_DEFAULT_ERROR; const int devid = piece->pipe->devid; const int width = roi_in->width; const int height = roi_in->height; switch(d->mode) { case LEGACY: { const float lift[4] = { 2.0f - (d->lift[CHANNEL_RED] * d->lift[CHANNEL_FACTOR]), 2.0f - (d->lift[CHANNEL_GREEN] * d->lift[CHANNEL_FACTOR]), 2.0f - (d->lift[CHANNEL_BLUE] * d->lift[CHANNEL_FACTOR]), 0.0f }, gamma[4] = { d->gamma[CHANNEL_RED] * d->gamma[CHANNEL_FACTOR], d->gamma[CHANNEL_GREEN] * d->gamma[CHANNEL_FACTOR], d->gamma[CHANNEL_BLUE] * d->gamma[CHANNEL_FACTOR], 0.0f }, gamma_inv[4] = { (gamma[0] != 0.0f) ? 1.0f / gamma[0] : 1000000.0f, (gamma[1] != 0.0f) ? 1.0f / gamma[1] : 1000000.0f, (gamma[2] != 0.0f) ? 1.0f / gamma[2] : 1000000.0f, 0.0f }, gain[4] = { d->gain[CHANNEL_RED] * d->gain[CHANNEL_FACTOR], d->gain[CHANNEL_GREEN] * d->gain[CHANNEL_FACTOR], d->gain[CHANNEL_BLUE] * d->gain[CHANNEL_FACTOR], 0.0f }, contrast = (d->contrast != 0.0f) ? 1.0f / d->contrast : 1000000.0f, grey = d->grey / 100.0f, saturation = d->saturation; err = dt_opencl_enqueue_kernel_2d_args(devid, gd->kernel_colorbalance, width, height, CLARG(dev_in), CLARG(dev_out), CLARG(width), CLARG(height), CLARG(lift), CLARG(gain), CLARG(gamma_inv), CLARG(saturation), CLARG(contrast), CLARG(grey)); if(err != CL_SUCCESS) goto error; return CL_SUCCESS; break; } case LIFT_GAMMA_GAIN: { const float lift[4] = { 2.0f - (d->lift[CHANNEL_RED] * d->lift[CHANNEL_FACTOR]), 2.0f - (d->lift[CHANNEL_GREEN] * d->lift[CHANNEL_FACTOR]), 2.0f - (d->lift[CHANNEL_BLUE] * d->lift[CHANNEL_FACTOR]), 0.0f }, gamma[4] = { d->gamma[CHANNEL_RED] * d->gamma[CHANNEL_FACTOR], d->gamma[CHANNEL_GREEN] * d->gamma[CHANNEL_FACTOR], d->gamma[CHANNEL_BLUE] * d->gamma[CHANNEL_FACTOR], 0.0f }, gamma_inv[4] = { (gamma[0] != 0.0f) ? 1.0f / gamma[0] : 1000000.0f, (gamma[1] != 0.0f) ? 1.0f / gamma[1] : 1000000.0f, (gamma[2] != 0.0f) ? 1.0f / gamma[2] : 1000000.0f, 0.0f }, gain[4] = { d->gain[CHANNEL_RED] * d->gain[CHANNEL_FACTOR], d->gain[CHANNEL_GREEN] * d->gain[CHANNEL_FACTOR], d->gain[CHANNEL_BLUE] * d->gain[CHANNEL_FACTOR], 0.0f }, contrast = (d->contrast != 0.0f) ? 1.0f / d->contrast : 1000000.0f, grey = d->grey / 100.0f, saturation = d->saturation, saturation_out = d->saturation_out; err = dt_opencl_enqueue_kernel_2d_args(devid, gd->kernel_colorbalance_lgg, width, height, CLARG(dev_in), CLARG(dev_out), CLARG(width), CLARG(height), CLARG(lift), CLARG(gain), CLARG(gamma_inv), CLARG(saturation), CLARG(contrast), CLARG(grey), CLARG(saturation_out)); if(err != CL_SUCCESS) goto error; return CL_SUCCESS; break; } case SLOPE_OFFSET_POWER: { const float lift[4] = { ( d->lift[CHANNEL_RED] + d->lift[CHANNEL_FACTOR] - 2.0f ), ( d->lift[CHANNEL_GREEN] + d->lift[CHANNEL_FACTOR] - 2.0f ), ( d->lift[CHANNEL_BLUE] + d->lift[CHANNEL_FACTOR] - 2.0f ), 0.0f }, gamma[4] = { (2.0f - d->gamma[CHANNEL_RED]) * (2.0f - d->gamma[CHANNEL_FACTOR]), (2.0f - d->gamma[CHANNEL_GREEN]) * (2.0f - d->gamma[CHANNEL_FACTOR]), (2.0f - d->gamma[CHANNEL_BLUE]) * (2.0f - d->gamma[CHANNEL_FACTOR]), 0.0f }, gain[4] = { d->gain[CHANNEL_RED] * d->gain[CHANNEL_FACTOR], d->gain[CHANNEL_GREEN] * d->gain[CHANNEL_FACTOR], d->gain[CHANNEL_BLUE] * d->gain[CHANNEL_FACTOR], 0.0f }, contrast = (d->contrast != 0.0f) ? 1.0f / d->contrast : 1000000.0f, grey = d->grey / 100.0f, saturation = d->saturation, saturation_out = d->saturation_out; err = dt_opencl_enqueue_kernel_2d_args(devid, gd->kernel_colorbalance_cdl, width, height, CLARG(dev_in), CLARG(dev_out), CLARG(width), CLARG(height), CLARG(lift), CLARG(gain), CLARG(gamma), CLARG(saturation), CLARG(contrast), CLARG(grey), CLARG(saturation_out)); if(err != CL_SUCCESS) goto error; return CL_SUCCESS; break; } } error: return err; } #endif static inline void update_saturation_slider_color(GtkWidget *slider, float hue) { dt_aligned_pixel_t rgb; if(hue != -1) { hsl2rgb(rgb, hue, 1.0, 0.5); dt_bauhaus_slider_set_stop(slider, 1.0, rgb[0], rgb[1], rgb[2]); hsl2rgb(rgb, hue, 0.0, 0.5); dt_bauhaus_slider_set_stop(slider, 0.0, rgb[0], rgb[1], rgb[2]); gtk_widget_queue_draw(GTK_WIDGET(slider)); } } static inline void set_RGB_sliders(GtkWidget *R, GtkWidget *G, GtkWidget *B, float hsl[3], float *p, int mode) { dt_aligned_pixel_t rgb = { 0.0f }; hsl2rgb(rgb, hsl[0], hsl[1], hsl[2]); if(hsl[0] != -1) { p[CHANNEL_RED] = rgb[0] * 2.0f; p[CHANNEL_GREEN] = rgb[1] * 2.0f; p[CHANNEL_BLUE] = rgb[2] * 2.0f; DT_ENTER_GUI_UPDATE(); dt_bauhaus_slider_set(R, p[CHANNEL_RED]); dt_bauhaus_slider_set(G, p[CHANNEL_GREEN]); dt_bauhaus_slider_set(B, p[CHANNEL_BLUE]); DT_LEAVE_GUI_UPDATE(); } } static inline void set_HSL_sliders(GtkWidget *hue, GtkWidget *sat, float RGB[4]) { /** HSL sliders are set from the RGB values at any time. * Only the RGB values are saved and used in the computations. * The HSL sliders are merely an interface. */ dt_aligned_pixel_t RGB_norm = { (RGB[CHANNEL_RED] / 2.0f), (RGB[CHANNEL_GREEN] / 2.0f), (RGB[CHANNEL_BLUE] / 2.0f) }; float h, s, l; rgb2hsl(RGB_norm, &h, &s, &l); if(h != -1.0f) { dt_bauhaus_slider_set(hue, h * 360.0f); dt_bauhaus_slider_set(sat, s * 100.0f); update_saturation_slider_color(GTK_WIDGET(sat), h); gtk_widget_queue_draw(GTK_WIDGET(sat)); } else { dt_bauhaus_slider_set(hue, -1.0f); dt_bauhaus_slider_set(sat, 0.0f); gtk_widget_queue_draw(GTK_WIDGET(sat)); } } static inline void _check_tuner_picker_labels(dt_iop_module_t *self) { dt_iop_colorbalance_gui_data_t *g = self->gui_data; if(g->luma_patches_flags[GAIN] == USER_SELECTED && g->luma_patches_flags[GAMMA] == USER_SELECTED && g->luma_patches_flags[LIFT] == USER_SELECTED) dt_bauhaus_widget_set_label(g->auto_luma, NULL, N_("optimize luma from patches")); else dt_bauhaus_widget_set_label(g->auto_luma, NULL, N_("optimize luma")); if(g->color_patches_flags[GAIN] == USER_SELECTED && g->color_patches_flags[GAMMA] == USER_SELECTED && g->color_patches_flags[LIFT] == USER_SELECTED) dt_bauhaus_widget_set_label(g->auto_color, NULL, N_("neutralize colors from patches")); else dt_bauhaus_widget_set_label(g->auto_color, NULL, N_("neutralize colors")); } static void apply_autogrey(dt_iop_module_t *self) { DT_GUARD_GUI_UPDATE(); dt_iop_colorbalance_params_t *p = self->params; dt_iop_colorbalance_gui_data_t *g = self->gui_data; dt_aligned_pixel_t XYZ = { 0.0f }; dt_aligned_pixel_t rgb = { 0.0f }; dt_Lab_to_XYZ((const float *)self->picked_color, XYZ); dt_XYZ_to_prophotorgb((const float *)XYZ, rgb); const dt_aligned_pixel_t lift = { (p->lift[CHANNEL_RED] + p->lift[CHANNEL_FACTOR] - 2.0f), (p->lift[CHANNEL_GREEN] + p->lift[CHANNEL_FACTOR] - 2.0f), (p->lift[CHANNEL_BLUE] + p->lift[CHANNEL_FACTOR] - 2.0f) }, gamma = { 2.0f - p->gamma[CHANNEL_RED] * p->gamma[CHANNEL_FACTOR], 2.0f - p->gamma[CHANNEL_GREEN] * p->gamma[CHANNEL_FACTOR], 2.0f - p->gamma[CHANNEL_BLUE] * p->gamma[CHANNEL_FACTOR] }, gain = { p->gain[CHANNEL_RED] * p->gain[CHANNEL_FACTOR], p->gain[CHANNEL_GREEN] * p->gain[CHANNEL_FACTOR], p->gain[CHANNEL_BLUE] * p->gain[CHANNEL_FACTOR] }; _apply_CDL(rgb, gain, lift, gamma); for_each_channel(c) { rgb[c] = CLAMP(rgb[c], 0.0f, 1.0f); } dt_prophotorgb_to_XYZ((const float *)rgb, XYZ); p->grey = XYZ[1] * 100.0f; DT_ENTER_GUI_UPDATE(); dt_bauhaus_slider_set(g->grey, p->grey); DT_LEAVE_GUI_UPDATE(); dt_dev_add_history_item(darktable.develop, self, TRUE); } static void apply_lift_neutralize(dt_iop_module_t *self) { DT_GUARD_GUI_UPDATE(); dt_iop_colorbalance_params_t *p = self->params; dt_iop_colorbalance_gui_data_t *g = self->gui_data; dt_aligned_pixel_t XYZ = { 0.0f }; dt_Lab_to_XYZ((const float *)self->picked_color, XYZ); dt_aligned_pixel_t RGB = { 0.0f }; dt_XYZ_to_prophotorgb((const float *)XYZ, RGB); // Save the patch color for the optimization for(int c = 0; c < 3; ++c) g->color_patches_lift[c] = RGB[c]; g->color_patches_flags[LIFT] = USER_SELECTED; // Compute the RGB values after the CDL factors for(int c = 0; c < 3; ++c) RGB[c] = CDL(RGB[c], p->gain[CHANNEL_FACTOR], p->lift[CHANNEL_FACTOR] - 1.0f, 2.0f - p->gamma[CHANNEL_FACTOR]); // Compute the luminance of the average grey dt_XYZ_to_prophotorgb((const float *)XYZ, RGB); // Get the parameter for(int c = 0; c < 3; ++c) RGB[c] = powf(XYZ[1], 1.0f/(2.0f - p->gamma[c+1])) - RGB[c] * p->gain[c+1]; p->lift[CHANNEL_RED] = RGB[0] + 1.0f; p->lift[CHANNEL_GREEN] = RGB[1] + 1.0f; p->lift[CHANNEL_BLUE] = RGB[2] + 1.0f; DT_ENTER_GUI_UPDATE(); dt_bauhaus_slider_set(g->lift_r, p->lift[CHANNEL_RED]); dt_bauhaus_slider_set(g->lift_g, p->lift[CHANNEL_GREEN]); dt_bauhaus_slider_set(g->lift_b, p->lift[CHANNEL_BLUE]); set_HSL_sliders(g->hue_lift, g->sat_lift, p->lift); DT_LEAVE_GUI_UPDATE(); dt_dev_add_history_item(darktable.develop, self, TRUE); } static void apply_gamma_neutralize(dt_iop_module_t *self) { DT_GUARD_GUI_UPDATE(); dt_iop_colorbalance_params_t *p = self->params; dt_iop_colorbalance_gui_data_t *g = self->gui_data; dt_aligned_pixel_t XYZ = { 0.0f }; dt_Lab_to_XYZ((const float *)self->picked_color, XYZ); dt_aligned_pixel_t RGB = { 0.0f }; dt_XYZ_to_prophotorgb((const float *)XYZ, RGB); // Save the patch color for the optimization for(int c = 0; c < 3; ++c) g->color_patches_gamma[c] = RGB[c]; g->color_patches_flags[GAMMA] = USER_SELECTED; // Compute the RGB values after the CDL factors for(int c = 0; c < 3; ++c) RGB[c] = CDL(RGB[c], p->gain[CHANNEL_FACTOR], p->lift[CHANNEL_FACTOR] - 1.0f, 2.0f - p->gamma[CHANNEL_FACTOR]); // Compute the luminance of the average grey dt_XYZ_to_prophotorgb((const float *)XYZ, RGB); // Get the parameter for(int c = 0; c < 3; ++c) RGB[c] = logf(XYZ[1])/ logf(RGB[c] * p->gain[c + 1] + p->lift[c + 1] - 1.0f); p->gamma[CHANNEL_RED] = CLAMP(2.0 - RGB[0], 0.0001f, 2.0f); p->gamma[CHANNEL_GREEN] = CLAMP(2.0 - RGB[1], 0.0001f, 2.0f); p->gamma[CHANNEL_BLUE] = CLAMP(2.0 - RGB[2], 0.0001f, 2.0f); DT_ENTER_GUI_UPDATE(); dt_bauhaus_slider_set(g->gamma_r, p->gamma[CHANNEL_RED]); dt_bauhaus_slider_set(g->gamma_g, p->gamma[CHANNEL_GREEN]); dt_bauhaus_slider_set(g->gamma_b, p->gamma[CHANNEL_BLUE]); set_HSL_sliders(g->hue_gamma, g->sat_gamma, p->gamma); DT_LEAVE_GUI_UPDATE(); dt_dev_add_history_item(darktable.develop, self, TRUE); } static void apply_gain_neutralize(dt_iop_module_t *self) { DT_GUARD_GUI_UPDATE(); dt_iop_colorbalance_params_t *p = self->params; dt_iop_colorbalance_gui_data_t *g = self->gui_data; dt_aligned_pixel_t XYZ = { 0.0f }; dt_Lab_to_XYZ((const float *)self->picked_color, XYZ); dt_aligned_pixel_t RGB = { 0.0f }; dt_XYZ_to_prophotorgb((const float *)XYZ, RGB); // Save the patch color for the optimization for(int c = 0; c < 3; c++) g->color_patches_gain[c] = RGB[c]; g->color_patches_flags[GAIN] = USER_SELECTED; // Compute the RGB values after the CDL factors for(int c = 0; c < 3; ++c) RGB[c] = CDL(RGB[c], p->gain[CHANNEL_FACTOR], p->lift[CHANNEL_FACTOR] - 1.0f, 2.0f - p->gamma[CHANNEL_FACTOR]); // Compute the luminance of the average grey dt_XYZ_to_prophotorgb((const float *)XYZ, RGB); // Get the parameter for(int c = 0; c < 3; ++c) RGB[c] = (powf(XYZ[1], 1.0f/(2.0f - p->gamma[c+1])) - p->lift[c+1] + 1.0f) / MAX(RGB[c], 0.000001f); p->gain[CHANNEL_RED] = RGB[0]; p->gain[CHANNEL_GREEN] = RGB[1]; p->gain[CHANNEL_BLUE] = RGB[2]; DT_ENTER_GUI_UPDATE(); dt_bauhaus_slider_set(g->gain_r, p->gain[CHANNEL_RED]); dt_bauhaus_slider_set(g->gain_g, p->gain[CHANNEL_GREEN]); dt_bauhaus_slider_set(g->gain_b, p->gain[CHANNEL_BLUE]); set_HSL_sliders(g->hue_gain, g->sat_gain, p->gain); DT_LEAVE_GUI_UPDATE(); dt_dev_add_history_item(darktable.develop, self, TRUE); } static void apply_lift_auto(dt_iop_module_t *self) { DT_GUARD_GUI_UPDATE(); dt_iop_colorbalance_params_t *p = self->params; dt_iop_colorbalance_gui_data_t *g = self->gui_data; dt_aligned_pixel_t XYZ = { 0.0f }; dt_Lab_to_XYZ((const float *)self->picked_color_min, XYZ); g->luma_patches[LIFT] = XYZ[1]; g->luma_patches_flags[LIFT] = USER_SELECTED; dt_aligned_pixel_t RGB = { 0.0f }; dt_XYZ_to_prophotorgb((const float *)XYZ, RGB); p->lift[CHANNEL_FACTOR] = -p->gain[CHANNEL_FACTOR] * XYZ[1] + 1.0f; DT_ENTER_GUI_UPDATE(); dt_bauhaus_slider_set(g->lift_factor, p->lift[CHANNEL_FACTOR]); DT_LEAVE_GUI_UPDATE(); dt_dev_add_history_item(darktable.develop, self, TRUE); } static void apply_gamma_auto(dt_iop_module_t *self) { DT_GUARD_GUI_UPDATE(); dt_iop_colorbalance_params_t *p = self->params; dt_iop_colorbalance_gui_data_t *g = self->gui_data; dt_aligned_pixel_t XYZ = { 0.0f }; dt_Lab_to_XYZ((const float *)self->picked_color, XYZ); g->luma_patches[GAMMA] = XYZ[1]; g->luma_patches_flags[GAMMA] = USER_SELECTED; dt_aligned_pixel_t RGB = { 0.0f }; dt_XYZ_to_prophotorgb((const float *)XYZ, RGB); p->gamma[CHANNEL_FACTOR] = 2.0f - logf(0.1842f) / logf(MAX(p->gain[CHANNEL_FACTOR] * XYZ[1] + p->lift[CHANNEL_FACTOR] - 1.0f, 0.000001f)); DT_ENTER_GUI_UPDATE(); dt_bauhaus_slider_set(g->gamma_factor, p->gamma[CHANNEL_FACTOR]); DT_LEAVE_GUI_UPDATE(); dt_dev_add_history_item(darktable.develop, self, TRUE); } static void apply_gain_auto(dt_iop_module_t *self) { DT_GUARD_GUI_UPDATE(); dt_iop_colorbalance_params_t *p = self->params; dt_iop_colorbalance_gui_data_t *g = self->gui_data; dt_aligned_pixel_t XYZ = { 0.0f }; dt_Lab_to_XYZ((const float *)self->picked_color_max, XYZ); g->luma_patches[GAIN] = XYZ[1]; g->luma_patches_flags[GAIN] = USER_SELECTED; dt_aligned_pixel_t RGB = { 0.0f }; dt_XYZ_to_prophotorgb((const float *)XYZ, RGB); p->gain[CHANNEL_FACTOR] = p->lift[CHANNEL_FACTOR] / (XYZ[1]); DT_ENTER_GUI_UPDATE(); dt_bauhaus_slider_set(g->gain_factor, p->gain[CHANNEL_FACTOR]); DT_LEAVE_GUI_UPDATE(); dt_dev_add_history_item(darktable.develop, self, TRUE); } static void apply_autocolor(dt_iop_module_t *self) { dt_iop_colorbalance_params_t *p = self->params; dt_iop_colorbalance_gui_data_t *g = self->gui_data; if(g->color_patches_flags[GAIN] == INVALID || g->color_patches_flags[GAMMA] == INVALID || g->color_patches_flags[LIFT] == INVALID) { /* * Some color patches were not picked by the user. Take a * picture-wide patch for these. */ dt_aligned_pixel_t XYZ = { 0.0f }; dt_Lab_to_XYZ((const float *)self->picked_color, XYZ); dt_aligned_pixel_t RGB = { 0.0f }; dt_XYZ_to_prophotorgb((const float *)XYZ, RGB); // Save the patch color for the optimization if(g->color_patches_flags[LIFT] == INVALID) { for(int c = 0; c < 3; c++) g->color_patches_lift[c] = RGB[c]; g->color_patches_flags[LIFT] = AUTO_SELECTED; } if(g->color_patches_flags[GAMMA] == INVALID) { for(int c = 0; c < 3; c++) g->color_patches_gamma[c] = RGB[c]; g->color_patches_flags[GAMMA] = AUTO_SELECTED; } if(g->color_patches_flags[GAIN] == INVALID) { for(int c = 0; c < 3; c++) g->color_patches_gain[c] = RGB[c]; g->color_patches_flags[GAIN] = AUTO_SELECTED; } } dt_iop_color_picker_reset(self, TRUE); // Build the CDL-corrected samples (after the factors) dt_aligned_pixel_t samples_lift = { 0.f }; dt_aligned_pixel_t samples_gamma = { 0.f }; dt_aligned_pixel_t samples_gain = { 0.f }; for(int c = 0; c < 3; ++c) { samples_lift[c] = CDL(g->color_patches_lift[c], p->gain[CHANNEL_FACTOR], p->lift[CHANNEL_FACTOR] - 1.0f, 2.0f - p->gamma[CHANNEL_FACTOR]); samples_gamma[c] = CDL(g->color_patches_gamma[c], p->gain[CHANNEL_FACTOR], p->lift[CHANNEL_FACTOR] - 1.0f, 2.0f - p->gamma[CHANNEL_FACTOR]); samples_gain[c] = CDL(g->color_patches_gain[c], p->gain[CHANNEL_FACTOR], p->lift[CHANNEL_FACTOR] - 1.0f, 2.0f - p->gamma[CHANNEL_FACTOR]); } // Get the average patches luma value (= neutral grey equivalents) after the CDL factors dt_aligned_pixel_t greys = { 0.0 }; dt_aligned_pixel_t XYZ = { 0.0 }; dt_prophotorgb_to_XYZ((const float *)samples_lift, (float *)XYZ); greys[0] = XYZ[1]; dt_prophotorgb_to_XYZ((const float *)samples_gamma, (float *)XYZ); greys[1] = XYZ[1]; dt_prophotorgb_to_XYZ((const float *)samples_gain, (float *)XYZ); greys[2] = XYZ[1]; // Get the current params dt_aligned_pixel_t RGB_lift = { p->lift[CHANNEL_RED] - 1.0f, p->lift[CHANNEL_GREEN] - 1.0f, p->lift[CHANNEL_BLUE] - 1.0f }; dt_aligned_pixel_t RGB_gamma = { p->gamma[CHANNEL_RED], p->gamma[CHANNEL_GREEN], p->gamma[CHANNEL_BLUE] }; dt_aligned_pixel_t RGB_gain = { p->gain[CHANNEL_RED], p->gain[CHANNEL_GREEN], p->gain[CHANNEL_BLUE] }; /** Optimization loop : * We try to find the CDL curves that neutralize the 3 input color patches, while not affecting the overall lightness. * But this is a non-linear overconstrained problem with tainted inputs, so the best we can do is a numerical optimization. * To do so, we compute each parameter of each RGB curve from the input color and the 2 other parameters. * Then, we loop over the previous optimization until the difference between 2 updates is insignificant. * This would need a proper stopping criterion based on convergence analysis, but it would be overkill here since * it should converge usually in 20 iterations, and maximum in 100. * Also, the convergence has not been proven formally. * For better color accuracy, we compute on luminance corrected RGB values (after the main factors corrections). * To avoid divergence, we constrain the parameters between +- 0.25 around the neutral value. * Experimentally, nothing good happens out of these bounds. */ for(int runs = 0 ; runs < 1000 ; ++runs) { // compute RGB slope/gain (powf(XYZ[1], 1.0f/(2.0f - p->gamma[c+1])) - p->lift[c+1] + 1.0f) / MAX(RGB[c], 0.000001f); for(int c = 0; c < 3; ++c) RGB_gain[c] = CLAMP((powf(greys[GAIN], 1.0f / (2.0f - RGB_gamma[c])) - RGB_lift[c]) / MAX(samples_gain[c], 0.000001f), 0.75f, 1.25f); // compute RGB offset/lift powf(XYZ[1], 1.0f/(2.0f - p->gamma[c+1])) - RGB[c] * p->gain[c+1]; for(int c = 0; c < 3; ++c) RGB_lift[c] = CLAMP(powf(greys[LIFT], 1.0f / (2.0f - RGB_gamma[c])) - samples_lift[c] * RGB_gain[c], -0.025f, 0.025f); // compute power/gamma 2.0f - logf(0.1842f) / logf(MAX(p->gain[CHANNEL_FACTOR] * XYZ[1] + p->lift[CHANNEL_FACTOR] - 1.0f, 0.000001f)); for(int c = 0; c < 3; ++c) RGB_gamma[c] = 2.0f - CLAMP(logf(MAX(greys[GAMMA], 0.000001f)) / logf(MAX(RGB_gain[c] * samples_gamma[c] + RGB_lift[c], 0.000001f)), 0.75f, 1.25f); } // save p->lift[CHANNEL_RED] = RGB_lift[0] + 1.0f; p->lift[CHANNEL_GREEN] = RGB_lift[1] + 1.0f; p->lift[CHANNEL_BLUE] = RGB_lift[2] + 1.0f; p->gamma[CHANNEL_RED] = RGB_gamma[0]; p->gamma[CHANNEL_GREEN] = RGB_gamma[1]; p->gamma[CHANNEL_BLUE] = RGB_gamma[2]; p->gain[CHANNEL_RED] = RGB_gain[0]; p->gain[CHANNEL_GREEN] = RGB_gain[1]; p->gain[CHANNEL_BLUE] = RGB_gain[2]; DT_ENTER_GUI_UPDATE(); dt_bauhaus_slider_set(g->lift_r, p->lift[CHANNEL_RED]); dt_bauhaus_slider_set(g->lift_g, p->lift[CHANNEL_GREEN]); dt_bauhaus_slider_set(g->lift_b, p->lift[CHANNEL_BLUE]); dt_bauhaus_slider_set(g->gamma_r, p->gamma[CHANNEL_RED]); dt_bauhaus_slider_set(g->gamma_g, p->gamma[CHANNEL_GREEN]); dt_bauhaus_slider_set(g->gamma_b, p->gamma[CHANNEL_BLUE]); dt_bauhaus_slider_set(g->gain_r, p->gain[CHANNEL_RED]); dt_bauhaus_slider_set(g->gain_g, p->gain[CHANNEL_GREEN]); dt_bauhaus_slider_set(g->gain_b, p->gain[CHANNEL_BLUE]); set_HSL_sliders(g->hue_lift, g->sat_lift, p->lift); set_HSL_sliders(g->hue_gamma, g->sat_gamma, p->gamma); set_HSL_sliders(g->hue_gain, g->sat_gain, p->gain); DT_LEAVE_GUI_UPDATE(); dt_dev_add_history_item(darktable.develop, self, TRUE); } static void apply_autoluma(dt_iop_module_t *self) { dt_iop_colorbalance_params_t *p = self->params; dt_iop_colorbalance_gui_data_t *g = self->gui_data; /* * If some luma patches were not picked by the user, take a * picture-wide patch for these. */ if(g->luma_patches_flags[LIFT] == INVALID) { dt_aligned_pixel_t XYZ = { 0.0f }; dt_Lab_to_XYZ((const float *)self->picked_color_min, XYZ); g->luma_patches[LIFT] = XYZ[1]; g->luma_patches_flags[LIFT] = AUTO_SELECTED; } if(g->luma_patches_flags[GAMMA] == INVALID) { dt_aligned_pixel_t XYZ = { 0.0f }; dt_Lab_to_XYZ((const float *)self->picked_color, XYZ); g->luma_patches[GAMMA] = XYZ[1]; g->luma_patches_flags[GAMMA] = AUTO_SELECTED; } if(g->luma_patches_flags[GAIN] == INVALID) { dt_aligned_pixel_t XYZ = { 0.0f }; dt_Lab_to_XYZ((const float *)self->picked_color_max, XYZ); g->luma_patches[GAIN] = XYZ[1]; g->luma_patches_flags[GAIN] = AUTO_SELECTED; } dt_iop_color_picker_reset(self, TRUE); /** Optimization loop : * We try to find the CDL curves that neutralize the 3 input luma patches */ for(int runs = 0 ; runs < 100 ; ++runs) { p->gain[CHANNEL_FACTOR] = CLAMP(p->lift[CHANNEL_FACTOR] / g->luma_patches[GAIN], 0.0f, 2.0f); p->lift[CHANNEL_FACTOR] = CLAMP(-p->gain[CHANNEL_FACTOR] * g->luma_patches[LIFT] + 1.0f, 0.0f, 2.0f); p->gamma[CHANNEL_FACTOR] = CLAMP(2.0f - logf(0.1842f) / logf(MAX(p->gain[CHANNEL_FACTOR] * g->luma_patches[GAMMA] + p->lift[CHANNEL_FACTOR] - 1.0f, 0.000001f)), 0.0f, 2.0f); } DT_ENTER_GUI_UPDATE(); dt_bauhaus_slider_set(g->lift_factor, p->lift[CHANNEL_FACTOR]); dt_bauhaus_slider_set(g->gamma_factor, p->gamma[CHANNEL_FACTOR]); dt_bauhaus_slider_set(g->gain_factor, p->gain[CHANNEL_FACTOR]); DT_LEAVE_GUI_UPDATE(); dt_dev_add_history_item(darktable.develop, self, TRUE); } void color_picker_apply(dt_iop_module_t *self, GtkWidget *picker, dt_dev_pixelpipe_t *pipe) { dt_iop_colorbalance_gui_data_t *g = self->gui_data; if (picker == g->hue_lift) apply_lift_neutralize(self); else if(picker == g->hue_gamma) apply_gamma_neutralize(self); else if(picker == g->hue_gain) apply_gain_neutralize(self); else if(picker == g->lift_factor) apply_lift_auto(self); else if(picker == g->gamma_factor) apply_gamma_auto(self); else if(picker == g->gain_factor) apply_gain_auto(self); else if(picker == g->grey) apply_autogrey(self); else if(picker == g->auto_luma) apply_autoluma(self); else if(picker == g->auto_color) apply_autocolor(self); else dt_print(DT_DEBUG_ALWAYS, "[colorbalance] unknown color picker"); _check_tuner_picker_labels(self); } void init_global(dt_iop_module_so_t *self) { const int program = 8; // extended.cl, from programs.conf dt_iop_colorbalance_global_data_t *gd = malloc(sizeof(dt_iop_colorbalance_global_data_t)); self->data = gd; gd->kernel_colorbalance = dt_opencl_create_kernel(program, "colorbalance"); gd->kernel_colorbalance_lgg = dt_opencl_create_kernel(program, "colorbalance_lgg"); gd->kernel_colorbalance_cdl = dt_opencl_create_kernel(program, "colorbalance_cdl"); } void cleanup_global(dt_iop_module_so_t *self) { dt_iop_colorbalance_global_data_t *gd = self->data; dt_opencl_free_kernel(gd->kernel_colorbalance); dt_opencl_free_kernel(gd->kernel_colorbalance_lgg); dt_opencl_free_kernel(gd->kernel_colorbalance_cdl); free(self->data); self->data = NULL; } 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_colorbalance_data_t *d = piece->data; dt_iop_colorbalance_params_t *p = (dt_iop_colorbalance_params_t *)p1; d->mode = p->mode; const dt_aligned_pixel_t lift = { p->lift[CHANNEL_RED], p->lift[CHANNEL_GREEN], p->lift[CHANNEL_BLUE] }; const dt_aligned_pixel_t gamma = { p->gamma[CHANNEL_RED], p->gamma[CHANNEL_GREEN], p->gamma[CHANNEL_BLUE] }; const dt_aligned_pixel_t gain = { p->gain[CHANNEL_RED], p->gain[CHANNEL_GREEN], p->gain[CHANNEL_BLUE] }; switch(d->mode) { case SLOPE_OFFSET_POWER: { // Correct the luminance in RGB parameters so we don't affect it dt_aligned_pixel_t XYZ; dt_prophotorgb_to_XYZ(lift, XYZ); d->lift[CHANNEL_FACTOR] = p->lift[CHANNEL_FACTOR]; d->lift[CHANNEL_RED] = (p->lift[CHANNEL_RED] - XYZ[1]) + 1.f; d->lift[CHANNEL_GREEN] = (p->lift[CHANNEL_GREEN] - XYZ[1]) + 1.f; d->lift[CHANNEL_BLUE] = (p->lift[CHANNEL_BLUE] - XYZ[1]) + 1.f; dt_prophotorgb_to_XYZ(gamma, XYZ); d->gamma[CHANNEL_FACTOR] = p->gamma[CHANNEL_FACTOR]; d->gamma[CHANNEL_RED] = (p->gamma[CHANNEL_RED] - XYZ[1]) + 1.f; d->gamma[CHANNEL_GREEN] = (p->gamma[CHANNEL_GREEN] - XYZ[1]) + 1.f; d->gamma[CHANNEL_BLUE] = (p->gamma[CHANNEL_BLUE] - XYZ[1]) + 1.f; dt_prophotorgb_to_XYZ(gain, XYZ); d->gain[CHANNEL_FACTOR] = p->gain[CHANNEL_FACTOR]; d->gain[CHANNEL_RED] = (p->gain[CHANNEL_RED] - XYZ[1]) + 1.f; d->gain[CHANNEL_GREEN] = (p->gain[CHANNEL_GREEN] - XYZ[1]) + 1.f; d->gain[CHANNEL_BLUE] = (p->gain[CHANNEL_BLUE] - XYZ[1]) + 1.f; break; } case LEGACY: { // Luminance is not corrected in lift/gamma/gain for compatibility for(int i = 0; i < CHANNEL_SIZE; i++) { d->lift[i] = p->lift[i]; d->gamma[i] = p->gamma[i]; d->gain[i] = p->gain[i]; } break; } case LIFT_GAMMA_GAIN: { // Correct the luminance in RGB parameters so we don't affect it dt_aligned_pixel_t XYZ; dt_prophotorgb_to_XYZ(lift, XYZ); d->lift[CHANNEL_FACTOR] = p->lift[CHANNEL_FACTOR]; d->lift[CHANNEL_RED] = (p->lift[CHANNEL_RED] - XYZ[1]) + 1.f; d->lift[CHANNEL_GREEN] = (p->lift[CHANNEL_GREEN] - XYZ[1]) + 1.f; d->lift[CHANNEL_BLUE] = (p->lift[CHANNEL_BLUE] - XYZ[1]) + 1.f; dt_prophotorgb_to_XYZ(gamma, XYZ); d->gamma[CHANNEL_FACTOR] = p->gamma[CHANNEL_FACTOR]; d->gamma[CHANNEL_RED] = (p->gamma[CHANNEL_RED] - XYZ[1]) + 1.f; d->gamma[CHANNEL_GREEN] = (p->gamma[CHANNEL_GREEN] - XYZ[1]) + 1.f; d->gamma[CHANNEL_BLUE] = (p->gamma[CHANNEL_BLUE] - XYZ[1]) + 1.f; dt_prophotorgb_to_XYZ(gain, XYZ); d->gain[CHANNEL_FACTOR] = p->gain[CHANNEL_FACTOR]; d->gain[CHANNEL_RED] = (p->gain[CHANNEL_RED] - XYZ[1]) + 1.f; d->gain[CHANNEL_GREEN] = (p->gain[CHANNEL_GREEN] - XYZ[1]) + 1.f; d->gain[CHANNEL_BLUE] = (p->gain[CHANNEL_BLUE] - XYZ[1]) + 1.f; break; } } d->grey = p->grey; d->saturation = p->saturation; d->saturation_out = p->saturation_out; d->contrast = p->contrast; } void init_pipe(dt_iop_module_t *self, dt_dev_pixelpipe_t *pipe, dt_dev_pixelpipe_iop_t *piece) { piece->data = calloc(1, sizeof(dt_iop_colorbalance_data_t)); } void cleanup_pipe(dt_iop_module_t *self, dt_dev_pixelpipe_t *pipe, dt_dev_pixelpipe_iop_t *piece) { free(piece->data); piece->data = NULL; } void set_visible_widgets(dt_iop_colorbalance_gui_data_t *g) { const int mode = dt_bauhaus_combobox_get(g->mode); const int control_mode = dt_bauhaus_combobox_get(g->controls); gtk_widget_set_visible(g->master_box, mode != LEGACY); dt_conf_set_string("plugins/darkroom/colorbalance/controls", control_mode == RGBL ? "RGBL" : control_mode == BOTH ? "BOTH" : "HSL"); gboolean show_rgbl = (control_mode == RGBL) || (control_mode == BOTH); gboolean show_hsl = (control_mode == HSL) || (control_mode == BOTH); gtk_widget_set_visible(g->lift_r, show_rgbl); gtk_widget_set_visible(g->lift_g, show_rgbl); gtk_widget_set_visible(g->lift_b, show_rgbl); gtk_widget_set_visible(g->gamma_r, show_rgbl); gtk_widget_set_visible(g->gamma_g, show_rgbl); gtk_widget_set_visible(g->gamma_b, show_rgbl); gtk_widget_set_visible(g->gain_r, show_rgbl); gtk_widget_set_visible(g->gain_g, show_rgbl); gtk_widget_set_visible(g->gain_b, show_rgbl); gtk_widget_set_visible(g->hue_lift, show_hsl); gtk_widget_set_visible(g->sat_lift, show_hsl); gtk_widget_set_visible(g->hue_gamma, show_hsl); gtk_widget_set_visible(g->sat_gamma, show_hsl); gtk_widget_set_visible(g->hue_gain, show_hsl); gtk_widget_set_visible(g->sat_gain, show_hsl); gtk_widget_set_visible(g->optimizer_box, mode == SLOPE_OFFSET_POWER); } void gui_update(dt_iop_module_t *self) { dt_iop_color_picker_reset(self, TRUE); _check_tuner_picker_labels(self); gui_changed(self, NULL, NULL); } void gui_reset(dt_iop_module_t *self) { dt_iop_colorbalance_gui_data_t *g = self->gui_data; for(int k=0; kcolor_patches_flags[k] = INVALID; g->luma_patches_flags[k] = INVALID; } _check_tuner_picker_labels(self); dt_bauhaus_combobox_set(g->controls, HSL); set_visible_widgets(g); dt_iop_color_picker_reset(self, TRUE); } static void _configure_slider_blocks(gpointer instance, dt_iop_module_t *self); void gui_changed(dt_iop_module_t *self, GtkWidget *w, void *previous) { dt_iop_colorbalance_params_t *p = self->params; dt_iop_colorbalance_gui_data_t *g = self->gui_data; if(!w || w == g->mode) { set_visible_widgets(g); _configure_slider_blocks(NULL, self); } DT_ENTER_GUI_UPDATE(); if(!w || w == g->lift_r || w == g->lift_g || w == g->lift_b) set_HSL_sliders(g->hue_lift, g->sat_lift, p->lift); if(!w || w == g->gamma_r || w == g->gamma_g || w == g->gamma_b) set_HSL_sliders(g->hue_gamma, g->sat_gamma, p->gamma); if(!w || w == g->gain_r || w == g->gain_g || w == g->gain_b) set_HSL_sliders(g->hue_gain, g->sat_gain, p->gain); DT_LEAVE_GUI_UPDATE(); } static void controls_callback(GtkWidget *combo, dt_iop_module_t *self) { DT_GUARD_GUI_UPDATE(); dt_iop_colorbalance_gui_data_t *g = self->gui_data; set_visible_widgets(g); dt_iop_color_picker_reset(self, TRUE); } #ifdef SHOW_COLOR_WHEELS static gboolean dt_iop_area_draw(GtkWidget *widget, cairo_t *cr, dt_iop_module_t *self) { float flt_bg = 0.5; if(gtk_widget_get_state_flags(widget) & GTK_STATE_FLAG_SELECTED) flt_bg = 0.6; float flt_dark = flt_bg / 1.5, flt_light = flt_bg * 1.5; uint32_t bg = ((255 << 24) | ((int)floor(flt_bg * 255 + 0.5) << 16) | ((int)floor(flt_bg * 255 + 0.5) << 8) | (int)floor(flt_bg * 255 + 0.5)); // bg = 0xffffffff; // uint32_t dark = ((255 << 24) | // ((int)floor(flt_dark * 255 + 0.5) << 16) | // ((int)floor(flt_dark * 255 + 0.5) << 8) | // (int)floor(flt_dark * 255 + 0.5)); uint32_t light = ((255 << 24) | ((int)floor(flt_light * 255 + 0.5) << 16) | ((int)floor(flt_light * 255 + 0.5) << 8) | (int)floor(flt_light * 255 + 0.5)); GtkAllocation allocation; gtk_widget_get_allocation(widget, &allocation); int width = allocation.width, height = allocation.height; if(width % 2 == 0) width--; if(height % 2 == 0) height--; double center_x = (float)width / 2.0, center_y = (float)height / 2.0; double diameter = MIN(width, height) - 4; double r_outside = diameter / 2.0, r_inside = r_outside * 0.87; double r_outside_2 = r_outside * r_outside, r_inside_2 = r_inside * r_inside; // clear the background cairo_set_source_rgb(cr, flt_bg, flt_bg, flt_bg); cairo_paint(cr); /* Create an image initialized with the ring colors */ gint stride = cairo_format_stride_for_width(CAIRO_FORMAT_RGB24, width); guint32 *buf = malloc(sizeof(guint32) * height * stride / 4); for(int y = 0; y < height; y++) { guint32 *p = buf + y * width; double dy = -(y + 0.5 - center_y); for(int x = 0; x < width; x++) { double dx = x + 0.5 - center_x; double dist = dx * dx + dy * dy; if(dist < r_inside_2 || dist > r_outside_2) { uint32_t col = bg; if((abs(dx) < 1 && abs(dy) < 3) || (abs(dx) < 3 && abs(dy) < 1)) col = light; *p++ = col; continue; } double angle = atan2(dy, dx) - M_PI_2; if(angle < 0.0) angle += 2.0 * M_PI; double hue = angle / (2.0 * M_PI); dt_aligned_pixel_t rgb; hsl2rgb(rgb, hue, 1.0, 0.5); *p++ = (((int)floor(rgb[0] * 255 + 0.5) << 16) | ((int)floor(rgb[1] * 255 + 0.5) << 8) | (int)floor(rgb[2] * 255 + 0.5)); } } cairo_surface_t *source = cairo_image_surface_create_for_data((unsigned char *)buf, CAIRO_FORMAT_RGB24, width, height, stride); cairo_set_source_surface(cr, source, 0.0, 0.0); cairo_paint(cr); free(buf); // draw border float line_width = 1; cairo_set_line_width(cr, line_width); cairo_set_source_rgb(cr, flt_bg, flt_bg, flt_bg); cairo_new_path(cr); cairo_arc(cr, center_x, center_y, r_outside, 0.0, 2.0 * M_PI); cairo_stroke(cr); cairo_arc(cr, center_x, center_y, r_inside, 0.0, 2.0 * M_PI); cairo_stroke(cr); cairo_set_source_rgb(cr, flt_dark, flt_dark, flt_dark); cairo_new_path(cr); cairo_arc(cr, center_x, center_y, r_outside, M_PI, 1.5 * M_PI); cairo_stroke(cr); cairo_arc(cr, center_x, center_y, r_inside, 0.0, 0.5 * M_PI); cairo_stroke(cr); cairo_set_source_rgb(cr, flt_light, flt_light, flt_light); cairo_new_path(cr); cairo_arc(cr, center_x, center_y, r_outside, 0.0, 0.5 * M_PI); cairo_stroke(cr); cairo_arc(cr, center_x, center_y, r_inside, M_PI, 1.5 * M_PI); cairo_stroke(cr); // draw selector double r = 255 / 255.0, g = 155 / 255.0, b = 40 / 255.0; double h, s, v; gtk_rgb_to_hsv(r, g, b, &h, &s, &v); cairo_save(cr); cairo_set_source_rgba(cr, 1.0, 1.0, 1.0, 0.7); cairo_translate(cr, center_x, center_y); cairo_rotate(cr, h * 2.0 * M_PI - M_PI_2); cairo_arc(cr, r_inside * v, 0.0, 3.0, 0, 2.0 * M_PI); cairo_stroke(cr); cairo_restore(cr); cairo_surface_destroy(source); return TRUE; } #endif static void _configure_slider_blocks(gpointer instance, dt_iop_module_t *self) { dt_iop_colorbalance_params_t *p = self->params; dt_iop_colorbalance_gui_data_t *g = self->gui_data; GtkWidget *new_container = NULL; GtkWidget *old_container = gtk_bin_get_child(GTK_BIN(g->main_box)); for(int i=0; i<3; i++) { g_object_ref(G_OBJECT(g->blocks[i])); if(old_container) gtk_container_remove(GTK_CONTAINER(old_container), g->blocks[i]); } if(old_container) gtk_widget_destroy(old_container); const gchar *short_label_ops[] = { C_("color", "offset"), C_("color", "power"), C_("color", "slope") }; const gchar *short_label_lgg[] = { C_("color", "lift"), C_("color", "gamma"), C_("color", "gain") }; const gchar **short_label = (p->mode == SLOPE_OFFSET_POWER) ? short_label_ops : short_label_lgg; const gchar *long_label[] = { NC_("section", "shadows: lift / offset"), NC_("section", "mid-tones: gamma / power"), NC_("section", "highlights: gain / slope") }; gchar *layout = dt_conf_get_string("plugins/darkroom/colorbalance/layout"); if(!g_strcmp0(layout, "list")) { new_container = dt_gui_vbox(); for(int i=0; i<3; i++) { if(i == 0) gtk_label_set_text(GTK_LABEL(g->main_label), Q_(long_label[0])); else { GtkWidget *label = dt_ui_section_label_new(Q_(long_label[i])); gtk_container_add(GTK_CONTAINER(new_container), label); if(old_container) gtk_widget_show(label); } gtk_container_add(GTK_CONTAINER(new_container), g->blocks[i]); } } else { gtk_label_set_text(GTK_LABEL(g->main_label), _("shadows / mid-tones / highlights")); GtkWidget *label[3]; for(int i=0; i<3; i++) { label[i] = gtk_label_new(_(short_label[i])); gtk_widget_set_tooltip_text(label[i], _(long_label[i])); gtk_label_set_ellipsize(GTK_LABEL(label[i]), PANGO_ELLIPSIZE_END); gtk_widget_set_hexpand(label[i], TRUE); } if(!g_strcmp0(layout, "columns")) { new_container = gtk_grid_new(); gtk_grid_set_column_homogeneous(GTK_GRID(new_container), TRUE); gtk_grid_set_column_spacing(GTK_GRID(new_container), 8); for(int i=0; i<3; i++) { dt_gui_add_class(label[i], "dt_section_label"); gtk_container_add(GTK_CONTAINER(new_container), label[i]); if(old_container) gtk_widget_show(label[i]); gtk_grid_attach_next_to(GTK_GRID(new_container), g->blocks[i], label[i], GTK_POS_BOTTOM, 1, 1); } } else { new_container = gtk_notebook_new(); for(int i=0; i<3; i++) gtk_notebook_append_page(GTK_NOTEBOOK(new_container), g->blocks[i], label[i]); } } g_free(layout); for(int i=0; i<3; i++) g_object_unref(G_OBJECT(g->blocks[i])); gtk_container_add(GTK_CONTAINER(g->main_box), new_container); if(old_container) gtk_widget_show(new_container); } static void _cycle_layout_callback(GtkGestureSingle *gesture, gint n_press, gdouble x, gdouble y, dt_iop_module_t *self) { gchar *layout = dt_conf_get_string("plugins/darkroom/colorbalance/layout"); dt_conf_set_string("plugins/darkroom/colorbalance/layout", !g_strcmp0(layout, "columns") ? "tabs" : !g_strcmp0(layout, "list") ? "columns" : "list"); g_free(layout); _configure_slider_blocks(NULL, self); } #define HSL_CALLBACK(which) \ static void which##_callback(GtkWidget *slider, dt_iop_module_t *self) \ { \ dt_iop_colorbalance_params_t *p = self->params; \ dt_iop_colorbalance_gui_data_t *g = self->gui_data; \ \ DT_GUARD_GUI_UPDATE(); \ \ dt_iop_color_picker_reset(self, TRUE); \ \ float hsl[3] = {dt_bauhaus_slider_get(g->hue_##which) / 360.0f, \ dt_bauhaus_slider_get(g->sat_##which) / 100.0f, \ 0.5f}; \ \ if(slider == g->hue_##which) \ update_saturation_slider_color(g->sat_##which, hsl[0]); \ set_RGB_sliders(g->which##_r, g->which##_g, g->which##_b, hsl, p->which, p->mode); \ dt_dev_add_history_item(darktable.develop, self, TRUE); \ } HSL_CALLBACK(lift) HSL_CALLBACK(gamma) HSL_CALLBACK(gain) void gui_init(dt_iop_module_t *self) { dt_iop_colorbalance_gui_data_t *g = IOP_GUI_ALLOC(colorbalance); g->mode = NULL; for(int k=0; kcolor_patches_flags[k] = INVALID; g->luma_patches_flags[k] = INVALID; } GtkWidget *mode_box = self->widget = dt_gui_vbox(); // mode choice g->mode = dt_bauhaus_combobox_from_params(self, N_("mode")); gtk_widget_set_tooltip_text(g->mode, _("color-grading mapping method")); // control choice g->controls = dt_bauhaus_combobox_new(self); dt_bauhaus_widget_set_label(g->controls, NULL, N_("color control sliders")); dt_bauhaus_combobox_add(g->controls, _("HSL")); dt_bauhaus_combobox_add(g->controls, _("RGBL")); dt_bauhaus_combobox_add(g->controls, _("both")); dt_gui_box_add(self->widget, g->controls); gtk_widget_set_tooltip_text(g->controls, _("color-grading mapping method")); g_signal_connect(G_OBJECT(g->controls), "value-changed", G_CALLBACK(controls_callback), self); const char *mode = dt_conf_get_string_const("plugins/darkroom/colorbalance/controls"); dt_bauhaus_combobox_set(g->controls, !g_strcmp0(mode, "RGBL") ? RGBL : !g_strcmp0(mode, "BOTH") ? BOTH : HSL); g->master_box = self->widget = dt_gui_vbox(dt_ui_section_label_new(C_("section", "master"))); g->saturation = dt_bauhaus_slider_from_params(self, "saturation"); dt_bauhaus_slider_set_soft_range(g->saturation, 0.5f, 1.5f); dt_bauhaus_slider_set_digits(g->saturation, 4); dt_bauhaus_slider_set_format(g->saturation, "%"); gtk_widget_set_tooltip_text(g->saturation, _("saturation correction before the color balance")); g->saturation_out = dt_bauhaus_slider_from_params(self, "saturation_out"); dt_bauhaus_slider_set_soft_range(g->saturation_out, 0.5f, 1.5f); dt_bauhaus_slider_set_digits(g->saturation_out, 4); dt_bauhaus_slider_set_format(g->saturation_out, "%"); gtk_widget_set_tooltip_text(g->saturation_out, _("saturation correction after the color balance")); g->grey = dt_color_picker_new(self, DT_COLOR_PICKER_AREA, dt_bauhaus_slider_from_params(self, "grey")); dt_bauhaus_slider_set_format(g->grey, "%"); gtk_widget_set_tooltip_text(g->grey, _("adjust to match a neutral tone")); g->contrast = dt_bauhaus_slider_from_params(self, N_("contrast")); dt_bauhaus_slider_set_soft_range(g->contrast, 0.5f, 1.5f); dt_bauhaus_slider_set_digits(g->contrast, 4); dt_bauhaus_slider_set_factor(g->contrast, -100.0f); dt_bauhaus_slider_set_offset(g->contrast, 100.0f); dt_bauhaus_slider_set_format(g->contrast, "%"); gtk_widget_set_tooltip_text(g->contrast, _("contrast")); g->main_label = dt_ui_section_label_new(""); // is set in _configure_slider_blocks gtk_widget_set_tooltip_text(g->main_label, _("click to cycle layout")); GtkWidget *main_label_box = gtk_event_box_new(); gtk_container_add(GTK_CONTAINER(main_label_box), g->main_label); dt_gui_connect_click(main_label_box, NULL, _cycle_layout_callback, self); g->main_box = gtk_event_box_new(); // is filled in _configure_slider_blocks char field_name[10]; #define ADD_CHANNEL(which, section, c, n, N, text, span) \ sprintf(field_name, "%s[%d]", #which, CHANNEL_##N); \ g->which##_##c = dt_bauhaus_slider_from_params(self, field_name); \ dt_bauhaus_slider_set_soft_range(g->which##_##c, -span+1.0, span+1.0); \ dt_bauhaus_slider_set_digits(g->which##_##c, 5); \ dt_bauhaus_slider_set_offset(g->which##_##c, -1.0); \ dt_bauhaus_slider_set_feedback(g->which##_##c, 0); \ gtk_widget_set_tooltip_text(g->which##_##c, _(text[CHANNEL_##N])); \ dt_bauhaus_widget_set_label(g->which##_##c, section, #n); \ #define ADD_BLOCK(blk, which, section, text, span, satspan) \ g->blocks[blk] = self->widget = dt_gui_vbox(); \ \ sprintf(field_name, "%s[%d]", #which, CHANNEL_FACTOR); \ g->which##_factor = dt_color_picker_new(self, DT_COLOR_PICKER_AREA, \ dt_bauhaus_slider_from_params(self, field_name)); \ dt_bauhaus_slider_set_soft_range(g->which##_factor, -span+1.0, span+1.0); \ dt_bauhaus_slider_set_digits(g->which##_factor, 4); \ dt_bauhaus_slider_set_factor(g->which##_factor, 100.0); \ dt_bauhaus_slider_set_offset(g->which##_factor, - 100.0); \ dt_bauhaus_slider_set_format(g->which##_factor,"%"); \ dt_bauhaus_slider_set_feedback(g->which##_factor, 0); \ dt_bauhaus_slider_set_stop(g->which##_factor, 0.0, 0.0, 0.0, 0.0); \ dt_bauhaus_slider_set_stop(g->which##_factor, 1.0, 1.0, 1.0, 1.0); \ gtk_widget_set_tooltip_text(g->which##_factor, _(text[CHANNEL_FACTOR])); \ dt_bauhaus_widget_set_label(g->which##_factor, section, N_("factor")); \ \ g->hue_##which = dt_color_picker_new(self, DT_COLOR_PICKER_AREA, \ dt_bauhaus_slider_new_with_range_and_feedback(self, \ 0.0f, 360.0f, 0, 0.0f, 2, 0)); \ dt_bauhaus_widget_set_label(g->hue_##which, section, N_("hue")); \ dt_bauhaus_slider_set_format(g->hue_##which, "°"); \ dt_bauhaus_slider_set_stop(g->hue_##which, 0.0f, 1.0f, 0.0f, 0.0f); \ dt_bauhaus_slider_set_stop(g->hue_##which, 0.166f, 1.0f, 1.0f, 0.0f); \ dt_bauhaus_slider_set_stop(g->hue_##which, 0.322f, 0.0f, 1.0f, 0.0f); \ dt_bauhaus_slider_set_stop(g->hue_##which, 0.498f, 0.0f, 1.0f, 1.0f); \ dt_bauhaus_slider_set_stop(g->hue_##which, 0.664f, 0.0f, 0.0f, 1.0f); \ dt_bauhaus_slider_set_stop(g->hue_##which, 0.830f, 1.0f, 0.0f, 1.0f); \ dt_bauhaus_slider_set_stop(g->hue_##which, 1.0f, 1.0f, 0.0f, 0.0f); \ gtk_widget_set_tooltip_text(g->hue_##which, _("select the hue")); \ g_signal_connect(G_OBJECT(g->hue_##which), "value-changed", \ G_CALLBACK(which##_callback), self); \ \ g->sat_##which = dt_bauhaus_slider_new_with_range_and_feedback(self, \ 0.0f, 100.0f, 0, 0.0f, 2, 0); \ dt_bauhaus_slider_set_soft_max(g->sat_##which, satspan); \ dt_bauhaus_widget_set_label(g->sat_##which, section, N_("saturation")); \ dt_bauhaus_slider_set_format(g->sat_##which, "%"); \ dt_bauhaus_slider_set_stop(g->sat_##which, 0.0f, 0.2f, 0.2f, 0.2f); \ dt_bauhaus_slider_set_stop(g->sat_##which, 1.0f, 1.0f, 1.0f, 1.0f); \ gtk_widget_set_tooltip_text(g->sat_##which, _("select the saturation")); \ g_signal_connect(G_OBJECT(g->sat_##which), "value-changed", \ G_CALLBACK(which##_callback), self); \ \ dt_gui_box_add(self->widget, g->hue_##which, g->sat_##which); \ \ ADD_CHANNEL(which, section, r, red, RED, text, span) \ dt_bauhaus_slider_set_stop(g->which##_r, 0.0, 0.0, 1.0, 1.0); \ dt_bauhaus_slider_set_stop(g->which##_r, 0.5, 1.0, 1.0, 1.0); \ dt_bauhaus_slider_set_stop(g->which##_r, 1.0, 1.0, 0.0, 0.0); \ ADD_CHANNEL(which, section, g, green, GREEN, text, span) \ dt_bauhaus_slider_set_stop(g->which##_g, 0.0, 1.0, 0.0, 1.0); \ dt_bauhaus_slider_set_stop(g->which##_g, 0.5, 1.0, 1.0, 1.0); \ dt_bauhaus_slider_set_stop(g->which##_g, 1.0, 0.0, 1.0, 0.0); \ ADD_CHANNEL(which, section, b, blue, BLUE, text, span) \ dt_bauhaus_slider_set_stop(g->which##_b, 0.0, 1.0, 1.0, 0.0); \ dt_bauhaus_slider_set_stop(g->which##_b, 0.5, 1.0, 1.0, 1.0); \ dt_bauhaus_slider_set_stop(g->which##_b, 1.0, 0.0, 0.0, 1.0); \ static const char *lift_messages[] = { N_("factor of lift/offset"), N_("factor of red for lift/offset"), N_("factor of green for lift/offset"), N_("factor of blue for lift/offset") }; static const char *gamma_messages[] = { N_("factor of gamma/power"), N_("factor of red for gamma/power"), N_("factor of green for gamma/power"), N_("factor of blue for gamma/power") }; static const char *gain_messages[] = { N_("factor of gain/slope"), N_("factor of red for gain/slope"), N_("factor of green for gain/slope"), N_("factor of blue for gain/slope") }; ADD_BLOCK(0, lift, N_("shadows"), lift_messages, 0.05f, 5.0f) ADD_BLOCK(1, gamma, N_("mid-tones"), gamma_messages, 0.5f, 20.0f) ADD_BLOCK(2, gain, N_("highlights"), gain_messages, 0.5f, 25.0f) _configure_slider_blocks(NULL, self); g->auto_luma = dt_color_picker_new(self, DT_COLOR_PICKER_AREA, dt_bauhaus_combobox_new(self)); dt_bauhaus_widget_set_label(g->auto_luma, NULL, N_("optimize luma")); gtk_widget_set_tooltip_text(g->auto_luma, _("fit the whole histogram and center the average luma")); g->auto_color = dt_color_picker_new(self, DT_COLOR_PICKER_AREA, dt_bauhaus_combobox_new(self)); dt_bauhaus_widget_set_label(g->auto_color, NULL, N_("neutralize colors")); gtk_widget_set_tooltip_text(g->auto_color, _("optimize the RGB curves to remove color casts")); g->optimizer_box = dt_gui_vbox(dt_ui_section_label_new(C_("section", "auto optimizers")), g->auto_luma, g->auto_color); // start building top level widget self->widget = dt_gui_vbox(mode_box, g->master_box, main_label_box, g->main_box, g->optimizer_box); DT_CONTROL_SIGNAL_HANDLE(DT_SIGNAL_PREFERENCES_CHANGE, _configure_slider_blocks); } // 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