Ansel 0.0
A darktable fork - bloat + design vision
Loading...
Searching...
No Matches
fast_guided_filter.h
Go to the documentation of this file.
1/*
2 This file is part of darktable,
3 Copyright (C) 2019-2020, 2025-2026 Aurélien PIERRE.
4 Copyright (C) 2019-2021 Pascal Obry.
5 Copyright (C) 2020-2021 Ralf Brown.
6 Copyright (C) 2020 rawfiner.
7 Copyright (C) 2020 Roman Lebedev.
8 Copyright (C) 2022 Martin Bařinka.
9 Copyright (C) 2022 Sakari Kapanen.
10 Copyright (C) 2023 Luca Zulberti.
11
12 darktable is free software: you can redistribute it and/or modify
13 it under the terms of the GNU General Public License as published by
14 the Free Software Foundation, either version 3 of the License, or
15 (at your option) any later version.
16
17 darktable is distributed in the hope that it will be useful,
18 but WITHOUT ANY WARRANTY; without even the implied warranty of
19 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 GNU General Public License for more details.
21
22 You should have received a copy of the GNU General Public License
23 along with darktable. If not, see <http://www.gnu.org/licenses/>.
24*/
25
26#ifndef DT_PIXEL_FAST_GUIDED_FILTER_H
27#define DT_PIXEL_FAST_GUIDED_FILTER_H
28
29#include <assert.h>
30#include <math.h>
31#include <stdlib.h>
32#include <stdio.h>
33#include <string.h>
34#include <time.h>
35
36#include "pixel/box_filters.h"
37#include "system/macros.h"
38#include "system/openmp.h"
40#include "system/mem_alloc.h"
42#include "common/imagebuf.h"
44
45#define MIN_FLOAT exp2f(-16.0f)
46
47
53
54
55/***
56 * DOCUMENTATION
57 *
58 * Fast Iterative Guided filter for surface blur
59 *
60 * This is a fast vectorized implementation of guided filter for grey images optimized for
61 * the special case where the guiding and the guided image are the same, which is useful
62 * for edge-aware surface blur.
63 *
64 * Since the guided filter is a linear application, we can safely downscale
65 * the guiding and the guided image by a factor of 4, using a bilinear interpolation,
66 * compute the guidance at this scale, then upscale back to the original size
67 * and get a free 10x speed-up.
68 *
69 * Then, the vectorization adds another substantial speed-up. Overall, it brings a x50 to x200
70 * speed-up compared to the guided_filter.h lib. Of course, it requires every buffer to be
71 * 64-bits aligned.
72 *
73 * On top of the default guided filter, several pre- and post-processing options are provided :
74 *
75 * - mask quantization : perform a posterization of the guiding image in log2 space to
76 * help the guiding to produce smoother areas,
77 *
78 * - blending : perform a regular (linear) blending of a and b parameters after the
79 * variance analysis (aka the by-the-book guided filter), or a geometric mean of the filter output (by-the-book)
80 * and the original image, which produces a pleasing trade-off.
81 *
82 * - iterations : apply the guided filtering recursively, with kernel size increasing by sqrt(2)
83 * between each iteration, to diffuse the filter and soften edges transitions.
84 *
85 * Reference :
86 * Kaiming He, Jian Sun, Microsoft : https://arxiv.org/abs/1505.00996
87 **/
88
89
91static inline float fast_clamp(const float value, const float bottom, const float top)
92{
93 // vectorizable clamping between bottom and top values
94 return fmaxf(fminf(value, top), bottom);
95}
96
97
99static inline void interpolate_bilinear(const float *const restrict in, const size_t width_in, const size_t height_in,
100 float *const restrict out, const size_t width_out, const size_t height_out,
101 const size_t ch)
102{
103 // Fast vectorized bilinear interpolation on ch channels
104 __OMP_PARALLEL_FOR__(collapse(2))
105 for(size_t i = 0; i < height_out; i++)
106 {
107 for(size_t j = 0; j < width_out; j++)
108 {
109 // Relative coordinates of the pixel in output space
110 const float x_out = (float)j /(float)width_out;
111 const float y_out = (float)i /(float)height_out;
112
113 // Corresponding absolute coordinates of the pixel in input space
114 const float x_in = x_out * (float)width_in;
115 const float y_in = y_out * (float)height_in;
116
117 // Nearest neighbours coordinates in input space
118 size_t x_prev = (size_t)floorf(x_in);
119 size_t x_next = x_prev + 1;
120 size_t y_prev = (size_t)floorf(y_in);
121 size_t y_next = y_prev + 1;
122
123 x_prev = (x_prev < width_in) ? x_prev : width_in - 1;
124 x_next = (x_next < width_in) ? x_next : width_in - 1;
125 y_prev = (y_prev < height_in) ? y_prev : height_in - 1;
126 y_next = (y_next < height_in) ? y_next : height_in - 1;
127
128 // Nearest pixels in input array (nodes in grid)
129 const size_t Y_prev = y_prev * width_in;
130 const size_t Y_next = y_next * width_in;
131 const float *const Q_NW = (float *)in + (Y_prev + x_prev) * ch;
132 const float *const Q_NE = (float *)in + (Y_prev + x_next) * ch;
133 const float *const Q_SE = (float *)in + (Y_next + x_next) * ch;
134 const float *const Q_SW = (float *)in + (Y_next + x_prev) * ch;
135
136 // Spatial differences between nodes
137 const float Dy_next = (float)y_next - y_in;
138 const float Dy_prev = 1.f - Dy_next; // because next - prev = 1
139 const float Dx_next = (float)x_next - x_in;
140 const float Dx_prev = 1.f - Dx_next; // because next - prev = 1
141
142 // Interpolate over ch layers
143 float *const pixel_out = (float *)out + (i * width_out + j) * ch;
144
145// //LLVM warns it can't unroll -- presumably because 'ch' is not a constant
146 for(size_t c = 0; c < ch; c++)
147 {
148 pixel_out[c] = Dy_prev * (Q_SW[c] * Dx_next + Q_SE[c] * Dx_prev) +
149 Dy_next * (Q_NW[c] * Dx_next + Q_NE[c] * Dx_prev);
150 }
151 }
152 }
153
154}
155
156
158static inline int variance_analyse(const float *const restrict guide, // I
159 const float *const restrict mask, //p
160 float *const restrict ab,
161 const size_t width, const size_t height,
162 const int radius, const float feathering)
163{
164 // Compute a box average (filter) on a grey image over a window of size 2*radius + 1
165 // then get the variance of the guide and covariance with its mask
166 // output a and b, the linear blending params
167 // p, the mask is the quantised guide I
168
169 const size_t Ndim = width * height;
170 const size_t Ndimch = Ndim * 4;
171
172 /*
173 * input is array of struct : { { guide , mask, guide * guide, guide * mask } }
174 */
175 float *const restrict input = dt_pixelpipe_cache_alloc_align_float_cache(Ndimch, 0);
176 if(IS_NULL_PTR(input)) return 1;
177
178 // Pre-multiply guide and mask and pack all inputs into an array of 4x1 SIMD struct
180 for(size_t k = 0; k < Ndim; k++)
181 {
182 const size_t index = k * 4;
183 input[index] = guide[k];
184 input[index + 1] = mask[k];
185 input[index + 2] = guide[k] * guide[k];
186 input[index + 3] = guide[k] * mask[k];
187 }
188
189 // blur the guide and mask as a four-channel image to exploit data locality and SIMD
190 if(dt_box_mean(input, height, width, 4, radius, 1) != 0)
191 {
193 return 1;
194 }
195
196 // blend the result and store in output buffer
198 for(size_t idx = 0; idx < width*height; idx++)
199 {
200 const float d = fmaxf((input[4*idx+2] - input[4*idx+0] * input[4*idx+0]) + feathering, 1e-15f); // avoid division by 0.
201 const float a = (input[4*idx+3] - input[4*idx+0] * input[4*idx+1]) / d;
202 const float b = input[4*idx+1] - a * input[4*idx+0];
203 ab[2*idx] = a;
204 ab[2*idx+1] = b;
205 }
206
208 return 0;
209}
210
211
213static inline void apply_linear_blending(float *const restrict image,
214 const float *const restrict ab,
215 const size_t num_elem)
216{
217 __OMP_PARALLEL_FOR_SIMD__(aligned(image, ab:64))
218 for(size_t k = 0; k < num_elem; k++)
219 {
220 // Note : image[k] is positive at the outside of the luminance mask
221 image[k] = fmaxf(image[k] * ab[k * 2] + ab[k * 2 + 1], MIN_FLOAT);
222 }
223}
224
225
227static inline void apply_linear_blending_w_geomean(float *const restrict image,
228 const float *const restrict ab,
229 const size_t num_elem)
230{
231 __OMP_PARALLEL_FOR_SIMD__(aligned(image, ab:64))
232 for(size_t k = 0; k < num_elem; k++)
233 {
234 // Note : image[k] is positive at the outside of the luminance mask
235 image[k] = sqrtf(image[k] * fmaxf(image[k] * ab[k * 2] + ab[k * 2 + 1], MIN_FLOAT));
236 }
237}
238
239
241static inline void quantize(const float *const restrict image,
242 float *const restrict out,
243 const size_t num_elem,
244 const float sampling, const float clip_min, const float clip_max)
245{
246 // Quantize in exposure levels evenly spaced in log by sampling
247
248 if(sampling == 0.0f)
249 {
250 // No-op
251 dt_iop_image_copy(out, image, num_elem);
252 }
253 else if(sampling == 1.0f)
254 {
255 // fast track
256 __OMP_PARALLEL_FOR_SIMD__(aligned(image, out:64))
257 for(size_t k = 0; k < num_elem; k++)
258 out[k] = fast_clamp(exp2f(floorf(log2f(image[k]))), clip_min, clip_max);
259 }
260
261 else
262 {
263 // slow track
264 __OMP_PARALLEL_FOR_SIMD__(aligned(image, out:64))
265 for(size_t k = 0; k < num_elem; k++)
266 out[k] = fast_clamp(exp2f(floorf(log2f(image[k]) / sampling) * sampling), clip_min, clip_max);
267 }
268}
269
270
272static inline int fast_surface_blur(float *const restrict image,
273 const size_t width, const size_t height,
274 const int radius, float feathering, const int iterations,
275 const dt_iop_guided_filter_blending_t filter, const float scale,
276 const float quantization, const float quantize_min, const float quantize_max)
277{
278 // Works in-place on a grey image
279
280 // A down-scaling of 4 seems empirically safe and consistent no matter the image zoom level
281 // see reference paper above for proof.
282 const float scaling = 4.0f;
283 const int ds_radius = (radius < 4) ? 1 : radius / scaling;
284
285 const size_t ds_height = height / scaling;
286 const size_t ds_width = width / scaling;
287
288 const size_t num_elem_ds = ds_width * ds_height;
289 const size_t num_elem = width * height;
290
291 float *const restrict ds_image = dt_pixelpipe_cache_alloc_align_float_cache(dt_round_size_sse(num_elem_ds), 0);
292 float *const restrict ds_mask = dt_pixelpipe_cache_alloc_align_float_cache(dt_round_size_sse(num_elem_ds), 0);
293 float *const restrict ds_ab = dt_pixelpipe_cache_alloc_align_float_cache(dt_round_size_sse(num_elem_ds * 2), 0);
294 float *const restrict ab = dt_pixelpipe_cache_alloc_align_float_cache(dt_round_size_sse(num_elem * 2), 0);
295
296 if(IS_NULL_PTR(ds_image) || IS_NULL_PTR(ds_mask) || IS_NULL_PTR(ds_ab) || IS_NULL_PTR(ab))
297 {
298 dt_control_log(_("fast guided filter failed to allocate memory, check your RAM settings"));
303 return 1;
304 }
305
306 // Downsample the image for speed-up
307 interpolate_bilinear(image, width, height, ds_image, ds_width, ds_height, 1);
308
309 // Iterations of filter models the diffusion, sort of
310 for(int i = 0; i < iterations; ++i)
311 {
312 // (Re)build the mask from the quantized image to help guiding
313 quantize(ds_image, ds_mask, ds_width * ds_height, quantization, quantize_min, quantize_max);
314
315 // Perform the patch-wise variance analyse to get
316 // the a and b parameters for the linear blending s.t. mask = a * I + b
317 if(variance_analyse(ds_mask, ds_image, ds_ab, ds_width, ds_height, ds_radius, feathering) != 0)
318 {
323 return 1;
324 }
325
326 // Compute the patch-wise average of parameters a and b
327 if(dt_box_mean(ds_ab, ds_height, ds_width, 2, ds_radius, 1) != 0)
328 {
333 return 1;
334 }
335
336 if(i != iterations - 1)
337 {
338 // Process the intermediate filtered image
339 apply_linear_blending(ds_image, ds_ab, num_elem_ds);
340 }
341 }
342
343 // Upsample the blending parameters a and b
344 interpolate_bilinear(ds_ab, ds_width, ds_height, ab, width, height, 2);
345
346 // Finally, blend the guided image
347 if(filter == DT_GF_BLENDING_LINEAR)
348 apply_linear_blending(image, ab, num_elem);
349 else if(filter == DT_GF_BLENDING_GEOMEAN)
350 apply_linear_blending_w_geomean(image, ab, num_elem);
351
356 return 0;
357}
358
359#endif // DT_PIXEL_FAST_GUIDED_FILTER_H
360
361// clang-format off
362// modelines: These editor modelines have been set for all relevant files by tools/update_modelines.py
363// vim: shiftwidth=2 expandtab tabstop=2 cindent
364// kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified;
365// clang-format on
int dt_box_mean(float *const buf, const size_t height, const size_t width, const int ch, const int radius, const unsigned iterations)
static const float scaling
const dt_colormatrix_t dt_aligned_pixel_t out
const float top
void dt_control_log(const char *msg,...)
Definition control.c:824
static __DT_CLONE_TARGETS__ int variance_analyse(const float *const restrict guide, const float *const restrict mask, float *const restrict ab, const size_t width, const size_t height, const int radius, const float feathering)
static __DT_CLONE_TARGETS__ int fast_surface_blur(float *const restrict image, const size_t width, const size_t height, const int radius, float feathering, const int iterations, const dt_iop_guided_filter_blending_t filter, const float scale, const float quantization, const float quantize_min, const float quantize_max)
dt_iop_guided_filter_blending_t
@ DT_GF_BLENDING_LINEAR
@ DT_GF_BLENDING_GEOMEAN
static __DT_CLONE_TARGETS__ void quantize(const float *const restrict image, float *const restrict out, const size_t num_elem, const float sampling, const float clip_min, const float clip_max)
#define MIN_FLOAT
static __DT_CLONE_TARGETS__ void apply_linear_blending_w_geomean(float *const restrict image, const float *const restrict ab, const size_t num_elem)
static __DT_CLONE_TARGETS__ void apply_linear_blending(float *const restrict image, const float *const restrict ab, const size_t num_elem)
static float fast_clamp(const float value, const float bottom, const float top)
static __DT_CLONE_TARGETS__ void interpolate_bilinear(const float *const restrict in, const size_t width_in, const size_t height_in, float *const restrict out, const size_t width_out, const size_t height_out, const size_t ch)
__DT_CLONE_TARGETS__ void dt_iop_image_copy(float *const __restrict__ out, const float *const __restrict__ in, const size_t nfloats)
Definition imagebuf.c:142
float *const restrict const size_t k
float *const restrict const size_t const size_t ch
#define IS_NULL_PTR(p)
C is way too permissive with !=, == and if(var) checks, which can mean too many things depending on w...
Definition macros.h:96
static size_t dt_round_size_sse(const size_t size)
Round size up to the next multiple of 64.
Definition mem_alloc.h:111
uint32_t width
Definition mipmap_cache.c:0
uint32_t height
Definition mipmap_cache.c:1
#define __OMP_DECLARE_SIMD__(...)
Definition openmp.h:100
#define __OMP_PARALLEL_FOR__(...)
Definition openmp.h:95
#define __OMP_PARALLEL_FOR_SIMD__(...)
Definition openmp.h:96
#define dt_pixelpipe_cache_alloc_align_float_cache(pixels, id)
#define dt_pixelpipe_cache_free_align(mem)
static const dt_aligned_pixel_simd_t value
Definition simd.h:144
#define __DT_CLONE_TARGETS__
Telling the user something happened.