Ansel 0.0
A darktable fork - bloat + design vision
Loading...
Searching...
No Matches
eigf.h
Go to the documentation of this file.
1/*
2 This file is part of darktable,
3 Copyright (C) 2020 rawfiner.
4 Copyright (C) 2021 Ralf Brown.
5 Copyright (C) 2022 Martin Bařinka.
6 Copyright (C) 2025-2026 Aurélien PIERRE.
7
8 darktable is free software: you can redistribute it and/or modify
9 it under the terms of the GNU General Public License as published by
10 the Free Software Foundation, either version 3 of the License, or
11 (at your option) any later version.
12
13 darktable is distributed in the hope that it will be useful,
14 but WITHOUT ANY WARRANTY; without even the implied warranty of
15 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 GNU General Public License for more details.
17
18 You should have received a copy of the GNU General Public License
19 along with darktable. If not, see <http://www.gnu.org/licenses/>.
20*/
21
22#pragma once
23
25#include "common/gaussian.h"
26
27/***
28 * DOCUMENTATION
29 *
30 * Exposure-Independent Guided Filter (EIGF)
31 *
32 * This filter is a modification of guided filter to make it exposure independent
33 * As variance depends on the exposure, the original guided filter preserves
34 * much better the edges in the highlights than in the shadows.
35 * In particular doing:
36 * (1) increase exposure by 1EV
37 * (2) guided filtering
38 * (3) decrease exposure by 1EV
39 * is NOT equivalent to doing the guided filtering only.
40 *
41 * To overcome this, instead of using variance directly to determine "a",
42 * we use a ratio:
43 * variance / (pixel_value)^2
44 * we tried also the following ratios:
45 * - variance / average^2
46 * - variance / (pixel_value * average)
47 * we kept variance / (pixel_value)^2 as it seemed to behave a bit better than
48 * the other (dividing by average^2 smoothed too much dark details surrounded
49 * by bright pixels).
50 *
51 * This modification makes the filter exposure-independent.
52 * However, due to the fact that the average advantages the bright pixels
53 * compared to dark pixels if we consider that human eye sees in log,
54 * we get strong bright halos.
55 * These are due to the spatial averaging of "a" and "b" that is performed at
56 * the end of the filter, especially due to the spatial averaging of "b".
57 * We decided to remove this final spatial averaging, as it is very hard
58 * to keep it without having either large unsmoothed regions or halos.
59 * Although the filter may blur a bit less without it, it remains sufficiently
60 * good at smoothing the image, and there are much less halos.
61 *
62 * The implementation EIGF uses downscaling to speed-up the filtering,
63 * just like what is done in fast_guided_filter.h
64**/
65
66/* computes average and variance of guide and mask, and put them in out.
67 * out has 4 channels:
68 * - average of guide
69 * - variance of guide
70 * - average of mask
71 * - covariance of mask and guide. */
73static inline int eigf_variance_analysis(const float *const restrict guide, // I
74 const float *const restrict mask, //p
75 float *const restrict out,
76 const size_t width, const size_t height,
77 const float sigma)
78{
79 // We also use gaussian blurs instead of the square blurs of the guided filter
80 const size_t Ndim = width * height;
81 float *const restrict in = dt_pixelpipe_cache_alloc_align_float_cache(Ndim * 4, 0);
82 dt_gaussian_t *g = NULL;
83 int err = 0;
84 if(in == NULL)
85 {
86 err = 1;
87 goto error;
88 }
89
90 float ming = 10000000.0f;
91 float maxg = 0.0f;
92 float minm = 10000000.0f;
93 float maxm = 0.0f;
94 float ming2 = 10000000.0f;
95 float maxg2 = 0.0f;
96 float minmg = 10000000.0f;
97 float maxmg = 0.0f;
98#ifdef _OPENMP
99#pragma omp parallel for default(none) \
100dt_omp_firstprivate(guide, mask, in, Ndim) \
101 schedule(simd:static) \
102 reduction(max:maxg, maxm, maxg2, maxmg)\
103 reduction(min:ming, minm, ming2, minmg)
104#endif
105 for(size_t k = 0; k < Ndim; k++)
106 {
107 const float pixelg = guide[k];
108 const float pixelm = mask[k];
109 const float pixelg2 = pixelg * pixelg;
110 const float pixelmg = pixelm * pixelg;
111 in[k * 4] = pixelg;
112 in[k * 4 + 1] = pixelg2;
113 in[k * 4 + 2] = pixelm;
114 in[k * 4 + 3] = pixelmg;
115 ming = MIN(ming,pixelg);
116 maxg = MAX(maxg,pixelg);
117 minm = MIN(minm,pixelm);
118 maxm = MAX(maxm,pixelm);
119 ming2 = MIN(ming2,pixelg2);
120 maxg2 = MAX(maxg2,pixelg2);
121 minmg = MIN(minmg,pixelmg);
122 maxmg = MAX(maxmg,pixelmg);
123 }
124
125 dt_aligned_pixel_t max = {maxg, maxg2, maxm, maxmg};
126 dt_aligned_pixel_t min = {ming, ming2, minm, minmg};
127 g = dt_gaussian_init(width, height, 4, max, min, sigma, 0);
128 if(g == NULL)
129 {
130 err = 1;
131 goto error;
132 }
134
135#ifdef _OPENMP
136#pragma omp parallel for simd default(none) \
137dt_omp_firstprivate(out, Ndim) \
138 schedule(simd:static) aligned(out:64)
139#endif
140 for(size_t k = 0; k < Ndim; k++)
141 {
142 out[4 * k + 1] -= out[4 * k] * out[4 * k];
143 out[4 * k + 3] -= out[4 * k] * out[4 * k + 2];
144 }
145
146error:
147 if(g) dt_gaussian_free(g);
149 return err;
150}
151
152// same function as above, but specialized for the case where guide == mask
153// for increased performance
155static inline int eigf_variance_analysis_no_mask(const float *const restrict guide, // I
156 float *const restrict out,
157 const size_t width, const size_t height,
158 const float sigma)
159{
160 // We also use gaussian blurs instead of the square blurs of the guided filter
161 const size_t Ndim = width * height;
162 float *const restrict in = dt_pixelpipe_cache_alloc_align_float_cache(Ndim * 2, 0);
163 dt_gaussian_t *g = NULL;
164 int err = 0;
165 if(in == NULL)
166 {
167 err = 1;
168 goto error;
169 }
170
171 float ming = 10000000.0f;
172 float maxg = 0.0f;
173 float ming2 = 10000000.0f;
174 float maxg2 = 0.0f;
175#ifdef _OPENMP
176#pragma omp parallel for default(none) \
177dt_omp_firstprivate(guide, in, Ndim) \
178 schedule(simd:static) \
179 reduction(max:maxg, maxg2)\
180 reduction(min:ming, ming2)
181#endif
182 for(size_t k = 0; k < Ndim; k++)
183 {
184 const float pixelg = guide[k];
185 const float pixelg2 = pixelg * pixelg;
186 in[2 * k] = pixelg;
187 in[2 * k + 1] = pixelg2;
188 ming = MIN(ming,pixelg);
189 maxg = MAX(maxg,pixelg);
190 ming2 = MIN(ming2,pixelg2);
191 maxg2 = MAX(maxg2,pixelg2);
192 }
193
194 float max[2] = {maxg, maxg2};
195 float min[2] = {ming, ming2};
196 g = dt_gaussian_init(width, height, 2, max, min, sigma, 0);
197 if(!g)
198 {
199 err = 1;
200 goto error;
201 }
202 dt_gaussian_blur(g, in, out);
203
204#ifdef _OPENMP
205#pragma omp parallel for simd default(none) \
206dt_omp_firstprivate(out, Ndim) \
207 schedule(simd:static) aligned(out:64)
208#endif
209 for(size_t k = 0; k < Ndim; k++)
210 {
211 const float avg = out[2 * k];
212 out[2 * k + 1] -= avg * avg;
213 }
214
215error:
216 if(g) dt_gaussian_free(g);
218 return err;
219}
220
222static inline void eigf_blending(float *const restrict image, const float *const restrict mask,
223 const float *const restrict av, const size_t Ndim,
224 const dt_iop_guided_filter_blending_t filter, const float feathering)
225{
226#ifdef _OPENMP
227#pragma omp parallel for simd default(none) \
228 dt_omp_firstprivate(image, mask, av, Ndim, feathering, filter) \
229 schedule(simd:static) aligned(image, mask, av:64)
230#endif
231 for(size_t k = 0; k < Ndim; k++)
232 {
233 const float avg_g = av[k * 4];
234 const float avg_m = av[k * 4 + 2];
235 const float var_g = av[k * 4 + 1];
236 const float covar_mg = av[k * 4 + 3];
237 const float norm_g = fmaxf(avg_g * image[k], 1E-6);
238 const float norm_m = fmaxf(avg_m * mask[k], 1E-6);
239 const float normalized_var_guide = var_g / norm_g;
240 const float normalized_covar = covar_mg / sqrtf(norm_g * norm_m);
241 const float a = normalized_covar / (normalized_var_guide + feathering);
242 const float b = avg_m - a * avg_g;
243 if(filter == DT_GF_BLENDING_LINEAR)
244 {
245 image[k] = fmaxf(image[k] * a + b, MIN_FLOAT);
246 }
247 else
248 {
249 // filter == DT_GF_BLENDING_GEOMEAN
250 image[k] *= fmaxf(image[k] * a + b, MIN_FLOAT);
251 image[k] = sqrtf(image[k]);
252 }
253 }
254}
255
256// same function as above, but specialized for the case where guide == mask
257// for increased performance
259static inline void eigf_blending_no_mask(float *const restrict image, const float *const restrict av,
260 const size_t Ndim, const dt_iop_guided_filter_blending_t filter,
261 const float feathering)
262{
263#ifdef _OPENMP
264#pragma omp parallel for simd default(none) \
265 dt_omp_firstprivate(image, av, Ndim, feathering, filter) \
266 schedule(simd:static) aligned(image, av:64)
267#endif
268 for(size_t k = 0; k < Ndim; k++)
269 {
270 const float avg_g = av[k * 2];
271 const float var_g = av[k * 2 + 1];
272 const float norm_g = fmaxf(avg_g * image[k], 1E-6);
273 const float normalized_var_guide = var_g / norm_g;
274 const float a = normalized_var_guide / (normalized_var_guide + feathering);
275 const float b = avg_g - a * avg_g;
276 if(filter == DT_GF_BLENDING_LINEAR)
277 {
278 image[k] = fmaxf(image[k] * a + b, MIN_FLOAT);
279 }
280 else
281 {
282 // filter == DT_GF_BLENDING_GEOMEAN
283 image[k] *= fmaxf(image[k] * a + b, MIN_FLOAT);
284 image[k] = sqrtf(image[k]);
285 }
286 }
287}
288
290static inline int fast_eigf_surface_blur(float *const restrict image,
291 const size_t width, const size_t height,
292 const float sigma, float feathering, const int iterations,
293 const dt_iop_guided_filter_blending_t filter, const float scale,
294 const float quantization, const float quantize_min, const float quantize_max)
295{
296 // Works in-place on a grey image
297 // mostly similar with fast_surface_blur from fast_guided_filter.h
298
299 int err = 0;
300
301 // A down-scaling of 4 seems empirically safe and consistent no matter the image zoom level
302 // see reference paper above for proof.
303 const float scaling = fmaxf(fminf(sigma, 4.0f), 1.0f);
304 const float ds_sigma = fmaxf(sigma / scaling, 1.0f);
305
306 const size_t ds_height = height / scaling;
307 const size_t ds_width = width / scaling;
308
309 const size_t num_elem_ds = ds_width * ds_height;
310 const size_t num_elem = width * height;
311
312 float *const restrict mask = dt_pixelpipe_cache_alloc_align_float_cache(dt_round_size_sse(num_elem), 0);
313 float *const restrict ds_image = dt_pixelpipe_cache_alloc_align_float_cache(dt_round_size_sse(num_elem_ds), 0);
314 float *const restrict ds_mask = dt_pixelpipe_cache_alloc_align_float_cache(dt_round_size_sse(num_elem_ds), 0);
315 // average - variance arrays: store the guide and mask averages and variances
316 float *const restrict ds_av = dt_pixelpipe_cache_alloc_align_float_cache(dt_round_size_sse(num_elem_ds * 4), 0);
317 float *const restrict av = dt_pixelpipe_cache_alloc_align_float_cache(dt_round_size_sse(num_elem * 4), 0);
318
319 if(!ds_image || !ds_mask || !ds_av || !av || !mask)
320 {
321 err = 1;
322 goto error;
323 }
324
325 // Iterations of filter models the diffusion, sort of
326 for(int i = 0; i < iterations; i++)
327 {
328 // blend linear for all intermediate images
330 // use filter for last iteration
331 if(i == iterations - 1)
332 blend = filter;
333
334 interpolate_bilinear(image, width, height, ds_image, ds_width, ds_height, 1);
335 if(quantization != 0.0f)
336 {
337 // (Re)build the mask from the quantized image to help guiding
338 quantize(image, mask, width * height, quantization, quantize_min, quantize_max);
339 // Downsample the image for speed-up
340 interpolate_bilinear(mask, width, height, ds_mask, ds_width, ds_height, 1);
341 if(eigf_variance_analysis(ds_mask, ds_image, ds_av, ds_width, ds_height, ds_sigma) != 0)
342 {
343 err = 1;
344 goto error;
345 }
346 // Upsample the variances and averages
347 interpolate_bilinear(ds_av, ds_width, ds_height, av, width, height, 4);
348 // Blend the guided image
349 eigf_blending(image, mask, av, num_elem, blend, feathering);
350 }
351 else
352 {
353 // no need to build a mask.
354 if(eigf_variance_analysis_no_mask(ds_image, ds_av, ds_width, ds_height, ds_sigma) != 0)
355 {
356 err = 1;
357 goto error;
358 }
359 // Upsample the variances and averages
360 interpolate_bilinear(ds_av, ds_width, ds_height, av, width, height, 2);
361 // Blend the guided image
362 eigf_blending_no_mask(image, av, num_elem, blend, feathering);
363 }
364 }
365
366error:
372 return err;
373}
374// clang-format off
375// modelines: These editor modelines have been set for all relevant files by tools/update_modelines.py
376// vim: shiftwidth=2 expandtab tabstop=2 cindent
377// kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified;
378// clang-format on
static void error(char *msg)
Definition ashift_lsd.c:202
int width
Definition bilateral.h:1
int height
Definition bilateral.h:1
static const float scaling
Definition chromatic_adaptation.h:299
const float i
Definition colorspaces_inline_conversions.h:669
const float g
Definition colorspaces_inline_conversions.h:925
static const float const float const float min
Definition colorspaces_inline_conversions.h:667
const float max
Definition colorspaces_inline_conversions.h:721
const float b
Definition colorspaces_inline_conversions.h:1326
const float a
Definition colorspaces_inline_conversions.h:1292
static const dt_colormatrix_t dt_aligned_pixel_t out
Definition colorspaces_inline_conversions.h:184
#define dt_pixelpipe_cache_alloc_align_float_cache(pixels, id)
Definition darktable.h:371
static size_t dt_round_size_sse(const size_t size)
Definition darktable.h:327
#define dt_pixelpipe_cache_free_align(mem)
Definition darktable.h:377
#define __DT_CLONE_TARGETS__
Definition darktable.h:291
static __DT_CLONE_TARGETS__ int eigf_variance_analysis(const float *const restrict guide, const float *const restrict mask, float *const restrict out, const size_t width, const size_t height, const float sigma)
Definition eigf.h:73
static __DT_CLONE_TARGETS__ int fast_eigf_surface_blur(float *const restrict image, const size_t width, const size_t height, const float sigma, 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)
Definition eigf.h:290
static __DT_CLONE_TARGETS__ int eigf_variance_analysis_no_mask(const float *const restrict guide, float *const restrict out, const size_t width, const size_t height, const float sigma)
Definition eigf.h:155
static __DT_CLONE_TARGETS__ void eigf_blending_no_mask(float *const restrict image, const float *const restrict av, const size_t Ndim, const dt_iop_guided_filter_blending_t filter, const float feathering)
Definition eigf.h:259
static __DT_CLONE_TARGETS__ void eigf_blending(float *const restrict image, const float *const restrict mask, const float *const restrict av, const size_t Ndim, const dt_iop_guided_filter_blending_t filter, const float feathering)
Definition eigf.h:222
dt_iop_guided_filter_blending_t
Definition fast_guided_filter.h:51
@ DT_GF_BLENDING_LINEAR
Definition fast_guided_filter.h:52
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)
Definition fast_guided_filter.h:264
#define MIN_FLOAT
Definition fast_guided_filter.h:47
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)
Definition fast_guided_filter.h:103
void dt_gaussian_free(dt_gaussian_t *g)
Definition gaussian.c:514
void dt_gaussian_blur(dt_gaussian_t *g, const float *const in, float *const out)
Definition gaussian.c:173
void dt_gaussian_blur_4c(dt_gaussian_t *g, const float *const in, float *const out)
Definition gaussian.c:503
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:125
Definition gaussian.h:40
#define E
Definition test_filmicrgb.c:61
#define MIN(a, b)
Definition thinplate.c:32
#define MAX(a, b)
Definition thinplate.c:29