Blender V4.3
van_vliet_gaussian_coefficients.cc
Go to the documentation of this file.
1/* SPDX-FileCopyrightText: 2024 Blender Authors
2 *
3 * SPDX-License-Identifier: GPL-2.0-or-later */
4
5/* -------------------------------------------------------------------------------------------------
6 * Van Vliet Gaussian Coefficients.
7 *
8 * Computes the coefficients of the fourth order IIR filter approximating a Gaussian filter
9 * computed using Van Vliet's design method. This is based on the following paper:
10 *
11 * Van Vliet, Lucas J., Ian T. Young, and Piet W. Verbeek. "Recursive Gaussian derivative
12 * filters." Proceedings. Fourteenth International Conference on Pattern Recognition (Cat. No.
13 * 98EX170). Vol. 1. IEEE, 1998.
14 *
15 *
16 * The filter is computed as the cascade of a causal and a non causal sequences of second order
17 * difference equations as can be seen in Equation (11) in Van Vliet's paper. The coefficients are
18 * the same for both the causal and non causal sequences.
19 *
20 * However, to improve its numerical stability, we decompose the 4th order filter into a parallel
21 * bank of second order filers using the methods of partial fractions as demonstrated in the follow
22 * book:
23 *
24 * Oppenheim, Alan V. Discrete-time signal processing. Pearson Education India, 1999.
25 *
26 */
27
28#include <array>
29#include <complex>
30#include <cstdint>
31#include <memory>
32
33#include "BLI_assert.h"
34#include "BLI_hash.hh"
35#include "BLI_math_base.hh"
36#include "BLI_math_vector.hh"
37
38#include "COM_context.hh"
40
42
43/* --------------------------------------------------------------------
44 * Van Vliet Gaussian Coefficients Key.
45 */
46
48
53
55{
56 return a.sigma == b.sigma;
57}
58
59/* -------------------------------------------------------------------------------------------------
60 * Van Vliet Gaussian Coefficients.
61 */
62
63/* Computes the variance of the Gaussian filter represented by the given poles scaled by the given
64 * scale factor. This is based on Equation (20) in Van Vliet's paper. */
65static double compute_scaled_poles_variance(const std::array<std::complex<double>, 4> &poles,
66 double scale_factor)
67{
68 std::complex<double> variance = std::complex<double>(0.0, 0.0);
69 for (const std::complex<double> &pole : poles) {
70 const double magnitude = std::pow(std::abs(pole), 1.0 / scale_factor);
71 const double phase = std::arg(pole) / scale_factor;
72 const std::complex<double> multiplier1 = std::polar(magnitude, phase);
73 const std::complex<double> multiplier2 = std::pow(magnitude - std::polar(1.0, phase), -2.0);
74 variance += 2.0 * multiplier1 * multiplier2;
75 }
76
77 /* The variance is actually real valued as guaranteed by Equations (10) and (2) since the poles
78 * are complex conjugate pairs. See Section 3.3 of the paper. */
79 return variance.real();
80}
81
82/* Computes the partial derivative with respect to the scale factor at the given scale factor of
83 * the variance of the Gaussian filter represented by the given poles scaled by the given scale
84 * factor. This is based on the partial derivative with respect to the scale factor of Equation
85 * (20) in Van Vliet's paper.
86 *
87 * The derivative is not listed in the paper, but was computed manually as the sum of the following
88 * for each of the poles:
89 *
90 * \frac{
91 * 2a^\frac{1}{x}e^\frac{ib}{x} (e^\frac{ib}{x}+a^\frac{1}{x}) (\ln(a)-ib)
92 * }{
93 * x^2 (a^\frac{1}{x}-e^\frac{ib}{x})^3
94 * }
95 *
96 * Where "x" is the scale factor, "a" is the magnitude of the pole, and "b" is its phase. */
98 const std::array<std::complex<double>, 4> &poles, double scale_factor)
99{
100 std::complex<double> variance_derivative = std::complex<double>(0.0, 0.0);
101 for (const std::complex<double> &pole : poles) {
102 const double magnitude = std::pow(std::abs(pole), 1.0 / scale_factor);
103 const double phase = std::arg(pole) / scale_factor;
104
105 const std::complex<double> multiplier1 = std::polar(magnitude, phase);
106 const std::complex<double> multiplier2 = magnitude + std::polar(1.0, phase);
107 const std::complex<double> multiplier3 = std::log(std::abs(pole)) -
108 std::complex<double>(0.0, std::arg(pole));
109
110 const std::complex<double> divisor1 = std::pow(magnitude - std::polar(1.0, phase), 3.0);
111 const std::complex<double> divisor2 = math::square(scale_factor);
112
113 variance_derivative += 2.0 * multiplier1 * multiplier2 * multiplier3 / (divisor1 * divisor2);
114 }
115
116 /* The variance derivative is actually real valued as guaranteed by Equations (10) and (2) since
117 * the poles are complex conjugate pairs. See Section 3.3 of the paper. */
118 return variance_derivative.real();
119}
120
121/* The poles were computed for a Gaussian filter with a sigma value of 2, in order to generalize
122 * that for any sigma value, we need to scale the poles by a certain scaling factor as described in
123 * Section 4.2 of Van Vliet's paper. To find the scaling factor, we start from an initial guess of
124 * half sigma, then iteratively improve the guess using Newton's method by computing the variance
125 * and its derivative based on Equation (20). */
126static double find_scale_factor(const std::array<std::complex<double>, 4> &poles,
127 float reference_sigma)
128{
129 const double reference_variance = math::square(reference_sigma);
130
131 /* Note that the poles were computed for a Gaussian filter with a sigma value of 2, so it is
132 * as if we have a base scale of 2, and we start with half sigma as an initial guess. See
133 * Section 4.2 for more information */
134 double scale_factor = reference_sigma / 2.0;
135
136 const int maximum_interations = 10;
137 for (int i = 0; i < maximum_interations; i++) {
138 const double variance = compute_scaled_poles_variance(poles, scale_factor);
139
140 /* Close enough, we have found our scale factor. */
141 if (math::abs(reference_variance - variance) < 1.0e-8) {
142 return scale_factor;
143 }
144
145 /* Improve guess using Newton's method. Notice that Newton's method is a root finding method,
146 * so we supply the difference to the reference variance as our function, since the zero point
147 * will be when the variance is equal to the reference one. The derivative is not affected
148 * since the reference variance is a constant. */
149 const double derivative = compute_scaled_poles_variance_derivative(poles, scale_factor);
150 scale_factor -= (variance - reference_variance) / derivative;
151 }
152
153 /* The paper mentions that only a few iterations are needed, so if we didn't converge after
154 * maximum_interations, something is probably wrong. */
156 return scale_factor;
157}
158
159/* The poles were computed for a Gaussian filter with a sigma value of 2, so scale them using
160 * Equation (19) in Van Vliet's paper to have the given sigma value. This involves finding the
161 * appropriate scale factor based on Equation (20), see Section 4.2 and the find_scale_factor
162 * method for more information. */
163static std::array<std::complex<double>, 4> computed_scaled_poles(
164 const std::array<std::complex<double>, 4> &poles, float sigma)
165{
166 const double scale_factor = find_scale_factor(poles, sigma);
167
168 std::array<std::complex<double>, 4> scaled_poles;
169 for (int i = 0; i < poles.size(); i++) {
170 const std::complex<double> &pole = poles[i];
171 const double magnitude = std::pow(std::abs(pole), 1.0 / scale_factor);
172 const double phase = std::arg(pole) / scale_factor;
173 scaled_poles[i] = std::polar(magnitude, phase);
174 }
175
176 return scaled_poles;
177}
178
179/* Compute the causal poles from the non causal ones. Since the Gaussian is a real even function,
180 * the causal poles are just the inverse of the non causal poles, as noted in Equation (2) in Van
181 * Vliet's paper. */
182static std::array<std::complex<double>, 4> compute_causal_poles(
183 const std::array<std::complex<double>, 4> &non_causal_poles)
184{
185 std::array<std::complex<double>, 4> causal_poles;
186 for (int i = 0; i < non_causal_poles.size(); i++) {
187 causal_poles[i] = 1.0 / non_causal_poles[i];
188 }
189
190 return causal_poles;
191}
192
193/* Computes the feedback coefficients from the given poles based on the equations in Equation (13)
194 * in Van Vliet's paper. See Section 3.2 for more information. */
195static double4 compute_feedback_coefficients(const std::array<std::complex<double>, 4> &poles)
196{
197 /* Compute the gain of the poles, which is the "b" at the end of Equation (13). */
198 std::complex<double> gain = std::complex<double>(1.0, 0.0);
199 for (const std::complex<double> &pole : poles) {
200 gain /= pole;
201 }
202
203 /* Compute the coefficients b4, b3, b2, and b1 based on the expressions b_N, b_N-1, b_N-2, and
204 * b_N-3 respectively in Equation (13). b4 and b3 are trivial, while b2 and b1 can be computed by
205 * drawing the following summation trees, where each path from the root to the leaf is multiplied
206 * and added:
207 *
208 * b2
209 * ____/|\____
210 * / | \
211 * i --> 2 3 4
212 * | / \ /|\
213 * j --> 1 1 2 1 2 3
214 *
215 * b1
216 * ___/ \___
217 * / \
218 * i --> 3 4
219 * | / \
220 * j --> 2 2 3
221 * | | / \
222 * k --> 1 1 1 2
223 *
224 * Notice that the values of i, j, and k are 1-index, so we need to subtract one when accessing
225 * the poles. */
226 const std::complex<double> b4 = gain;
227 const std::complex<double> b3 = -gain * (poles[0] + poles[1] + poles[2] + poles[3]);
228 const std::complex<double> b2 = gain * (poles[1] * poles[0] + poles[2] * poles[0] +
229 poles[2] * poles[1] + poles[3] * poles[0] +
230 poles[3] * poles[1] + poles[3] * poles[2]);
231 const std::complex<double> b1 = -gain * (poles[2] * poles[1] * poles[0] +
232 poles[3] * poles[1] * poles[0] +
233 poles[3] * poles[2] * poles[0] +
234 poles[3] * poles[2] * poles[1]);
235
236 /* The coefficients are actually real valued as guaranteed by Equations (10) and (2) since
237 * the poles are complex conjugate pairs. See Section 3.3 of the paper. */
238 const double4 coefficients = double4(b1.real(), b2.real(), b3.real(), b4.real());
239
240 return coefficients;
241}
242
243/* Computes the feedforward coefficient from the feedback coefficients based on Equation (12) of
244 * Van Vliet's paper. See Section 3.2 for more information. */
245static double compute_feedforward_coefficient(const double4 &feedback_coefficients)
246{
247 return 1.0 + math::reduce_add(feedback_coefficients);
248}
249
250/* Computes the residue of the partial fraction of the transfer function of the given causal poles
251 * and gain for the given target pole. This essentially evaluates Equation (3.41) in Oppenheim's
252 * book, where d_k is the target pole and assuming the transfer function is in the form given in
253 * Equation (3.39), where d_k are the poles. See the following derivation for the gain value.
254 *
255 * For the particular case of the Van Vliet's system, there are no zeros, so the numerator in
256 * Equation (3.39) is one. Further note that Van Vliet's formulation is different from the expected
257 * form, so we need to rearrange Equation (3) in to match the form in Equation (3.39), which is
258 * shown below.
259 *
260 * Start from the causal term of Equation (3):
261 *
262 * H_+(z) = \prod_{i=1}^N \frac{d_i - 1}{d_i - z^{-1}}
263 *
264 * Divide by d_i:
265 *
266 * H_+(z) = \prod_{i=1}^N \frac{1 - d_i^{-1}}{1 - d_i^{-1}z^{-1}}
267 *
268 * Move the numerator to its own product:
269 *
270 * H_+(z) = \prod_{i=1}^N 1 - d_i^{-1} \prod_{i=1}^N \frac{1}{1 - d_i^{-1}z^{-1}}
271 *
272 * And we reach the same form as Equation (3.39). Where the first product term is b0 / a0 and is
273 * also the given gain value, which is also the same as the feedforward coefficient denoted by
274 * the alpha in Equation (12). Further d_i^{-1} in our derivation is the same as d_k in Equation
275 * (3.39), the discrepancy in the inverse operator is the fact that Van Vliet's derivation assume
276 * non causal poles, while Oppenheim's assume causal poles, which are inverse of each other as can
277 * be seen in the compute_causal_poles function. */
278static std::complex<double> compute_partial_fraction_residue(
279 const std::array<std::complex<double>, 4> &poles,
280 const std::complex<double> &target_pole,
281 double gain)
282{
283 /* Evaluating Equation (3.41) actually corresponds to omitting the terms in Equation (3.39) that
284 * corresponds to the target pole or its conjugate, because they get canceled by the first term
285 * in Equation (3.41). That's we are essentially evaluating the limit as the expression tends to
286 * the target pole. */
287 std::complex<double> target_pole_inverse = 1.0 / target_pole;
288 std::complex<double> residue = std::complex<double>(1.0, 0.0);
289 for (const std::complex<double> &pole : poles) {
290 if (pole != target_pole && pole != std::conj(target_pole)) {
291 residue *= 1.0 - pole * target_pole_inverse;
292 }
293 }
294
295 /* Remember that the gain is the b0 / a0 expression in Equation (3.39). */
296 return gain / residue;
297}
298
299/* Evaluates the causal transfer function at the reciprocal of the given pole, which will be the
300 * non causal pole if the given pole is a causal one, as discussed in the compute_causal_poles
301 * function. The causal transfer function is given in Equation (3) in Van Vliet's paper, but we
302 * compute it in the form derived in the description of the compute_partial_fraction_residue
303 * function, also see the aforementioned function for the gain value. */
305 const std::array<std::complex<double>, 4> &poles,
306 const std::complex<double> &target_pole,
307 double gain)
308{
309 std::complex<double> result = std::complex<double>(1.0, 0.0);
310 for (const std::complex<double> &pole : poles) {
311 result *= 1.0 - pole * target_pole;
312 }
313
314 return gain / result;
315}
316
317/* Combine each pole and its conjugate counterpart into a second order section and assign its
318 * coefficients to the given coefficients value. The residue of the pole and its transfer value in
319 * the partial fraction of its transfer function are given.
320 *
321 * TODO: Properly document this function and prove its equations. */
322static void compute_second_order_section(const std::complex<double> &pole,
323 const std::complex<double> &residue,
324 const std::complex<double> &transfer_value,
325 double2 &r_feedback_coefficients,
326 double2 &r_causal_feedforward_coefficients,
327 double2 &r_non_causal_feedforward_coefficients)
328{
329 const std::complex<double> parallel_residue = residue * transfer_value;
330 const std::complex<double> pole_inverse = 1.0 / pole;
331
332 r_feedback_coefficients = double2(-2.0 * pole.real(), std::norm(pole));
333
334 const double causal_feedforward_1 = parallel_residue.imag() / pole_inverse.imag();
335 const double causal_feedforward_0 = parallel_residue.real() -
336 causal_feedforward_1 * pole_inverse.real();
337 r_causal_feedforward_coefficients = double2(causal_feedforward_0, causal_feedforward_1);
338
339 const double non_causal_feedforward_1 = causal_feedforward_1 -
340 causal_feedforward_0 * r_feedback_coefficients[0];
341 const double non_causal_feedforward_2 = -causal_feedforward_0 * r_feedback_coefficients[1];
342 r_non_causal_feedforward_coefficients = double2(non_causal_feedforward_1,
343 non_causal_feedforward_2);
344}
345
346/* The IIR filter difference equation relies on previous outputs to compute new outputs, those
347 * previous outputs are not really defined at the start of the filter. To do Neumann boundary
348 * condition, we initialize the previous output with a special value that is a function of the
349 * boundary value. This special value is computed by multiply the boundary value with a coefficient
350 * to simulate an infinite stream of the boundary value.
351 *
352 * The function for the coefficient can be derived by substituting the boundary value for previous
353 * inputs, equating all current and previous outputs to the same value, and finally rearranging to
354 * compute that same output value.
355 *
356 * Start by the difference equation where b_i are the feedforward coefficients and a_i are the
357 * feedback coefficients:
358 *
359 * y[n] = \sum_{i = 0}^3 b_i x[n - i] - \sum_{i = 0}^3 a_i y[n - i]
360 *
361 * Assume all outputs are y and all inputs are x, which is the boundary value:
362 *
363 * y = \sum_{i = 0}^3 b_i x - \sum_{i = 0}^3 a_i y
364 *
365 * Now rearrange to compute y:
366 *
367 * y = x \sum_{i = 0}^3 b_i - y \sum_{i = 0}^3 a_i
368 * y + y \sum_{i = 0}^3 a_i = x \sum_{i = 0}^3 b_i
369 * y (1 + \sum_{i = 0}^3 a_i) = x \sum_{i = 0}^3 b_i
370 * y = x \cdot \frac{\sum_{i = 0}^3 b_i}{1 + \sum_{i = 0}^3 a_i}
371 *
372 * So our coefficient is the value that is multiplied by the boundary value x. Had x been zero,
373 * that is, we are doing Dirichlet boundary condition, the equations still hold. */
374static double compute_boundary_coefficient(const double2 &feedback_coefficients,
375 const double2 &feedforward_coefficients)
376{
377 return math::reduce_add(feedforward_coefficients) /
378 (1.0 + math::reduce_add(feedback_coefficients));
379}
380
381/* Computes the feedback and feedforward coefficients for the 4th order Van Vliet Gaussian filter
382 * given a target Gaussian sigma value. We first scale the poles of the filter to match the sigma
383 * value based on the method described in Section 4.2 of Van Vliet's paper, then we compute the
384 * coefficients from the scaled poles based on Equations (12) and (13). */
386{
387
388 /* The 4th order (N=4) poles for the Gaussian filter of a sigma of 2 computed by minimizing the
389 * maximum error (L-infinity) to true Gaussian as provided in Van Vliet's paper Table (1) fourth
390 * column. Notice that the second and fourth poles are the complex conjugates of the first and
391 * third poles respectively as noted in the table description. */
392 const std::array<std::complex<double>, 4> poles = {
393 std::complex<double>(1.12075, 1.27788),
394 std::complex<double>(1.12075, -1.27788),
395 std::complex<double>(1.76952, 0.46611),
396 std::complex<double>(1.76952, -0.46611),
397 };
398
399 const std::array<std::complex<double>, 4> scaled_poles = computed_scaled_poles(poles, sigma);
400
401 /* The given poles are actually the non causal poles, since they are outside of the unit circle,
402 * as demonstrated in Section 3.4 of Van Vliet's paper. And we compute the causal poles from
403 * those. */
404 const std::array<std::complex<double>, 4> non_causal_poles = scaled_poles;
405 const std::array<std::complex<double>, 4> causal_poles = compute_causal_poles(non_causal_poles);
406
407 /* Compute the feedforward and feedback coefficients, noting that those are functions of the non
408 * causal poles. */
409 const double4 feedback_coefficients = compute_feedback_coefficients(non_causal_poles);
410 const double feedforward_coefficient = compute_feedforward_coefficient(feedback_coefficients);
411
412 /* We only compute the residue for two of the causal poles, since the other two are complex
413 * conjugates of those two, and their residues will also be the complex conjugate of their
414 * respective counterpart. The gain is the feedforward coefficient as discussed in the function
415 * description. */
416 const std::complex<double> first_residue = compute_partial_fraction_residue(
417 causal_poles, causal_poles[0], feedforward_coefficient);
418 const std::complex<double> second_residue = compute_partial_fraction_residue(
419 causal_poles, causal_poles[2], feedforward_coefficient);
420
421 /* We only compute the transfer value of for two of the non causal poles, since the other two are
422 * complex conjugates of those two, and their transfer values will also be the complex conjugate
423 * of their respective counterpart. The gain is the feedforward coefficient as discussed in the
424 * function description. */
425 const std::complex<double> first_transfer_value =
427 causal_poles, causal_poles[0], feedforward_coefficient);
428 const std::complex<double> second_transfer_value =
430 causal_poles, causal_poles[2], feedforward_coefficient);
431
432 /* Combine each pole and its conjugate counterpart into a second order section and assign its
433 * coefficients. */
434 compute_second_order_section(causal_poles[0],
435 first_residue,
436 first_transfer_value,
437 first_feedback_coefficients_,
438 first_causal_feedforward_coefficients_,
439 first_non_causal_feedforward_coefficients_);
440 compute_second_order_section(causal_poles[2],
441 second_residue,
442 second_transfer_value,
443 second_feedback_coefficients_,
444 second_causal_feedforward_coefficients_,
445 second_non_causal_feedforward_coefficients_);
446
447 /* Compute the boundary coefficients for all four of second order sections. */
448 first_causal_boundary_coefficient_ = compute_boundary_coefficient(
449 first_feedback_coefficients_, first_causal_feedforward_coefficients_);
450 first_non_causal_boundary_coefficient_ = compute_boundary_coefficient(
451 first_feedback_coefficients_, first_non_causal_feedforward_coefficients_);
452 second_causal_boundary_coefficient_ = compute_boundary_coefficient(
453 second_feedback_coefficients_, second_causal_feedforward_coefficients_);
454 second_non_causal_boundary_coefficient_ = compute_boundary_coefficient(
455 second_feedback_coefficients_, second_non_causal_feedforward_coefficients_);
456}
457
459{
460 return first_causal_feedforward_coefficients_;
461}
462
464{
465 return first_non_causal_feedforward_coefficients_;
466}
467
469{
470 return first_feedback_coefficients_;
471}
472
474{
475 return second_causal_feedforward_coefficients_;
476}
477
479{
480 return second_non_causal_feedforward_coefficients_;
481}
482
484{
485 return second_feedback_coefficients_;
486}
487
489{
490 return first_causal_boundary_coefficient_;
491}
492
494{
495 return first_non_causal_boundary_coefficient_;
496}
497
499{
500 return second_causal_boundary_coefficient_;
501}
502
504{
505 return second_non_causal_boundary_coefficient_;
506}
507
508/* --------------------------------------------------------------------
509 * Van Vliet Gaussian Coefficients Container.
510 */
511
513{
514 /* First, delete all resources that are no longer needed. */
515 map_.remove_if([](auto item) { return !item.value->needed; });
516
517 /* Second, reset the needed status of the remaining resources to false to ready them to track
518 * their needed status for the next evaluation. */
519 for (auto &value : map_.values()) {
520 value->needed = false;
521 }
522}
523
525 float sigma)
526{
527 const VanVlietGaussianCoefficientsKey key(sigma);
528
529 auto &deriche_gaussian_coefficients = *map_.lookup_or_add_cb(
530 key, [&]() { return std::make_unique<VanVlietGaussianCoefficients>(context, sigma); });
531
532 deriche_gaussian_coefficients.needed = true;
533 return deriche_gaussian_coefficients;
534}
535
536} // namespace blender::realtime_compositor
#define BLI_assert_unreachable()
Definition BLI_assert.h:97
VanVlietGaussianCoefficients & get(Context &context, float sigma)
local_group_size(16, 16) .push_constant(Type b
T square(const T &a)
T abs(const T &a)
T reduce_add(const VecBase< T, Size > &a)
static double compute_feedforward_coefficient(const double4 &feedback_coefficients)
static double compute_scaled_poles_variance_derivative(const std::array< std::complex< double >, 4 > &poles, double scale_factor)
static std::complex< double > compute_partial_fraction_residue(const std::array< std::complex< double >, 4 > &poles, const std::complex< double > &target_pole, double gain)
bool operator==(const BokehKernelKey &a, const BokehKernelKey &b)
static std::array< std::complex< double >, 4 > computed_scaled_poles(const std::array< std::complex< double >, 4 > &poles, float sigma)
static std::complex< double > compute_causal_transfer_function_at_non_causal_pole(const std::array< std::complex< double >, 4 > &poles, const std::complex< double > &target_pole, double gain)
static double find_scale_factor(const std::array< std::complex< double >, 4 > &poles, float reference_sigma)
static std::array< std::complex< double >, 4 > compute_causal_poles(const std::array< std::complex< double >, 4 > &non_causal_poles)
static double compute_boundary_coefficient(const double4 &feedforward_coefficients, const double4 &feedback_coefficients)
static void compute_second_order_section(const std::complex< double > &pole, const std::complex< double > &residue, const std::complex< double > &transfer_value, double2 &r_feedback_coefficients, double2 &r_causal_feedforward_coefficients, double2 &r_non_causal_feedforward_coefficients)
static double4 compute_feedback_coefficients(const std::array< std::complex< double >, 4 > &poles)
static double compute_scaled_poles_variance(const std::array< std::complex< double >, 4 > &poles, double scale_factor)
VecBase< double, 2 > double2
uint64_t get_default_hash(const T &v)
Definition BLI_hash.hh:219
VecBase< double, 4 > double4
unsigned __int64 uint64_t
Definition stdint.h:90