/* This file is part of darktable, Copyright (C) 2010-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 . */ #include "bauhaus/bauhaus.h" #include "common/colorspaces_inline_conversions.h" #include "common/darktable.h" #include "common/math.h" #include "common/opencl.h" #include "control/control.h" #include "develop/develop.h" #include "develop/imageop_math.h" #include "develop/imageop_gui.h" #include "dtgtk/button.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 #include #include DT_MODULE_INTROSPECTION(2, dt_iop_profilegamma_params_t) typedef enum dt_iop_profilegamma_mode_t { PROFILEGAMMA_LOG = 0, // $DESCRIPTION: "logarithmic" PROFILEGAMMA_GAMMA = 1 // $DESCRIPTION: "gamma" } dt_iop_profilegamma_mode_t; typedef struct dt_iop_profilegamma_params_t { dt_iop_profilegamma_mode_t mode; // $DEFAULT: PROFILEGAMMA_LOG float linear; // $MIN: 0.0 $MAX: 1.0 $DEFAULT: 0.1 float gamma; // $MIN: 0.0 $MAX: 1.0 $DEFAULT: 0.45 float dynamic_range; // $MIN: 0.01 $MAX: 32.0 $DEFAULT: 10.0 $DESCRIPTION: "dynamic range" float grey_point; // $MIN: 0.1 $MAX: 100.0 $DEFAULT: 18.0 $DESCRIPTION: "middle gray luma" float shadows_range; // $MIN: -16.0 $MAX: 16.0 $DEFAULT: -5.0 $DESCRIPTION: "black relative exposure" float security_factor; // $MIN: -100.0 $MAX: 100.0 $DEFAULT: 0.0 $DESCRIPTION: "safety factor" } dt_iop_profilegamma_params_t; typedef struct dt_iop_profilegamma_gui_data_t { GtkWidget *mode; GtkWidget *mode_stack; GtkWidget *linear; GtkWidget *gamma; GtkWidget *dynamic_range; GtkWidget *grey_point; GtkWidget *shadows_range; GtkWidget *security_factor; GtkWidget *auto_button; } dt_iop_profilegamma_gui_data_t; typedef struct dt_iop_profilegamma_data_t { dt_iop_profilegamma_mode_t mode; float linear; float gamma; float table[0x10000]; // precomputed look-up table float unbounded_coeffs[3]; // approximation for extrapolation of curve float dynamic_range; float grey_point; float shadows_range; float security_factor; } dt_iop_profilegamma_data_t; typedef struct dt_iop_profilegamma_global_data_t { int kernel_profilegamma; int kernel_profilegamma_log; } dt_iop_profilegamma_global_data_t; const char *name() { return _("unbreak input profile"); } const char **description(dt_iop_module_t *self) { return dt_iop_set_description(self, _("correct input color profiles meant to be applied on non-linear RGB"), _("corrective"), _("linear, RGB, display-referred"), _("non-linear, RGB"), _("non-linear, RGB, display-referred")); } int default_group() { return IOP_GROUP_COLOR | IOP_GROUP_TECHNICAL; } int flags() { return IOP_FLAGS_ONE_INSTANCE | IOP_FLAGS_ALLOW_TILING | IOP_FLAGS_INCLUDE_IN_STYLES | IOP_FLAGS_SUPPORTS_BLENDING; } dt_iop_colorspace_type_t default_colorspace(dt_iop_module_t *self, dt_dev_pixelpipe_t *pipe, dt_dev_pixelpipe_iop_t *piece) { return IOP_CS_RGB; } void init_presets(dt_iop_module_so_t *self) { dt_iop_profilegamma_params_t p; memset(&p, 0, sizeof(p)); p.mode = PROFILEGAMMA_LOG; p.grey_point = 18.00f; p.security_factor = 0.0f; // 16 EV preset p.dynamic_range = 16.0f; p.shadows_range = -12.0f; dt_gui_presets_add_generic(_("16 EV dynamic range (generic)"), self->op, self->version(), &p, sizeof(p), TRUE, DEVELOP_BLEND_CS_RGB_DISPLAY); // 14 EV preset p.dynamic_range = 14.0f; p.shadows_range = -10.50f; dt_gui_presets_add_generic(_("14 EV dynamic range (generic)"), self->op, self->version(), &p, sizeof(p), TRUE, DEVELOP_BLEND_CS_RGB_DISPLAY); // 12 EV preset p.dynamic_range = 12.0f; p.shadows_range = -9.0f; dt_gui_presets_add_generic(_("12 EV dynamic range (generic)"), self->op, self->version(), &p, sizeof(p), TRUE, DEVELOP_BLEND_CS_RGB_DISPLAY); // 10 EV preset p.dynamic_range = 10.0f; p.shadows_range = -7.50f; dt_gui_presets_add_generic(_("10 EV dynamic range (generic)"), self->op, self->version(), &p, sizeof(p), TRUE, DEVELOP_BLEND_CS_RGB_DISPLAY); // 08 EV preset p.dynamic_range = 8.0f; p.shadows_range = -6.0f; dt_gui_presets_add_generic(_("08 EV dynamic range (generic)"), self->op, self->version(), &p, sizeof(p), TRUE, DEVELOP_BLEND_CS_RGB_DISPLAY); } 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_profilegamma_params_v2_t { dt_iop_profilegamma_mode_t mode; float linear; float gamma; float dynamic_range; float grey_point; float shadows_range; float security_factor; } dt_iop_profilegamma_params_v2_t; if(old_version == 1) { typedef struct dt_iop_profilegamma_params_v1_t { float linear; float gamma; } dt_iop_profilegamma_params_v1_t; const dt_iop_profilegamma_params_v1_t *o = old_params; dt_iop_profilegamma_params_v2_t *n = malloc(sizeof(dt_iop_profilegamma_params_v2_t)); n->linear = o->linear; n->gamma = o->gamma; n->mode = PROFILEGAMMA_GAMMA; n->dynamic_range = 10.0f; n->grey_point = 18.0f; n->shadows_range = -5.0f; n->security_factor = 0.0f; *new_params = n; *new_params_size = sizeof(dt_iop_profilegamma_params_v2_t); *new_version = 2; return 0; } return 1; } #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_profilegamma_data_t *d = piece->data; dt_iop_profilegamma_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; cl_mem dev_table = NULL; cl_mem dev_coeffs = NULL; if(d->mode == PROFILEGAMMA_LOG) { const float dynamic_range = d->dynamic_range; const float shadows_range = d->shadows_range; const float grey = d->grey_point / 100.0f; err = dt_opencl_enqueue_kernel_2d_args(devid, gd->kernel_profilegamma_log, width, height, CLARG(dev_in), CLARG(dev_out), CLARG(width), CLARG(height), CLARG(dynamic_range), CLARG(shadows_range), CLARG(grey)); } else if(d->mode == PROFILEGAMMA_GAMMA) { dev_table = dt_opencl_copy_host_to_image(devid, d->table, 256, 256, sizeof(float)); if(dev_table == NULL) goto error; dev_coeffs = dt_opencl_copy_host_to_device_constant(devid, sizeof(float) * 3, d->unbounded_coeffs); if(dev_coeffs == NULL) goto error; err = dt_opencl_enqueue_kernel_2d_args(devid, gd->kernel_profilegamma, width, height, CLARG(dev_in), CLARG(dev_out), CLARG(width), CLARG(height), CLARG(dev_table), CLARG(dev_coeffs)); } error: dt_opencl_release_mem_object(dev_table); dt_opencl_release_mem_object(dev_coeffs); return err; } #endif 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_profilegamma_data_t *data = piece->data; const int ch = piece->colors; switch(data->mode) { case PROFILEGAMMA_LOG: { const float grey = data->grey_point / 100.0f; /** The log2(x) -> -INF when x -> 0 * thus very low values (noise) will get even lower, resulting in noise negative amplification, * which leads to pepper noise in shadows. To avoid that, we need to clip values that are noise for sure. * Using 16 bits RAW data, the black value (known by rawspeed for every manufacturer) could be used as a threshold. * However, at this point of the pixelpipe, the RAW levels have already been corrected and everything can happen with black levels * in the exposure module. So we define the threshold as the first non-null 16 bit integer */ const float noise = powf(2.0f, -16.0f); DT_OMP_FOR() for(size_t k = 0; k < (size_t)ch * roi_out->width * roi_out->height; k++) { float tmp = ((const float *)ivoid)[k] / grey; if(tmp < noise) tmp = noise; tmp = (fastlog2(tmp) - data->shadows_range) / (data->dynamic_range); if(tmp < noise) { ((float *)ovoid)[k] = noise; } else { ((float *)ovoid)[k] = tmp; } } break; } case PROFILEGAMMA_GAMMA: { DT_OMP_FOR() for(int k = 0; k < roi_out->height; k++) { const float *in = ((float *)ivoid) + (size_t)ch * k * roi_out->width; float *out = ((float *)ovoid) + (size_t)ch * k * roi_out->width; for(int j = 0; j < roi_out->width; j++, in += ch, out += ch) { for(int i = 0; i < 3; i++) { // use base curve for values < 1, else use extrapolation. if(in[i] < 1.0f) out[i] = data->table[CLAMP((int)(in[i] * 0x10000ul), 0, 0xffff)]; else out[i] = dt_iop_eval_exp(data->unbounded_coeffs, in[i]); } } } break; } } if(piece->pipe->mask_display & DT_DEV_PIXELPIPE_DISPLAY_MASK) dt_iop_alpha_copy(ivoid, ovoid, roi_out->width, roi_out->height); } static void apply_auto_grey(dt_iop_module_t *self) { DT_GUARD_GUI_UPDATE(); dt_iop_profilegamma_params_t *p = self->params; dt_iop_profilegamma_gui_data_t *g = self->gui_data; const float grey = max3f(self->picked_color); p->grey_point = 100.f * grey; DT_ENTER_GUI_UPDATE(); dt_bauhaus_slider_set(g->grey_point, p->grey_point); DT_LEAVE_GUI_UPDATE(); dt_dev_add_history_item(darktable.develop, self, TRUE); } static void apply_auto_black(dt_iop_module_t *self) { DT_GUARD_GUI_UPDATE(); dt_iop_profilegamma_params_t *p = self->params; dt_iop_profilegamma_gui_data_t *g = self->gui_data; const float noise = powf(2.0f, -16.0f); // Black const float black = max3f(self->picked_color_min); float EVmin = Log2Thres(black / (p->grey_point / 100.0f), noise); EVmin *= (1.0f + p->security_factor / 100.0f); p->shadows_range = EVmin; DT_ENTER_GUI_UPDATE(); dt_bauhaus_slider_set(g->shadows_range, p->shadows_range); DT_LEAVE_GUI_UPDATE(); dt_dev_add_history_item(darktable.develop, self, TRUE); } static void apply_auto_dynamic_range(dt_iop_module_t *self) { DT_GUARD_GUI_UPDATE(); dt_iop_profilegamma_params_t *p = self->params; dt_iop_profilegamma_gui_data_t *g = self->gui_data; const float noise = powf(2.0f, -16.0f); // Black float EVmin = p->shadows_range; // White const float white = max3f(self->picked_color_max); float EVmax = Log2Thres(white / (p->grey_point / 100.0f), noise); EVmax *= (1.0f + p->security_factor / 100.0f); p->dynamic_range = EVmax - EVmin; DT_ENTER_GUI_UPDATE(); dt_bauhaus_slider_set(g->dynamic_range, p->dynamic_range); DT_LEAVE_GUI_UPDATE(); dt_dev_add_history_item(darktable.develop, self, TRUE); } static void apply_autotune(dt_iop_module_t *self) { dt_iop_profilegamma_params_t *p = self->params; dt_iop_profilegamma_gui_data_t *g = self->gui_data; const float noise = powf(2.0f, -16.0f); // Grey const float grey = max3f(self->picked_color); p->grey_point = 100.f * grey; // Black const float black = max3f(self->picked_color_min); float EVmin = Log2Thres(black / (p->grey_point / 100.0f), noise); EVmin *= (1.0f + p->security_factor / 100.0f); // White const float white = max3f(self->picked_color_max); float EVmax = Log2Thres(white / (p->grey_point / 100.0f), noise); EVmax *= (1.0f + p->security_factor / 100.0f); p->shadows_range = EVmin; p->dynamic_range = EVmax - EVmin; DT_ENTER_GUI_UPDATE(); dt_bauhaus_slider_set(g->grey_point, p->grey_point); dt_bauhaus_slider_set(g->shadows_range, p->shadows_range); dt_bauhaus_slider_set(g->dynamic_range, p->dynamic_range); DT_LEAVE_GUI_UPDATE(); dt_dev_add_history_item(darktable.develop, self, TRUE); } void gui_changed(dt_iop_module_t *self, GtkWidget *w, void *previous) { dt_iop_profilegamma_gui_data_t *g = self->gui_data; dt_iop_profilegamma_params_t *p = self->params; if(w == g->mode) { if(p->mode == PROFILEGAMMA_LOG) { gtk_stack_set_visible_child_name(GTK_STACK(g->mode_stack), "log"); } else { gtk_stack_set_visible_child_name(GTK_STACK(g->mode_stack), "gamma"); } } else if(w == g->security_factor) { const float prev = *(float *)previous; const float ratio = (p->security_factor - prev) / (prev + 100.0f); float EVmin = p->shadows_range; EVmin = EVmin + ratio * EVmin; float EVmax = p->dynamic_range + p->shadows_range; EVmax = EVmax + ratio * EVmax; p->dynamic_range = EVmax - EVmin; p->shadows_range = EVmin; DT_ENTER_GUI_UPDATE(); dt_bauhaus_slider_set(g->dynamic_range, p->dynamic_range); dt_bauhaus_slider_set(g->shadows_range, p->shadows_range); DT_LEAVE_GUI_UPDATE(); } } void color_picker_apply(dt_iop_module_t *self, GtkWidget *picker, dt_dev_pixelpipe_t *pipe) { dt_iop_profilegamma_gui_data_t *g = self->gui_data; if (picker == g->grey_point) apply_auto_grey(self); else if(picker == g->shadows_range) apply_auto_black(self); else if(picker == g->dynamic_range) apply_auto_dynamic_range(self); else if(picker == g->auto_button) apply_autotune(self); else dt_print(DT_DEBUG_ALWAYS, "[profile_gamma] unknown color picker"); } 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_profilegamma_params_t *p = (dt_iop_profilegamma_params_t *)p1; dt_iop_profilegamma_data_t *d = piece->data; const float linear = p->linear; const float gamma = p->gamma; d->linear = p->linear; d->gamma = p->gamma; float a, b, c, g; if(gamma == 1.0) { DT_OMP_FOR() for(int k = 0; k < 0x10000; k++) d->table[k] = 1.0 * k / 0x10000; } else { if(linear == 0.0) { DT_OMP_FOR() for(int k = 0; k < 0x10000; k++) d->table[k] = powf(1.00 * k / 0x10000, gamma); } else { if(linear < 1.0) { g = gamma * (1.0 - linear) / (1.0 - gamma * linear); a = 1.0 / (1.0 + linear * (g - 1)); b = linear * (g - 1) * a; c = powf(a * linear + b, g) / linear; } else { a = b = g = 0.0; c = 1.0; } DT_OMP_FOR() for(int k = 0; k < 0x10000; k++) { float tmp; if(k < 0x10000 * linear) tmp = c * k / 0x10000; else tmp = powf(a * k / 0x10000 + b, g); d->table[k] = tmp; } } } // now the extrapolation stuff: const float x[4] = { 0.7f, 0.8f, 0.9f, 1.0f }; const float y[4] = { d->table[CLAMP((int)(x[0] * 0x10000ul), 0, 0xffff)], d->table[CLAMP((int)(x[1] * 0x10000ul), 0, 0xffff)], d->table[CLAMP((int)(x[2] * 0x10000ul), 0, 0xffff)], d->table[CLAMP((int)(x[3] * 0x10000ul), 0, 0xffff)] }; dt_iop_estimate_exp(x, y, 4, d->unbounded_coeffs); d->dynamic_range = p->dynamic_range; d->grey_point = p->grey_point; d->shadows_range = p->shadows_range; d->security_factor = p->security_factor; d->mode = p->mode; //piece->process_cl_ready = 1; // no OpenCL for log yet. //if(d->mode == PROFILEGAMMA_LOG) piece->process_cl_ready = 0; } 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_profilegamma_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 gui_reset(dt_iop_module_t *self) { dt_iop_color_picker_reset(self, TRUE); } void gui_update(dt_iop_module_t *self) { dt_iop_profilegamma_gui_data_t *g = self->gui_data; dt_iop_color_picker_reset(self, TRUE); gui_changed(self, g->mode, 0); } void init_global(dt_iop_module_so_t *self) { const int program = 2; // basic.cl, from programs.conf dt_iop_profilegamma_global_data_t *gd = malloc(sizeof(dt_iop_profilegamma_global_data_t)); self->data = gd; gd->kernel_profilegamma = dt_opencl_create_kernel(program, "profilegamma"); gd->kernel_profilegamma_log = dt_opencl_create_kernel(program, "profilegamma_log"); } void cleanup_global(dt_iop_module_so_t *self) { dt_iop_profilegamma_global_data_t *gd = self->data; dt_opencl_free_kernel(gd->kernel_profilegamma); dt_opencl_free_kernel(gd->kernel_profilegamma_log); free(self->data); self->data = NULL; } void gui_init(dt_iop_module_t *self) { dt_iop_profilegamma_gui_data_t *g = IOP_GUI_ALLOC(profilegamma); // prepare the modes widgets stack g->mode_stack = gtk_stack_new(); gtk_stack_set_homogeneous(GTK_STACK(g->mode_stack), FALSE); /**** GAMMA MODE ***/ GtkWidget *vbox_gamma = self->widget = dt_gui_vbox(); g->linear = dt_bauhaus_slider_from_params(self, N_("linear")); dt_bauhaus_slider_set_digits(g->linear, 4); gtk_widget_set_tooltip_text(g->linear, _("linear part")); g->gamma = dt_bauhaus_slider_from_params(self, N_("gamma")); dt_bauhaus_slider_set_digits(g->gamma, 4); gtk_widget_set_tooltip_text(g->gamma, _("gamma exponential factor")); gtk_stack_add_named(GTK_STACK(g->mode_stack), vbox_gamma, "gamma"); /**** LOG MODE ****/ GtkWidget *vbox_log = self->widget = dt_gui_vbox(); g->grey_point = dt_color_picker_new(self, DT_COLOR_PICKER_AREA, dt_bauhaus_slider_from_params(self, "grey_point")); dt_bauhaus_slider_set_format(g->grey_point, "%"); gtk_widget_set_tooltip_text(g->grey_point, _("adjust to match the average luma of the subject")); g->shadows_range = dt_color_picker_new(self, DT_COLOR_PICKER_AREA, dt_bauhaus_slider_from_params(self, "shadows_range")); dt_bauhaus_slider_set_soft_max(g->shadows_range, 0.0); dt_bauhaus_slider_set_format(g->shadows_range, _(" EV")); gtk_widget_set_tooltip_text(g->shadows_range, _("number of stops between middle gray and pure black\nthis is a reading a light meter would give you on the scene")); g->dynamic_range = dt_color_picker_new(self, DT_COLOR_PICKER_AREA, dt_bauhaus_slider_from_params(self, "dynamic_range")); dt_bauhaus_slider_set_soft_range(g->dynamic_range, 0.5, 16.0); dt_bauhaus_slider_set_format(g->dynamic_range, _(" EV")); gtk_widget_set_tooltip_text(g->dynamic_range, _("number of stops between pure black and pure white\nthis is a reading a light meter would give you on the scene")); dt_gui_box_add(vbox_log, dt_ui_section_label_new(C_("section", "optimize automatically"))); g->security_factor = dt_bauhaus_slider_from_params(self, "security_factor"); dt_bauhaus_slider_set_format(g->security_factor, "%"); gtk_widget_set_tooltip_text(g->security_factor, _("increase or decrease the computed dynamic range\nthis is useful when noise distorts the measurement")); g->auto_button = dt_color_picker_new(self, DT_COLOR_PICKER_AREA, dt_bauhaus_combobox_new(self)); dt_bauhaus_widget_set_label(g->auto_button, NULL, N_("auto tune levels")); gtk_widget_set_tooltip_text(g->auto_button, _("make an optimization with some guessing")); dt_gui_box_add(vbox_log, g->auto_button); gtk_stack_add_named(GTK_STACK(g->mode_stack), vbox_log, "log"); // start building top level widget self->widget = dt_gui_vbox(); g->mode = dt_bauhaus_combobox_from_params(self, N_("mode")); gtk_widget_set_tooltip_text(g->mode, _("tone mapping method")); dt_gui_box_add(self->widget, g->mode_stack); } // 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