Blender V5.0
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
41namespace blender::compositor {
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
102 const std::array<std::complex<double>, 4> &poles, double scale_factor)
103{
104 std::complex<double> variance_derivative = std::complex<double>(0.0, 0.0);
105 for (const std::complex<double> &pole : poles) {
106 const double magnitude = std::pow(std::abs(pole), 1.0 / scale_factor);
107 const double phase = std::arg(pole) / scale_factor;
108
109 const std::complex<double> multiplier1 = std::polar(magnitude, phase);
110 const std::complex<double> multiplier2 = magnitude + std::polar(1.0, phase);
111 const std::complex<double> multiplier3 = std::log(std::abs(pole)) -
112 std::complex<double>(0.0, std::arg(pole));
113
114 const std::complex<double> divisor1 = std::pow(magnitude - std::polar(1.0, phase), 3.0);
115 const std::complex<double> divisor2 = math::square(scale_factor);
116
117 variance_derivative += 2.0 * multiplier1 * multiplier2 * multiplier3 / (divisor1 * divisor2);
118 }
119
120 /* The variance derivative is actually real valued as guaranteed by Equations (10) and (2) since
121 * the poles are complex conjugate pairs. See Section 3.3 of the paper. */
122 return variance_derivative.real();
123}
124
125/* The poles were computed for a Gaussian filter with a sigma value of 2, in order to generalize
126 * that for any sigma value, we need to scale the poles by a certain scaling factor as described in
127 * Section 4.2 of Van Vliet's paper. To find the scaling factor, we start from an initial guess of
128 * half sigma, then iteratively improve the guess using Newton's method by computing the variance
129 * and its derivative based on Equation (20). */
130static double find_scale_factor(const std::array<std::complex<double>, 4> &poles,
131 double reference_sigma)
132{
133 const double reference_variance = math::square(reference_sigma);
134
135 /* Note that the poles were computed for a Gaussian filter with a sigma value of 2, so it is
136 * as if we have a base scale of 2, and we start with half sigma as an initial guess. See
137 * Section 4.2 for more information */
138 double scale_factor = reference_sigma / 2.0;
139
140 const int maximum_interations = 10;
141 for (int i = 0; i < maximum_interations; i++) {
142 const double variance = compute_scaled_poles_variance(poles, scale_factor);
143
144 /* Close enough, we have found our scale factor. */
145 if (math::abs(reference_variance - variance) < 1.0e-7) {
146 return scale_factor;
147 }
148
149 /* Improve guess using Newton's method. Notice that Newton's method is a root finding method,
150 * so we supply the difference to the reference variance as our function, since the zero point
151 * will be when the variance is equal to the reference one. The derivative is not affected
152 * since the reference variance is a constant. */
153 const double derivative = compute_scaled_poles_variance_derivative(poles, scale_factor);
154 scale_factor -= (variance - reference_variance) / derivative;
155 }
156
157 /* The paper mentions that only a few iterations are needed, so if we didn't converge after
158 * maximum_interations, something is probably wrong. */
160 return scale_factor;
161}
162
163/* The poles were computed for a Gaussian filter with a sigma value of 2, so scale them using
164 * Equation (19) in Van Vliet's paper to have the given sigma value. This involves finding the
165 * appropriate scale factor based on Equation (20), see Section 4.2 and the find_scale_factor
166 * method for more information. */
167static std::array<std::complex<double>, 4> computed_scaled_poles(
168 const std::array<std::complex<double>, 4> &poles, float sigma)
169{
170 const double scale_factor = find_scale_factor(poles, sigma);
171
172 std::array<std::complex<double>, 4> scaled_poles;
173 for (int i = 0; i < poles.size(); i++) {
174 const std::complex<double> &pole = poles[i];
175 const double magnitude = std::pow(std::abs(pole), 1.0 / scale_factor);
176 const double phase = std::arg(pole) / scale_factor;
177 scaled_poles[i] = std::polar(magnitude, phase);
178 }
179
180 return scaled_poles;
181}
182
183/* Compute the causal poles from the non causal ones. Since the Gaussian is a real even function,
184 * the causal poles are just the inverse of the non causal poles, as noted in Equation (2) in Van
185 * Vliet's paper. */
186static std::array<std::complex<double>, 4> compute_causal_poles(
187 const std::array<std::complex<double>, 4> &non_causal_poles)
188{
189 std::array<std::complex<double>, 4> causal_poles;
190 for (int i = 0; i < non_causal_poles.size(); i++) {
191 causal_poles[i] = 1.0 / non_causal_poles[i];
192 }
193
194 return causal_poles;
195}
196
197/* Computes the feedback coefficients from the given poles based on the equations in Equation (13)
198 * in Van Vliet's paper. See Section 3.2 for more information. */
199static double4 compute_feedback_coefficients(const std::array<std::complex<double>, 4> &poles)
200{
201 /* Compute the gain of the poles, which is the "b" at the end of Equation (13). */
202 std::complex<double> gain = std::complex<double>(1.0, 0.0);
203 for (const std::complex<double> &pole : poles) {
204 gain /= pole;
205 }
206
207 /* Compute the coefficients b4, b3, b2, and b1 based on the expressions b_N, b_N-1, b_N-2, and
208 * b_N-3 respectively in Equation (13). b4 and b3 are trivial, while b2 and b1 can be computed by
209 * drawing the following summation trees, where each path from the root to the leaf is multiplied
210 * and added:
211 *
212 * b2
213 * ____/|\____
214 * / | \
215 * i --> 2 3 4
216 * | / \ /|\
217 * j --> 1 1 2 1 2 3
218 *
219 * b1
220 * ___/ \___
221 * / \
222 * i --> 3 4
223 * | / \
224 * j --> 2 2 3
225 * | | / \
226 * k --> 1 1 1 2
227 *
228 * Notice that the values of i, j, and k are 1-index, so we need to subtract one when accessing
229 * the poles. */
230 const std::complex<double> b4 = gain;
231 const std::complex<double> b3 = -gain * (poles[0] + poles[1] + poles[2] + poles[3]);
232 const std::complex<double> b2 = gain * (poles[1] * poles[0] + poles[2] * poles[0] +
233 poles[2] * poles[1] + poles[3] * poles[0] +
234 poles[3] * poles[1] + poles[3] * poles[2]);
235 const std::complex<double> b1 = -gain * (poles[2] * poles[1] * poles[0] +
236 poles[3] * poles[1] * poles[0] +
237 poles[3] * poles[2] * poles[0] +
238 poles[3] * poles[2] * poles[1]);
239
240 /* The coefficients are actually real valued as guaranteed by Equations (10) and (2) since
241 * the poles are complex conjugate pairs. See Section 3.3 of the paper. */
242 const double4 coefficients = double4(b1.real(), b2.real(), b3.real(), b4.real());
243
244 return coefficients;
245}
246
247/* Computes the feedforward coefficient from the feedback coefficients based on Equation (12) of
248 * Van Vliet's paper. See Section 3.2 for more information. */
249static double compute_feedforward_coefficient(const double4 &feedback_coefficients)
250{
251 return 1.0 + math::reduce_add(feedback_coefficients);
252}
253
290static std::complex<double> compute_partial_fraction_residue(
291 const std::array<std::complex<double>, 4> &poles,
292 const std::complex<double> &target_pole,
293 double gain)
294{
295 /* Evaluating Equation (3.41) actually corresponds to omitting the terms in Equation (3.39) that
296 * corresponds to the target pole or its conjugate, because they get canceled by the first term
297 * in Equation (3.41). That's we are essentially evaluating the limit as the expression tends to
298 * the target pole. */
299 std::complex<double> target_pole_inverse = 1.0 / target_pole;
300 std::complex<double> residue = std::complex<double>(1.0, 0.0);
301 for (const std::complex<double> &pole : poles) {
302 if (pole != target_pole && pole != std::conj(target_pole)) {
303 residue *= 1.0 - pole * target_pole_inverse;
304 }
305 }
306
307 /* Remember that the gain is the b0 / a0 expression in Equation (3.39). */
308 return gain / residue;
309}
310
311/* Evaluates the causal transfer function at the reciprocal of the given pole, which will be the
312 * non causal pole if the given pole is a causal one, as discussed in the compute_causal_poles
313 * function. The causal transfer function is given in Equation (3) in Van Vliet's paper, but we
314 * compute it in the form derived in the description of the compute_partial_fraction_residue
315 * function, also see the aforementioned function for the gain value. */
317 const std::array<std::complex<double>, 4> &poles,
318 const std::complex<double> &target_pole,
319 double gain)
320{
321 std::complex<double> result = std::complex<double>(1.0, 0.0);
322 for (const std::complex<double> &pole : poles) {
323 result *= 1.0 - pole * target_pole;
324 }
325
326 return gain / result;
327}
328
329/* Combine each pole and its conjugate counterpart into a second order section and assign its
330 * coefficients to the given coefficients value. The residue of the pole and its transfer value in
331 * the partial fraction of its transfer function are given.
332 *
333 * TODO: Properly document this function and prove its equations. */
334static void compute_second_order_section(const std::complex<double> &pole,
335 const std::complex<double> &residue,
336 const std::complex<double> &transfer_value,
337 double2 &r_feedback_coefficients,
338 double2 &r_causal_feedforward_coefficients,
339 double2 &r_non_causal_feedforward_coefficients)
340{
341 const std::complex<double> parallel_residue = residue * transfer_value;
342 const std::complex<double> pole_inverse = 1.0 / pole;
343
344 r_feedback_coefficients = double2(-2.0 * pole.real(), std::norm(pole));
345
346 const double causal_feedforward_1 = parallel_residue.imag() / pole_inverse.imag();
347 const double causal_feedforward_0 = parallel_residue.real() -
348 causal_feedforward_1 * pole_inverse.real();
349 r_causal_feedforward_coefficients = double2(causal_feedforward_0, causal_feedforward_1);
350
351 const double non_causal_feedforward_1 = causal_feedforward_1 -
352 causal_feedforward_0 * r_feedback_coefficients[0];
353 const double non_causal_feedforward_2 = -causal_feedforward_0 * r_feedback_coefficients[1];
354 r_non_causal_feedforward_coefficients = double2(non_causal_feedforward_1,
355 non_causal_feedforward_2);
356}
357
394static double compute_boundary_coefficient(const double2 &feedback_coefficients,
395 const double2 &feedforward_coefficients)
396{
397 return math::reduce_add(feedforward_coefficients) /
398 (1.0 + math::reduce_add(feedback_coefficients));
399}
400
401/* Computes the feedback and feedforward coefficients for the 4th order Van Vliet Gaussian filter
402 * given a target Gaussian sigma value. We first scale the poles of the filter to match the sigma
403 * value based on the method described in Section 4.2 of Van Vliet's paper, then we compute the
404 * coefficients from the scaled poles based on Equations (12) and (13). */
406{
407
408 /* The 4th order (N=4) poles for the Gaussian filter of a sigma of 2 computed by minimizing the
409 * maximum error (L-infinity) to true Gaussian as provided in Van Vliet's paper Table (1) fourth
410 * column. Notice that the second and fourth poles are the complex conjugates of the first and
411 * third poles respectively as noted in the table description. */
412 const std::array<std::complex<double>, 4> poles = {
413 std::complex<double>(1.12075, 1.27788),
414 std::complex<double>(1.12075, -1.27788),
415 std::complex<double>(1.76952, 0.46611),
416 std::complex<double>(1.76952, -0.46611),
417 };
418
419 const std::array<std::complex<double>, 4> scaled_poles = computed_scaled_poles(poles, sigma);
420
421 /* The given poles are actually the non causal poles, since they are outside of the unit circle,
422 * as demonstrated in Section 3.4 of Van Vliet's paper. And we compute the causal poles from
423 * those. */
424 const std::array<std::complex<double>, 4> non_causal_poles = scaled_poles;
425 const std::array<std::complex<double>, 4> causal_poles = compute_causal_poles(non_causal_poles);
426
427 /* Compute the feedforward and feedback coefficients, noting that those are functions of the non
428 * causal poles. */
429 const double4 feedback_coefficients = compute_feedback_coefficients(non_causal_poles);
430 const double feedforward_coefficient = compute_feedforward_coefficient(feedback_coefficients);
431
432 /* We only compute the residue for two of the causal poles, since the other two are complex
433 * conjugates of those two, and their residues will also be the complex conjugate of their
434 * respective counterpart. The gain is the feedforward coefficient as discussed in the function
435 * description. */
436 const std::complex<double> first_residue = compute_partial_fraction_residue(
437 causal_poles, causal_poles[0], feedforward_coefficient);
438 const std::complex<double> second_residue = compute_partial_fraction_residue(
439 causal_poles, causal_poles[2], feedforward_coefficient);
440
441 /* We only compute the transfer value of for two of the non causal poles, since the other two are
442 * complex conjugates of those two, and their transfer values will also be the complex conjugate
443 * of their respective counterpart. The gain is the feedforward coefficient as discussed in the
444 * function description. */
445 const std::complex<double> first_transfer_value =
447 causal_poles, causal_poles[0], feedforward_coefficient);
448 const std::complex<double> second_transfer_value =
450 causal_poles, causal_poles[2], feedforward_coefficient);
451
452 /* Combine each pole and its conjugate counterpart into a second order section and assign its
453 * coefficients. */
454 compute_second_order_section(causal_poles[0],
455 first_residue,
456 first_transfer_value,
457 first_feedback_coefficients_,
458 first_causal_feedforward_coefficients_,
459 first_non_causal_feedforward_coefficients_);
460 compute_second_order_section(causal_poles[2],
461 second_residue,
462 second_transfer_value,
463 second_feedback_coefficients_,
464 second_causal_feedforward_coefficients_,
465 second_non_causal_feedforward_coefficients_);
466
467 /* Compute the boundary coefficients for all four of second order sections. */
468 first_causal_boundary_coefficient_ = compute_boundary_coefficient(
469 first_feedback_coefficients_, first_causal_feedforward_coefficients_);
470 first_non_causal_boundary_coefficient_ = compute_boundary_coefficient(
471 first_feedback_coefficients_, first_non_causal_feedforward_coefficients_);
472 second_causal_boundary_coefficient_ = compute_boundary_coefficient(
473 second_feedback_coefficients_, second_causal_feedforward_coefficients_);
474 second_non_causal_boundary_coefficient_ = compute_boundary_coefficient(
475 second_feedback_coefficients_, second_non_causal_feedforward_coefficients_);
476}
477
479{
480 return first_causal_feedforward_coefficients_;
481}
482
484{
485 return first_non_causal_feedforward_coefficients_;
486}
487
489{
490 return first_feedback_coefficients_;
491}
492
494{
495 return second_causal_feedforward_coefficients_;
496}
497
499{
500 return second_non_causal_feedforward_coefficients_;
501}
502
504{
505 return second_feedback_coefficients_;
506}
507
509{
510 return first_causal_boundary_coefficient_;
511}
512
514{
515 return first_non_causal_boundary_coefficient_;
516}
517
519{
520 return second_causal_boundary_coefficient_;
521}
522
524{
525 return second_non_causal_boundary_coefficient_;
526}
527
528/* --------------------------------------------------------------------
529 * Van Vliet Gaussian Coefficients Container.
530 */
531
533{
534 /* First, delete all resources that are no longer needed. */
535 map_.remove_if([](auto item) { return !item.value->needed; });
536
537 /* Second, reset the needed status of the remaining resources to false to ready them to track
538 * their needed status for the next evaluation. */
539 for (auto &value : map_.values()) {
540 value->needed = false;
541 }
542}
543
545 float sigma)
546{
547 const VanVlietGaussianCoefficientsKey key(sigma);
548
549 auto &deriche_gaussian_coefficients = *map_.lookup_or_add_cb(
550 key, [&]() { return std::make_unique<VanVlietGaussianCoefficients>(context, sigma); });
551
552 deriche_gaussian_coefficients.needed = true;
553 return deriche_gaussian_coefficients;
554}
555
556} // namespace blender::compositor
#define BLI_assert_unreachable()
Definition BLI_assert.h:93
unsigned long long int uint64_t
VanVlietGaussianCoefficients & get(Context &context, float sigma)
static double compute_scaled_poles_variance_derivative(const std::array< std::complex< double >, 4 > &poles, double scale_factor)
static double find_scale_factor(const std::array< std::complex< double >, 4 > &poles, double reference_sigma)
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 double compute_scaled_poles_variance(const std::array< std::complex< double >, 4 > &poles, double scale_factor)
static std::array< std::complex< double >, 4 > compute_causal_poles(const std::array< std::complex< double >, 4 > &non_causal_poles)
static double compute_feedforward_coefficient(const double4 &feedback_coefficients)
static double compute_boundary_coefficient(const double4 &feedforward_coefficients, const double4 &feedback_coefficients)
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 std::array< std::complex< double >, 4 > computed_scaled_poles(const std::array< std::complex< double >, 4 > &poles, float sigma)
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)
T square(const T &a)
T abs(const T &a)
T reduce_add(const VecBase< T, Size > &a)
VecBase< double, 2 > double2
uint64_t get_default_hash(const T &v, const Args &...args)
Definition BLI_hash.hh:233
VecBase< double, 4 > double4
i
Definition text_draw.cc:230