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