/* This file is part of darktable, Copyright (C) 2020-2026 darktable developers. darktable is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. darktable is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with darktable. If not, see . */ #include "bauhaus/bauhaus.h" #include "common/debug.h" #include "common/gaussian.h" #include "common/opencl.h" #include "common/imagebuf.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/noise_generator.h" #include "develop/tiling.h" #include "gui/accelerators.h" #include "gui/gtk.h" #include "gui/presets.h" #include "iop/iop_api.h" #include #include #include #include #include DT_MODULE_INTROSPECTION(1, dt_iop_censorize_params_t) typedef struct dt_iop_censorize_params_t { float radius_1; // $MIN: 0.0 $MAX: 500.0 $DEFAULT: 0.0 $DESCRIPTION: "input blur radius" float pixelate; // $MIN: 0.0 $MAX: 500.0 $DEFAULT: 0.0 $DESCRIPTION: "pixelization radius" float radius_2; // $MIN: 0.0 $MAX: 500.0 $DEFAULT: 0.0 $DESCRIPTION: "output blur radius" float noise; // $MIN: 0.0 $MAX: 1.0 $DEFAULT: 0.0 $DESCRIPTION: "noise level" } dt_iop_censorize_params_t; typedef struct dt_iop_censorize_gui_data_t { GtkWidget *radius_1; GtkWidget *pixelate; GtkWidget *radius_2; GtkWidget *noise; } dt_iop_censorize_gui_data_t; typedef dt_iop_censorize_params_t dt_iop_censorize_data_t; typedef struct dt_iop_censorize_global_data_t { int kernel_lowpass_mix; } dt_iop_censorize_global_data_t; typedef struct point_t { size_t x; size_t y; } point_t; const char *name() { return _("censorize"); } const char **description(dt_iop_module_t *self) { return dt_iop_set_description(self, _("censorize license plates and body parts for privacy"), _("creative"), _("linear or non-linear, RGB, scene-referred"), _("frequential, RGB"), _("special, RGB, scene-referred")); } int flags() { return IOP_FLAGS_INCLUDE_IN_STYLES | IOP_FLAGS_SUPPORTS_BLENDING; } int default_group() { return IOP_GROUP_EFFECT | IOP_GROUP_EFFECTS; } dt_iop_colorspace_type_t default_colorspace(dt_iop_module_t *self, dt_dev_pixelpipe_t *pipe, dt_dev_pixelpipe_iop_t *piece) { return IOP_CS_RGB; } static inline void make_noise(float *const output, const float noise, const size_t width, const size_t height) { const uint32_t splitmix32_1337 = splitmix32(1337); const uint32_t splitmix32_666 = splitmix32(666); DT_OMP_FOR(collapse(2)) for(size_t i = 0; i < height; i++) for(size_t j = 0; j < width; j++) { // Init random number generator uint32_t DT_ALIGNED_ARRAY state[4] = { splitmix32(j + 1), splitmix32((j + 1) * (i + 3)), splitmix32_1337, splitmix32_666 }; xoshiro128plus(state); xoshiro128plus(state); xoshiro128plus(state); xoshiro128plus(state); const size_t index = (i * width + j) * 4; float *const restrict pix_out = DT_IS_ALIGNED_PIXEL(output + index); const float norm = pix_out[1]; // create statistical noise const float epsilon = gaussian_noise(norm, noise * norm, i % 2 || j % 2, state) / norm; // add noise to output for(size_t c = 0; c < 3; c++) pix_out[c] = fmaxf(pix_out[c] * epsilon, 0.f); } } void process(dt_iop_module_t *self, dt_dev_pixelpipe_iop_t *piece, const void *const ivoid, void *const ovoid, const dt_iop_roi_t *const roi_in, const dt_iop_roi_t *const roi_out) { if(!dt_iop_have_required_input_format(4 /*we need full-color pixels*/, self, piece->colors, ivoid, ovoid, roi_in, roi_out)) // image has been copied through to output and module's trouble flag has been updated return; float *restrict temp; if(!dt_iop_alloc_image_buffers(self, roi_in, roi_out, 4 | DT_IMGSZ_INPUT, &temp, 0, NULL)) { dt_iop_copy_image_roi(ovoid, ivoid, piece->colors, roi_in, roi_out); return; } dt_iop_censorize_data_t *data = piece->data; const float *const restrict in = DT_IS_ALIGNED((const float *const restrict)ivoid); float *const restrict out = DT_IS_ALIGNED((float *const restrict)ovoid); const int width = roi_in->width; const int height = roi_in->height; const int ch = 4; const float sigma_1 = data->radius_1 * roi_in->scale / piece->iscale; const float sigma_2 = data->radius_2 * roi_in->scale / piece->iscale; const size_t pixel_radius = data->pixelate * roi_in->scale / piece->iscale; // used to adjuste blur level depending on size. Don't amplify noise if magnified > 100% const float scale = fmaxf(piece->iscale / roi_in->scale, 1.f); const float noise = data->noise / scale; dt_aligned_pixel_t RGBmax, RGBmin; for(int k = 0; k < 4; k++) { RGBmax[k] = FLT_MAX; RGBmin[k] = 0.f; } const float *restrict input = in; float *restrict output = out; // first blurring step if(sigma_1 != 0.f) { dt_gaussian_t *g = dt_gaussian_init(width, height, ch, RGBmax, RGBmin, sigma_1, 0); if(!g) return; dt_gaussian_blur_4c(g, input, output); dt_gaussian_free(g); input = output; } output = temp; // pixelate if(pixel_radius != 0) { const size_t pixels_x = width / (2 * pixel_radius); const size_t pixels_y = height / (2 * pixel_radius); DT_OMP_FOR(collapse(2)) for(size_t j = 0; j < pixels_y + 1; j++) for(size_t i = 0; i < pixels_x + 1; i++) { // get the top left coordinate of the big pixel const point_t tl = { CLAMP(2 * pixel_radius * i, 0, width - 1), CLAMP(2 * pixel_radius * j, 0, height - 1) }; // get the center of the big pixel const point_t cc = { CLAMP(tl.x + pixel_radius, 0, width - 1), CLAMP(tl.y + pixel_radius, 0, height - 1) }; // get the bottom right coordinate of the big pixel const point_t br = { CLAMP(cc.x + pixel_radius, 0, width - 1), CLAMP(cc.y + pixel_radius, 0, height - 1) }; // get the bounding box + center point coordinates const point_t box[5] = { tl, { br.x, tl.y }, cc, { tl.x, br.y }, br }; // find the average color over the big pixel dt_aligned_pixel_t RGB = { 0.f }; for(size_t k = 0; k < 5; k++) { const float *const restrict pix_in = DT_IS_ALIGNED_PIXEL(input + (width * box[k].y + box[k].x) * 4); for_four_channels(c) RGB[c] += pix_in[c] / 5.f; } // paint the big pixel with solid color == average. Each big pixel // stops short of its bottom right corner, which the next one // paints, except at the image border: there the last big pixel // that has any width covers the last column or row too const size_t end_x = (br.x == width - 1 && tl.x < br.x) ? width : br.x; const size_t end_y = (br.y == height - 1 && tl.y < br.y) ? height : br.y; for(size_t jj = tl.y; jj < end_y; jj++) for(size_t ii = tl.x; ii < end_x; ii++) { float *const restrict pix_out = DT_IS_ALIGNED_PIXEL(output + (jj * width + ii) * 4); for_four_channels(c) pix_out[c] = RGB[c]; } } input = output; } // second blurring step if(sigma_2 != 0.f) { output = out; dt_gaussian_t *g = dt_gaussian_init(width, height, ch, RGBmax, RGBmin, sigma_2, 0); if(!g) return; dt_gaussian_blur_4c(g, input, output); dt_gaussian_free(g); } else { output = out; dt_simd_memcpy(input, output, (size_t)width * height * ch); } if(noise != 0.f) make_noise(output, noise, width, height); dt_free_align(temp); } // OpenCL not implemented yet, but the following only needs a slight modification to get it working #if FALSE int process_cl(dt_iop_module_t *self, dt_dev_pixelpipe_iop_t *piece, cl_mem dev_in, cl_mem dev_out, const dt_iop_roi_t *const roi_in, const dt_iop_roi_t *const roi_out) { dt_iop_censorize_data_t *d = piece->data; dt_iop_censorize_global_data_t *gd = self->global_data; cl_int err = DT_OPENCL_DEFAULT_ERROR; const int devid = piece->pipe->devid; const int width = roi_in->width; const int height = roi_in->height; const int channels = piece->colors; const float radius_1 = fmax(0.1f, d->radius_1); const float sigma = radius_1 * roi_in->scale / piece->iscale; const float saturation = d->saturation; const int order = d->order; const int unbound = d->unbound; cl_mem dev_cm = NULL; cl_mem dev_ccoeffs = NULL; cl_mem dev_lm = NULL; cl_mem dev_lcoeffs = NULL; cl_mem dev_tmp = NULL; dt_gaussian_cl_t *g = NULL; dt_bilateral_cl_t *b = NULL; float RGBmax[] = { 100.0f, 128.0f, 128.0f, 1.0f }; float RGBmin[] = { 0.0f, -128.0f, -128.0f, 0.0f }; if(unbound) { for(int k = 0; k < 4; k++) RGBmax[k] = FLT_MAX; for(int k = 0; k < 4; k++) RGBmin[k] = -FLT_MAX; } if(d->lowpass_algo == LOWPASS_ALGO_GAUSSIAN) { g = dt_gaussian_init_cl(devid, width, height, channels, RGBmax, RGBmin, sigma, order); if(!g) goto error; err = dt_gaussian_blur_cl(g, dev_in, dev_out); if(err != CL_SUCCESS) goto error; dt_gaussian_free_cl(g); g = NULL; } else { const float sigma_r = 100.0f; // does not depend on scale const float sigma_s = sigma; const float detail = -1.0f; // we want the bilateral base layer b = dt_bilateral_init_cl(devid, width, height, sigma_s, sigma_r); if(!b) goto error; err = dt_bilateral_splat_cl(b, dev_in); 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_in, dev_out, detail); if(err != CL_SUCCESS) goto error; dt_bilateral_free_cl(b); b = NULL; // make sure we don't clean it up twice } err = CL_MEM_OBJECT_ALLOCATION_FAILURE; dev_tmp = dt_opencl_duplicate_image(devid, dev_out); if(dev_tmp == NULL) goto error; dev_cm = dt_opencl_copy_host_to_image(devid, d->ctable, 256, 256, sizeof(float)); if(dev_cm == NULL) goto error; dev_ccoeffs = dt_opencl_copy_host_to_device_constant(devid, sizeof(float) * 3, d->cunbounded_coeffs); if(dev_ccoeffs == NULL) goto error; dev_lm = dt_opencl_copy_host_to_image(devid, d->ltable, 256, 256, sizeof(float)); if(dev_lm == NULL) goto error; dev_lcoeffs = dt_opencl_copy_host_to_device_constant(devid, sizeof(float) * 3, d->lunbounded_coeffs); if(dev_lcoeffs == NULL) goto error; err = dt_opencl_enqueue_kernel_2d_args(devid, gd->kernel_lowpass_mix, width, height, CLARG(dev_tmp), CLARG(dev_out), CLARG(width), CLARG(height), CLARG(saturation), CLARG(dev_cm), CLARG(dev_ccoeffs), CLARG(dev_lm), CLARG(dev_lcoeffs), CLARG(unbound)); error: if(g) dt_gaussian_free_cl(g); if(b) dt_bilateral_free_cl(b); dt_opencl_release_mem_object(dev_tmp); dt_opencl_release_mem_object(dev_lcoeffs); dt_opencl_release_mem_object(dev_lm); dt_opencl_release_mem_object(dev_ccoeffs); dt_opencl_release_mem_object(dev_cm); return err; } void tiling_callback(dt_iop_module_t *self, dt_dev_pixelpipe_iop_t *piece, const dt_iop_roi_t *roi_in, const dt_iop_roi_t *roi_out, dt_develop_tiling_t *tiling) { tiling->factor = 3.0f; tiling->factor_cl = 5.0f; tiling->maxbuf = 1.0f; tiling->maxbuf_cl = 1.0f; tiling->overhead = 0; tiling->overlap = 0; tiling->align = 1; } void init_global(dt_iop_module_so_t *self) { const int program = 6; // gaussian.cl, from programs.conf dt_iop_censorize_global_data_t *gd = malloc(sizeof(dt_iop_censorize_global_data_t)); self->data = gd; gd->kernel_lowpass_mix = dt_opencl_create_kernel(program, "lowpass_mix"); } void cleanup_global(dt_iop_module_so_t *self) { dt_iop_censorize_global_data_t *gd = self->data; dt_opencl_free_kernel(gd->kernel_lowpass_mix); free(self->data); self->data = NULL; } #endif void gui_init(dt_iop_module_t *self) { dt_iop_censorize_gui_data_t *g = IOP_GUI_ALLOC(censorize); g->radius_1 = dt_bauhaus_slider_from_params(self, "radius_1"); g->pixelate = dt_bauhaus_slider_from_params(self, "pixelate"); g->radius_2 = dt_bauhaus_slider_from_params(self, "radius_2"); g->noise = dt_bauhaus_slider_from_params(self, "noise"); gtk_widget_set_tooltip_text(g->radius_1, _("radius of gaussian blur before pixelization")); gtk_widget_set_tooltip_text(g->radius_2, _("radius of gaussian blur after pixelization")); gtk_widget_set_tooltip_text(g->pixelate, _("radius of the intermediate pixelization")); gtk_widget_set_tooltip_text(g->noise, _("amount of noise to add at the end")); } // clang-format off // modelines: These editor modelines have been set for all relevant files by tools/update_modelines.py // vim: shiftwidth=2 expandtab tabstop=2 cindent // kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified; // clang-format on