Ansel 0.0
A darktable fork - bloat + design vision
Loading...
Searching...
No Matches
blur.c
Go to the documentation of this file.
1/*
2 This file is part of Ansel,
3 Copyright (C) 2026 Aurélien PIERRE.
4
5 Ansel is free software: you can redistribute it and/or modify
6 it under the terms of the GNU General Public License as published by
7 the Free Software Foundation, either version 3 of the License, or
8 (at your option) any later version.
9
10 Ansel is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU General Public License for more details.
14
15 You should have received a copy of the GNU General Public License
16 along with darktable. If not, see <http://www.gnu.org/licenses/>.
17 */
18
19// Gaussian blur helpers + per-region blur, and the OpenCL blur / device-timing runtime prelude. (implementation;
20// see blur.h for the public API.)
21
22#include "iop/highlights/blur.h"
24
25static __thread _hl_gauss_slot_t _hl_gauss_cache[HL_GAUSS_SLOTS] = { { 0 } };
26static __thread int _hl_gauss_rr = 0;
27
28dt_gaussian_t *_hl_gauss_get(const int width, const int height, const int channels, const float sigma)
29{
30 for(int i = 0; i < HL_GAUSS_SLOTS; i++)
32 && _hl_gauss_cache[i].channels == channels && _hl_gauss_cache[i].sigma == sigma)
34
36 if(slot->gaussian) dt_gaussian_free(slot->gaussian);
37 float vmax[4] = { 1e9f, 1e9f, 1e9f, 1e9f };
38 float vmin[4] = { -1e9f, -1e9f, -1e9f, -1e9f };
39 slot->gaussian = dt_gaussian_init(width, height, channels, vmax, vmin, sigma, 0);
40 slot->width = width;
41 slot->height = height;
42 slot->channels = channels;
43 slot->sigma = sigma;
44 return slot->gaussian;
45}
46
55
56// and main pipes can run _region_guided_filter concurrently; each accumulates on its own thread.
57
58// ============================ OpenCL ============================
59
60#ifdef HAVE_OPENCL
61// host spends BLOCKED on the device (reads, finishes) vs pure enqueue counts, plus the
62// host-side sparse Cholesky work. Accumulated per thread, reset at the middle's entry,
63// printed with the "gpu middle" line.
64#endif // HAVE_OPENCL
65
66#ifdef HAVE_OPENCL
67dt_gaussian_cl_t *_region_blur_handle(const int devid, const int region_w, const int region_h, const float sigma)
68{
69 const float vmax[4] = { 1e9f, 1e9f, 1e9f, 1e9f };
70 const float vmin[4] = { -1e9f, -1e9f, -1e9f, -1e9f };
71 return dt_gaussian_init_cl(devid, region_w, region_h, 4, vmax, vmin, sigma, 0);
72}
73
74cl_int _region_blur_cl(const int devid, cl_mem in, cl_mem out, const int region_w, const int region_h,
75 const float sigma)
76{
77 dt_gaussian_cl_t *gaussian = _region_blur_handle(devid, region_w, region_h, sigma);
79 const cl_int cl_err = dt_gaussian_blur_cl(gaussian, in, out);
81 return cl_err;
82}
83
84#endif // HAVE_OPENCL
85
86// ===== edge-aware transport of the fit windows (domain transform) =============================
87// The coefficient field fits a colour line inside a window of sigma = radius/6 (40 px on the
88// DSC_1267.NEF ridge, so a +/-120 px reach). An isotropic gaussian carries samples straight across
89// a silhouette, so at a pixel 10 px from a mountain edge the "local" colour line is a mixture of
90// sky and rock, and the predicted channel is biased -- measured as a ring of +6% magenta peaking
91// 8-12 px from the edge, gone by 30 px. Shrinking the window removes the ring but costs a third of
92// the reconstruction strength everywhere, and no PER-SAMPLE photometric test can separate the two
93// materials here: they differ by 0.0105 in chromaticity while the sky's own spread is 0.0229, and
94// their luminances overlap. What does separate them is the EDGE between them, which is exactly what
95// a domain transform keys on.
96//
97// Gastal & Oliveira 2011, "Domain Transform for Edge-Aware Image and Video Processing", recursive
98// (RF) variant: warp each row/column so that distance grows with the guide's gradient, then run a
99// first-order IIR in the warped domain. Three iterations with halving sigma approximate a gaussian
100// while refusing to transport across an edge. O(pixels) per pass, like the Young-van-Vliet gaussian
101// it replaces, and normalized by construction (unit DC gain) so the moments stay consistent with
102// the mass plane they are divided by.
103void _region_edge_blur(const float *const restrict in, float *const restrict out,
104 const float *const restrict guide, float *const restrict step_x,
105 float *const restrict step_y, const int region_w, const int region_h,
106 const float sigma_s, const float sigma_r)
107{
108 const size_t region_pixels = (size_t)region_w * region_h;
109 memcpy(out, in, region_pixels * 4 * sizeof(float));
110 if(!(sigma_s > 0.f) || !(sigma_r > 0.f)) return;
111
112 // domain transform: the local warp rate, 1 where the guide is flat, large across an edge
113 const float range_scale = sigma_s / sigma_r;
114 HL_PFOR()
115 for(int y = 0; y < region_h; y++)
116 for(int x = 0; x < region_w; x++)
117 {
118 const size_t i = (size_t)y * region_w + x;
119 const float dx = (x > 0) ? fabsf(guide[i] - guide[i - 1]) : 0.f;
120 const float dy = (y > 0) ? fabsf(guide[i] - guide[i - region_w]) : 0.f;
121 step_x[i] = 1.f + range_scale * dx;
122 step_y[i] = 1.f + range_scale * dy;
123 }
124
125 const int n_iterations = 3;
126 for(int iteration = 0; iteration < n_iterations; iteration++)
127 {
128 // sigma of this iteration: halving, so the three together approximate one gaussian of sigma_s
129 const float sigma_i = sigma_s * sqrtf(3.f) * (float)(1 << (n_iterations - 1 - iteration))
130 / sqrtf((float)((1 << (2 * n_iterations)) - 1));
131 const float feedback = expf(-sqrtf(2.f) / fmaxf(sigma_i, 1e-6f));
132
133 HL_PFOR() // rows: forward then backward, in the warped domain
134 for(int y = 0; y < region_h; y++)
135 {
136 float *const restrict row = out + (size_t)y * region_w * 4;
137 const float *const restrict step = step_x + (size_t)y * region_w;
138 for(int x = 1; x < region_w; x++)
139 {
140 const float a = powf(feedback, step[x]);
141 for(int c = 0; c < 4; c++) row[x * 4 + c] += a * (row[(x - 1) * 4 + c] - row[x * 4 + c]);
142 }
143 for(int x = region_w - 2; x >= 0; x--)
144 {
145 const float a = powf(feedback, step[x + 1]);
146 for(int c = 0; c < 4; c++) row[x * 4 + c] += a * (row[(x + 1) * 4 + c] - row[x * 4 + c]);
147 }
148 }
149
150 HL_PFOR() // columns: same, down then up
151 for(int x = 0; x < region_w; x++)
152 {
153 for(int y = 1; y < region_h; y++)
154 {
155 const size_t i = (size_t)y * region_w + x;
156 const float a = powf(feedback, step_y[i]);
157 for(int c = 0; c < 4; c++)
158 out[i * 4 + c] += a * (out[(i - region_w) * 4 + c] - out[i * 4 + c]);
159 }
160 for(int y = region_h - 2; y >= 0; y--)
161 {
162 const size_t i = (size_t)y * region_w + x;
163 const float a = powf(feedback, step_y[i + region_w]);
164 for(int c = 0; c < 4; c++)
165 out[i * 4 + c] += a * (out[(i + region_w) * 4 + c] - out[i * 4 + c]);
166 }
167 }
168 }
169}
170
171#ifdef HAVE_OPENCL
172// Device twin of _region_edge_blur. The moment planes are images, but a recursive sweep must read
173// and write the same line, which an OpenCL 1.2 image cannot do, so the filter runs on a buffer:
174// image -> buffer, warp + three iterations of row/column sweeps, buffer -> image. `guide` is the
175// already-smoothed single-channel guide buffer (the caller smooths it once per region, not once
176// per moment plane). All scratch is supplied by the caller so nothing allocates per call.
177cl_int _region_edge_blur_cl(const int devid, void *gd_void, cl_mem in_image, cl_mem out_image,
178 cl_mem guide, cl_mem data, cl_mem step_x, cl_mem step_y, const int region_w,
179 const int region_h, const float sigma_s, const float sigma_r)
180{
182 const size_t origin[] = { 0, 0, 0 };
183 const size_t region[] = { (size_t)region_w, (size_t)region_h, 1 };
184 size_t size_2d[3] = { ROUNDUPDWD(region_w, devid), ROUNDUPDHT(region_h, devid), 1 };
185
186 cl_int cl_err = dt_opencl_enqueue_copy_image_to_buffer(devid, in_image, data, (size_t *)origin,
187 (size_t *)region, 0);
188 if(cl_err != CL_SUCCESS) return cl_err;
189 if(!(sigma_s > 0.f) || !(sigma_r > 0.f))
190 return dt_opencl_enqueue_copy_buffer_to_image(devid, data, out_image, 0, (size_t *)origin,
191 (size_t *)region);
192
193 const float range_scale = sigma_s / sigma_r;
194 {
195 const int kernel = global_data->kernel_hl_dt_warp;
196 dt_opencl_set_kernel_arg(devid, kernel, 0, sizeof(cl_mem), &guide);
197 dt_opencl_set_kernel_arg(devid, kernel, 1, sizeof(cl_mem), &step_x);
198 dt_opencl_set_kernel_arg(devid, kernel, 2, sizeof(cl_mem), &step_y);
199 dt_opencl_set_kernel_arg(devid, kernel, 3, sizeof(int), &region_w);
200 dt_opencl_set_kernel_arg(devid, kernel, 4, sizeof(int), &region_h);
201 dt_opencl_set_kernel_arg(devid, kernel, 5, sizeof(float), &range_scale);
202 cl_err = dt_opencl_enqueue_kernel_2d(devid, kernel, size_2d);
203 if(cl_err != CL_SUCCESS) return cl_err;
204 }
205
206 const int n_iterations = 3;
207 for(int iteration = 0; iteration < n_iterations && cl_err == CL_SUCCESS; iteration++)
208 {
209 const float sigma_i = sigma_s * sqrtf(3.f) * (float)(1 << (n_iterations - 1 - iteration))
210 / sqrtf((float)((1 << (2 * n_iterations)) - 1));
211 const float feedback = expf(-sqrtf(2.f) / fmaxf(sigma_i, 1e-6f));
212
213 { // one work-item per row
214 const int kernel = global_data->kernel_hl_dt_rows;
215 size_t size_rows[3] = { ROUNDUP(region_h, 64), 1, 1 };
216 dt_opencl_set_kernel_arg(devid, kernel, 0, sizeof(cl_mem), &data);
217 dt_opencl_set_kernel_arg(devid, kernel, 1, sizeof(cl_mem), &step_x);
218 dt_opencl_set_kernel_arg(devid, kernel, 2, sizeof(int), &region_w);
219 dt_opencl_set_kernel_arg(devid, kernel, 3, sizeof(int), &region_h);
220 dt_opencl_set_kernel_arg(devid, kernel, 4, sizeof(float), &feedback);
221 cl_err = dt_opencl_enqueue_kernel_2d(devid, kernel, size_rows);
222 }
223 if(cl_err != CL_SUCCESS) break;
224 { // one work-item per column
225 const int kernel = global_data->kernel_hl_dt_cols;
226 size_t size_cols[3] = { ROUNDUP(region_w, 64), 1, 1 };
227 dt_opencl_set_kernel_arg(devid, kernel, 0, sizeof(cl_mem), &data);
228 dt_opencl_set_kernel_arg(devid, kernel, 1, sizeof(cl_mem), &step_y);
229 dt_opencl_set_kernel_arg(devid, kernel, 2, sizeof(int), &region_w);
230 dt_opencl_set_kernel_arg(devid, kernel, 3, sizeof(int), &region_h);
231 dt_opencl_set_kernel_arg(devid, kernel, 4, sizeof(float), &feedback);
232 cl_err = dt_opencl_enqueue_kernel_2d(devid, kernel, size_cols);
233 }
234 }
235 if(cl_err != CL_SUCCESS) return cl_err;
236
237 return dt_opencl_enqueue_copy_buffer_to_image(devid, data, out_image, 0, (size_t *)origin,
238 (size_t *)region);
239}
240#endif
float sigma_s
Definition bilateral.h:3
float sigma_r
Definition bilateral.h:3
void _region_edge_blur(const float *const restrict in, float *const restrict out, const float *const restrict guide, float *const restrict step_x, float *const restrict step_y, const int region_w, const int region_h, const float sigma_s, const float sigma_r)
Definition blur.c:103
static __thread _hl_gauss_slot_t _hl_gauss_cache[HL_GAUSS_SLOTS]
Definition blur.c:25
cl_int _region_edge_blur_cl(const int devid, void *gd_void, cl_mem in_image, cl_mem out_image, cl_mem guide, cl_mem data, cl_mem step_x, cl_mem step_y, const int region_w, const int region_h, const float sigma_s, const float sigma_r)
Definition blur.c:177
dt_gaussian_t * _hl_gauss_get(const int width, const int height, const int channels, const float sigma)
Definition blur.c:28
static __thread int _hl_gauss_rr
Definition blur.c:26
void _hl_gauss_cache_flush(void)
Definition blur.c:47
cl_int _region_blur_cl(const int devid, cl_mem in, cl_mem out, const int region_w, const int region_h, const float sigma)
Definition blur.c:74
dt_gaussian_cl_t * _region_blur_handle(const int devid, const int region_w, const int region_h, const float sigma)
Definition blur.c:67
static const float x
const dt_colormatrix_t dt_aligned_pixel_t out
static const int row
static float gaussian(float x, float std)
Definition filmic.c:404
void dt_gaussian_free(dt_gaussian_t *g)
Definition gaussian.c:335
void dt_gaussian_free_cl(dt_gaussian_cl_t *g)
Definition gaussian.c:365
cl_int dt_gaussian_blur_cl(dt_gaussian_cl_t *g, cl_mem dev_in, cl_mem dev_out)
Definition gaussian.c:453
dt_gaussian_cl_t * dt_gaussian_init_cl(const int devid, const int width, const int height, const int channels, const float *max, const float *min, const float sigma, const int order)
Definition gaussian.c:376
dt_gaussian_t * dt_gaussian_init(const int width, const int height, const int channels, const float *max, const float *min, const float sigma, const int order)
Definition gaussian.c:127
static float kernel(const float *x, const float *y)
uint32_t width
Definition mipmap_cache.c:0
uint32_t height
Definition mipmap_cache.c:1
int dt_opencl_enqueue_kernel_2d(const int dev, const int kernel, const size_t *sizes)
Definition opencl.c:2554
int dt_opencl_enqueue_copy_buffer_to_image(const int devid, cl_mem src_buffer, cl_mem dst_image, size_t offset, size_t *origin, size_t *region)
Definition opencl.c:2702
int dt_opencl_set_kernel_arg(const int dev, const int kernel, const int num, const size_t size, const void *arg)
Definition opencl.c:2545
int dt_opencl_enqueue_copy_image_to_buffer(const int devid, cl_mem src_image, cl_mem dst_buffer, size_t *origin, size_t *region, size_t offset)
Definition opencl.c:2690
#define DT_OPENCL_DEFAULT_ERROR
Definition opencl.h:61
#define ROUNDUP(a, n)
Definition opencl.h:82
#define ROUNDUPDHT(a, b)
Definition opencl.h:86
#define ROUNDUPDWD(a, b)
Definition opencl.h:85
#define HL_PFOR(...)
#define HL_GAUSS_SLOTS
const float sigma