diff --git a/data/darktableconfig.xml.in b/data/darktableconfig.xml.in index 233c91d70feb..0e2f2bdbb4ff 100644 --- a/data/darktableconfig.xml.in +++ b/data/darktableconfig.xml.in @@ -3362,6 +3362,13 @@ + + plugins/darkroom/histogram/show_luma + bool + false + + + plugins/darkroom/histogram/vectorscope/harmony_type diff --git a/data/themes/darktable.css b/data/themes/darktable.css index f1ab12955fe6..ae832ad977e3 100644 --- a/data/themes/darktable.css +++ b/data/themes/darktable.css @@ -224,6 +224,7 @@ Why that? Just because it's a developer choice and most classes use underscore i @define-color graph_red rgb(237,30,20); @define-color graph_green rgb(28,235,26); @define-color graph_blue rgb(14,14,233); +@define-color graph_white @grey_95; @define-color colorlabel_red rgb(230,0,0); @define-color colorlabel_green rgb(0,230,0); @@ -745,6 +746,11 @@ dialog .sidebar row:selected:hover label, background-color: alpha(@graph_blue, 0.2); } +#luma-toggle-button +{ + background-color: alpha(white, 0.2); +} + /* set now them active state */ #red-channel-button:checked { @@ -761,6 +767,11 @@ dialog .sidebar row:selected:hover label, background-color: alpha(@graph_blue, 0.66); } +#luma-toggle-button:checked +{ + background-color: alpha(white, 0.66); +} + /* set now hover state */ #red-channel-button:hover { @@ -777,6 +788,27 @@ dialog .sidebar row:selected:hover label, background-color: alpha(@graph_blue, 0.5); } +#luma-toggle-button:hover +{ + background-color: alpha(white, 0.5); +} + +/* inactive state */ +#red-channel-button:disabled +{ + background-color: alpha(@graph_red, 0.1); +} + +#green-channel-button:disabled +{ + background-color: alpha(@graph_green, 0.1); +} + +#blue-channel-button:disabled +{ + background-color: alpha(@graph_blue, 0.1); +} + /*------------------ - Dialog windows - ------------------*/ diff --git a/src/bauhaus/bauhaus.c b/src/bauhaus/bauhaus.c index 28bc786ecc6c..adb95386de7b 100644 --- a/src/bauhaus/bauhaus.c +++ b/src/bauhaus/bauhaus.c @@ -811,6 +811,8 @@ void dt_bauhaus_load_theme() &bh->graph_colors[1]); gtk_style_context_lookup_color(ctx, "graph_blue", &bh->graph_colors[2]); + gtk_style_context_lookup_color(ctx, "graph_white", + &bh->graph_colors[3]); gtk_style_context_lookup_color(ctx, "colorlabel_red", &bh->colorlabels[DT_COLORLABELS_RED]); gtk_style_context_lookup_color(ctx, "colorlabel_yellow", diff --git a/src/bauhaus/bauhaus.h b/src/bauhaus/bauhaus.h index e9ddd0943839..1279f08ec551 100644 --- a/src/bauhaus/bauhaus.h +++ b/src/bauhaus/bauhaus.h @@ -152,7 +152,7 @@ typedef struct dt_bauhaus_t // colors for graphs GdkRGBA graph_bg, graph_exterior, graph_border, graph_fg, graph_grid, graph_fg_active, graph_overlay, inset_histogram; - GdkRGBA graph_colors[3]; // primaries + GdkRGBA graph_colors[4]; // additive primaries and luma white GdkRGBA colorlabels[DT_COLORLABELS_LAST]; } dt_bauhaus_t; diff --git a/src/common/histogram.c b/src/common/histogram.c index 464b53419280..4a883c59c861 100644 --- a/src/common/histogram.c +++ b/src/common/histogram.c @@ -1,6 +1,6 @@ /* This file is part of darktable, - Copyright (C) 2014-2023 darktable developers. + Copyright (C) 2014-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 @@ -70,6 +70,28 @@ static inline void _bin_raw(const dt_dev_histogram_collection_params_t *const pa //------------------------------------------------------------------------------ +static inline void _bin_luma(const dt_dev_histogram_collection_params_t *const params, + const void *const restrict pixel, + uint32_t *const restrict histogram, + const int j, + const dt_iop_order_iccprofile_info_t *const profile) +{ + const dt_histogram_roi_t *roi = params->roi; + float *in = (float *)pixel + 4 * (roi->width * j + roi->crop_x); + const float max_bin = params->bins_count - 1; + + for(int i = 0; i < roi->width - roi->crop_right - roi->crop_x; i++) + { + const float luma = profile->matrix_in[1][0] * in[i*4] + + profile->matrix_in[1][1] * in[i*4+1] + + profile->matrix_in[1][2] * in[i*4+2]; + const size_t bin = CLAMP(max_bin * luma, 0.0f, max_bin); + histogram[bin]++; + } +} + +//------------------------------------------------------------------------------ + static inline void _bin_rgb(const dt_dev_histogram_collection_params_t *const params, const void *const restrict pixel, uint32_t *const restrict histogram, @@ -225,15 +247,23 @@ void dt_histogram_helper(dt_dev_histogram_collection_params_t *histogram_params, break; case IOP_CS_RGB: - histogram_stats->ch = 3u; - if(compensate_middle_grey && profile_info) - // for rgbcurve (compensated) + if(histogram_stats->ch == 1u) + // for histogram utility module _hist_worker(histogram_params, histogram_stats, pixel, histogram, - _bin_rgb_compensated, profile_info); + _bin_luma, profile_info); else - // used by levels, rgbcurve (uncompensated), rgblevels - _hist_worker(histogram_params, histogram_stats, pixel, histogram, - _bin_rgb, profile_info); + { + histogram_stats->ch = 3u; + if(compensate_middle_grey && profile_info) + // for rgbcurve (compensated) + _hist_worker(histogram_params, histogram_stats, pixel, histogram, + _bin_rgb_compensated, profile_info); + else + // used by levels, rgbcurve (uncompensated), rgblevels, and + // histogram utility module + _hist_worker(histogram_params, histogram_stats, pixel, histogram, + _bin_rgb, profile_info); + } break; case IOP_CS_LAB: @@ -257,7 +287,7 @@ void dt_histogram_helper(dt_dev_histogram_collection_params_t *histogram_params, if(*histogram && histogram_max) { // RGB, Lab, and LCh - if(cst == IOP_CS_RGB || IOP_CS_LAB) + if(cst == IOP_CS_RGB || cst == IOP_CS_LAB) { uint32_t *hist = *histogram; @@ -271,9 +301,15 @@ void dt_histogram_helper(dt_dev_histogram_collection_params_t *histogram_params, m[2] = hist[2]; } - for(int k = 4; k < 4 * histogram_stats->bins_count; k += 4) - for_each_channel(ch,aligned(hist:64) aligned(m:16)) - m[ch] = MAX(m[ch], hist[k+ch]); + // intentionally skip first bucket to show bit more y-axis range + // for underexposed images + if(histogram_stats->ch == 1u) + for(int k = 1; k < histogram_stats->bins_count; k++) + m[0] = MAX(m[0], hist[k]); + else + for(int k = 4; k < 4 * histogram_stats->bins_count; k += 4) + for_each_channel(ch,aligned(hist:64) aligned(m:16)) + m[ch] = MAX(m[ch], hist[k+ch]); } else // raw max not implemented, as is only seen in exposure diff --git a/src/develop/pixelpipe.h b/src/develop/pixelpipe.h index 0dbe3c3ab2ca..ac5c27ba8635 100644 --- a/src/develop/pixelpipe.h +++ b/src/develop/pixelpipe.h @@ -110,7 +110,7 @@ typedef struct dt_dev_histogram_stats_t size_t buf_size; /** count of pixels sampled during histogram capture. */ uint32_t pixels; - /** count of channels: 1 for RAW, 3 for rgb/Lab. */ + /** count of channels: 1 for RAW/luma, 3 for rgb/Lab. */ uint32_t ch; } dt_dev_histogram_stats_t; diff --git a/src/libs/histogram.c b/src/libs/histogram.c index fed945e8c177..3d7fcf24578d 100644 --- a/src/libs/histogram.c +++ b/src/libs/histogram.c @@ -41,10 +41,10 @@ static void _lib_histogram_get_sector_angles(dt_lib_module_t *self, float *angles, int *n); -static const gchar *rgb_names[DT_SCOPES_RGB_N] = +static const gchar *channel_names[DT_SCOPES_CH_N] = { N_("red"), N_("green"), - N_("blue") + N_("blue"), }; const char *name(dt_lib_module_t *self) @@ -442,7 +442,7 @@ static void _mode_toggle(GtkWidget *button, dt_scopes_t *s) lib_histogram_update_tooltip(s); dt_scopes_call(prior_mode, mode_leave); - gtk_widget_set_visible(s->button_box_rgb, + gtk_widget_set_visible(s->button_box_channels, dt_scopes_func_exists(s->cur_mode, draw_scope_channels)); dt_scopes_call(s->cur_mode, update_buttons); dt_scopes_call(s->cur_mode, mode_enter); @@ -457,12 +457,12 @@ static void _mode_toggle(GtkWidget *button, dt_scopes_t *s) static void _channel_toggle(GtkWidget *button, dt_scopes_t *s) { - for(int i = 0; i < DT_SCOPES_RGB_N; i++) - if(s->channel_buttons[i] == button) + for(int i = 0; i < DT_SCOPES_CH_N; i++) + if(s->channel_btns[i] == button) { char conf[48]; g_snprintf(conf, sizeof(conf), - "plugins/darkroom/histogram/show_%s", rgb_names[i]); + "plugins/darkroom/histogram/show_%s", channel_names[i]); s->channels[i] = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(button)); dt_conf_set_bool(conf, s->channels[i]); @@ -537,7 +537,7 @@ static void _eventbox_enter_notify_callback(GtkEventControllerMotion *controller if(s->cur_mode != &s->modes[i]) dt_scopes_call(&s->modes[i], mode_leave); dt_scopes_call(s->cur_mode, mode_enter); - gtk_widget_set_visible(s->button_box_rgb, + gtk_widget_set_visible(s->button_box_channels, dt_scopes_func_exists(s->cur_mode, draw_scope_channels)); gtk_widget_show(s->button_box_left); gtk_widget_show(s->button_box_right); @@ -675,11 +675,11 @@ void gui_init(dt_lib_module_t *self) dt_pthread_mutex_init(&s->lock, NULL); - s->channels[DT_SCOPES_RGB_RED] + s->channels[DT_SCOPES_CH_RED] = dt_conf_get_bool("plugins/darkroom/histogram/show_red"); - s->channels[DT_SCOPES_RGB_GREEN] + s->channels[DT_SCOPES_CH_GREEN] = dt_conf_get_bool("plugins/darkroom/histogram/show_green"); - s->channels[DT_SCOPES_RGB_BLUE] + s->channels[DT_SCOPES_CH_BLUE] = dt_conf_get_bool("plugins/darkroom/histogram/show_blue"); // proxy functions and data so that pixelpipe or tether can @@ -804,14 +804,14 @@ void gui_init(dt_lib_module_t *self) GDK_KEY_H, GDK_CONTROL_MASK | GDK_SHIFT_MASK); // RGB channel buttons - s->button_box_rgb = dt_gui_hbox(); - gtk_widget_set_valign(s->button_box_rgb, GTK_ALIGN_CENTER); - gtk_widget_set_halign(s->button_box_rgb, GTK_ALIGN_CENTER); - // red/green/blue channel on/off - for(int i=DT_SCOPES_RGB_RED; i < DT_SCOPES_RGB_N; i++) + s->button_box_channels = dt_gui_hbox(); + gtk_widget_set_valign(s->button_box_channels, GTK_ALIGN_CENTER); + gtk_widget_set_halign(s->button_box_channels, GTK_ALIGN_CENTER); + // red/green/blue on/off + for(int i=DT_SCOPES_CH_RED; i < DT_SCOPES_CH_N; i++) { - g_autofree char *name = g_strdup_printf("%s-channel-button", rgb_names[i]); - g_autofree char *tip = g_strdup_printf(_("toggle %s channel"), _(rgb_names[i])); + g_autofree char *name = g_strdup_printf("%s-channel-button", channel_names[i]); + g_autofree char *tip = g_strdup_printf(_("toggle %s channel"), _(channel_names[i])); GtkWidget *btn = dtgtk_togglebutton_new(dtgtk_cairo_paint_color, CPF_NONE, NULL); dt_gui_add_class(btn, "rgb_toggle"); @@ -819,13 +819,16 @@ void gui_init(dt_lib_module_t *self) gtk_widget_set_tooltip_text(btn, tip); gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(btn), s->channels[i]); - dt_action_define(dark, N_("toggle colors"), rgb_names[i], btn, &dt_action_def_toggle); - dt_gui_box_add(s->button_box_rgb, btn); + dt_action_define(dark, N_("toggle colors"), channel_names[i], btn, &dt_action_def_toggle); g_signal_connect(G_OBJECT(btn), "toggled", G_CALLBACK(_channel_toggle), s); - s->channel_buttons[i] = btn; + s->channel_btns[i] = btn; } - // RGB channels are always rightmost - dt_gui_box_add(s->button_box_right, s->button_box_rgb); + dt_gui_box_add(s->button_box_channels, + s->channel_btns[DT_SCOPES_CH_RED], + s->channel_btns[DT_SCOPES_CH_GREEN], + s->channel_btns[DT_SCOPES_CH_BLUE]); + // channels are always rightmost + dt_gui_box_add(s->button_box_right, s->button_box_channels); for(dt_scopes_mode_type_t i = 0; i < DT_SCOPES_MODE_N; i++) { diff --git a/src/libs/scopes.h b/src/libs/scopes.h index 3c83edd56687..ebab476c5f6f 100644 --- a/src/libs/scopes.h +++ b/src/libs/scopes.h @@ -43,15 +43,15 @@ typedef enum dt_scopes_highlight_t DT_SCOPES_HIGHLIGHT_EXPOSURE } dt_scopes_highlight_t; -typedef enum dt_scopes_rgb_t +typedef enum dt_scopes_channels_t { - DT_SCOPES_RGB_RED = 0, - DT_SCOPES_RGB_GREEN, - DT_SCOPES_RGB_BLUE, - DT_SCOPES_RGB_N // needs to be the last one -} dt_scopes_rgb_t; + DT_SCOPES_CH_RED = 0, + DT_SCOPES_CH_GREEN, + DT_SCOPES_CH_BLUE, + DT_SCOPES_CH_N // needs to be the last one +} dt_scopes_channels_t; -typedef gboolean scopes_channels_t[DT_SCOPES_RGB_N]; +typedef gboolean dt_scopes_channels_list_t[DT_SCOPES_CH_N]; struct dt_scopes_t; struct dt_scopes_mode_t; @@ -66,7 +66,7 @@ typedef struct dt_scopes_functions_t const float *const input, // FIXME: should ROI by dt_histogram_roi_t or another type? dt_histogram_roi_t *const roi, - const dt_iop_order_iccprofile_info_t *vs_prof); + const dt_iop_order_iccprofile_info_t *profile); // FIXME: do want a proper clear function or just tag as not up to date? void (*clear)(struct dt_scopes_mode_t *const self); void (*draw_bkgd)(const struct dt_scopes_mode_t *const self, @@ -90,7 +90,7 @@ typedef struct dt_scopes_functions_t cairo_t *cr, const int width, const int height, - const scopes_channels_t channels); + const dt_scopes_channels_list_t channels); // FIXME: rename to something more sensible dt_scopes_highlight_t (*get_highlight)(const struct dt_scopes_mode_t *const self, const double posx, @@ -140,7 +140,7 @@ typedef struct dt_scopes_t dt_scopes_mode_t modes[DT_SCOPES_MODE_N]; // all available modes int update_counter; // most recent pixelpipe vs mode data dt_scopes_highlight_t highlight; // depends on mouse position - scopes_channels_t channels; // display state chosen by RGB buttons + dt_scopes_channels_list_t channels; // RGB channels to display gboolean dragging; // to block motion handling during drag gdouble last_offset_x, last_offset_y; // for drag handling // UI elements @@ -148,8 +148,8 @@ typedef struct dt_scopes_t GtkWidget *button_box_left; // GtkBox -- scope mode buttons GtkWidget *button_box_split; // GtkBox -- option buttons for left scope GtkWidget *button_box_right; // GtkBox -- option buttons for main scope - GtkWidget *button_box_rgb; // GtkBox -- RGB channels buttons - GtkWidget *channel_buttons[DT_SCOPES_RGB_N]; // Array of GtkToggleButton -- RGB channels + GtkWidget *button_box_channels; // GtkBox -- RGB channel buttons + GtkWidget *channel_btns[DT_SCOPES_CH_N]; // Array of GtkToggleButton -- channels GtkWidget *scope_draw; // GtkDrawingArea -- scope & resize // for access to data during process/draw dt_pthread_mutex_t lock; diff --git a/src/libs/scopes/histogram.c b/src/libs/scopes/histogram.c index 3aa32a60a212..1f119b5dc4e8 100644 --- a/src/libs/scopes/histogram.c +++ b/src/libs/scopes/histogram.c @@ -38,10 +38,15 @@ static const gchar *dt_hist_scale_names[DT_HIST_SCALE_N] = typedef struct dt_scopes_hist_t { + // buffers/data uint32_t *histogram; uint32_t histogram_max; - dt_hist_scale_t scale; + // buttons GtkWidget *scale_button; // GtkButton -- linear or logarithmic histogram + GtkWidget *luma_button; // GtkButton -- luma/rgb toggle + // state set by buttons + dt_hist_scale_t scale; + gboolean is_rgb; } dt_scopes_hist_t; @@ -53,20 +58,21 @@ const char* _hist_name(const dt_scopes_mode_t *const self) static void _hist_process(dt_scopes_mode_t *const self, const float *const input, dt_histogram_roi_t *const roi, - const dt_iop_order_iccprofile_info_t *vs_prof) + const dt_iop_order_iccprofile_info_t *profile) { dt_scopes_hist_t *const d = self->data; + const uint32_t channels = d->is_rgb ? 4u : 1u; dt_dev_histogram_collection_params_t histogram_params = { 0 }; const dt_iop_colorspace_type_t cst = IOP_CS_RGB; dt_dev_histogram_stats_t histogram_stats = { .bins_count = HISTOGRAM_BINS, - .ch = 4, + .ch = channels, .pixels = 0, - .buf_size = sizeof(uint32_t) * 4 * HISTOGRAM_BINS }; + .buf_size = sizeof(uint32_t) * channels * HISTOGRAM_BINS }; uint32_t histogram_max[4] = { 0 }; d->histogram_max = 0; - memset(d->histogram, 0, sizeof(uint32_t) * 4 * HISTOGRAM_BINS); + memset(d->histogram, 0, sizeof(uint32_t) * channels * HISTOGRAM_BINS); histogram_params.roi = roi; histogram_params.bins_count = HISTOGRAM_BINS; @@ -77,8 +83,11 @@ static void _hist_process(dt_scopes_mode_t *const self, // FIXME: set up "custom" histogram worker which can do colorspace // conversion on fly -- in cases that we need to do that -- may need // to add from colorspace to dt_dev_histogram_collection_params_t - dt_histogram_helper(&histogram_params, &histogram_stats, cst, IOP_CS_NONE, - input, &d->histogram, histogram_max, FALSE, NULL); + dt_histogram_helper(&histogram_params, &histogram_stats, + cst, IOP_CS_NONE, + input, + &d->histogram, histogram_max, + FALSE, profile); d->histogram_max = MAX(MAX(histogram_max[0], histogram_max[1]), histogram_max[2]); self->update_counter = self->scopes->update_counter; } @@ -126,7 +135,7 @@ static void _hist_draw(const dt_scopes_mode_t *const self, cairo_t *cr, const int width, const int height, - const scopes_channels_t channels) + const dt_scopes_channels_list_t channels) { const dt_scopes_hist_t *const d = self->data; @@ -146,16 +155,22 @@ static void _hist_draw(const dt_scopes_mode_t *const self, cairo_scale(cr, width / 255.0, -(height - 10) / hist_max); cairo_set_operator(cr, CAIRO_OPERATOR_ADD); cairo_set_line_width(cr, DT_PIXEL_APPLY_DPI(1.)); - for(int k = 0; k < DT_SCOPES_RGB_N; k++) - if(channels[k]) - { - // FIXME: this is the last place in dt these are used -- if can - // eliminate, then can directly set button colors in CSS and - // simplify things - set_color(cr, darktable.bauhaus->graph_colors[k]); - dt_draw_histogram_8(cr, d->histogram, 4, k, - d->scale == DT_HIST_SCALE_LINEAR); - } + if(d->is_rgb) + { + for(int k = 0; k < DT_SCOPES_CH_N; k++) + if(channels[k]) + { + set_color(cr, darktable.bauhaus->graph_colors[k]); + dt_draw_histogram_8(cr, d->histogram, 4, k, + d->scale == DT_HIST_SCALE_LINEAR); + } + } + else + { + set_color(cr, darktable.bauhaus->graph_colors[3]); + dt_draw_histogram_8(cr, d->histogram, 1, 0, + d->scale == DT_HIST_SCALE_LINEAR); + } cairo_pop_group_to_source(cr); cairo_set_operator(cr, CAIRO_OPERATOR_ADD); cairo_paint_with_alpha(cr, 0.5); @@ -172,6 +187,7 @@ static void _hist_mode_enter(dt_scopes_mode_t *const self) { dt_scopes_hist_t *d = self->data; gtk_widget_show(d->scale_button); + gtk_widget_show(d->luma_button); // FIXME: can call _hist_scale_update() here instead of in gui_init_options? } @@ -179,6 +195,7 @@ static void _hist_mode_leave(const dt_scopes_mode_t *const self) { dt_scopes_hist_t *d = self->data; gtk_widget_hide(d->scale_button); + gtk_widget_hide(d->luma_button); } static void _hist_gui_init(dt_scopes_mode_t *const self, @@ -201,6 +218,7 @@ static void _hist_gui_init(dt_scopes_mode_t *const self, static void _hist_update_buttons(const dt_scopes_mode_t *const self) { dt_scopes_hist_t *d = self->data; + switch(d->scale) { case DT_HIST_SCALE_LOGARITHMIC: @@ -216,6 +234,10 @@ static void _hist_update_buttons(const dt_scopes_mode_t *const self) case DT_HIST_SCALE_N: dt_unreachable_codepath(); } + + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(d->luma_button), !d->is_rgb); + gtk_widget_set_sensitive(self->scopes->button_box_channels, d->is_rgb); + // FIXME: this should really redraw current iop if its background is // a histogram (check request_histogram) darktable.lib->proxy.histogram.is_linear = @@ -235,17 +257,39 @@ static void _hist_scale_clicked(GtkWidget *button, dt_scopes_mode_t *self) dt_scopes_refresh(self->scopes); } +static void _hist_luma_toggle(GtkWidget *button, dt_scopes_mode_t *self) +{ + dt_scopes_hist_t *d = self->data; + d->is_rgb = !gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(button)); + dt_conf_set_bool("plugins/darkroom/histogram/show_luma", !d->is_rgb); + d->histogram_max = 0; + _hist_update_buttons(self); + dt_scopes_reprocess(); +} + static void _hist_add_options(dt_scopes_mode_t *const self, dt_action_t *dark) { dt_scopes_hist_t *d = self->data; + d->scale_button = dtgtk_button_new(dtgtk_cairo_paint_empty, CPF_NONE, NULL); gtk_widget_set_valign(d->scale_button, GTK_ALIGN_START); dt_action_define(dark, NULL, N_("switch histogram scale"), d->scale_button, &dt_action_def_button); - dt_gui_box_add(self->options_box, d->scale_button); g_signal_connect(G_OBJECT(d->scale_button), "clicked", G_CALLBACK(_hist_scale_clicked), self); + + d->luma_button = dtgtk_togglebutton_new(dtgtk_cairo_paint_color, + CPF_NONE, NULL); + gtk_widget_set_valign(d->luma_button, GTK_ALIGN_START); + // FIXME: s/rgb_toggle/color_toggle/? + dt_gui_add_class(d->luma_button, "rgb_toggle"); + gtk_widget_set_name(d->luma_button, "luma-toggle-button"); + gtk_widget_set_tooltip_text(d->luma_button, "toggle luma/rgb"); + dt_action_define(dark, N_("toggle colors"), N_("histogram luma/rgb"), d->luma_button, &dt_action_def_toggle); + g_signal_connect(G_OBJECT(d->luma_button), "toggled", G_CALLBACK(_hist_luma_toggle), self); + + dt_gui_box_add(self->options_box, d->scale_button, d->luma_button); } static void _hist_gui_cleanup(dt_scopes_mode_t *const self) diff --git a/src/libs/scopes/split.c b/src/libs/scopes/split.c index 08b86dfade27..b53f56c30b6f 100644 --- a/src/libs/scopes/split.c +++ b/src/libs/scopes/split.c @@ -33,11 +33,11 @@ const char* _split_name(const dt_scopes_mode_t *const self) static void _split_process(dt_scopes_mode_t *const self, const float *const input, dt_histogram_roi_t *const roi, - const dt_iop_order_iccprofile_info_t *vs_prof) + const dt_iop_order_iccprofile_info_t *profile) { dt_scopes_split_t *const d = self->data; - dt_scopes_call(d->left, process, input, roi, vs_prof); - dt_scopes_call(d->right, process, input, roi, vs_prof); + dt_scopes_call(d->left, process, input, roi, profile); + dt_scopes_call(d->right, process, input, roi, profile); self->update_counter = self->scopes->update_counter; } @@ -90,7 +90,7 @@ static void _split_draw(const dt_scopes_mode_t *const self, cairo_t *cr, const int width, const int height, - const scopes_channels_t channels) + const dt_scopes_channels_list_t channels) { const dt_scopes_split_t *const d = self->data; const int half_width = width / 2; @@ -224,8 +224,8 @@ static void _responsive_buttons(dt_scopes_t *const s) if(min_w == 0.0) return; const int mode_btns_hori = DT_SCOPES_MODE_N; - const int opt_btns_wave = 1; // FIXME: change this if there are more - const int opt_btns_hori = DT_SCOPES_RGB_N + opt_btns_wave; + const int opt_btns_wave = 2; // FIXME: change this if there are more + const int opt_btns_hori = DT_SCOPES_CH_N + opt_btns_wave; const int opt_btns_vert = 1 + opt_btns_wave; const int estd_margin = 6; // for both boxes and buttons const double estd_btn_width = min_w + estd_margin; @@ -247,7 +247,7 @@ static void _responsive_buttons(dt_scopes_t *const s) // compact layout should show all buttons without overlap on the // smallest panel width and scope height gtk_orientable_set_orientation(GTK_ORIENTABLE(s->button_box_left), orient_btn_left); - gtk_orientable_set_orientation(GTK_ORIENTABLE(s->button_box_rgb), orient_btn_rgb); + gtk_orientable_set_orientation(GTK_ORIENTABLE(s->button_box_channels), orient_btn_rgb); } static void _reparent(GtkWidget *src, GtkWidget *dest, GtkWidget *child) @@ -279,7 +279,7 @@ static void _split_mode_enter(dt_scopes_mode_t *const self) d->left->options_box); if(d->left->functions->draw_scope_channels) _reparent(self->scopes->button_box_right, self->scopes->button_box_split, - self->scopes->button_box_rgb); + self->scopes->button_box_channels); gtk_widget_show_all(self->scopes->button_box_split); } @@ -293,11 +293,11 @@ static void _split_mode_leave(const dt_scopes_mode_t *const self) d->left->options_box); if(d->left->functions->draw_scope_channels) _reparent(self->scopes->button_box_split, self->scopes->button_box_right, - self->scopes->button_box_rgb); + self->scopes->button_box_channels); gtk_widget_hide(self->scopes->button_box_split); gtk_orientable_set_orientation(GTK_ORIENTABLE(self->scopes->button_box_left), GTK_ORIENTATION_HORIZONTAL); - gtk_orientable_set_orientation(GTK_ORIENTABLE(self->scopes->button_box_rgb), + gtk_orientable_set_orientation(GTK_ORIENTABLE(self->scopes->button_box_channels), GTK_ORIENTATION_HORIZONTAL); } diff --git a/src/libs/scopes/waveform.c b/src/libs/scopes/waveform.c index 7091365838c4..7f2b75fcd271 100644 --- a/src/libs/scopes/waveform.c +++ b/src/libs/scopes/waveform.c @@ -42,11 +42,17 @@ static const gchar *dt_wave_orient_names[DT_WAVE_ORIENT_N] = typedef struct dt_scopes_wave_t { - uint8_t *waveform_img[3]; // image per RGB channel - int waveform_bins, waveform_tones, waveform_max_bins; - GtkWidget *orient_button; // GtkButton -- horizontal or vertical + // buffers/data + uint8_t *waveform_img[3]; // processed data per channel + int waveform_max_bins; // for spatial sampling of image + int waveform_bins; // spatial sampling of currently processed data + int waveform_tones; // # tones/bin + // buttons + GtkWidget *orient_button; // GtkButton -- horizontal or vertical + GtkWidget *luma_button; // GtkButton -- luma/rgb toggle // state set by buttons dt_wave_orient_t orient; + gboolean is_rgb; // is waveform buffer data RGB or luma } dt_scopes_wave_t; @@ -58,7 +64,7 @@ const char* _wave_name(const dt_scopes_mode_t *const self) static void _wave_process(dt_scopes_mode_t *const self, const float *const input, dt_histogram_roi_t *const roi, - const dt_iop_order_iccprofile_info_t *vs_prof) + const dt_iop_order_iccprofile_info_t *img_prof) { dt_scopes_wave_t *const d = self->data; // FIXME: for point sample, calculate whole graph and the point @@ -72,6 +78,8 @@ static void _wave_process(dt_scopes_mode_t *const self, // orientation, bin_width will generally be 4, for "portrait" it // will generally be 3. Note that waveform_bins varies, depending on // preview image width and # of bins. + // FIXME: check these #s, waveform_max_bins, waveform_tones, and #s + // below now that mipf is mip3 resolution and CPUs are a bit faster const dt_wave_orient_t orient = d->orient; const int to_bin = orient == DT_WAVE_ORIENT_HORI ? sample_width : sample_height; const size_t samples_per_bin = ceilf(to_bin / (float)d->waveform_max_bins); @@ -81,11 +89,15 @@ static void _wave_process(dt_scopes_mode_t *const self, // Note that, with current constants, the input buffer is from the // preview pixelpipe and should be <= 1440x900x4. The output buffer - // will be <= 360x160x3. Hence process works with a relatively small + // will be <= 720x160x4. Hence process works with a relatively small // quantity of data. size_t bin_pad; + const gboolean is_rgb = d->is_rgb + || self == &self->scopes->modes[DT_SCOPES_MODE_PARADE]; + const size_t num_channels = (is_rgb ? 3U : 1U); uint32_t *const restrict partial_binned = - dt_calloc_perthread(3U * num_bins * num_tones, sizeof(uint32_t), &bin_pad); + dt_calloc_perthread(num_channels * num_bins * num_tones, + sizeof(uint32_t), &bin_pad); DT_OMP_FOR() for(size_t y=0; ycrop_x) + ch]; - // Using ceilf brings everything <= 0 to bottom tone, - // everything > 1.0f/(num_tones-1) to top tone. - tone[ch] = ceilf(CLAMPS(v, 0.0f, 1.0f) * (num_tones-1)); + if(is_rgb) + { // RGB waveform + size_t tone[4] DT_ALIGNED_PIXEL; + for_each_channel(ch, aligned(px,tone:16)) + { + // 1.0 is at 8/9 of the height! + const float v = (8.0f / 9.0f) * px[4U * (x + roi->crop_x) + ch]; + // Using ceilf brings everything <= 0 to bottom tone, + // everything > 1.0f/(num_tones-1) to top tone. + tone[ch] = ceilf(CLAMPS(v, 0.0f, 1.0f) * (num_tones-1)); + } + // must be for_three_ch as binned[3] does not exist + for_three_channels(ch, aligned(tone,binned:16)) + binned[num_tones * (ch * num_bins + bin) + tone[ch]]++; + } + else + { // luma/luminance waveform + dt_aligned_pixel_t RGB; + for_each_channel(ch, aligned(px,RGB:16)) + RGB[ch] = px[4U * (x + roi->crop_x) + ch]; + // luma (Y') from linearized data by pretending this is a + // linear colorspace + const int nonlinearlut_fake = FALSE; + // FIXME: also have option for showing a linear waveform, then + // calculate relative luminance (Y) from linearized data + const float luma = + dt_ioppr_get_rgb_matrix_luminance(RGB, + img_prof->matrix_in, + img_prof->lut_in, + img_prof->unbounded_coeffs_in, + img_prof->lutsize, + nonlinearlut_fake); + const float v = (8.0f / 9.0f) * luma; + const size_t tone = ceilf(CLAMPS(v, 0.0f, 1.0f) * (num_tones-1)); + binned[num_tones * bin + tone]++; } - for(size_t ch = 0; ch < 3; ch++) - binned[num_tones * (ch * num_bins + bin) + tone[ch]]++; } } @@ -127,7 +164,7 @@ static void _wave_process(dt_scopes_mode_t *const self, // NOTE: if constant is decreased, will brighten output // FIXME: instead of using an area-beased scale, figure out max bin - // count and scale to that? + // count and scale to that? const float brightness = num_tones / 40.0f; const float scale = brightness / ((orient == DT_WAVE_ORIENT_HORI @@ -136,13 +173,14 @@ static void _wave_process(dt_scopes_mode_t *const self, const size_t nthreads = dt_get_num_threads(); DT_OMP_FOR(collapse(3)) - for(size_t ch = 0; ch < 3; ch++) + for(size_t ch = 0; ch < num_channels; ch++) for(size_t bin = 0; bin < num_bins; bin++) for(size_t tone = 0; tone < num_tones; tone++) { uint8_t *const restrict wf_img = DT_IS_ALIGNED((uint8_t *const restrict)d->waveform_img[ch]); uint32_t acc = 0; + // FIXME: can use openmp accumlate method? for(size_t n = 0; n < nthreads; n++) { uint32_t *const restrict binned = dt_get_bythread(partial_binned, bin_pad, n); @@ -158,10 +196,14 @@ static void _wave_process(dt_scopes_mode_t *const self, dt_free_align(partial_binned); - // waveform and rgb parade share underlying data, so updates to one update both - self->scopes->modes[DT_SCOPES_MODE_WAVEFORM].update_counter = - self->scopes->modes[DT_SCOPES_MODE_PARADE].update_counter = - self->scopes->update_counter; + // waveform and rgb parade share underlying RGB (but not luminosity) + // data, so some updates will update both + if(is_rgb) + self->scopes->modes[DT_SCOPES_MODE_WAVEFORM].update_counter = + self->scopes->modes[DT_SCOPES_MODE_PARADE].update_counter = + self->scopes->update_counter; + else + self->update_counter = self->scopes->update_counter; } static void _wave_draw_grid(const dt_scopes_mode_t *const self, @@ -208,7 +250,7 @@ static void _wave_draw(const dt_scopes_mode_t *const self, cairo_t *cr, const int width, const int height, - const scopes_channels_t channels) + const dt_scopes_channels_list_t channels) { const dt_scopes_wave_t *const d = self->data; @@ -217,37 +259,58 @@ static void _wave_draw(const dt_scopes_mode_t *const self, // composite before scaling to screen dimensions, as scaling each // layer on draw causes a >2x slowdown - const double alpha_chroma = 0.75, desat_over = 0.75, alpha_over = 0.35; + const double alpha_chroma = 0.75; + const double desat_over = 0.75; + const double alpha_over = d->is_rgb ? 0.35 : 0.65; const int img_width = d->orient == DT_WAVE_ORIENT_HORI - ? d->waveform_bins : d->waveform_tones; + ? d->waveform_bins : d->waveform_tones; const int img_height = d->orient == DT_WAVE_ORIENT_HORI - ? d->waveform_tones : d->waveform_bins; + ? d->waveform_tones : d->waveform_bins; const size_t img_stride = cairo_format_stride_for_width(CAIRO_FORMAT_A8, img_width); - cairo_surface_t *cs[3] = { NULL, NULL, NULL }; - cairo_surface_t *cst = cairo_image_surface_create - (CAIRO_FORMAT_ARGB32, img_width, img_height); + cairo_surface_t *cst = + cairo_image_surface_create(CAIRO_FORMAT_ARGB32, img_width, img_height); cairo_t *crt = cairo_create(cst); cairo_set_operator(crt, CAIRO_OPERATOR_ADD); - for(int ch = 0; ch < 3; ch++) - if(channels[ch]) - { - cs[ch] = cairo_image_surface_create_for_data(d->waveform_img[ch], CAIRO_FORMAT_A8, - img_width, img_height, img_stride); - cairo_set_source_rgba(crt, ch==0 ? 1.:0., ch==1 ? 1.:0., ch==2 ? 1.:0., alpha_chroma); - cairo_mask_surface(crt, cs[ch], 0., 0.); - } - cairo_set_operator(crt, CAIRO_OPERATOR_HARD_LIGHT); - for(int ch = 0; ch < 3; ch++) - if(cs[ch]) - { - cairo_set_source_rgba(crt, - ch==0 ? 1.:desat_over, - ch==1 ? 1.:desat_over, - ch==2 ? 1.:desat_over, alpha_over); - cairo_mask_surface(crt, cs[ch], 0., 0.); - cairo_surface_destroy(cs[ch]); - } + if(d->is_rgb) + { + cairo_surface_t *cs[3] = { NULL, NULL, NULL }; + for(int ch = 0; ch < DT_SCOPES_CH_N; ch++) + if(channels[ch]) + { + cs[ch] = + cairo_image_surface_create_for_data(d->waveform_img[ch], CAIRO_FORMAT_A8, + img_width, img_height, img_stride); + cairo_set_source_rgba(crt, + ch==0 ? 1.:0., + ch==1 ? 1.:0., + ch==2 ? 1.:0., alpha_chroma); + cairo_mask_surface(crt, cs[ch], 0., 0.); + } + cairo_set_operator(crt, CAIRO_OPERATOR_HARD_LIGHT); + for(int ch = 0; ch < DT_SCOPES_CH_N; ch++) + if(cs[ch]) + { + cairo_set_source_rgba(crt, + ch==0 ? 1.:desat_over, + ch==1 ? 1.:desat_over, + ch==2 ? 1.:desat_over, alpha_over); + cairo_mask_surface(crt, cs[ch], 0., 0.); + cairo_surface_destroy(cs[ch]); + } + } + else + { + cairo_surface_t *cs = + cairo_image_surface_create_for_data(d->waveform_img[0], CAIRO_FORMAT_A8, + img_width, img_height, img_stride); + cairo_set_source_rgba(crt, 1., 1., 1., alpha_chroma); + cairo_mask_surface(crt, cs, 0., 0.); + cairo_set_operator(crt, CAIRO_OPERATOR_HARD_LIGHT); + cairo_set_source_rgba(crt, 1., 1., 1., alpha_over); + cairo_mask_surface(crt, cs, 0., 0.); + cairo_surface_destroy(cs); + } cairo_destroy(crt); // scale and write to output buffer @@ -302,6 +365,7 @@ static void _wave_clear(dt_scopes_mode_t *const self) static void _wave_update_buttons(const dt_scopes_mode_t *const self) { dt_scopes_wave_t *d = self->data; + switch(d->orient) { case DT_WAVE_ORIENT_HORI: @@ -317,18 +381,32 @@ static void _wave_update_buttons(const dt_scopes_mode_t *const self) case DT_WAVE_ORIENT_N: dt_unreachable_codepath(); } + + if(self == &self->scopes->modes[DT_SCOPES_MODE_WAVEFORM]) + { + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(d->luma_button), !d->is_rgb); + gtk_widget_set_sensitive(self->scopes->button_box_channels, d->is_rgb); + } } static void _wave_mode_enter(dt_scopes_mode_t *const self) { - const dt_scopes_wave_t *const d = self->data; + dt_scopes_wave_t *const d = self->data; gtk_widget_show(d->orient_button); + gtk_widget_show(d->luma_button); + // force reprocess if have valid RGB parade data but are switching + // to a luma waveform + const gboolean cur_buffer_is_rgb = d->is_rgb; + d->is_rgb = !gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(d->luma_button)); + if(cur_buffer_is_rgb && !d->is_rgb) + self->update_counter--; } static void _wave_mode_leave(const dt_scopes_mode_t *const self) { const dt_scopes_wave_t *const d = self->data; gtk_widget_hide(d->orient_button); + gtk_widget_hide(d->luma_button); } static void _wave_gui_init(dt_scopes_mode_t *const self, @@ -341,13 +419,14 @@ static void _wave_gui_init(dt_scopes_mode_t *const self, for(dt_wave_orient_t i=0; iorient = i; + d->is_rgb = !dt_conf_get_bool("plugins/darkroom/waveform/show_luma"); // Waveform buffer doesn't need to be coupled with the scopes // widget size. The waveform is almost always scaled when // drawn. Choose buffer dimensions which produces workable detail, // don't use too much CPU/memory, and allow reasonable gradations // of tone. - + // // Don't use absurd amounts of memory, exceed width of DT_MIPMAP_F // (which will be darktable.mipmap_cache->max_width[DT_MIPMAP_F]*2 // for mosaiced images), nor make it too slow to calculate @@ -356,7 +435,7 @@ static void _wave_gui_init(dt_scopes_mode_t *const self, // width will vary with integral binning of image. // // FIXME: increasing waveform_max_bins increases processing speed - // less than increasing waveform_tones -- tune these better? + // less than increasing waveform_tones -- tune these better? d->waveform_max_bins = darktable.mipmap_cache->max_width[DT_MIPMAP_F]/2; // initially no waveform to draw d->waveform_bins = 0; @@ -369,18 +448,15 @@ static void _wave_gui_init(dt_scopes_mode_t *const self, // of tonal gradation. 256 would match the # of bins in a regular // histogram. d->waveform_tones = 160; - // FIXME: combine waveform_8bit and vectorscope_graph, as only ever - // use one or the other - // // FIXME: keep alignment instead via single alloc via - // dt_alloc_perthread()? + // dt_alloc_perthread()? const size_t bytes_hori = d->waveform_tones * cairo_format_stride_for_width(CAIRO_FORMAT_A8, d->waveform_max_bins); const size_t bytes_vert = d->waveform_max_bins * cairo_format_stride_for_width(CAIRO_FORMAT_A8, d->waveform_tones); - for(int ch=0; ch<3; ch++) + for(int ch=0; ch <= DT_SCOPES_CH_BLUE; ch++) d->waveform_img[ch] = dt_alloc_align_uint8(MAX(bytes_hori, bytes_vert)); } @@ -395,17 +471,38 @@ static void _wave_orient_clicked(GtkWidget *button, dt_scopes_mode_t *const self dt_scopes_reprocess(); } +static void _wave_luma_toggle(GtkWidget *button, dt_scopes_mode_t *self) +{ + dt_scopes_wave_t *d = self->data; + d->is_rgb = !gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(button)); + dt_conf_set_bool("plugins/darkroom/waveform/show_luma", !d->is_rgb); + d->waveform_bins = 0; + _wave_update_buttons(self); + dt_scopes_reprocess(); +} + static void _wave_add_options(dt_scopes_mode_t *const self, dt_action_t *dark) { dt_scopes_wave_t *const d = self->data; + d->orient_button = dtgtk_button_new(dtgtk_cairo_paint_empty, CPF_NONE, NULL); gtk_widget_set_valign(d->orient_button, GTK_ALIGN_START); dt_action_define(dark, NULL, N_("switch scope orientation"), d->orient_button, &dt_action_def_button); - dt_gui_box_add(self->options_box, d->orient_button); g_signal_connect(G_OBJECT(d->orient_button), "clicked", G_CALLBACK(_wave_orient_clicked), self); + + d->luma_button = dtgtk_togglebutton_new(dtgtk_cairo_paint_color, + CPF_NONE, NULL); + gtk_widget_set_valign(d->luma_button, GTK_ALIGN_START); + dt_gui_add_class(d->luma_button, "rgb_toggle"); + gtk_widget_set_name(d->luma_button, "luma-toggle-button"); + gtk_widget_set_tooltip_text(d->luma_button, "toggle luma/rgb"); + dt_action_define(dark, N_("toggle colors"), N_("waveform luma/rgb"), d->luma_button, &dt_action_def_toggle); + g_signal_connect(G_OBJECT(d->luma_button), "toggled", G_CALLBACK(_wave_luma_toggle), self); + + dt_gui_box_add(self->options_box, d->orient_button, d->luma_button); } static void _wave_gui_cleanup(dt_scopes_mode_t *const self) @@ -514,6 +611,18 @@ static void _parade_draw(const dt_scopes_mode_t *const self, cairo_restore(cr); } +static void _parade_mode_enter(dt_scopes_mode_t *const self) +{ + const dt_scopes_wave_t *const d = self->data; + gtk_widget_show(d->orient_button); +} + +static void _parade_mode_leave(const dt_scopes_mode_t *const self) +{ + const dt_scopes_wave_t *const d = self->data; + gtk_widget_hide(d->orient_button); +} + static void _parade_gui_init(dt_scopes_mode_t *const self, dt_scopes_t *const scopes) { @@ -545,8 +654,8 @@ const dt_scopes_functions_t dt_scopes_functions_parade = { .append_to_tooltip = NULL, .eventbox_scroll = NULL, .update_buttons = _wave_update_buttons, - .mode_enter = _wave_mode_enter, - .mode_leave = _wave_mode_leave, + .mode_enter = _parade_mode_enter, + .mode_leave = _parade_mode_leave, .gui_init = _parade_gui_init, .add_options = NULL, .gui_cleanup = _parade_gui_cleanup