v2.0.0
Loading...
Searching...
No Matches
inv_hpi_fit.cpp
Go to the documentation of this file.
1//=============================================================================================================
20
21//=============================================================================================================
22// INCLUDES
23//=============================================================================================================
24
25#include "inv_hpi_fit.h"
26#include "inv_hpi_fit_data.h"
27#include "inv_sensor_set.h"
29#include "inv_signal_model.h"
31
32#include <utils/ioutils.h>
33
34// MSVC builds already define _USE_MATH_DEFINES globally (see src/CMakeLists.txt),
35// so define it here only for the toolchains that do not.
36#ifndef _USE_MATH_DEFINES
37#define _USE_MATH_DEFINES
38#endif
39#include <cmath>
40#include <vector>
41#include <numeric>
42#include <fiff/fiff_cov.h>
44#include <fstream>
45
46#include <fwd/fwd_coil_set.h>
47
48//=============================================================================================================
49// EIGEN INCLUDES
50//=============================================================================================================
51
52#include <Eigen/Dense>
53
54//=============================================================================================================
55// QT INCLUDES
56//=============================================================================================================
57
58#include <QVector>
59#include <QFuture>
60#include <QtConcurrent/QtConcurrent>
61
62//=============================================================================================================
63// USED NAMESPACES
64//=============================================================================================================
65
66using namespace Eigen;
67using namespace INVLIB;
68using namespace FIFFLIB;
69using namespace FWDLIB;
70
71//=============================================================================================================
72// DEFINE GLOBAL METHODS
73//=============================================================================================================
74
75//=============================================================================================================
76// DEFINE MEMBER METHODS
77//=============================================================================================================
78
79//=============================================================================================================
80
82: m_sensors(sensorSet)
83, m_signalModel(InvSignalModel())
84{
85}
86
87//=============================================================================================================
88
90{
91 if (m_sensors != sensorSet) {
92 m_sensors = sensorSet;
93 }
94}
95
96//=============================================================================================================
97
98void InvHpiFit::fit(const MatrixXd& matProjectedData,
99 const MatrixXd& matProjectors,
100 const InvHpiModelParameters& hpiModelParameters,
101 const MatrixXd& matCoilsHead,
102 HpiFitResult& hpiFitResult)
103{
104 fit(matProjectedData, matProjectors, hpiModelParameters, matCoilsHead, false, hpiFitResult);
105}
106
107//=============================================================================================================
108
109void InvHpiFit::fit(const MatrixXd& matProjectedData,
110 const MatrixXd& matProjectors,
111 const InvHpiModelParameters& hpiModelParameters,
112 const MatrixXd& matCoilsHead,
113 const bool bOrderFrequencies,
114 HpiFitResult& hpiFitResult)
115{
116 if (matProjectedData.rows() != matProjectors.rows()) {
117 qWarning("InvHpiFit::fit - Projector and data dimensions do not match. Returning.");
118 return;
119 } else if (hpiModelParameters.iNHpiCoils() != matCoilsHead.rows()) {
120 qWarning("InvHpiFit::fit - Number of coils and hpi digitizers do not match. Returning.");
121 return;
122 } else if (matProjectedData.rows() == 0 || matProjectors.rows() == 0) {
123 qWarning("InvHpiFit::fit - No data or Projectors passed. Returning.");
124 return;
125 } else if (m_sensors.ncoils() != matProjectedData.rows()) {
126 qWarning("InvHpiFit::fit - Number of channels in sensors and data do not match. Returning.");
127 return;
128 }
129
130 const MatrixXd matAmplitudes = computeAmplitudes(matProjectedData,
131 hpiModelParameters);
132
133 const MatrixXd matCoilsSeed = computeSeedPoints(matAmplitudes,
134 hpiFitResult.devHeadTrans,
135 hpiFitResult.errorDistances,
136 matCoilsHead);
137
138 CoilParam fittedCoilParams = dipfit(matCoilsSeed,
139 m_sensors,
140 matAmplitudes,
141 hpiModelParameters.iNHpiCoils(),
142 matProjectors,
143 500,
144 1e-9f);
145
146 if (bOrderFrequencies) {
147 const std::vector<int> vecOrder = findCoilOrder(fittedCoilParams.pos,
148 matCoilsHead);
149
150 fittedCoilParams.pos = order(vecOrder, fittedCoilParams.pos);
151 hpiFitResult.hpiFreqs = order(vecOrder, hpiModelParameters.vecHpiFreqs());
152 }
153
154 hpiFitResult.GoF = computeGoF(fittedCoilParams.dpfiterror);
155
156 hpiFitResult.fittedCoils = getFittedPointSet(fittedCoilParams.pos);
157
158 hpiFitResult.devHeadTrans = computeDeviceHeadTransformation(fittedCoilParams.pos,
159 matCoilsHead);
160
161 hpiFitResult.errorDistances = computeEstimationError(fittedCoilParams.pos,
162 matCoilsHead,
163 hpiFitResult.devHeadTrans);
164}
165
166//=============================================================================================================
167
168Eigen::MatrixXd InvHpiFit::computeAmplitudes(const Eigen::MatrixXd& matProjectedData,
169 const InvHpiModelParameters& hpiModelParameters)
170{
171 // fit model
172 MatrixXd matTopo = m_signalModel.fitData(hpiModelParameters, matProjectedData);
173 matTopo.transposeInPlace();
174
175 // split into sine and cosine amplitudes
176 const int iNumCoils = hpiModelParameters.iNHpiCoils();
177
178 MatrixXd matAmpSine(matProjectedData.cols(), iNumCoils);
179 MatrixXd matAmpCosine(matProjectedData.cols(), iNumCoils);
180
181 matAmpSine = matTopo.leftCols(iNumCoils);
182 matAmpCosine = matTopo.middleCols(iNumCoils, iNumCoils);
183
184 // Select sine or cosine component depending on their contributions to the amplitudes
185 for (int j = 0; j < iNumCoils; ++j) {
186 float fNS = 0.0;
187 float fNC = 0.0;
188 fNS = matAmpSine.col(j).array().square().sum();
189 fNC = matAmpCosine.col(j).array().square().sum();
190 if (fNC > fNS) {
191 matAmpSine.col(j) = matAmpCosine.col(j);
192 }
193 }
194
195 return matAmpSine;
196}
197
198//=============================================================================================================
199
200Eigen::MatrixXd InvHpiFit::computeSeedPoints(const Eigen::MatrixXd& matAmplitudes,
201 const FIFFLIB::FiffCoordTrans& transDevHead,
202 const QVector<double>& vecError,
203 const Eigen::MatrixXd& matCoilsHead)
204{
205 const int iNumCoils = matCoilsHead.rows();
206 MatrixXd matCoilsSeed = MatrixXd::Zero(iNumCoils, 3);
207
208 const double dError = std::accumulate(vecError.begin(), vecError.end(), .0) / vecError.size();
209
210 if (transDevHead.trans != MatrixXd::Identity(4, 4).cast<float>() && dError < 0.010) {
211 // if good last fit, use old trafo
212 matCoilsSeed = transDevHead.apply_inverse_trans(matCoilsHead.cast<float>()).cast<double>();
213 } else {
214 // if not, find max amplitudes in channels
215 VectorXi vecChIdcs(iNumCoils);
216
217 for (int j = 0; j < iNumCoils; j++) {
218 int iChIdx = 0;
219 VectorXd::Index indMax;
220 matAmplitudes.col(j).maxCoeff(&indMax);
221 if (indMax < m_sensors.ncoils()) {
222 iChIdx = indMax;
223 }
224 vecChIdcs(j) = iChIdx;
225 }
226 // and go 3 cm inwards from max channels
227 for (int j = 0; j < vecChIdcs.rows(); ++j) {
228 if (vecChIdcs(j) < m_sensors.ncoils()) {
229 Vector3d r0 = m_sensors.r0(vecChIdcs(j));
230 Vector3d ez = m_sensors.ez(vecChIdcs(j));
231 matCoilsSeed.row(j) = (-1 * ez * 0.03 + r0).transpose();
232 }
233 }
234 }
235 return matCoilsSeed;
236}
237
238//=============================================================================================================
239
240CoilParam InvHpiFit::dipfit(const MatrixXd matCoilsSeed,
241 const InvSensorSet& sensors,
242 const MatrixXd& matData,
243 const int iNumCoils,
244 const MatrixXd& matProjectors,
245 const int iMaxIterations,
246 const float fAbortError)
247{
248 //Do this in conncurrent mode
249 //Generate QList structure which can be handled by the QConcurrent framework
250 QList<InvHpiFitData> lCoilData;
251
252 for (qint32 i = 0; i < iNumCoils; ++i) {
253 InvHpiFitData coilData;
254 coilData.m_coilPos = matCoilsSeed.row(i);
255 coilData.m_sensorData = matData.col(i);
256 coilData.m_sensors = sensors;
257 coilData.m_matProjector = matProjectors;
258 coilData.m_iMaxIterations = iMaxIterations;
259 coilData.m_fAbortError = fAbortError;
260
261 lCoilData.append(coilData);
262 }
263 //Do the concurrent filtering
264 CoilParam coil(iNumCoils);
265
266 if (!lCoilData.isEmpty()) {
267 // //Do sequential
268 // for(int l = 0; l < lCoilData.size(); ++l) {
269 // doDipfitConcurrent(lCoilData[l]);
270 // }
271
272 //Do concurrent
273 QFuture<void> future = QtConcurrent::map(lCoilData,
275 future.waitForFinished();
276
277 //Transform results to final coil information
278 for (qint32 i = 0; i < lCoilData.size(); ++i) {
279 coil.pos.row(i) = lCoilData.at(i).m_coilPos;
280 coil.mom = lCoilData.at(i).m_errorInfo.moment.transpose();
281 coil.dpfiterror(i) = lCoilData.at(i).m_errorInfo.error;
282 coil.dpfitnumitr(i) = lCoilData.at(i).m_errorInfo.numIterations;
283 }
284 }
285 return coil;
286}
287
288//=============================================================================================================
289
290std::vector<int> InvHpiFit::findCoilOrder(const MatrixXd& matCoilsDev,
291 const MatrixXd& matCoilsHead)
292{
293 // extract digitized and fitted coils
294 MatrixXd matCoilTemp = matCoilsDev;
295 const int iNumCoils = matCoilsDev.rows();
296
297 std::vector<int> vecOrder(iNumCoils);
298 std::iota(vecOrder.begin(), vecOrder.end(), 0);
299 ;
300
301 // maximum 10 mm mean error
302 const double dErrorMin = 0.010;
303 double dErrorActual = 0.0;
304 double dErrorBest = dErrorMin;
305
306 MatrixXd matTrans(4, 4);
307 std::vector<int> vecOrderBest = vecOrder;
308
309 // permutation
310 do {
311 for (int i = 0; i < iNumCoils; i++) {
312 matCoilTemp.row(i) = matCoilsDev.row(vecOrder[i]);
313 }
314 matTrans = computeTransformation(matCoilsHead, matCoilTemp);
315 dErrorActual = objectTrans(matCoilsHead, matCoilTemp, matTrans);
316 if (dErrorActual < dErrorMin && dErrorActual < dErrorBest) {
317 // exit
318 dErrorBest = dErrorActual;
319 vecOrderBest = vecOrder;
320 }
321 } while (std::next_permutation(vecOrder.begin(), vecOrder.end()));
322 return vecOrderBest;
323}
324
325//=============================================================================================================
326
327double InvHpiFit::objectTrans(const MatrixXd& matHeadCoil,
328 const MatrixXd& matCoil,
329 const MatrixXd& matTrans)
330{
331 // Compute the fiducial registration error - the lower, the better.
332 const int iNumCoils = matHeadCoil.rows();
333 MatrixXd matTemp = matCoil;
334
335 // homogeneous coordinates
336 matTemp.conservativeResize(matCoil.rows(), matCoil.cols() + 1);
337 matTemp.block(0, 3, iNumCoils, 1).setOnes();
338 matTemp.transposeInPlace();
339
340 // apply transformation
341 MatrixXd matTestPos = matTrans * matTemp;
342
343 // remove
344 MatrixXd matDiff = matTestPos.block(0, 0, 3, iNumCoils) - matHeadCoil.transpose();
345 VectorXd vecError = matDiff.colwise().norm();
346
347 // compute error
348 double dError = matDiff.colwise().norm().mean();
349 ;
350
351 return dError;
352}
353
354//=============================================================================================================
355
356Eigen::MatrixXd InvHpiFit::order(const std::vector<int>& vecOrder,
357 const Eigen::MatrixXd& matToOrder)
358{
359 const int iNumCoils = static_cast<int>(vecOrder.size());
360 MatrixXd matToOrderTemp = matToOrder;
361
362 for (int i = 0; i < iNumCoils; i++) {
363 matToOrderTemp.row(i) = matToOrder.row(vecOrder[i]);
364 }
365 return matToOrderTemp;
366}
367
368//=============================================================================================================
369
370QVector<int> InvHpiFit::order(const std::vector<int>& vecOrder,
371 const QVector<int>& vecToOrder)
372{
373 const int iNumCoils = static_cast<int>(vecOrder.size());
374 QVector<int> vecToOrderTemp = vecToOrder;
375
376 for (int i = 0; i < iNumCoils; i++) {
377 vecToOrderTemp[i] = vecToOrder[vecOrder[i]];
378 }
379 return vecToOrderTemp;
380}
381
382//=============================================================================================================
383
384Eigen::VectorXd InvHpiFit::computeGoF(const Eigen::VectorXd& vecDipFitError)
385{
386 VectorXd vecGoF(vecDipFitError.size());
387 for (int i = 0; i < vecDipFitError.size(); ++i) {
388 vecGoF(i) = 1 - vecDipFitError(i);
389 }
390 return vecGoF;
391}
392
393//=============================================================================================================
394
395FIFFLIB::FiffCoordTrans InvHpiFit::computeDeviceHeadTransformation(const Eigen::MatrixXd& matCoilsDev,
396 const Eigen::MatrixXd& matCoilsHead)
397{
398 const MatrixXd matTrans = computeTransformation(matCoilsHead, matCoilsDev);
399 return FiffCoordTrans(1, 4, matTrans.cast<float>(), true);
400}
401
402//=============================================================================================================
403
404Eigen::Matrix4d InvHpiFit::computeTransformation(Eigen::MatrixXd matNH, MatrixXd matBT)
405{
406 MatrixXd matXdiff, matYdiff, matZdiff, matC, matQ;
407 Matrix4d matTransFinal = Matrix4d::Identity(4, 4);
408 Matrix4d matRot = Matrix4d::Zero(4, 4);
409 Matrix4d matTrans = Matrix4d::Identity(4, 4);
410 double dMeanX, dMeanY, dMeanZ;
411
412 for (int i = 0; i < 15; ++i) {
413 // Calculate mean translation for all points -> centroid of both data sets
414 matXdiff = matNH.col(0) - matBT.col(0);
415 matYdiff = matNH.col(1) - matBT.col(1);
416 matZdiff = matNH.col(2) - matBT.col(2);
417
418 dMeanX = matXdiff.mean();
419 dMeanY = matYdiff.mean();
420 dMeanZ = matZdiff.mean();
421
422 // Apply translation -> bring both data sets to the same center location
423 for (int j = 0; j < matBT.rows(); ++j) {
424 matBT(j, 0) = matBT(j, 0) + dMeanX;
425 matBT(j, 1) = matBT(j, 1) + dMeanY;
426 matBT(j, 2) = matBT(j, 2) + dMeanZ;
427 }
428
429 // Estimate rotation component
430 matC = matBT.transpose() * matNH;
431
432 JacobiSVD<MatrixXd> svd(matC, Eigen::ComputeThinU | ComputeThinV);
433
434 matQ = svd.matrixU() * svd.matrixV().transpose();
435
436 //Handle special reflection case
437 if (matQ.determinant() < 0) {
438 matQ(0, 2) = matQ(0, 2) * -1;
439 matQ(1, 2) = matQ(1, 2) * -1;
440 matQ(2, 2) = matQ(2, 2) * -1;
441 }
442
443 // Apply rotation on translated points
444 matBT = matBT * matQ;
445
446 // Store rotation part to transformation matrix
447 matRot(3, 3) = 1;
448 for (int j = 0; j < 3; ++j) {
449 for (int k = 0; k < 3; ++k) {
450 matRot(j, k) = matQ(k, j);
451 }
452 }
453
454 // Store translation part to transformation matrix
455 matTrans(0, 3) = dMeanX;
456 matTrans(1, 3) = dMeanY;
457 matTrans(2, 3) = dMeanZ;
458
459 // Safe rotation and translation to final matrix for next iteration step
460 // This step is safe to do since we change one of the input point sets (matBT)
461 // ToDo: Replace this for loop with a least square solution process
462 matTransFinal = matRot * matTrans * matTransFinal;
463 }
464 return matTransFinal;
465}
466
467//=============================================================================================================
468
469QVector<double> InvHpiFit::computeEstimationError(const Eigen::MatrixXd& matCoilsDev,
470 const Eigen::MatrixXd& matCoilsHead,
471 const FIFFLIB::FiffCoordTrans& transDevHead)
472{
473 //Calculate Error
474 MatrixXd matTemp = matCoilsDev;
475 MatrixXd matTestPos = transDevHead.apply_trans(matTemp.cast<float>()).cast<double>();
476 MatrixXd matDiffPos = matTestPos - matCoilsHead;
477
478 // compute error
479 int iNumCoils = matCoilsDev.rows();
480 QVector<double> vecError(iNumCoils);
481 for (int i = 0; i < matDiffPos.rows(); ++i) {
482 vecError[i] = matDiffPos.row(i).norm();
483 }
484 return vecError;
485}
486
487//=============================================================================================================
488
489FIFFLIB::FiffDigPointSet InvHpiFit::getFittedPointSet(const Eigen::MatrixXd& matCoilsDev)
490{
491 FiffDigPointSet fittedPointSet;
492 const int iNumCoils = matCoilsDev.rows();
493
494 for (int i = 0; i < iNumCoils; ++i) {
495 FiffDigPoint digPoint;
496 digPoint.kind = FIFFV_POINT_EEG; //Store as EEG so they have a different color
497 digPoint.ident = i;
498 digPoint.r[0] = matCoilsDev(i, 0);
499 digPoint.r[1] = matCoilsDev(i, 1);
500 digPoint.r[2] = matCoilsDev(i, 2);
501
502 fittedPointSet << digPoint;
503 }
504 return fittedPointSet;
505}
506
507//=============================================================================================================
508
510 const Eigen::MatrixXf& transDevHead,
511 Eigen::MatrixXd& matPosition,
512 const Eigen::VectorXd& vecGoF,
513 const QVector<double>& vecError)
514
515{
516 Matrix3f matRot = transDevHead.block(0, 0, 3, 3);
517
518 Eigen::Quaternionf quatHPI(matRot);
519 double dError = std::accumulate(vecError.begin(), vecError.end(), .0) / vecError.size(); // HPI estimation Error
520
521 matPosition.conservativeResize(matPosition.rows() + 1, 10);
522 matPosition(matPosition.rows() - 1, 0) = fTime;
523 matPosition(matPosition.rows() - 1, 1) = quatHPI.x();
524 matPosition(matPosition.rows() - 1, 2) = quatHPI.y();
525 matPosition(matPosition.rows() - 1, 3) = quatHPI.z();
526 matPosition(matPosition.rows() - 1, 4) = transDevHead(0, 3);
527 matPosition(matPosition.rows() - 1, 5) = transDevHead(1, 3);
528 matPosition(matPosition.rows() - 1, 6) = transDevHead(2, 3);
529 matPosition(matPosition.rows() - 1, 7) = vecGoF.mean();
530 matPosition(matPosition.rows() - 1, 8) = dError;
531 matPosition(matPosition.rows() - 1, 9) = 0;
532}
533
534//=============================================================================================================
535
536bool InvHpiFit::compareTransformation(const MatrixX4f& mDevHeadT,
537 const MatrixX4f& mDevHeadDest,
538 const float& fThreshRot,
539 const float& fThreshTrans)
540{
541 bool bState = false;
542
543 Matrix3f mRot = mDevHeadT.block(0, 0, 3, 3);
544 Matrix3f mRotDest = mDevHeadDest.block(0, 0, 3, 3);
545
546 VectorXf vTrans = mDevHeadT.block(0, 3, 3, 1);
547 VectorXf vTransDest = mDevHeadDest.block(0, 3, 3, 1);
548
549 Quaternionf quat(mRot);
550 Quaternionf quatNew(mRotDest);
551
552 // Compare Rotation
553 float fAngle = quat.angularDistance(quatNew);
554 fAngle = fAngle * 180 / M_PI;
555
556 // Compare translation
557 float fMove = (vTrans - vTransDest).norm();
558
559 // compare to thresholds and update
560 if (fMove > fThreshTrans) {
561 qInfo() << "Large movement: " << fMove * 1000 << "mm";
562 bState = true;
563 } else if (fAngle > fThreshRot) {
564 qInfo() << "Large rotation: " << fAngle << "degree";
565 bState = true;
566 } else {
567 bState = false;
568 }
569
570 return bState;
571}
constexpr auto FIFFV_POINT_EEG
Container for the FIFF_DIG_POINT records of a measurement (a parsed FIFFB_ISOTRAK block).
return FiffCoordTrans(from_frame, to_frame, R, moveVec)
Eigen::JacobiSVD< Eigen::Matrix3f > svd(S, Eigen::ComputeFullU|Eigen::ComputeFullV)
Noise / data covariance matrix as stored under FIFFB_MNE_COV, with channel names, kind,...
Header-only Eigen matrix text I/O — round-trips dense matrices to whitespace-separated ASCII for cros...
Container of FwdCoil instances representing either a sensor-type template database or a concrete per-...
Sinusoidal HPI signal model — builds and inverts the regressor matrix that extracts coil amplitudes f...
Per-coil magnetic-dipole fitting workspace — Nelder-Mead optimiser plus leadfield computation for HPI...
Compact MEG sensor-geometry container (positions, orientations, integration weights) used by the HPI ...
Immutable configuration for the HPI signal model — coil drive frequencies, sample rate,...
Pre-processing front-end for HPI fitting — re-shapes raw MEG data, projectors and digitised coils int...
HPI (Head Position Indicator) fitting — estimates the MEG dewar-to-head transform from coil-current s...
FIFF file I/O, in-memory data structures and high-level readers/writers.
Inverse source estimation (MNE, dSPM, sLORETA, dipole fitting).
Forward modelling — BEM solver, spherical models, sensor/coil definitions and the lead-field assembly...
Definition compute_fwd.h:85
Eigen::MatrixX3f apply_inverse_trans(const Eigen::MatrixX3f &rr, bool do_move=true) const
Eigen::MatrixX3f apply_trans(const Eigen::MatrixX3f &rr, bool do_move=true) const
Eigen::Matrix< float, 4, 4, Eigen::DontAlign > trans
Estimated dipole parameters (position, moment, goodness-of-fit) for a single HPI coil.
Definition inv_hpi_fit.h:88
Eigen::MatrixXd pos
Definition inv_hpi_fit.h:89
Eigen::VectorXd dpfiterror
Definition inv_hpi_fit.h:91
Complete HPI fit output: per-coil dipole parameters, head-to-device transform, fit error,...
FIFFLIB::FiffCoordTrans devHeadTrans
QVector< int > hpiFreqs
QVector< double > errorDistances
Eigen::VectorXd GoF
FIFFLIB::FiffDigPointSet fittedCoils
static bool compareTransformation(const Eigen::MatrixX4f &mDevHeadT, const Eigen::MatrixX4f &mDevHeadDest, const float &fThreshRot, const float &fThreshTrans)
void fit(const Eigen::MatrixXd &matProjectedData, const Eigen::MatrixXd &matProjectors, const InvHpiModelParameters &hpiModelParameters, const Eigen::MatrixXd &matCoilsHead, HpiFitResult &hpiFitResult)
void checkForUpdate(const InvSensorSet &sensorSet)
static void storeHeadPosition(float fTime, const Eigen::MatrixXf &matTransDevHead, Eigen::MatrixXd &matPosition, const Eigen::VectorXd &vecGoF, const QVector< double > &vecError)
Eigen::RowVectorXd m_sensorData
Eigen::MatrixXd m_matProjector
Eigen::MatrixXd m_coilPos
Configuration parameters for the HPI signal model (line frequency, coil frequencies,...
Stores MEG sensor geometry (positions, orientations, weights, coil count) for a single sensor type.
Generates the forward sinusoidal model matrix for HPI coil signals at known drive frequencies.