v2.0.0
Loading...
Searching...
No Matches
maxwell_movement_comp.cpp
Go to the documentation of this file.
1//=============================================================================================================
12
13//=============================================================================================================
14// INCLUDES
15//=============================================================================================================
16
18
19//=============================================================================================================
20// QT INCLUDES
21//=============================================================================================================
22
23#include <QFile>
24#include <QTextStream>
25#include <QDebug>
26#include <QRegularExpression>
27
28//=============================================================================================================
29// USED NAMESPACES
30//=============================================================================================================
31
32using namespace UTILSLIB;
33using namespace FIFFLIB;
34using namespace Eigen;
35
36//=============================================================================================================
37// DEFINE MEMBER METHODS
38//=============================================================================================================
39
40FiffInfo MaxwellMovementComp::transformFiffInfo(const FiffInfo& fiffInfo,
41 const HeadPosEntry& headPosRef,
42 const HeadPosEntry& headPosCurrent)
43{
44 FiffInfo info(fiffInfo);
45
46 // Compute relative rotation: from current → ref
47 // R_rel = R_ref * R_current^-1
48 Matrix3d rotRef = headPosRef.rotation.toRotationMatrix();
49 Matrix3d rotCur = headPosCurrent.rotation.toRotationMatrix();
50 Matrix3d rotRel = rotRef * rotCur.transpose();
51
52 // Translation: t_rel = t_ref - R_rel * t_current
53 Vector3d tRel = headPosRef.translation - rotRel * headPosCurrent.translation;
54
55 // Apply transform to MEG channel positions
56 for (int i = 0; i < info.chs.size(); ++i) {
57 if (info.chs[i].kind == FIFFV_MEG_CH || info.chs[i].kind == FIFFV_REF_MEG_CH) {
58 // Transform position
59 Vector3f r0 = info.chs[i].chpos.r0;
60 Vector3d r0d = r0.cast<double>();
61 r0d = rotRel * r0d + tRel;
62 info.chs[i].chpos.r0 = r0d.cast<float>();
63
64 // Transform orientation vectors
65 Vector3f ex = info.chs[i].chpos.ex;
66 Vector3d exd = ex.cast<double>();
67 exd = rotRel * exd;
68 info.chs[i].chpos.ex = exd.cast<float>();
69
70 Vector3f ey = info.chs[i].chpos.ey;
71 Vector3d eyd = ey.cast<double>();
72 eyd = rotRel * eyd;
73 info.chs[i].chpos.ey = eyd.cast<float>();
74
75 Vector3f ez = info.chs[i].chpos.ez;
76 Vector3d ezd = ez.cast<double>();
77 ezd = rotRel * ezd;
78 info.chs[i].chpos.ez = ezd.cast<float>();
79 }
80 }
81
82 return info;
83}
84
85//=============================================================================================================
86
87MatrixXd MaxwellMovementComp::apply(const MatrixXd& matData,
88 const FiffInfo& fiffInfo,
89 const QList<HeadPosEntry>& headPos,
90 double dSFreq,
91 const MaxwellMoveCompParams& params)
92{
93 if (headPos.isEmpty()) {
94 qWarning() << "[MaxwellMovementComp::apply] No head positions provided.";
95 return matData;
96 }
97
98 const int nChannels = matData.rows();
99 const int nSamples = matData.cols();
100
101 // Determine reference head position
102 HeadPosEntry refPos;
103 if (params.iRefIdx >= 0 && params.iRefIdx < headPos.size()) {
104 refPos = headPos[params.iRefIdx];
105 } else {
106 // Mean position
107 Vector3d meanTrans = Vector3d::Zero();
108 for (const auto& hp : headPos) {
109 meanTrans += hp.translation;
110 }
111 meanTrans /= headPos.size();
112 refPos.translation = meanTrans;
113 refPos.rotation = headPos[0].rotation; // Use first rotation as reference
114 }
115
116 // Compute reference SSS basis
117 SSSParams sssParams;
118 sssParams.iOrderIn = params.iOrderIn;
119 sssParams.iOrderOut = params.iOrderOut;
120 sssParams.origin = params.origin;
121 sssParams.dRegIn = params.dRegIn;
122
123 SSS::Basis basisRef = SSS::computeBasis(fiffInfo, sssParams);
124
125 MatrixXd result = matData;
126
127 // Process each time segment
128 for (int iSeg = 0; iSeg < headPos.size(); ++iSeg) {
129 // Determine sample range for this segment
130 int iStart = static_cast<int>(headPos[iSeg].dTime * dSFreq);
131 int iEnd;
132 if (iSeg + 1 < headPos.size()) {
133 iEnd = static_cast<int>(headPos[iSeg + 1].dTime * dSFreq);
134 } else {
135 iEnd = nSamples;
136 }
137
138 iStart = qBound(0, iStart, nSamples);
139 iEnd = qBound(iStart, iEnd, nSamples);
140 if (iStart >= iEnd) continue;
141
142 int nSegSamples = iEnd - iStart;
143
144 // Transform FiffInfo to current head position
145 FiffInfo infoCurrentToRef = transformFiffInfo(fiffInfo, refPos, headPos[iSeg]);
146
147 // Compute SSS basis at current head position (as seen from reference)
148 SSS::Basis basisCurrent = SSS::computeBasis(infoCurrentToRef, sssParams);
149
150 // Extract segment data
151 MatrixXd segData = matData.middleCols(iStart, nSegSamples);
152
153 // Project into multipole space using current-position basis
154 // multipoles = pinv_current * data_meg
155 MatrixXd megData(basisCurrent.megChannelIdx.size(), nSegSamples);
156 for (int ch = 0; ch < basisCurrent.megChannelIdx.size(); ++ch) {
157 megData.row(ch) = segData.row(basisCurrent.megChannelIdx[ch]);
158 }
159
160 MatrixXd multipoles = basisCurrent.matPinvAll * megData;
161
162 // Reconstruct at reference position using only internal multipoles
163 MatrixXd reconstructed = basisRef.matSin * multipoles.topRows(basisRef.iNin);
164
165 // Place reconstructed data back
166 for (int ch = 0; ch < basisRef.megChannelIdx.size(); ++ch) {
167 result.row(basisRef.megChannelIdx[ch]).segment(iStart, nSegSamples) = reconstructed.row(ch);
168 }
169 }
170
171 return result;
172}
173
174//=============================================================================================================
175
176QList<HeadPosEntry> MaxwellMovementComp::readHeadPos(const QString& sPath)
177{
178 QList<HeadPosEntry> positions;
179
180 QFile file(sPath);
181 if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
182 qWarning() << "[MaxwellMovementComp::readHeadPos] Cannot open file:" << sPath;
183 return positions;
184 }
185
186 QTextStream in(&file);
187 while (!in.atEnd()) {
188 QString line = in.readLine().trimmed();
189 if (line.isEmpty() || line.startsWith('#')) continue;
190
191 QStringList parts = line.split(QRegularExpression("\\s+"), Qt::SkipEmptyParts);
192 if (parts.size() < 7) continue;
193
194 bool ok = false;
195 HeadPosEntry entry;
196 entry.dTime = parts[0].toDouble(&ok); if (!ok) continue;
197
198 // Quaternion (scalar-last or scalar-first depends on convention)
199 double q1 = parts[1].toDouble(&ok); if (!ok) continue;
200 double q2 = parts[2].toDouble(&ok); if (!ok) continue;
201 double q3 = parts[3].toDouble(&ok); if (!ok) continue;
202
203 double tx = parts[4].toDouble(&ok); if (!ok) continue;
204 double ty = parts[5].toDouble(&ok); if (!ok) continue;
205 double tz = parts[6].toDouble(&ok); if (!ok) continue;
206
207 if (parts.size() >= 8) {
208 entry.dGof = parts[7].toDouble();
209 }
210
211 // MNE convention: quaternion stored as (q1, q2, q3) with q0 computed
212 double q0sq = 1.0 - q1*q1 - q2*q2 - q3*q3;
213 double q0 = (q0sq > 0.0) ? std::sqrt(q0sq) : 0.0;
214 entry.rotation = Quaterniond(q0, q1, q2, q3);
215 entry.translation = Vector3d(tx, ty, tz);
216
217 positions.append(entry);
218 }
219
220 file.close();
221 return positions;
222}
223
224//=============================================================================================================
225
226bool MaxwellMovementComp::writeHeadPos(const QString& sPath,
227 const QList<HeadPosEntry>& headPos)
228{
229 QFile file(sPath);
230 if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
231 qWarning() << "[MaxwellMovementComp::writeHeadPos] Cannot open file:" << sPath;
232 return false;
233 }
234
235 QTextStream out(&file);
236 out << "# Head position file\n";
237 out << "# time q1 q2 q3 tx ty tz gof\n";
238
239 for (const auto& entry : headPos) {
240 // Store quaternion as (q1, q2, q3), dropping q0 (recoverable)
241 out << QString::number(entry.dTime, 'f', 6) << " "
242 << QString::number(entry.rotation.x(), 'f', 6) << " "
243 << QString::number(entry.rotation.y(), 'f', 6) << " "
244 << QString::number(entry.rotation.z(), 'f', 6) << " "
245 << QString::number(entry.translation.x(), 'f', 6) << " "
246 << QString::number(entry.translation.y(), 'f', 6) << " "
247 << QString::number(entry.translation.z(), 'f', 6) << " "
248 << QString::number(entry.dGof, 'f', 6) << "\n";
249 }
250
251 file.close();
252 return true;
253}
#define FIFFV_REF_MEG_CH
#define FIFFV_MEG_CH
Maxwell movement compensation for MEG.
FIFF file I/O, in-memory data structures and high-level readers/writers.
Shared utilities (I/O helpers, spectral analysis, layout management, warp algorithms).
A head position entry (time, translation, rotation quaternion).
Parameters for Maxwell movement compensation.
static Eigen::MatrixXd apply(const Eigen::MatrixXd &matData, const FIFFLIB::FiffInfo &fiffInfo, const QList< HeadPosEntry > &headPos, double dSFreq, const MaxwellMoveCompParams &params=MaxwellMoveCompParams())
Apply movement compensation via SSS.
static bool writeHeadPos(const QString &sPath, const QList< HeadPosEntry > &headPos)
Write head positions to a file.
static QList< HeadPosEntry > readHeadPos(const QString &sPath)
Read head positions from a file.
Implements Signal Space Separation (SSS) and temporal SSS (tSSS) for MEG data.
Definition sss.h:100
double dRegIn
Definition sss.h:104
Eigen::Vector3d origin
Definition sss.h:103
static Basis computeBasis(const FIFFLIB::FiffInfo &fiffInfo, const Params &params=Params())
Definition sss.cpp:280
Precomputed SSS basis and projectors for a given sensor array.
Definition sss.h:119
Eigen::MatrixXd matSin
Definition sss.h:120
Eigen::MatrixXd matPinvAll
Definition sss.h:123
QVector< int > megChannelIdx
Definition sss.h:124
Full FIFF measurement info: per-channel descriptors, sampling and filter setup, projectors,...
Definition fiff_info.h:88