v2.0.0
Loading...
Searching...
No Matches
resample.cpp
Go to the documentation of this file.
1//=============================================================================================================
12
13//=============================================================================================================
14// INCLUDES
15//=============================================================================================================
16
17#include "resample.h"
18
19//=============================================================================================================
20// EIGEN INCLUDES
21//=============================================================================================================
22
23#include <Eigen/Core>
24
25//=============================================================================================================
26// QT INCLUDES
27//=============================================================================================================
28
29#include <QDebug>
30
31//=============================================================================================================
32// C++ INCLUDES
33//=============================================================================================================
34
35#include <cmath>
36#include <algorithm>
37
38//=============================================================================================================
39// USED NAMESPACES
40//=============================================================================================================
41
42using namespace UTILSLIB;
43using namespace Eigen;
44
45//=============================================================================================================
46// PRIVATE
47//=============================================================================================================
48
49int Resample::gcd(int a, int b)
50{
51 while (b) {
52 int t = b;
53 b = a % b;
54 a = t;
55 }
56 return a;
57}
58
59//=============================================================================================================
60
61RowVectorXd Resample::buildKernel(int p, int q, int iNZeros)
62{
63 int M = std::max(p, q);
64 int halfLen = iNZeros * M;
65 int L = 2 * halfLen + 1;
66
67 // Cutoff as a fraction of the sampling frequency at the upsampled rate p*oldSFreq.
68 // We want to cut at the lower Nyquist: min(oldSFreq, newSFreq)/2.
69 // In normalised terms (fraction of upsampled fs):
70 // cutoff_norm = min(p,q) / (2.0 * max(p,q)) ... but the conventional sinc parameterisation
71 // uses cutoff as a fraction of fs (not Nyquist), i.e. in [0,0.5].
72 // So fc_fs = min(p,q) / (2.0 * max(p,q)).
73 const double fc = static_cast<double>(std::min(p, q)) / (2.0 * static_cast<double>(M));
74
75 RowVectorXd h(L);
76 for (int k = 0; k < L; ++k) {
77 double n = k - halfLen;
78 double win = 0.54 - 0.46 * std::cos(2.0 * M_PI * k / (L - 1)); // Hamming
79
80 if (std::abs(n) < 1e-10) {
81 h(k) = 2.0 * fc * win;
82 } else {
83 // sinc(2*fc*n) * 2*fc = sin(2π*fc*n) / (π*n)
84 h(k) = std::sin(2.0 * M_PI * fc * n) / (M_PI * n) * win;
85 }
86 }
87
88 // Scale by p to restore unity gain after the conceptual upsampling-by-p step.
89 h *= static_cast<double>(p);
90 return h;
91}
92
93//=============================================================================================================
94
95RowVectorXd Resample::polyphaseConv(const RowVectorXd& vecX,
96 const RowVectorXd& vecH,
97 int p,
98 int q,
99 int halfLen)
100{
101 const long long nIn = static_cast<long long>(vecX.size());
102 const long long L = static_cast<long long>(vecH.size()); // = 2*halfLen + 1
103
104 // Output length: ceil(nIn * p / q)
105 const long long nOut = (nIn * p + q - 1) / q;
106
107 RowVectorXd y(nOut);
108
109 for (long long m = 0; m < nOut; ++m) {
110 // The filter delay (halfLen upsampled samples) is absorbed into the center index so that
111 // output sample 0 aligns with input sample 0.
112 long long center = m * static_cast<long long>(q) + halfLen;
113
114 // Iterate over all input indices j whose upsampled position j*p lies within
115 // the filter window [center - L + 1, center].
116 // That is: j*p in [center - L + 1, center]
117 // => j in [ceil((center-L+1)/p), floor(center/p)] ∩ [0, nIn-1]
118 long long j_min = (center - L + 1 + p - 1) / p; // ceil division
119 if (j_min < 0) j_min = 0;
120 long long j_max = center / p;
121 if (j_max >= nIn) j_max = nIn - 1;
122
123 double val = 0.0;
124 for (long long j = j_min; j <= j_max; ++j) {
125 long long tap = center - j * static_cast<long long>(p);
126 if (tap >= 0 && tap < L) {
127 val += vecH(static_cast<int>(tap)) * vecX(static_cast<int>(j));
128 }
129 }
130 y(static_cast<int>(m)) = val;
131 }
132
133 return y;
134}
135
136//=============================================================================================================
137// PUBLIC
138//=============================================================================================================
139
140RowVectorXd Resample::resample(const RowVectorXd& vecData,
141 double dNewSFreq,
142 double dOldSFreq,
143 int iNZeros)
144{
145 if (vecData.size() == 0) {
146 qWarning() << "Resample::resample: empty input.";
147 return vecData;
148 }
149 if (dNewSFreq <= 0.0 || dOldSFreq <= 0.0) {
150 qWarning() << "Resample::resample: sampling frequencies must be positive.";
151 return vecData;
152 }
153
154 // Represent ratio as integers to avoid floating-point drift.
155 // Multiply both rates by 1000 and reduce by GCD (handles e.g. 600.0/1000.0 → 3/5).
156 const int scale = 1000;
157 int p_raw = static_cast<int>(std::round(dNewSFreq * scale));
158 int q_raw = static_cast<int>(std::round(dOldSFreq * scale));
159 int g = gcd(p_raw, q_raw);
160 int p = p_raw / g;
161 int q = q_raw / g;
162
163 if (p == q) {
164 return vecData; // Same rate after reduction
165 }
166
167 const int halfLen = iNZeros * std::max(p, q);
168 RowVectorXd h = buildKernel(p, q, iNZeros);
169
170 return polyphaseConv(vecData, h, p, q, halfLen);
171}
172
173//=============================================================================================================
174
175MatrixXd Resample::resampleMatrix(const MatrixXd& matData,
176 double dNewSFreq,
177 double dOldSFreq,
178 const RowVectorXi& vecPicks,
179 int iNZeros)
180{
181 if (matData.size() == 0) return matData;
182
183 // Pre-build kernel once for all channels
184 const int scale = 1000;
185 int p_raw = static_cast<int>(std::round(dNewSFreq * scale));
186 int q_raw = static_cast<int>(std::round(dOldSFreq * scale));
187 int g = gcd(p_raw, q_raw);
188 int p = p_raw / g;
189 int q = q_raw / g;
190
191 if (p == q) return matData;
192
193 const int halfLen = iNZeros * std::max(p, q);
194 RowVectorXd h = buildKernel(p, q, iNZeros);
195
196 const int nIn = static_cast<int>(matData.cols());
197 const int nOut = static_cast<int>((static_cast<long long>(nIn) * p + q - 1) / q);
198 const int nCh = static_cast<int>(matData.rows());
199
200 MatrixXd result(nCh, nOut);
201
202 if (vecPicks.size() == 0) {
203 for (int i = 0; i < nCh; ++i) {
204 result.row(i) = polyphaseConv(matData.row(i), h, p, q, halfLen);
205 }
206 } else {
207 // Initialise to zero then fill picked rows (non-picked rows remain zero —
208 // callers using picks should be aware rows are not copied; use full resampling
209 // if a copy of non-MEG channels is needed).
210 result.setZero();
211 for (int k = 0; k < vecPicks.size(); ++k) {
212 int i = vecPicks(k);
213 if (i >= 0 && i < nCh) {
214 result.row(i) = polyphaseConv(matData.row(i), h, p, q, halfLen);
215 }
216 }
217 }
218
219 return result;
220}
#define M_PI
Declaration of Resample — polyphase anti-aliased rational resampling for MEG/EEG data.
Shared utilities (I/O helpers, spectral analysis, layout management, warp algorithms).
static Eigen::RowVectorXd resample(const Eigen::RowVectorXd &vecData, double dNewSFreq, double dOldSFreq, int iNZeros=10)
Definition resample.cpp:140
static Eigen::MatrixXd resampleMatrix(const Eigen::MatrixXd &matData, double dNewSFreq, double dOldSFreq, const Eigen::RowVectorXi &vecPicks=Eigen::RowVectorXi(), int iNZeros=10)
Definition resample.cpp:175