diff --git a/data/kernels/negadoctor.cl b/data/kernels/negadoctor.cl index f8b7dde20850..f8eabe91582d 100644 --- a/data/kernels/negadoctor.cl +++ b/data/kernels/negadoctor.cl @@ -22,7 +22,7 @@ kernel void negadoctor (read_only image2d_t in, write_only image2d_t out, int width, int height, const float4 Dmin, const float4 wb_high, const float4 offset, - const float exposure, const float black, const float gamma, const float soft_clip, const float soft_clip_comp) + const float exposure, const float black, const float gamma, const float soft_clip, const float soft_clip_comp, const int slide_film) { const unsigned int x = get_global_id(0); const unsigned int y = get_global_id(1); @@ -38,9 +38,11 @@ negadoctor (read_only image2d_t in, write_only image2d_t out, int width, int hei // Correct density in log space o = wb_high * o + offset; - // Print density on paper : ((1 - 10^corrected_de + black) * exposure)^gamma rewritten for FMA - o = -((float4)exposure * native_exp10(o) + (float4)black); - o = native_powr(fmax(o, (float4)0.0f), gamma); // note : this is always > 0 + // Print density on paper (negative film) : ((1 - 10^corrected_de + black) * exposure)^gamma rewritten for FMA + o = native_exp10(o); + o = (slide_film) ? ((float4)exposure * o + (float4)black) + : -((float4)exposure * o + (float4)black); + o = native_powr(fmax(o, (float4)0.0f), gamma); // note : this is always > 0 // Compress highlights and clip negatives. from https://lists.gnu.org/archive/html/openexr-devel/2005-03/msg00009.html o = (o > (float4)soft_clip) ? soft_clip + ((float4)1.0f - native_exp(-(o - (float4)soft_clip) / (float4)soft_clip_comp)) * (float4)soft_clip_comp diff --git a/src/iop/negadoctor.c b/src/iop/negadoctor.c index bcbd4c63b038..68e490738d18 100644 --- a/src/iop/negadoctor.c +++ b/src/iop/negadoctor.c @@ -76,20 +76,21 @@ DT_MODULE_INTROSPECTION(2, dt_iop_negadoctor_params_t) typedef enum dt_iop_negadoctor_filmstock_t { // What kind of emulsion are we working on ? - DT_FILMSTOCK_NB = 0, // $DESCRIPTION: "black and white film" - DT_FILMSTOCK_COLOR = 1 // $DESCRIPTION: "color film" + DT_FILMSTOCK_NB_NEG = 0, // $DESCRIPTION: "Black and white negative" + DT_FILMSTOCK_COLOR_NEG, // $DESCRIPTION: "Color negative" + DT_FILMSTOCK_SLIDE // $DESCRIPTION: "Positive" } dt_iop_negadoctor_filmstock_t; typedef struct dt_iop_negadoctor_params_t { - dt_iop_negadoctor_filmstock_t film_stock; /* $DEFAULT: DT_FILMSTOCK_COLOR $DESCRIPTION: "film stock" */ + dt_iop_negadoctor_filmstock_t film_stock; /* $DEFAULT: DT_FILMSTOCK_COLOR_NEG $DESCRIPTION: "film stock" */ float Dmin[4]; /* color of film substrate $MIN: 0.00001 $MAX: 1.5 $DEFAULT: 1.0 */ float wb_high[4]; /* white balance RGB coeffs (illuminant) - $MIN: 0.25 $MAX: 2 $DEFAULT: 1.0 */ + $MIN: 0.01 $MAX: 2 $DEFAULT: 1.0 */ float wb_low[4]; /* white balance RGB offsets (base light) - $MIN: 0.25 $MAX: 2 $DEFAULT: 1.0 */ + $MIN: 0.01 $MAX: 2 $DEFAULT: 1.0 */ float D_max; /* max density of film $MIN: 0.1 $MAX: 6 $DEFAULT: 2.046 */ float offset; /* inversion offset @@ -115,6 +116,7 @@ typedef struct dt_iop_negadoctor_data_t float soft_clip; // highlights roll-off float soft_clip_comp; // 1 - softclip, complement to 1 float exposure; // extra exposure + int slide_film; } dt_iop_negadoctor_data_t; @@ -129,8 +131,9 @@ typedef struct dt_iop_negadoctor_gui_data_t GtkWidget *offset; GtkWidget *black, *gamma, *soft_clip, *exposure; GtkWidget *Dmin_picker, *Dmin_sampler; - GtkWidget *WB_high_picker, *WB_high_norm, *WB_high_sampler; - GtkWidget *WB_low_picker, *WB_low_norm, *WB_low_sampler; + GtkWidget *WB_high_label, *WB_high_picker, *WB_high_norm, *WB_high_sampler; + GtkWidget *WB_low_label, *WB_low_picker, *WB_low_norm, *WB_low_sampler; + } dt_iop_negadoctor_gui_data_t; @@ -195,12 +198,11 @@ int legacy_params(dt_iop_module_t *self, const void *const old_params, const int float exposure; // extra exposure } dt_iop_negadoctor_params_v1_t; - dt_iop_negadoctor_params_v1_t *o = (dt_iop_negadoctor_params_v1_t *)old_params; + const dt_iop_negadoctor_params_v1_t *o = (dt_iop_negadoctor_params_v1_t *)old_params; dt_iop_negadoctor_params_t *n = (dt_iop_negadoctor_params_t *)new_params; - dt_iop_negadoctor_params_t *d = (dt_iop_negadoctor_params_t *)self->default_params; + const dt_iop_negadoctor_params_t *d = (dt_iop_negadoctor_params_t *)self->default_params; *n = *d; // start with a fresh copy of default parameters - // WARNING: when copying the arrays in a for loop, gcc wrongly assumed // that n and o were aligned and used AVX instructions for me, // which segfaulted. let's hope this doesn't get optimized too much. @@ -235,20 +237,37 @@ void commit_params(dt_iop_module_t *self, dt_iop_params_t *p1, dt_dev_pixelpipe_ const dt_iop_negadoctor_params_t *const p = (dt_iop_negadoctor_params_t *)p1; dt_iop_negadoctor_data_t *const d = (dt_iop_negadoctor_data_t *)piece->data; + d->slide_film = (p->film_stock == DT_FILMSTOCK_SLIDE); + // keep WB_high even in B&W mode to apply sepia or warm tone look // but premultiply it aheard with Dmax to spare one div per pixel - for(size_t c = 0; c < 4; c++) d->wb_high[c] = p->wb_high[c] / p->D_max; - for(size_t c = 0; c < 4; c++) d->offset[c] = p->wb_high[c] * p->offset * p->wb_low[c]; + for(int c = 0; c < 3; c++) + { + if(d->slide_film) + { + // prepare for slide film + d->wb_high[c] = (2 - p->wb_high[c]) / p->D_max; + d->offset[c] = (2 - p->wb_high[c]) * p->offset * (2 - p->wb_low[c]); + } + else + { + d->wb_high[c] = p->wb_high[c] / p->D_max; + d->offset[c] = p->wb_high[c] * p->offset * p->wb_low[c]; + } + } // ensure we use a monochrome Dmin for B&W film - if(p->film_stock == DT_FILMSTOCK_COLOR) + if(p->film_stock == DT_FILMSTOCK_COLOR_NEG) for(size_t c = 0; c < 4; c++) d->Dmin[c] = p->Dmin[c]; - else if(p->film_stock == DT_FILMSTOCK_NB) + else if(p->film_stock == DT_FILMSTOCK_NB_NEG) + for(size_t c = 0; c < 4; c++) d->Dmin[c] = p->Dmin[0]; + else if(p->film_stock == DT_FILMSTOCK_SLIDE) for(size_t c = 0; c < 4; c++) d->Dmin[c] = p->Dmin[0]; // arithmetic trick allowing to rewrite the pixel inversion as FMA - d->black = -p->exposure * (1.0f + p->black); + d->black = (d->slide_film) ? p->exposure * p->black + : -p->exposure * (1.0f + p->black); // highlights soft clip d->soft_clip = p->soft_clip; @@ -270,35 +289,52 @@ void process(struct dt_iop_module_t *const self, dt_dev_pixelpipe_iop_t *const p const float *const restrict in = (float *)ivoid; float *const restrict out = (float *)ovoid; + dt_aligned_pixel_t gamma; + dt_aligned_pixel_t black; + dt_aligned_pixel_t exposure; + dt_aligned_pixel_t soft_clip; + dt_aligned_pixel_t soft_clip_comp; + + for(int c = 0; c < 3; c++) + { + gamma[c] = d->gamma; + black[c] = d->black; + exposure[c] = d->exposure; + soft_clip[c] = d->soft_clip; + soft_clip_comp[c] = d->soft_clip_comp; + } + + // Unpack vectors one by one with extra pragmas to be sure the compiler understands they can be vectorized + const gboolean slide_film = d->slide_film; + const float *const restrict Dmin = __builtin_assume_aligned(d->Dmin, 16); + const float *const restrict wb_high = __builtin_assume_aligned(d->wb_high, 16); + const float *const restrict offset = __builtin_assume_aligned(d->offset, 16); #ifdef _OPENMP #pragma omp parallel for simd default(none) \ - dt_omp_firstprivate(d, in, out, roi_out) \ - aligned(in, out:64) collapse(2) + dt_omp_firstprivate(in, out, roi_out, Dmin, wb_high, offset, gamma, black, exposure, soft_clip, soft_clip_comp, slide_film) \ + schedule(static) #endif for(size_t k = 0; k < (size_t)roi_out->height * roi_out->width * 4; k += 4) { - for(size_t c = 0; c < 4; c++) - { - // Unpack vectors one by one with extra pragmas to be sure the compiler understands they can be vectorized - const float *const restrict pix_in = in + k; - float *const restrict pix_out = out + k; - const float *const restrict Dmin = __builtin_assume_aligned(d->Dmin, 16); - const float *const restrict wb_high = __builtin_assume_aligned(d->wb_high, 16); - const float *const restrict offset = __builtin_assume_aligned(d->offset, 16); + const float *const pix_in = in + k; + float *const pix_out = out + k; + for(size_t c = 0; c < 3; c++) + { // Convert transmission to density using Dmin as a fulcrum const float density = - log10f(Dmin[c] / fmaxf(pix_in[c], THRESHOLD)); // threshold to -32 EV // Correct density in log space const float corrected_de = wb_high[c] * density + offset[c]; - // Print density on paper : ((1 - 10^corrected_de + black) * exposure)^gamma rewritten for FMA - const float print_linear = -(d->exposure * fast_exp10f(corrected_de) + d->black); - const float print_gamma = powf(fmaxf(print_linear, 0.0f), d->gamma); // note : this is always > 0 + // Print density on paper (negative film): ((1 - 10^corrected_de + black) * exposure)^gamma rewritten for FMA + const float print_linear = slide_film ? exposure[c] * fast_exp10f(corrected_de) + black[c] + : - (exposure[c] * fast_exp10f(corrected_de) + black[c]); + const float print_gamma = powf(fmaxf(print_linear, 0.0f), gamma[c]); // note : this is always > 0 // Compress highlights. from https://lists.gnu.org/archive/html/openexr-devel/2005-03/msg00009.html - pix_out[c] = (print_gamma > d->soft_clip) ? d->soft_clip + (1.0f - fast_expf(-(print_gamma - d->soft_clip) / d->soft_clip_comp)) * d->soft_clip_comp + pix_out[c] = (print_gamma > soft_clip[c]) ? soft_clip[c] + (1.0f - fast_expf(-(print_gamma - soft_clip[c]) / soft_clip_comp[c])) * soft_clip_comp[c] : print_gamma; } } @@ -335,6 +371,7 @@ int process_cl(struct dt_iop_module_t *const self, dt_dev_pixelpipe_iop_t *const dt_opencl_set_kernel_arg(devid, gd->kernel_negadoctor, 9, sizeof(float), (void *)&d->gamma); dt_opencl_set_kernel_arg(devid, gd->kernel_negadoctor, 10, sizeof(float), (void *)&d->soft_clip); dt_opencl_set_kernel_arg(devid, gd->kernel_negadoctor, 11, sizeof(float), (void *)&d->soft_clip_comp); + dt_opencl_set_kernel_arg(devid, gd->kernel_negadoctor, 12, sizeof(int), (void *)&d->slide_film); err = dt_opencl_enqueue_kernel_2d(devid, gd->kernel_negadoctor, sizes); if(err != CL_SUCCESS) goto error; @@ -360,7 +397,7 @@ void init(dt_iop_module_t *module) void init_presets(dt_iop_module_so_t *self) { - dt_iop_negadoctor_params_t tmp = (dt_iop_negadoctor_params_t){ .film_stock = DT_FILMSTOCK_COLOR, + dt_iop_negadoctor_params_t tmp = (dt_iop_negadoctor_params_t){ .film_stock = DT_FILMSTOCK_COLOR_NEG, .Dmin = { 1.13f, 0.49f, 0.27f, 0.0f}, .wb_high = { 1.0f, 1.0f, 1.0f, 0.0f }, .wb_low = { 1.0f, 1.0f, 1.0f, 0.0f }, @@ -372,10 +409,10 @@ void init_presets(dt_iop_module_so_t *self) .black = 0.0755f }; - dt_gui_presets_add_generic(_("color film"), self->op, + dt_gui_presets_add_generic(_("color negative film"), self->op, self->version(), &tmp, sizeof(tmp), 1, DEVELOP_BLEND_CS_RGB_DISPLAY); - dt_iop_negadoctor_params_t tmq = (dt_iop_negadoctor_params_t){ .film_stock = DT_FILMSTOCK_NB, + dt_iop_negadoctor_params_t tmq = (dt_iop_negadoctor_params_t){ .film_stock = DT_FILMSTOCK_NB_NEG, .Dmin = { 1.0f, 1.0f, 1.0f, 0.0f}, .wb_high = { 1.0f, 1.0f, 1.0f, 0.0f }, .wb_low = { 1.0f, 1.0f, 1.0f, 0.0f }, @@ -387,8 +424,23 @@ void init_presets(dt_iop_module_so_t *self) .black = 0.0755f }; - dt_gui_presets_add_generic(_("black and white film"), self->op, + dt_gui_presets_add_generic(_("black and white negative film"), self->op, self->version(), &tmq, sizeof(tmq), 1, DEVELOP_BLEND_CS_RGB_DISPLAY); + + dt_iop_negadoctor_params_t slide = (dt_iop_negadoctor_params_t){ .film_stock = DT_FILMSTOCK_SLIDE, + .Dmin = { 1.0f, 1.0f, 1.0f, 0.0f}, + .wb_high = { 1.0f, 1.0f, 1.0f, 0.0f }, + .wb_low = { 1.0f, 1.0f, 1.0f, 0.0f }, + .D_max = 2.20f, + .offset = 0.00f, + .gamma = 2.85f, + .soft_clip = 0.75f, + .exposure = 1.f, + .black = 0.0f }; + + + dt_gui_presets_add_generic(_("slide film"), self->op, + self->version(), &slide, sizeof(slide), 1, DEVELOP_BLEND_CS_RGB_DISPLAY); } void init_global(dt_iop_module_so_t *module) @@ -431,27 +483,192 @@ static void setup_color_variables(dt_iop_negadoctor_gui_data_t *const g, const g } +static inline void _gui_set_positive_mode(dt_iop_module_t *const self) +{ + dt_iop_negadoctor_gui_data_t *const g = (dt_iop_negadoctor_gui_data_t *)self->gui_data; + + // Offset + gtk_widget_set_tooltip_text(g->offset, _("correct the exposure of the scanner, for all RGB channels,\n" + "before the inversion, so whites are neither clipped or too grey.")); + + // Dmax + dt_bauhaus_slider_set_default(g->D_max, 2.2f); + gtk_widget_set_tooltip_text(g->D_max, _("Maximum density of the film, corresponding to black.\n" + "This value depends on the film specifications, the developing process,\n" + "The dynamic range of the scene and the scanner exposure settings.")); + // WB_low + gtk_label_set_label(GTK_LABEL(g->WB_low_label),_("Highlight color cast")); + gtk_color_button_set_title(GTK_COLOR_BUTTON(g->WB_low_picker), _("select color of highlights from a swatch")); + gtk_widget_set_tooltip_text(g->WB_low_norm, _("normalize highlight white balance settings")); + gtk_widget_set_tooltip_text(g->WB_low_sampler, _("pick highlight color from image")); + gtk_widget_set_tooltip_text(g->WB_low_sampler, _("pick whites color from image")); + + dt_bauhaus_widget_set_label(g->wb_low_R, N_("Highlight red offset")); + gtk_widget_set_tooltip_text(g->wb_low_R, _("correct the color cast in highlights\n" + "Setting this value before the shadows\n" + "color balance will help recovering the\n" + "global white balance in difficult cases.")); + + dt_bauhaus_widget_set_label(g->wb_low_G, N_("Highlight green offset")); + gtk_widget_set_tooltip_text(g->wb_low_G, _("Correct the color cast in highlights\n" + "Setting this value before the shadows\n" + "color balance will help recovering the\n" + "global white balance in difficult cases.")); + dt_bauhaus_widget_set_label(g->wb_low_B, N_("Highlight blue offset")); + gtk_widget_set_tooltip_text(g->wb_low_B, _("Correct the color cast in highlights\n" + "Setting this value before the shadows\n" + "color balance will help recovering the\n" + "global white balance in difficult cases.")); + + // WB_high + gtk_label_set_label(GTK_LABEL(g->WB_high_label),_("Shadows color balance")); + gtk_color_button_set_title(GTK_COLOR_BUTTON(g->WB_high_picker), _("select color of shadows from a swatch")); + gtk_widget_set_tooltip_text(g->WB_high_norm, _("normalize shadows color balance settings")); + gtk_widget_set_tooltip_text(g->WB_high_sampler, _("pick shadows color from image")); + + dt_bauhaus_widget_set_label(g->wb_high_R, N_("Red gain")); + gtk_widget_set_tooltip_text(g->wb_high_R, _("correct the color of the shadows so they are\n" + "truly achromatic. Setting this value after\n" + "the highlight color cast will help\n" + "recovering the global white balance in difficult cases.")); + + dt_bauhaus_widget_set_label(g->wb_high_G, N_("Green gain")); + gtk_widget_set_tooltip_text(g->wb_high_G, _("correct the color of the shadows so they are\n" + "truly achromatic. Setting this value after\n" + "the highlight color cast will help\n" + "recovering the global white balance in difficult cases.")); + + dt_bauhaus_widget_set_label(g->wb_high_B, N_("Blue gain")); + gtk_widget_set_tooltip_text(g->wb_high_B, _("correct the color of the shadows so they are\n" + "truly achromatic. Setting this value after\n" + "the highlight color cast will help\n" + "recovering the global white balance in difficult cases.")); + // Black + dt_bauhaus_slider_set_default(g->black, 0.0f); + + // Exposure + dt_bauhaus_slider_set_default(g->exposure, 0.f); + + // Gamma + dt_bauhaus_slider_set_default(g->gamma, 2.85f); +} + +static inline void _gui_set_negative_mode(dt_iop_module_t *const self) +{ + dt_iop_negadoctor_gui_data_t *const g = (dt_iop_negadoctor_gui_data_t *)self->gui_data; + + // Dmax + dt_bauhaus_slider_set_default(g->D_max, 2.046f); + gtk_widget_set_tooltip_text(g->D_max, _("maximum density of the film, corresponding to white after inversion.\n" + "this value depends on the film specifications, the developing process,\n" + "the dynamic range of the scene and the scanner exposure settings.")); + // Offset + gtk_widget_set_tooltip_text(g->offset, _("correct the exposure of the scanner, for all RGB channels,\n" + "before the inversion, so blacks are neither clipped or too pale.")); + + // WB_low + gtk_label_set_label(GTK_LABEL(g->WB_low_label),_("shadows color cast")); + gtk_color_button_set_title(GTK_COLOR_BUTTON(g->WB_low_picker), _("select color of shadows from a swatch")); + gtk_widget_set_tooltip_text(g->WB_low_norm, _("normalize shadows color balance settings")); + gtk_widget_set_tooltip_text(g->WB_low_sampler, _("pick shadows color from image")); + + gtk_widget_set_tooltip_text(g->WB_low_sampler, _("pick shadows color from image")); + + dt_bauhaus_widget_set_label(g->wb_low_R, N_("shadows red offset")); + gtk_widget_set_tooltip_text(g->wb_low_R, _("correct the color cast in shadows so blacks are\n" + "truly achromatic. Setting this value before\n" + "the highlights illuminant white balance will help\n" + "recovering the global white balance in difficult cases.")); + + dt_bauhaus_widget_set_label(g->wb_low_G, N_("shadows green offset")); + gtk_widget_set_tooltip_text(g->wb_low_G, _("correct the color cast in shadows so blacks are\n" + "truly achromatic. Setting this value before\n" + "the highlights illuminant white balance will help\n" + "recovering the global white balance in difficult cases.")); + + dt_bauhaus_widget_set_label(g->wb_low_B, N_("shadows blue offset")); + gtk_widget_set_tooltip_text(g->wb_low_B, _("correct the color cast in shadows so blacks are\n" + "truly achromatic. Setting this value before\n" + "the highlights illuminant white balance will help\n" + "recovering the global white balance in difficult cases.")); + + // WB_high + gtk_label_set_label(GTK_LABEL(g->WB_high_label),_("highlights white balance")); + gtk_color_button_set_title(GTK_COLOR_BUTTON(g->WB_high_picker), _("select color of illuminant from a swatch")); + gtk_widget_set_tooltip_text(g->WB_high_norm, _("normalize highlight white balance settings")); + gtk_widget_set_tooltip_text(g->WB_high_sampler , _("pick illuminant color from image")); + + dt_bauhaus_widget_set_label(g->wb_high_R, N_("illuminant red gain")); + gtk_widget_set_tooltip_text(g->wb_high_R, _("correct the color of the illuminant so whites are\n" + "truly achromatic. Setting this value after\n" + "the shadows color cast will help\n" + "recovering the global white balance in difficult cases.")); + + dt_bauhaus_widget_set_label(g->wb_high_G, N_("illuminant green gain")); + gtk_widget_set_tooltip_text(g->wb_high_G, _("correct the color of the illuminant so whites are\n" + "truly achromatic. Setting this value after\n" + "the shadows color cast will help\n" + "recovering the global white balance in difficult cases.")); + + dt_bauhaus_widget_set_label(g->wb_high_B, N_("illuminant blue gain")); + gtk_widget_set_tooltip_text(g->wb_high_B, _("correct the color of the illuminant so whites are\n" + "truly achromatic. Setting this value after\n" + "the shadows color cast will help\n" + "recovering the global white balance in difficult cases.")); + // Black + dt_bauhaus_slider_set_default(g->black, 0.0755f); + + // Exposure + dt_bauhaus_slider_set_default(g->exposure, 0.9245f); + + // Gamma + dt_bauhaus_slider_set_default(g->gamma, 4.00f); +} + static void toggle_stock_controls(dt_iop_module_t *const self) { dt_iop_negadoctor_gui_data_t *const g = (dt_iop_negadoctor_gui_data_t *)self->gui_data; const dt_iop_negadoctor_params_t *const p = (dt_iop_negadoctor_params_t *)self->params; - if(p->film_stock == DT_FILMSTOCK_NB) + switch (p->film_stock) { + case DT_FILMSTOCK_NB_NEG: + _gui_set_negative_mode(self); // Hide color controls setup_color_variables(g, FALSE); dt_bauhaus_widget_set_label(g->Dmin_R, N_("D min")); - } - else if(p->film_stock == DT_FILMSTOCK_COLOR) - { + gtk_widget_set_tooltip_text(g->Dmin_R, _("adjust the film transparent base.\n" + "this value depends on the film material, \n" + "the chemical fog produced while developing the film,\n" + "and the scanner white balance.")); + break; + + case DT_FILMSTOCK_SLIDE: + _gui_set_positive_mode(self); + // Hide color controls + setup_color_variables(g, FALSE); + dt_bauhaus_widget_set_label(g->Dmin_R, N_("D min")); + gtk_widget_set_tooltip_text(g->Dmin_R, _("adjust the film transparent base.\n" + "this value depends on the film material, \n" + "the chemical fog produced while developing the film,\n" + "and the scanner white balance.")); + break; + + case DT_FILMSTOCK_COLOR_NEG: + _gui_set_negative_mode(self); // Show color controls setup_color_variables(g, TRUE); dt_bauhaus_widget_set_label(g->Dmin_R, N_("D min red component")); - } - else - { + gtk_widget_set_tooltip_text(g->Dmin_R, _("adjust the color and shade of the film transparent base.\n" + "this value depends on the film material, \n" + "the chemical fog produced while developing the film,\n" + "and the scanner white balance.")); + break; + + default: // We shouldn't be there fprintf(stderr, "negadoctor film stock: undefined behaviour\n"); + break; } } @@ -464,13 +681,13 @@ static void Dmin_picker_update(dt_iop_module_t *self) GdkRGBA color; color.alpha = 1.0f; - if(p->film_stock == DT_FILMSTOCK_COLOR) + if(p->film_stock == DT_FILMSTOCK_COLOR_NEG) { color.red = p->Dmin[0]; color.green = p->Dmin[1]; color.blue = p->Dmin[2]; } - else if(p->film_stock == DT_FILMSTOCK_NB) + else if(p->film_stock == DT_FILMSTOCK_NB_NEG || p->film_stock == DT_FILMSTOCK_SLIDE) { color.red = color.green = color.blue = p->Dmin[0]; } @@ -597,50 +814,80 @@ static void WB_high_picker_callback(GtkColorButton *widget, dt_iop_module_t *sel dt_dev_add_history_item(darktable.develop, self, TRUE); } -static void Wb_low_norm_callback(GtkColorButton *widget, dt_iop_module_t *self) +static void _Wb_low_normalize(dt_aligned_pixel_t RGB) { if(darktable.gui->reset) return; - dt_iop_negadoctor_gui_data_t *const g = (dt_iop_negadoctor_gui_data_t *)self->gui_data; - dt_iop_negadoctor_params_t *p = (dt_iop_negadoctor_params_t *)self->params; + const float WB_low_max = v_maxf(RGB); + for(size_t c = 0; c < 3; ++c) + { + RGB[c] /= WB_low_max; + } +} +static void _Wb_high_normalize(dt_aligned_pixel_t RGB) +{ + if(darktable.gui->reset) return; - const float WB_low_max = v_maxf(p->wb_low); + const float WB_high_min = v_minf(RGB); for(size_t c = 0; c < 3; ++c) - p->wb_low[c] /= WB_low_max; + RGB[c] /= WB_high_min; +} +static void Wb_low_norm_callback(GtkWidget *widget, dt_iop_module_t *self) +{ + if(darktable.gui->reset) return; + + dt_iop_negadoctor_params_t *p = (dt_iop_negadoctor_params_t *)self->params; + dt_iop_negadoctor_gui_data_t *g = (dt_iop_negadoctor_gui_data_t *)self->gui_data; + + dt_aligned_pixel_t RGB; + for(int c = 0; c < 3; c++) + RGB[c] = p->wb_low[c]; + + _Wb_low_normalize(RGB); + + for(int c = 0; c < 3; c++) + p->wb_low[c] = RGB[c]; ++darktable.gui->reset; - dt_bauhaus_slider_set(g->wb_low_R, p->wb_low[0]); - dt_bauhaus_slider_set(g->wb_low_G, p->wb_low[1]); - dt_bauhaus_slider_set(g->wb_low_B, p->wb_low[2]); + dt_bauhaus_slider_set(g->wb_low_R, RGB[0]); + dt_bauhaus_slider_set(g->wb_low_G, RGB[1]); + dt_bauhaus_slider_set(g->wb_low_B, RGB[2]); --darktable.gui->reset; WB_low_picker_update(self); - dt_control_queue_redraw_widget(self->widget); + dt_control_queue_redraw_widget(g->wb_low_R); + dt_control_queue_redraw_widget(g->wb_low_G); + dt_control_queue_redraw_widget(g->wb_low_B); dt_dev_add_history_item(darktable.develop, self, TRUE); } -static void Wb_high_norm_callback(GtkColorButton *widget, dt_iop_module_t *self) +static void Wb_high_norm_callback(GtkWidget *widget, dt_iop_module_t *self) { if(darktable.gui->reset) return; - dt_iop_negadoctor_gui_data_t *const g = (dt_iop_negadoctor_gui_data_t *)self->gui_data; dt_iop_negadoctor_params_t *p = (dt_iop_negadoctor_params_t *)self->params; + dt_iop_negadoctor_gui_data_t *g = (dt_iop_negadoctor_gui_data_t *)self->gui_data; - const float WB_high_min = v_minf(p->wb_high); + dt_aligned_pixel_t RGB; + for(int c = 0; c < 3; c++) + RGB[c] = p->wb_high[c]; + _Wb_high_normalize(RGB); + for(size_t c = 0; c < 3; ++c) - p->wb_high[c] /= WB_high_min; - + p->wb_high[c] = RGB[c]; ++darktable.gui->reset; - dt_bauhaus_slider_set(g->wb_high_R, p->wb_high[0]); - dt_bauhaus_slider_set(g->wb_high_G, p->wb_high[1]); - dt_bauhaus_slider_set(g->wb_high_B, p->wb_high[2]); + dt_bauhaus_slider_set(g->wb_high_R, RGB[0]); + dt_bauhaus_slider_set(g->wb_high_G, RGB[1]); + dt_bauhaus_slider_set(g->wb_high_B, RGB[2]); --darktable.gui->reset; - WB_low_picker_update(self); - dt_control_queue_redraw_widget(self->widget); + WB_high_picker_update(self); + dt_control_queue_redraw_widget(g->wb_high_R); + dt_control_queue_redraw_widget(g->wb_high_G); + dt_control_queue_redraw_widget(g->wb_high_B); dt_dev_add_history_item(darktable.develop, self, TRUE); } @@ -654,6 +901,8 @@ static void apply_auto_Dmin(dt_iop_module_t *self) dt_iop_negadoctor_params_t *p = (dt_iop_negadoctor_params_t *)self->params; for(int k = 0; k < 4; k++) p->Dmin[k] = self->picked_color[k]; + if(p->film_stock == DT_FILMSTOCK_SLIDE) + p->Dmin[0] = p->Dmin[1] = p->Dmin[2] = v_maxf(p->Dmin); ++darktable.gui->reset; dt_bauhaus_slider_set(g->Dmin_R, p->Dmin[0]); @@ -679,7 +928,7 @@ static void apply_auto_Dmax(dt_iop_module_t *self) RGB[c] = log10f(p->Dmin[c] / fmaxf(self->picked_color_min[c], THRESHOLD)); } - // Take the max(RGB) for safety. Big values unclip whites + // Take the max(RGB) for safety. Big values unclip whites (or blacks for positive images) p->D_max = v_maxf(RGB); ++darktable.gui->reset; @@ -723,9 +972,10 @@ static void apply_auto_WB_low(dt_iop_module_t *self) dt_aligned_pixel_t RGB_min; for(int c = 0; c < 3; c++) RGB_min[c] = log10f(p->Dmin[c] / fmaxf(self->picked_color[c], THRESHOLD)) / p->D_max; + + _Wb_low_normalize(RGB_min); - const float RGB_v_min = v_minf(RGB_min); // warning: can be negative - for(int c = 0; c < 3; c++) p->wb_low[c] = RGB_v_min / RGB_min[c]; + for(int c = 0; c < 3; c++) p->wb_low[c] = RGB_min[c]; ++darktable.gui->reset; dt_bauhaus_slider_set(g->wb_low_R, p->wb_low[0]); @@ -748,10 +998,30 @@ static void apply_auto_WB_high(dt_iop_module_t *self) dt_aligned_pixel_t RGB_min; for(int c = 0; c < 3; c++) - RGB_min[c] = fabsf(-1.0f / (p->offset * p->wb_low[c] - log10f(p->Dmin[c] / fmaxf(self->picked_color[c], THRESHOLD)) / p->D_max)); + { + const float low = p->wb_low[c]; + + RGB_min[c] = -log10f(p->Dmin[c] / fmaxf(self->picked_color[c], THRESHOLD)); + RGB_min[c] /= p->D_max; + RGB_min[c] = p->offset * low + RGB_min[c] ; + RGB_min[c] = fabsf(-1.0f / RGB_min[c]); + } - const float RGB_v_min = v_minf(RGB_min); // warning : must be positive - for(int c = 0; c < 3; c++) p->wb_high[c] = RGB_min[c] / RGB_v_min; + // Prepare for slide, else RGB will be inverted to CMY + + if(p->film_stock == DT_FILMSTOCK_SLIDE) + { + const float WB_high_min = v_minf(RGB_min); + for(size_t c = 0; c < 3; ++c) + { + RGB_min[c] /= WB_high_min; + RGB_min[c] = 2.f - RGB_min[c]; + } + } + + _Wb_high_normalize(RGB_min); + + for(size_t c = 0; c < 3; ++c) p->wb_high[c] = RGB_min[c]; ++darktable.gui->reset; dt_bauhaus_slider_set(g->wb_high_R, p->wb_high[0]); @@ -775,10 +1045,26 @@ static void apply_auto_black(dt_iop_module_t *self) dt_aligned_pixel_t RGB; for(int c = 0; c < 3; c++) { - RGB[c] = -log10f(p->Dmin[c] / fmaxf(self->picked_color_max[c], THRESHOLD)); - RGB[c] *= p->wb_high[c] / p->D_max; - RGB[c] += p->wb_low[c] * p->offset * p->wb_high[c]; - RGB[c] = 0.1f - (1.0f - fast_exp10f(RGB[c])); // actually, remap between -3.32 EV and infinity for safety because gamma comes later + const float high = (p->film_stock == DT_FILMSTOCK_SLIDE) ? 2 - p->wb_high[c] : p->wb_high[c]; + const float low = (p->film_stock == DT_FILMSTOCK_SLIDE) ? 2 - p->wb_low[c] : p->wb_low[c]; + const float picked_color = (p->film_stock == DT_FILMSTOCK_SLIDE) + ? self->picked_color_min[c] // we need the lower values for slide film + : self->picked_color_max[c]; + + RGB[c] = -log10f(p->Dmin[c] / fmaxf(picked_color, THRESHOLD)); + RGB[c] *= high / p->D_max; + RGB[c] += low * p->offset * high; + + if(p->film_stock == DT_FILMSTOCK_SLIDE) + { + RGB[c] = fast_exp10f(RGB[c]); + RGB[c] = -RGB[c]; + } + else + { + RGB[c] = 1.0f - fast_exp10f(RGB[c]); + RGB[c] = 0.1f - RGB[c]; // actually, remap between -3.32 EV and infinity for safety because gamma comes later + } } p->black = v_maxf(RGB); @@ -801,10 +1087,17 @@ static void apply_auto_exposure(dt_iop_module_t *self) dt_aligned_pixel_t RGB; for(int c = 0; c < 3; c++) { - RGB[c] = -log10f(p->Dmin[c] / fmaxf(self->picked_color_min[c], THRESHOLD)); - RGB[c] *= p->wb_high[c] / p->D_max; - RGB[c] += p->wb_low[c] * p->offset; - RGB[c] = 0.96f / (1.0f - fast_exp10f(RGB[c]) + p->black); // actually, remap in [0; 0.96] for safety + const float high = (p->film_stock == DT_FILMSTOCK_SLIDE) ? 2 - p->wb_high[c] : p->wb_high[c]; + const float low = (p->film_stock == DT_FILMSTOCK_SLIDE) ? 2 - p->wb_low[c] : p->wb_low[c]; + const float picked_color = (p->film_stock == DT_FILMSTOCK_SLIDE) + ? fmaxf(self->picked_color_max[c], THRESHOLD) // we need the higher values for slide film + : fmaxf(self->picked_color_min[c], THRESHOLD); + + RGB[c] = -log10f(p->Dmin[c] / picked_color); + RGB[c] *= high / p->D_max; + RGB[c] += low * p->offset; + RGB[c] = (p->film_stock == DT_FILMSTOCK_SLIDE) ? fast_exp10f(RGB[c]) : 1.0f - fast_exp10f(RGB[c]); + RGB[c] = 0.96f / (RGB[c] + p->black); // actually, remap in [0; 0.96] for safety } p->exposure = v_minf(RGB); @@ -917,7 +1210,8 @@ void gui_init(dt_iop_module_t *self) GtkWidget *page2 = self->widget = dt_ui_notebook_page(g->notebook, N_("corrections"), NULL); // WB shadows - gtk_box_pack_start(GTK_BOX(page2), dt_ui_section_label_new(_("shadows color cast")), FALSE, FALSE, 0); + g->WB_low_label = dt_ui_section_label_new(_("shadows color cast")); + gtk_box_pack_start(GTK_BOX(page2), g->WB_low_label, FALSE, FALSE, 0); GtkWidget *row3 = GTK_WIDGET(gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0)); @@ -927,7 +1221,7 @@ void gui_init(dt_iop_module_t *self) gtk_box_pack_start(GTK_BOX(row3), GTK_WIDGET(g->WB_low_picker), TRUE, TRUE, 0); g_signal_connect(G_OBJECT(g->WB_low_picker), "color-set", G_CALLBACK(WB_low_picker_callback), self); - g->WB_low_norm = dt_action_button_new((dt_lib_module_t *)self, N_("normalize"), Wb_low_norm_callback, self, _("normalize shadows white balance settings"), 0, 0); + g->WB_low_norm = dt_action_button_new((dt_lib_module_t *)self, N_("Normalize"), Wb_low_norm_callback, self, _("normalize shadows color balance settings"), 0, 0); gtk_box_pack_start(GTK_BOX(row3), GTK_WIDGET(g->WB_low_norm), FALSE, FALSE, 0); g->WB_low_sampler = dt_color_picker_new(self, DT_COLOR_PICKER_AREA, row3); @@ -957,7 +1251,8 @@ void gui_init(dt_iop_module_t *self) "recovering the global white balance in difficult cases.")); // WB highlights - gtk_box_pack_start(GTK_BOX(page2), dt_ui_section_label_new(_("highlights white balance")), FALSE, FALSE, 0); + g->WB_high_label = dt_ui_section_label_new(_("highlights white balance")); + gtk_box_pack_start(GTK_BOX(page2), g->WB_high_label, FALSE, FALSE, 0); GtkWidget *row2 = GTK_WIDGET(gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0)); @@ -967,7 +1262,7 @@ void gui_init(dt_iop_module_t *self) gtk_box_pack_start(GTK_BOX(row2), GTK_WIDGET(g->WB_high_picker), TRUE, TRUE, 0); g_signal_connect(G_OBJECT(g->WB_high_picker), "color-set", G_CALLBACK(WB_high_picker_callback), self); - g->WB_high_norm = dt_action_button_new((dt_lib_module_t *)self, N_("normalize"), Wb_high_norm_callback, self, _("normalize highlight white balance settings"), 0, 0); + g->WB_high_norm = dt_action_button_new((dt_lib_module_t *)self, N_("Normalize"), Wb_high_norm_callback, self, _("normalize highlight color balance settings"), 0, 0); gtk_box_pack_start(GTK_BOX(row2), GTK_WIDGET(g->WB_high_norm), FALSE, FALSE, 0); g->WB_high_sampler = dt_color_picker_new(self, DT_COLOR_PICKER_AREA, row2); @@ -1021,13 +1316,12 @@ void gui_init(dt_iop_module_t *self) dt_bauhaus_slider_set_format(g->soft_clip, "%"); gtk_widget_set_tooltip_text(g->soft_clip, _("gradually compress specular highlights past this value\n" "to avoid clipping while pushing the exposure for mid-tones.\n" - "this somewhat reproduces the behaviour of matte paper.")); + "this somewhat reproduces the behavior of matte paper.")); gtk_box_pack_start(GTK_BOX(page3), dt_ui_section_label_new(_("virtual print emulation")), FALSE, FALSE, 0); g->exposure = dt_color_picker_new(self, DT_COLOR_PICKER_AREA, dt_bauhaus_slider_from_params(self, "exposure")); - dt_bauhaus_slider_set_soft_min(g->exposure, -1.0); - dt_bauhaus_slider_set_soft_max(g->exposure, 1.0); + dt_bauhaus_slider_set_soft_range(g->exposure, -1.0, 1.0); dt_bauhaus_slider_set_default(g->exposure, 0.0); dt_bauhaus_slider_set_format(g->exposure, _(" EV")); gtk_widget_set_tooltip_text(g->exposure, _("correct the printing exposure after inversion to adjust\n" @@ -1062,8 +1356,10 @@ void gui_changed(dt_iop_module_t *self, GtkWidget *w, void *previous) toggle_stock_controls(self); Dmin_picker_update(self); } - else if(w == g->Dmin_R && p->film_stock == DT_FILMSTOCK_NB) + else if(p->film_stock != DT_FILMSTOCK_COLOR_NEG && (w == g->Dmin_R || w == g->Dmin_sampler)) { + p->Dmin[2] = p->Dmin[1] = p->Dmin[0]; + dt_bauhaus_slider_set(g->Dmin_R, p->Dmin[0]); dt_bauhaus_slider_set(g->Dmin_G, p->Dmin[0]); dt_bauhaus_slider_set(g->Dmin_B, p->Dmin[0]); }