CPPxDIC
C++ Digital Image Correlation library for fingertip 3D reconstruction
Loading...
Searching...
No Matches
temporal_filter.cpp
Go to the documentation of this file.
1
6#include "temporal_filter.h"
7#include <algorithm>
8#include <numeric>
9#include <cmath>
10#include <complex>
11#include <iostream>
12
13namespace cppxdic {
14
15using namespace detail;
16
17std::vector<std::vector<double>> filterTime(const std::vector<std::vector<double>>& data,
18 double freq_filt, double freq_acq) {
19 if (data.empty() || data[0].empty()) {
20 return data;
21 }
22
23 size_t nPoints = data.size();
24 size_t nFrames = data[0].size();
25
26 // Design Butterworth filter
27 double wn = freq_filt / (freq_acq / 2.0); // Normalized frequency
28 if (wn >= 1.0) wn = 0.99; // Ensure stability
29 if (wn <= 0.0) wn = 0.01;
30
31 auto [b, a] = butterLowPass(4, wn); // 4th order
32
33 // Prepare output
34 std::vector<std::vector<double>> filtered(nPoints, std::vector<double>(nFrames, NAN));
35
36 int npad = 5; // Padding to reduce border effects
37
38 // Process each point's time series
39 for (size_t ipt = 0; ipt < nPoints; ++ipt) {
40 // Check if all frames are valid (no NaN)
41 bool has_nan = false;
42 for (size_t iframe = 0; iframe < nFrames; ++iframe) {
43 if (std::isnan(data[ipt][iframe])) {
44 has_nan = true;
45 break;
46 }
47 }
48
49 if (has_nan) {
50 // MATLAB: points with any NaN → all frames NaN in output
51 // (data_out initialized to NaN, only mask==true points get filtered values)
52 // filtered[ipt] is already initialized to NaN
53 continue;
54 }
55
56 // Pad the signal
57 std::vector<double> padded;
58 padded.reserve(nFrames + 2 * npad);
59
60 // Pad beginning with first value
61 for (int i = 0; i < npad; ++i) {
62 padded.push_back(data[ipt][0]);
63 }
64
65 // Copy data
66 for (size_t iframe = 0; iframe < nFrames; ++iframe) {
67 padded.push_back(data[ipt][iframe]);
68 }
69
70 // Pad end with last value
71 for (int i = 0; i < npad; ++i) {
72 padded.push_back(data[ipt][nFrames - 1]);
73 }
74
75 // Apply zero-phase filter
76 std::vector<double> result = filtfilt(b, a, padded);
77
78 // Extract unpadded result
79 for (size_t iframe = 0; iframe < nFrames; ++iframe) {
80 filtered[ipt][iframe] = result[npad + iframe];
81 }
82 }
83
84 return filtered;
85}
86
87std::tuple<std::vector<std::vector<double>>, std::vector<std::vector<double>>,
88 std::vector<std::vector<double>>>
89filterTime3D(const std::vector<std::vector<double>>& disp_x,
90 const std::vector<std::vector<double>>& disp_y,
91 const std::vector<std::vector<double>>& disp_z, double freq_filt, double freq_acq) {
92 auto filt_x = filterTime(disp_x, freq_filt, freq_acq);
93 auto filt_y = filterTime(disp_y, freq_filt, freq_acq);
94 auto filt_z = filterTime(disp_z, freq_filt, freq_acq);
95
96 return {filt_x, filt_y, filt_z};
97}
98
99namespace detail {
100
101std::pair<std::vector<double>, std::vector<double>> butterLowPass(int order, double wn) {
102 // Butterworth filter design using bilinear transformation
103 // This is a simplified implementation for low-pass filters
104
105 // For 4th order Butterworth, we use the standard coefficients
106 // Analog prototype poles for normalized Butterworth filter
107 std::vector<std::complex<double>> poles;
108 for (int k = 0; k < order; ++k) {
109 double theta = M_PI * (2.0 * k + order + 1) / (2.0 * order);
110 poles.emplace_back(std::cos(theta), std::sin(theta));
111 }
112
113 // Pre-warp frequency for bilinear transform
114 double wp = 2.0 * std::tan(M_PI * wn / 2.0);
115
116 // Scale poles by cutoff frequency
117 for (auto& p : poles) {
118 p *= wp;
119 }
120
121 // Bilinear transformation: s = 2*(z-1)/(z+1)
122 std::vector<std::complex<double>> z_poles;
123 for (const auto& p : poles) {
124 z_poles.push_back((2.0 + p) / (2.0 - p));
125 }
126
127 // Compute filter coefficients from poles
128 // For simplicity, we'll use pre-computed coefficients for 4th order
129 // This matches MATLAB's butter(4, wn) closely
130
131 std::vector<double> b, a;
132
133 if (order == 4) {
134 // Compute gain
135 double k = 1.0;
136 for (const auto& zp : z_poles) {
137 k *= std::abs(1.0 - zp);
138 }
139 k = k / std::pow(2.0, order);
140
141 // Numerator (all zeros at z=-1 for low-pass)
142 b = {k, 4 * k, 6 * k, 4 * k, k};
143
144 // Denominator (from poles): A(z) = prod_i (z - zp_i)
145 a.resize(5);
146
147 // Multiply out (z - pole_i) terms. After the loop, poly[i] holds the
148 // coefficient of z^i, so poly[4] == 1 (leading) and poly[0] is the
149 // constant term.
150 std::vector<std::complex<double>> poly(5, 0.0);
151 poly[0] = 1.0;
152
153 for (const auto& zp : z_poles) {
154 for (int i = 4; i >= 1; --i) {
155 poly[i] = poly[i - 1] - zp * poly[i];
156 }
157 poly[0] = -zp * poly[0];
158 }
159
160 // The difference-equation form used by filter()/filtfilt() expects the
161 // coefficients in DESCENDING powers of z (equivalently ascending powers
162 // of z^-1), with a[0] the leading coefficient used for normalization.
163 // poly[] is in ascending powers of z, so reverse it. Forgetting this
164 // reversal leaves a[0] = (small) constant term and a recursion that is
165 // not normalized -> the filter diverges (the original DC-instability bug).
166 for (int i = 0; i < 5; ++i) {
167 a[i] = poly[4 - i].real();
168 }
169 } else {
170 // Fallback: simple 2nd order filter
171 double alpha = std::sin(M_PI * wn) / (2.0 * 0.707); // Q = 0.707 for Butterworth
172 double cos_w = std::cos(M_PI * wn);
173
174 b = {alpha, 2 * alpha, alpha};
175 a = {1.0 + alpha, -2.0 * cos_w, 1.0 - alpha};
176 }
177
178 return {b, a};
179}
180
181namespace {
182
183// Direct-Form-II-transposed IIR filtering with an explicit initial state `z`
184// (length max(nb,na)-1). `b`/`a` are in descending powers of z with a[0] the
185// leading coefficient. This is the building block scipy/MATLAB use for `lfilter`
186// and lets filtfilt() inject the steady-state initial conditions that keep a
187// constant (DC) input from producing a startup transient.
188std::vector<double> filterDF2T(const std::vector<double>& b, const std::vector<double>& a,
189 const std::vector<double>& x, std::vector<double> z) {
190 const size_t n = x.size();
191 const size_t order = std::max(b.size(), a.size()) - 1;
192
193 // Normalize coefficients by a[0] and right-pad to length order+1.
194 const double a0 = a[0];
195 std::vector<double> bn(order + 1, 0.0), an(order + 1, 0.0);
196 for (size_t i = 0; i < b.size(); ++i) bn[i] = b[i] / a0;
197 for (size_t i = 0; i < a.size(); ++i) an[i] = a[i] / a0;
198
199 z.resize(order, 0.0);
200
201 std::vector<double> y(n, 0.0);
202 for (size_t i = 0; i < n; ++i) {
203 const double xi = x[i];
204 const double yi = bn[0] * xi + z[0];
205 for (size_t j = 0; j + 1 < order; ++j) {
206 z[j] = bn[j + 1] * xi + z[j + 1] - an[j + 1] * yi;
207 }
208 if (order > 0) {
209 z[order - 1] = bn[order] * xi - an[order] * yi;
210 }
211 y[i] = yi;
212 }
213 return y;
214}
215
216// Steady-state initial conditions for a step input, scaled later by the first
217// sample. Mirrors scipy.signal.lfilter_zi: solving (I - A) zi = B for the
218// transposed companion form, expressed via the closed-form cumulative sums.
219std::vector<double> lfilterZi(const std::vector<double>& b_in, const std::vector<double>& a_in) {
220 const size_t order = std::max(b_in.size(), a_in.size()) - 1;
221 const double a0 = a_in[0];
222 std::vector<double> b(order + 1, 0.0), a(order + 1, 0.0);
223 for (size_t i = 0; i < b_in.size(); ++i) b[i] = b_in[i] / a0;
224 for (size_t i = 0; i < a_in.size(); ++i) a[i] = a_in[i] / a0;
225
226 std::vector<double> zi(order, 0.0);
227 if (order == 0) return zi;
228
229 double a_sum = 0.0, b_minus = 0.0;
230 for (size_t k = 0; k <= order; ++k) a_sum += a[k]; // sum(a)
231 for (size_t k = 1; k <= order; ++k) b_minus += b[k] - a[k] * b[0];
232 zi[0] = b_minus / a_sum;
233
234 double asum = 1.0, csum = 0.0;
235 for (size_t k = 1; k < order; ++k) {
236 asum += a[k];
237 csum += b[k] - a[k] * b[0];
238 zi[k] = asum * zi[0] - csum;
239 }
240 return zi;
241}
242
243} // namespace
244
245std::vector<double> filter(const std::vector<double>& b, const std::vector<double>& a,
246 const std::vector<double>& x) {
247 // Zero initial state.
248 return filterDF2T(b, a, x, {});
249}
250
251std::vector<double> filtfilt(const std::vector<double>& b, const std::vector<double>& a,
252 const std::vector<double>& x) {
253 if (x.empty()) return x;
254
255 // Steady-state initial conditions, scaled by the first/last sample of each
256 // pass. Without this, even a constant input leaves a large boundary
257 // transient (DC gain is met only asymptotically), so smoothing of a
258 // near-constant displacement series would be visibly wrong.
259 const std::vector<double> zi = lfilterZi(b, a);
260
261 // Forward pass.
262 std::vector<double> zi_fwd = zi;
263 for (auto& v : zi_fwd) v *= x.front();
264 std::vector<double> y_forward = filterDF2T(b, a, x, zi_fwd);
265
266 // Reverse, backward pass.
267 std::vector<double> y_reversed(y_forward.rbegin(), y_forward.rend());
268 std::vector<double> zi_bwd = zi;
269 for (auto& v : zi_bwd) v *= y_reversed.front();
270 std::vector<double> y_backward = filterDF2T(b, a, y_reversed, zi_bwd);
271
272 // Reverse back to restore original time order.
273 std::vector<double> y(y_backward.rbegin(), y_backward.rend());
274 return y;
275}
276
277} // namespace detail
278
279} // namespace cppxdic
std::vector< double > filter(const std::vector< double > &b, const std::vector< double > &a, const std::vector< double > &x)
Forward IIR filter.
std::pair< std::vector< double >, std::vector< double > > butterLowPass(int order, double wn)
Design Butterworth filter coefficients.
std::vector< double > filtfilt(const std::vector< double > &b, const std::vector< double > &a, const std::vector< double > &x)
Zero-phase digital filtering (MATLAB filtfilt equivalent)
DataSerializer - Abstract base class for format-agnostic DIC data I/O.
std::tuple< std::vector< std::vector< double > >, std::vector< std::vector< double > >, std::vector< std::vector< double > > > filterTime3D(const std::vector< std::vector< double > > &disp_x, const std::vector< std::vector< double > > &disp_y, const std::vector< std::vector< double > > &disp_z, double freq_filt=10.0, double freq_acq=50.0)
Apply temporal filtering to 3D displacement vectors.
std::vector< std::vector< double > > filterTime(const std::vector< std::vector< double > > &data, double freq_filt=10.0, double freq_acq=50.0)
Apply temporal Butterworth low-pass filter to displacement data.
std::vector< int > order