/* This file is part of darktable, Copyright (C) 2013-2020 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 . */ #ifdef HAVE_CONFIG_H #include "config.h" #endif #include "bauhaus/bauhaus.h" #include "common/bilateral.h" #include "common/bilateralcl.h" #include "common/colorspaces.h" #include "common/imagebuf.h" #include "common/opencl.h" #include "common/points.h" #include "control/control.h" #include "develop/develop.h" #include "develop/imageop.h" #include "develop/imageop_math.h" #include "develop/imageop_gui.h" #include "develop/tiling.h" #include "dtgtk/drawingarea.h" #include "dtgtk/resetlabel.h" #include "gui/accelerators.h" #include "gui/gtk.h" #include "iop/iop_api.h" #include #include #include #include #include #include /** * color transfer somewhat based on the glorious paper `color transfer between images' * by erik reinhard, michael ashikhmin, bruce gooch, and peter shirley, 2001. * chosen because it officially cites the playboy. * * workflow: * - open the target image, press acquire button * - right click store as preset * - open image you want to transfer the color to * - right click and apply the preset */ DT_MODULE_INTROSPECTION(1, dt_iop_colormapping_params_t) #define HISTN (1 << 11) #define MAXN 5 typedef float float2[2]; typedef enum dt_iop_colormapping_flags_t { NEUTRAL = 0, HAS_SOURCE = 1 << 0, HAS_TARGET = 1 << 1, HAS_SOURCE_TARGET = HAS_SOURCE | HAS_TARGET, ACQUIRE = 1 << 2, GET_SOURCE = 1 << 3, GET_TARGET = 1 << 4 } dt_iop_colormapping_flags_t; typedef struct dt_iop_colormapping_flowback_t { float hist[HISTN]; // n-means (max 5?) with mean/variance float2 mean[MAXN]; float2 var[MAXN]; float weight[MAXN]; // number of gaussians used. int n; // $MIN: 1 $MAX: 5 $DEFAULT: 1 $DESCRIPTION: "number of clusters" } dt_iop_colormapping_flowback_t; typedef struct dt_iop_colormapping_params_t { dt_iop_colormapping_flags_t flag; // $DEFAULT: NEUTRAL // number of gaussians used. int n; // $MIN: 1 $MAX: 5 $DEFAULT: 3 $DESCRIPTION: "number of clusters" // relative importance of color dominance vs. color proximity float dominance; // $MIN: 0.0 $MAX: 100.0 $DEFAULT: 100.0 $DESCRIPTION: "color dominance" // level of histogram equalization float equalization; // $MIN: 0.0 $MAX: 100.0 $DEFAULT: 50.0 $DESCRIPTION: "histogram equalization" // hist matching table for source image float source_ihist[HISTN]; // n-means (max 5) with mean/variance for source image float2 source_mean[MAXN]; float2 source_var[MAXN]; float source_weight[MAXN]; // hist matching table for destination image int target_hist[HISTN]; // n-means (max 5) with mean/variance for source image float2 target_mean[MAXN]; float2 target_var[MAXN]; float target_weight[MAXN]; } dt_iop_colormapping_params_t; /** and pixelpipe data is just the same */ typedef struct dt_iop_colormapping_params_t dt_iop_colormapping_data_t; typedef struct dt_iop_colormapping_gui_data_t { int flag; float *buffer; int width; int height; int ch; int flowback_set; dt_iop_colormapping_flowback_t flowback; GtkWidget *acquire_source_button; GtkWidget *acquire_target_button; GtkWidget *source_area; GtkWidget *target_area; GtkWidget *clusters; GtkWidget *dominance; GtkWidget *equalization; cmsHTRANSFORM xform; } dt_iop_colormapping_gui_data_t; typedef struct dt_iop_colormapping_global_data_t { int kernel_histogram; int kernel_mapping; } dt_iop_colormapping_global_data_t; const char *name() { return _("color mapping"); } const char *description(struct dt_iop_module_t *self) { return dt_iop_set_description(self, _("transfer a color palette and tonal repartition from one image to another"), _("creative"), _("linear or non-linear, Lab, display-referred"), _("non-linear, Lab"), _("non-linear, Lab, display-referred")); } int default_group() { return IOP_GROUP_EFFECT | IOP_GROUP_EFFECTS; } int flags() { return IOP_FLAGS_ONE_INSTANCE | IOP_FLAGS_SUPPORTS_BLENDING; } int default_colorspace(dt_iop_module_t *self, dt_dev_pixelpipe_t *pipe, dt_dev_pixelpipe_iop_t *piece) { return iop_cs_Lab; } static void capture_histogram(const float *col, const int width, const int height, int *hist) { // build separate histogram memset(hist, 0, sizeof(int) * HISTN); for(int k = 0; k < height; k++) for(int i = 0; i < width; i++) { const int bin = CLAMP(HISTN * col[4 * (k * width + i) + 0] / 100.0, 0, HISTN - 1); hist[bin]++; } // accumulated start distribution of G1 G2 for(int k = 1; k < HISTN; k++) hist[k] += hist[k - 1]; for(int k = 0; k < HISTN; k++) hist[k] = (int)CLAMP(hist[k] * (HISTN / (float)hist[HISTN - 1]), 0, HISTN - 1); // for(int i=0;i<100;i++) printf("#[%d] %d \n", (int)CLAMP(HISTN*i/100.0, 0, HISTN-1), // hist[(int)CLAMP(HISTN*i/100.0, 0, HISTN-1)]); } static void invert_histogram(const int *hist, float *inv_hist) { // invert non-normalised accumulated hist #if 0 int last = 0; for(int i=0; i= i) { last = k; inv_hist[i] = 100.0*k/(float)HISTN; break; } #else int last = 31; for(int i = 0; i <= last; i++) inv_hist[i] = 100.0f * i / (float)HISTN; for(int i = last + 1; i < HISTN; i++) for(int k = last; k < HISTN; k++) if(hist[k] >= i) { last = k; inv_hist[i] = 100.0f * k / (float)HISTN; break; } #endif // printf("inv histogram debug:\n"); // for(int i=0;i<100;i++) printf("%d => %f\n", i, inv_hist[hist[(int)CLAMP(HISTN*i/100.0, 0, HISTN-1)]]); // for(int i=0;i<100;i++) printf("[%d] %f => %f\n", (int)CLAMP(HISTN*i/100.0, 0, HISTN-1), // hist[(int)CLAMP(HISTN*i/100.0, 0, HISTN-1)]/(float)HISTN, inv_hist[(int)CLAMP(HISTN*i/100.0, 0, // HISTN-1)]); } static void get_cluster_mapping(const int n, float2 *mi, const float *wi, float2 *mo, const float *wo, const float dominance, int *mapio) { const float weightscale = 10000.0f; for(int ki = 0; ki < n; ki++) { // for each input cluster float mdist = FLT_MAX; for(int ko = 0; ko < n; ko++) { // find the best target cluster (the same could be used more than once) const float colordist = (mo[ko][0] - mi[ki][0]) * (mo[ko][0] - mi[ki][0]) + (mo[ko][1] - mi[ki][1]) * (mo[ko][1] - mi[ki][1]); const float weightdist = weightscale * (wo[ko] - wi[ki]) * (wo[ko] - wi[ki]); const float dist = colordist * (1.0f - dominance) + weightdist * dominance; if(dist < mdist) { // printf("[%d] => [%d] dominance: %f, colordist: %f, weightdist: %f, dist: %f\n", ki, ko, dominance, // colordist, weightdist, dist); mdist = dist; mapio[ki] = ko; } } } // printf("cluster mapping:\n"); // for(int i=0;i [%d]\n", i, mapio[i]); } // inverse distant weighting according to D. Shepard's method; with power parameter 2.0 static void get_clusters(const float *col, const int n, float2 *mean, float *weight) { float mdist = FLT_MAX; for(int k = 0; k < n; k++) { const float dist2 = (col[1] - mean[k][0]) * (col[1] - mean[k][0]) + (col[2] - mean[k][1]) * (col[2] - mean[k][1]); // dist^2 weight[k] = dist2 > 1.0e-6f ? 1.0f / dist2 : -1.0f; // direct hits marked as -1 if(dist2 < mdist) mdist = dist2; } if(mdist < 1.0e-6f) for(int k = 0; k < n; k++) weight[k] = weight[k] < 0.0f ? 1.0f : 0.0f; // correction in case of direct hits float sum = 0.0f; for(int k = 0; k < n; k++) sum += weight[k]; if(sum > 0.0f) for(int k = 0; k < n; k++) weight[k] /= sum; } static int get_cluster(const float *col, const int n, float2 *mean) { float mdist = FLT_MAX; int cluster = 0; for(int k = 0; k < n; k++) { const float dist = (col[1] - mean[k][0]) * (col[1] - mean[k][0]) + (col[2] - mean[k][1]) * (col[2] - mean[k][1]); if(dist < mdist) { mdist = dist; cluster = k; } } return cluster; } static void kmeans(const float *col, const int width, const int height, const int n, float2 *mean_out, float2 *var_out, float *weight_out) { const int nit = 40; // number of iterations const int samples = width * height * 0.2; // samples: only a fraction of the buffer. float2 *const mean = malloc(sizeof(float2) * n); float2 *const var = malloc(sizeof(float2) * n); int *const cnt = malloc(sizeof(int) * n); int count; float a_min = FLT_MAX, b_min = FLT_MAX, a_max = FLT_MIN, b_max = FLT_MIN; for(int s = 0; s < samples; s++) { const int j = CLAMP(dt_points_get() * height, 0, height - 1); const int i = CLAMP(dt_points_get() * width, 0, width - 1); const float a = col[4 * (width * j + i) + 1]; const float b = col[4 * (width * j + i) + 2]; a_min = fminf(a, a_min); a_max = fmaxf(a, a_max); b_min = fminf(b, b_min); b_max = fmaxf(b, b_max); } // init n clusters for a, b channels at random for(int k = 0; k < n; k++) { mean_out[k][0] = 0.9f * (a_min + (a_max - a_min) * dt_points_get()); mean_out[k][1] = 0.9f * (b_min + (b_max - b_min) * dt_points_get()); var_out[k][0] = var_out[k][1] = weight_out[k] = 0.0f; mean[k][0] = mean[k][1] = var[k][0] = var[k][1] = 0.0f; } for(int it = 0; it < nit; it++) { for(int k = 0; k < n; k++) cnt[k] = 0; // randomly sample col positions inside roi #ifdef _OPENMP #pragma omp parallel for default(none) \ dt_omp_firstprivate(cnt, height, mean, n, samples, var, width) \ shared(col, mean_out) \ schedule(static) #endif for(int s = 0; s < samples; s++) { const int j = CLAMP(dt_points_get() * height, 0, height - 1); const int i = CLAMP(dt_points_get() * width, 0, width - 1); // for each sample: determine cluster, update new mean, update var for(int k = 0; k < n; k++) { const float L = col[4 * (width * j + i)]; const float Lab[3] = { L, col[4 * (width * j + i) + 1], col[4 * (width * j + i) + 2] }; // determine dist to mean_out const int c = get_cluster(Lab, n, mean_out); #ifdef _OPENMP #pragma omp atomic #endif cnt[c]++; // update mean, var #ifdef _OPENMP #pragma omp atomic #endif var[c][0] += Lab[1] * Lab[1]; #ifdef _OPENMP #pragma omp atomic #endif var[c][1] += Lab[2] * Lab[2]; #ifdef _OPENMP #pragma omp atomic #endif mean[c][0] += Lab[1]; #ifdef _OPENMP #pragma omp atomic #endif mean[c][1] += Lab[2]; } } // swap old/new means for(int k = 0; k < n; k++) { if(cnt[k] == 0) continue; mean_out[k][0] = mean[k][0] / cnt[k]; mean_out[k][1] = mean[k][1] / cnt[k]; var_out[k][0] = var[k][0] / cnt[k] - mean_out[k][0] * mean_out[k][0]; var_out[k][1] = var[k][1] / cnt[k] - mean_out[k][1] * mean_out[k][1]; mean[k][0] = mean[k][1] = var[k][0] = var[k][1] = 0.0f; } // determine weight of clusters count = 0; for(int k = 0; k < n; k++) count += cnt[k]; for(int k = 0; k < n; k++) weight_out[k] = (count > 0) ? (float)cnt[k] / count : 0.0f; // printf("it %d %d means:\n", it, n); // for(int k=0;k weight_out[j + 1]) { float2 temp_mean = { mean_out[j + 1][0], mean_out[j + 1][1] }; float2 temp_var = { var_out[j + 1][0], var_out[j + 1][1] }; float temp_weight = weight_out[j + 1]; mean_out[j + 1][0] = mean_out[j][0]; mean_out[j + 1][1] = mean_out[j][1]; var_out[j + 1][0] = var_out[j][0]; var_out[j + 1][1] = var_out[j][1]; weight_out[j + 1] = weight_out[j]; mean_out[j][0] = temp_mean[0]; mean_out[j][1] = temp_mean[1]; var_out[j][0] = temp_var[0]; var_out[j][1] = temp_var[1]; weight_out[j] = temp_weight; } } } } void process(struct 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_colormapping_data_t *const restrict data = (dt_iop_colormapping_data_t *)piece->data; dt_iop_colormapping_gui_data_t *const restrict g = (dt_iop_colormapping_gui_data_t *)self->gui_data; float *const restrict in = (float *)ivoid; float *const restrict out = (float *)ovoid; const int width = roi_in->width; const int height = roi_in->height; if (!dt_iop_have_required_input_format(4 /*we need full-color pixels*/, self, piece->colors, in, out, roi_in, roi_out)) return; // image has been copied through to output and module's trouble flag has been updated const float scale = piece->iscale / roi_in->scale; const float sigma_s = 50.0f / scale; const float sigma_r = 8.0f; // does not depend on scale // save a copy of preview input buffer so we can get histogram and color statistics out of it if(self->dev->gui_attached && g && (piece->pipe->type & DT_DEV_PIXELPIPE_PREVIEW) == DT_DEV_PIXELPIPE_PREVIEW && (data->flag & ACQUIRE)) { dt_iop_gui_enter_critical_section(self); if(g->buffer) dt_free_align(g->buffer); g->buffer = dt_iop_image_alloc(width, height, 4); g->width = width; g->height = height; g->ch = 4; if(g->buffer) dt_iop_image_copy_by_size(g->buffer, in, width, height, 4); dt_iop_gui_leave_critical_section(self); } // process image if all mapping information is present in the parameter set if(data->flag & HAS_TARGET && data->flag & HAS_SOURCE) { // for all pixels: find input cluster, transfer to mapped target cluster and apply histogram const float dominance = data->dominance / 100.0f; const float equalization = data->equalization / 100.0f; // get mapping from input clusters to target clusters int *const mapio = malloc(sizeof(int) * data->n); get_cluster_mapping(data->n, data->target_mean, data->target_weight, data->source_mean, data->source_weight, dominance, mapio); float2 *const var_ratio = malloc(sizeof(float2) * data->n); for(int i = 0; i < data->n; i++) { var_ratio[i][0] = (data->target_var[i][0] > 0.0f) ? data->source_var[mapio[i]][0] / data->target_var[i][0] : 0.0f; var_ratio[i][1] = (data->target_var[i][1] > 0.0f) ? data->source_var[mapio[i]][1] / data->target_var[i][1] : 0.0f; } const size_t npixels = (size_t)height * width; // first get delta L of equalized L minus original image L, scaled to fit into [0 .. 100] #ifdef _OPENMP #pragma omp parallel for default(none) \ dt_omp_firstprivate(npixels) \ dt_omp_sharedconst(in, out, data, equalization) \ schedule(static) #endif for(size_t k = 0; k < npixels * 4; k += 4) { const float L = in[k]; out[k] = 0.5f * ((L * (1.0f - equalization) + data->source_ihist[data->target_hist[(int)CLAMP( HISTN * L / 100.0f, 0.0f, (float)HISTN - 1.0f)]] * equalization) - L) + 50.0f; out[k] = CLAMP(out[k], 0.0f, 100.0f); } if(equalization > 0.001f) { // bilateral blur of delta L to avoid artifacts caused by limited histogram resolution dt_bilateral_t *b = dt_bilateral_init(width, height, sigma_s, sigma_r); if(!b) { free(var_ratio); free(mapio); return; } dt_bilateral_splat(b, out); dt_bilateral_blur(b); dt_bilateral_slice(b, out, out, -1.0f); dt_bilateral_free(b); } size_t allocsize; float *const weight_buf = dt_alloc_perthread(data->n, sizeof(float), &allocsize); #ifdef _OPENMP #pragma omp parallel default(none) \ dt_omp_firstprivate(npixels, mapio, var_ratio, weight_buf, allocsize) \ dt_omp_sharedconst(data, in, out, equalization) #endif { // get a thread-private scratch buffer; do this before the actual loop so we don't have to look it up for // every single pixel float *const restrict weight = dt_get_perthread(weight_buf,allocsize); #ifdef _OPENMP #pragma omp for schedule(static) #endif for(size_t j = 0; j < 4*npixels; j += 4) { const float L = in[j]; const float Lab[3] = { L, in[j + 1], in[j + 2] }; // transfer back scaled and blurred delta L to output L out[j] = 2.0f * (out[j] - 50.0f) + L; out[j] = CLAMP(out[j], 0.0f, 100.0f); get_clusters(in + j, data->n, data->target_mean, weight); // zero the 'a' and 'b' channels out[j + 1] = out[j + 2] = 0.0f; // then accumulate a weighted average for a and b for(int c = 0; c < data->n; c++) { out[j + 1] += weight[c] * ((Lab[1] - data->target_mean[c][0]) * var_ratio[c][0] + data->source_mean[mapio[c]][0]); out[j + 2] += weight[c] * ((Lab[2] - data->target_mean[c][1]) * var_ratio[c][1] + data->source_mean[mapio[c]][1]); } // pass through the alpha channel out[j + 3] = in[j + 3]; } } dt_free_align(weight_buf); free(var_ratio); free(mapio); } // incomplete parameter set -> do nothing else { dt_iop_image_copy_by_size(out, in, width, height, 4); } } #ifdef HAVE_OPENCL int process_cl(struct 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_colormapping_data_t *data = (dt_iop_colormapping_data_t *)piece->data; dt_iop_colormapping_global_data_t *gd = (dt_iop_colormapping_global_data_t *)self->global_data; dt_iop_colormapping_gui_data_t *g = (dt_iop_colormapping_gui_data_t *)self->gui_data; cl_int err = -999; const int devid = piece->pipe->devid; const int width = roi_in->width; const int height = roi_in->height; const int ch = piece->colors; const float scale = piece->iscale / roi_in->scale; const float sigma_s = 50.0f / scale; const float sigma_r = 8.0f; // does not depend on scale float dominance = data->dominance / 100.0f; float equalization = data->equalization / 100.0f; dt_bilateral_cl_t *b = NULL; cl_mem dev_tmp = NULL; cl_mem dev_target_hist = NULL; cl_mem dev_source_ihist = NULL; cl_mem dev_target_mean = NULL; cl_mem dev_source_mean = NULL; cl_mem dev_var_ratio = NULL; cl_mem dev_mapio = NULL; // save a copy of preview input buffer so we can get histogram and color statistics out of it if(self->dev->gui_attached && g && (piece->pipe->type & DT_DEV_PIXELPIPE_PREVIEW) == DT_DEV_PIXELPIPE_PREVIEW && (data->flag & ACQUIRE)) { dt_iop_gui_enter_critical_section(self); dt_free_align(g->buffer); g->buffer = dt_iop_image_alloc(width, height, ch); g->width = width; g->height = height; g->ch = ch; if(g->buffer) err = dt_opencl_copy_device_to_host(devid, g->buffer, dev_in, width, height, ch * sizeof(float)); dt_iop_gui_leave_critical_section(self); if(err != CL_SUCCESS) goto error; } // process image if all mapping information is present in the parameter set if(data->flag & HAS_TARGET && data->flag & HAS_SOURCE) { // get mapping from input clusters to target clusters int mapio[MAXN]; get_cluster_mapping(data->n, data->target_mean, data->target_weight, data->source_mean, data->source_weight, dominance, mapio); float2 var_ratio[MAXN]; for(int i = 0; i < data->n; i++) { var_ratio[i][0] = (data->target_var[i][0] > 0.0f) ? data->source_var[mapio[i]][0] / data->target_var[i][0] : 0.0f; var_ratio[i][1] = (data->target_var[i][1] > 0.0f) ? data->source_var[mapio[i]][1] / data->target_var[i][1] : 0.0f; } dev_tmp = dt_opencl_alloc_device(devid, width, height, sizeof(float) * 4); if(dev_tmp == NULL) goto error; dev_target_hist = dt_opencl_copy_host_to_device_constant(devid, sizeof(int) * HISTN, data->target_hist); if(dev_target_hist == NULL) goto error; dev_source_ihist = dt_opencl_copy_host_to_device_constant(devid, sizeof(float) * HISTN, data->source_ihist); if(dev_source_ihist == NULL) goto error; dev_target_mean = dt_opencl_copy_host_to_device_constant(devid, sizeof(float) * MAXN * 2, data->target_mean); if(dev_target_mean == NULL) goto error; dev_source_mean = dt_opencl_copy_host_to_device_constant(devid, sizeof(float) * MAXN * 2, data->source_mean); if(dev_source_mean == NULL) goto error; dev_var_ratio = dt_opencl_copy_host_to_device_constant(devid, sizeof(float) * MAXN * 2, var_ratio); if(dev_var_ratio == NULL) goto error; dev_mapio = dt_opencl_copy_host_to_device_constant(devid, sizeof(int) * MAXN, mapio); if(dev_mapio == NULL) goto error; size_t sizes[3] = { ROUNDUPWD(width), ROUNDUPHT(height), 1 }; dt_opencl_set_kernel_arg(devid, gd->kernel_histogram, 0, sizeof(cl_mem), (void *)&dev_in); dt_opencl_set_kernel_arg(devid, gd->kernel_histogram, 1, sizeof(cl_mem), (void *)&dev_out); dt_opencl_set_kernel_arg(devid, gd->kernel_histogram, 2, sizeof(int), (void *)&width); dt_opencl_set_kernel_arg(devid, gd->kernel_histogram, 3, sizeof(int), (void *)&height); dt_opencl_set_kernel_arg(devid, gd->kernel_histogram, 4, sizeof(float), (void *)&equalization); dt_opencl_set_kernel_arg(devid, gd->kernel_histogram, 5, sizeof(cl_mem), (void *)&dev_target_hist); dt_opencl_set_kernel_arg(devid, gd->kernel_histogram, 6, sizeof(cl_mem), (void *)&dev_source_ihist); err = dt_opencl_enqueue_kernel_2d(devid, gd->kernel_histogram, sizes); if(err != CL_SUCCESS) goto error; if(equalization > 0.001f) { b = dt_bilateral_init_cl(devid, width, height, sigma_s, sigma_r); if(!b) goto error; err = dt_bilateral_splat_cl(b, dev_out); if(err != CL_SUCCESS) goto error; err = dt_bilateral_blur_cl(b); if(err != CL_SUCCESS) goto error; err = dt_bilateral_slice_cl(b, dev_out, dev_tmp, -1.0f); if(err != CL_SUCCESS) goto error; dt_bilateral_free_cl(b); b = NULL; // make sure we don't clean it up twice } else { size_t origin[] = { 0, 0, 0 }; size_t region[] = { width, height, 1 }; err = dt_opencl_enqueue_copy_image(devid, dev_out, dev_tmp, origin, origin, region); if(err != CL_SUCCESS) goto error; } dt_opencl_set_kernel_arg(devid, gd->kernel_mapping, 0, sizeof(cl_mem), (void *)&dev_in); dt_opencl_set_kernel_arg(devid, gd->kernel_mapping, 1, sizeof(cl_mem), (void *)&dev_tmp); dt_opencl_set_kernel_arg(devid, gd->kernel_mapping, 2, sizeof(cl_mem), (void *)&dev_out); dt_opencl_set_kernel_arg(devid, gd->kernel_mapping, 3, sizeof(int), (void *)&width); dt_opencl_set_kernel_arg(devid, gd->kernel_mapping, 4, sizeof(int), (void *)&height); dt_opencl_set_kernel_arg(devid, gd->kernel_mapping, 5, sizeof(int), (void *)&data->n); dt_opencl_set_kernel_arg(devid, gd->kernel_mapping, 6, sizeof(cl_mem), (void *)&dev_target_mean); dt_opencl_set_kernel_arg(devid, gd->kernel_mapping, 7, sizeof(cl_mem), (void *)&dev_source_mean); dt_opencl_set_kernel_arg(devid, gd->kernel_mapping, 8, sizeof(cl_mem), (void *)&dev_var_ratio); dt_opencl_set_kernel_arg(devid, gd->kernel_mapping, 9, sizeof(cl_mem), (void *)&dev_mapio); err = dt_opencl_enqueue_kernel_2d(devid, gd->kernel_mapping, sizes); if(err != CL_SUCCESS) goto error; dt_opencl_release_mem_object(dev_tmp); dt_opencl_release_mem_object(dev_target_hist); dt_opencl_release_mem_object(dev_source_ihist); dt_opencl_release_mem_object(dev_target_mean); dt_opencl_release_mem_object(dev_source_mean); dt_opencl_release_mem_object(dev_var_ratio); dt_opencl_release_mem_object(dev_mapio); return TRUE; } else { size_t origin[] = { 0, 0, 0 }; size_t region[] = { width, height, 1 }; err = dt_opencl_enqueue_copy_image(devid, dev_in, dev_out, origin, origin, region); if(err != CL_SUCCESS) goto error; return TRUE; } error: if(b != NULL) dt_bilateral_free_cl(b); dt_opencl_release_mem_object(dev_tmp); dt_opencl_release_mem_object(dev_target_hist); dt_opencl_release_mem_object(dev_source_ihist); dt_opencl_release_mem_object(dev_target_mean); dt_opencl_release_mem_object(dev_source_mean); dt_opencl_release_mem_object(dev_var_ratio); dt_opencl_release_mem_object(dev_mapio); dt_print(DT_DEBUG_OPENCL, "[opencl_colormapping] couldn't enqueue kernel! %d\n", err); return FALSE; } #endif void tiling_callback(struct dt_iop_module_t *self, struct dt_dev_pixelpipe_iop_t *piece, const dt_iop_roi_t *roi_in, const dt_iop_roi_t *roi_out, struct dt_develop_tiling_t *tiling) { const float scale = piece->iscale / roi_in->scale; const float sigma_s = 50.0f / scale; const float sigma_r = 8.0f; // does not depend on scale const int width = roi_in->width; const int height = roi_in->height; const int channels = piece->colors; const size_t basebuffer = sizeof(float) * channels * width * height; tiling->factor = 3.0f + (float)dt_bilateral_memory_use(width, height, sigma_s, sigma_r) / basebuffer; tiling->maxbuf = fmaxf(1.0f, (float)dt_bilateral_singlebuffer_size(width, height, sigma_s, sigma_r) / basebuffer); tiling->overhead = 0; tiling->overlap = ceilf(4 * sigma_s); tiling->xalign = 1; tiling->yalign = 1; } void commit_params(struct dt_iop_module_t *self, dt_iop_params_t *p1, dt_dev_pixelpipe_t *pipe, dt_dev_pixelpipe_iop_t *piece) { dt_iop_colormapping_params_t *p = (dt_iop_colormapping_params_t *)p1; dt_iop_colormapping_data_t *d = (dt_iop_colormapping_data_t *)piece->data; memcpy(d, p, sizeof(dt_iop_colormapping_params_t)); #ifdef HAVE_OPENCL if(d->equalization > 0.1f) piece->process_cl_ready = (piece->process_cl_ready && !(darktable.opencl->avoid_atomics)); #endif } void gui_changed(dt_iop_module_t *self, GtkWidget *w, void *previous) { dt_iop_colormapping_params_t *p = (dt_iop_colormapping_params_t *)self->params; dt_iop_colormapping_gui_data_t *g = (dt_iop_colormapping_gui_data_t *)self->gui_data; if(w == g->clusters) { // only reset source/target when changing number of clusters memset(p->source_ihist, 0, sizeof(float) * HISTN); memset(p->source_mean, 0, sizeof(float) * MAXN * 2); memset(p->source_var, 0, sizeof(float) * MAXN * 2); memset(p->source_weight, 0, sizeof(float) * MAXN); memset(p->target_hist, 0, sizeof(int) * HISTN); memset(p->target_mean, 0, sizeof(float) * MAXN * 2); memset(p->target_var, 0, sizeof(float) * MAXN * 2); memset(p->target_weight, 0, sizeof(float) * MAXN); p->flag = NEUTRAL; dt_control_queue_redraw_widget(g->source_area); dt_control_queue_redraw_widget(g->target_area); } } static void acquire_source_button_pressed(GtkButton *button, dt_iop_module_t *self) { if(darktable.gui->reset) return; dt_iop_colormapping_params_t *p = (dt_iop_colormapping_params_t *)self->params; p->flag |= ACQUIRE; p->flag |= GET_SOURCE; p->flag &= ~HAS_SOURCE; dt_iop_request_focus(self); dt_dev_add_history_item(darktable.develop, self, TRUE); } static void acquire_target_button_pressed(GtkButton *button, dt_iop_module_t *self) { if(darktable.gui->reset) return; dt_iop_colormapping_params_t *p = (dt_iop_colormapping_params_t *)self->params; p->flag |= ACQUIRE; p->flag |= GET_TARGET; p->flag &= ~HAS_TARGET; dt_iop_request_focus(self); dt_dev_add_history_item(darktable.develop, self, TRUE); } void init_pipe(struct dt_iop_module_t *self, dt_dev_pixelpipe_t *pipe, dt_dev_pixelpipe_iop_t *piece) { piece->data = malloc(sizeof(dt_iop_colormapping_data_t)); } void cleanup_pipe(struct dt_iop_module_t *self, dt_dev_pixelpipe_t *pipe, dt_dev_pixelpipe_iop_t *piece) { free(piece->data); piece->data = NULL; } void gui_update(struct dt_iop_module_t *self) { dt_iop_colormapping_params_t *p = (dt_iop_colormapping_params_t *)self->params; dt_iop_colormapping_gui_data_t *g = (dt_iop_colormapping_gui_data_t *)self->gui_data; dt_bauhaus_slider_set(g->clusters, p->n); dt_bauhaus_slider_set(g->dominance, p->dominance); dt_bauhaus_slider_set(g->equalization, p->equalization); dt_control_queue_redraw_widget(self->widget); } void init_global(dt_iop_module_so_t *module) { const int program = 8; // extended.cl, from programs.conf dt_iop_colormapping_global_data_t *gd = (dt_iop_colormapping_global_data_t *)malloc(sizeof(dt_iop_colormapping_global_data_t)); module->data = gd; gd->kernel_histogram = dt_opencl_create_kernel(program, "colormapping_histogram"); gd->kernel_mapping = dt_opencl_create_kernel(program, "colormapping_mapping"); } void cleanup_global(dt_iop_module_so_t *module) { dt_iop_colormapping_global_data_t *gd = (dt_iop_colormapping_global_data_t *)module->data; dt_opencl_free_kernel(gd->kernel_histogram); dt_opencl_free_kernel(gd->kernel_mapping); free(module->data); module->data = NULL; } void reload_defaults(dt_iop_module_t *module) { dt_iop_colormapping_params_t *d = module->default_params; dt_iop_colormapping_gui_data_t *g = (dt_iop_colormapping_gui_data_t *)module->gui_data; if(module->dev->gui_attached && g && g->flowback_set) { memcpy(d->source_ihist, g->flowback.hist, sizeof(float) * HISTN); memcpy(d->source_mean, g->flowback.mean, sizeof(float) * MAXN * 2); memcpy(d->source_var, g->flowback.var, sizeof(float) * MAXN * 2); memcpy(d->source_weight, g->flowback.weight, sizeof(float) * MAXN); d->n = g->flowback.n; d->flag = HAS_SOURCE; } } static gboolean cluster_preview_draw(GtkWidget *widget, cairo_t *crf, dt_iop_module_t *self) { dt_iop_colormapping_params_t *p = (dt_iop_colormapping_params_t *)self->params; dt_iop_colormapping_gui_data_t *g = (dt_iop_colormapping_gui_data_t *)self->gui_data; float2 *mean; float2 *var; if(widget == g->source_area) { mean = p->source_mean; var = p->source_var; } else { mean = p->target_mean; var = p->target_var; } GtkAllocation allocation; gtk_widget_get_allocation(widget, &allocation); const int inset = 5; int width = allocation.width, height = allocation.height; cairo_surface_t *cst = dt_cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height); cairo_t *cr = cairo_create(cst); cairo_set_source_rgb(cr, .2, .2, .2); cairo_paint(cr); cairo_translate(cr, inset, inset); width -= 2 * inset; height -= 2 * inset; const float sep = DT_PIXEL_APPLY_DPI(2.0); const float qwd = (width - (p->n - 1) * sep) / (float)p->n; for(int cl = 0; cl < p->n; cl++) { // draw cluster for(int j = -1; j <= 1; j++) for(int i = -1; i <= 1; i++) { // draw 9x9 grid showing mean and variance of this cluster. double rgb[3] = { 0.5, 0.5, 0.5 }; cmsCIELab Lab; Lab.L = 53.390011; Lab.a = (mean[cl][0] + i * var[cl][0]); Lab.b = (mean[cl][1] + j * var[cl][1]); cmsDoTransform(g->xform, &Lab, rgb, 1); cairo_set_source_rgb(cr, rgb[0], rgb[1], rgb[2]); cairo_rectangle(cr, qwd * (i + 1) / 3.0, height * (j + 1) / 3.0, qwd / 3.0 - DT_PIXEL_APPLY_DPI(.5), height / 3.0 - DT_PIXEL_APPLY_DPI(.5)); cairo_fill(cr); } cairo_translate(cr, qwd + sep, 0); } cairo_destroy(cr); cairo_set_source_surface(crf, cst, 0, 0); cairo_paint(crf); cairo_surface_destroy(cst); return TRUE; } static void process_clusters(gpointer instance, gpointer user_data) { dt_iop_module_t *self = (dt_iop_module_t *)user_data; dt_iop_colormapping_params_t *p = (dt_iop_colormapping_params_t *)self->params; dt_iop_colormapping_gui_data_t *g = (dt_iop_colormapping_gui_data_t *)self->gui_data; int new_source_clusters = 0; if(!g || !g->buffer) return; if(!(p->flag & ACQUIRE)) return; ++darktable.gui->reset; dt_iop_gui_enter_critical_section(self); const int width = g->width; const int height = g->height; const int ch = g->ch; float *const restrict buffer = dt_iop_image_alloc(width, height, ch); if(!buffer) { dt_iop_gui_leave_critical_section(self); return; } dt_iop_image_copy_by_size(buffer, g->buffer, width, height, ch); dt_iop_gui_leave_critical_section(self); if(p->flag & GET_SOURCE) { int hist[HISTN]; // get histogram of L capture_histogram(buffer, width, height, hist); // invert histogram invert_histogram(hist, p->source_ihist); // get n color clusters kmeans(buffer, width, height, p->n, p->source_mean, p->source_var, p->source_weight); p->flag |= HAS_SOURCE; new_source_clusters = 1; dt_control_queue_redraw_widget(g->source_area); } else if(p->flag & GET_TARGET) { // get histogram of L capture_histogram(buffer, width, height, p->target_hist); // get n color clusters kmeans(buffer, width, height, p->n, p->target_mean, p->target_var, p->target_weight); p->flag |= HAS_TARGET; dt_control_queue_redraw_widget(g->target_area); } dt_free_align(buffer); if(new_source_clusters) { memcpy(g->flowback.hist, p->source_ihist, sizeof(float) * HISTN); memcpy(g->flowback.mean, p->source_mean, sizeof(float) * MAXN * 2); memcpy(g->flowback.var, p->source_var, sizeof(float) * MAXN * 2); memcpy(g->flowback.weight, p->source_weight, sizeof(float) * MAXN); g->flowback.n = p->n; g->flowback_set = 1; FILE *f = g_fopen("/tmp/dt_colormapping_loaded", "wb"); if(f) { if(fwrite(&g->flowback, sizeof(g->flowback), 1, f) < 1) fprintf(stderr, "[colormapping] could not write flowback file /tmp/dt_colormapping_loaded\n"); fclose(f); } } p->flag &= ~(GET_TARGET | GET_SOURCE | ACQUIRE); --darktable.gui->reset; if(p->flag & HAS_SOURCE) dt_dev_add_history_item(darktable.develop, self, TRUE); dt_control_queue_redraw(); } void gui_init(struct dt_iop_module_t *self) { dt_iop_colormapping_gui_data_t *g = IOP_GUI_ALLOC(colormapping); g->flag = NEUTRAL; g->flowback_set = 0; cmsHPROFILE hsRGB = dt_colorspaces_get_profile(DT_COLORSPACE_SRGB, "", DT_PROFILE_DIRECTION_IN)->profile; cmsHPROFILE hLab = dt_colorspaces_get_profile(DT_COLORSPACE_LAB, "", DT_PROFILE_DIRECTION_ANY)->profile; g->xform = cmsCreateTransform(hLab, TYPE_Lab_DBL, hsRGB, TYPE_RGB_DBL, INTENT_PERCEPTUAL, 0); g->buffer = NULL; self->widget = GTK_WIDGET(gtk_box_new(GTK_ORIENTATION_VERTICAL, DT_BAUHAUS_SPACE)); gtk_box_pack_start(GTK_BOX(self->widget), dt_ui_label_new(_("source clusters:")), TRUE, TRUE, 0); g->source_area = dtgtk_drawing_area_new_with_aspect_ratio(1.0 / 3.0); gtk_box_pack_start(GTK_BOX(self->widget), g->source_area, TRUE, TRUE, 0); g_signal_connect(G_OBJECT(g->source_area), "draw", G_CALLBACK(cluster_preview_draw), self); gtk_box_pack_start(GTK_BOX(self->widget), dt_ui_label_new(_("target clusters:")), TRUE, TRUE, 0); g->target_area = dtgtk_drawing_area_new_with_aspect_ratio(1.0 / 3.0); gtk_box_pack_start(GTK_BOX(self->widget), g->target_area, TRUE, TRUE, 0); g_signal_connect(G_OBJECT(g->target_area), "draw", G_CALLBACK(cluster_preview_draw), self); GtkWidget *box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 5); gtk_box_pack_start(GTK_BOX(self->widget), box, TRUE, TRUE, 0); g->acquire_source_button = dt_iop_button_new(self, N_("acquire as source"), G_CALLBACK(acquire_source_button_pressed), FALSE, 0, 0, NULL, 0, box); gtk_label_set_ellipsize(GTK_LABEL(gtk_bin_get_child(GTK_BIN(g->acquire_source_button))), PANGO_ELLIPSIZE_START); gtk_widget_set_tooltip_text(g->acquire_source_button, _("analyze this image as a source image")); g->acquire_target_button = dt_iop_button_new(self, N_("acquire as target"), G_CALLBACK(acquire_target_button_pressed), FALSE, 0, 0, NULL, 0, box); gtk_label_set_ellipsize(GTK_LABEL(gtk_bin_get_child(GTK_BIN(g->acquire_target_button))), PANGO_ELLIPSIZE_START); gtk_widget_set_tooltip_text(g->acquire_target_button, _("analyze this image as a target image")); g->clusters = dt_bauhaus_slider_from_params(self, "n"); gtk_widget_set_tooltip_text(g->clusters, _("number of clusters to find in image. value change resets all clusters")); g->dominance = dt_bauhaus_slider_from_params(self, "dominance"); gtk_widget_set_tooltip_text(g->dominance, _("how clusters are mapped. low values: based on color " "proximity, high values: based on color dominance")); dt_bauhaus_slider_set_format(g->dominance, "%.02f%%"); g->equalization = dt_bauhaus_slider_from_params(self, "equalization"); gtk_widget_set_tooltip_text(g->equalization, _("level of histogram equalization")); dt_bauhaus_slider_set_format(g->equalization, "%.02f%%"); /* add signal handler for preview pipe finished: process clusters if requested */ DT_DEBUG_CONTROL_SIGNAL_CONNECT(darktable.signals, DT_SIGNAL_DEVELOP_PREVIEW_PIPE_FINISHED, G_CALLBACK(process_clusters), self); FILE *f = g_fopen("/tmp/dt_colormapping_loaded", "rb"); if(f) { if(fread(&g->flowback, sizeof(g->flowback), 1, f) > 0) g->flowback_set = 1; fclose(f); } } void gui_cleanup(struct dt_iop_module_t *self) { dt_iop_colormapping_gui_data_t *g = (dt_iop_colormapping_gui_data_t *)self->gui_data; DT_DEBUG_CONTROL_SIGNAL_DISCONNECT(darktable.signals, G_CALLBACK(process_clusters), self); cmsDeleteTransform(g->xform); dt_free_align(g->buffer); IOP_GUI_FREE; } // modelines: These editor modelines have been set for all relevant files by tools/update_modelines.sh // vim: shiftwidth=2 expandtab tabstop=2 cindent // kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified;