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#define _USE_MATH_DEFINES
35#include <cmath>
36#include <iostream>
37#include <vector>
38#include <numeric>
39#include <fiff/fiff_cov.h>
41#include <fstream>
42
43#include <fwd/fwd_coil_set.h>
44
45//=============================================================================================================
46// EIGEN INCLUDES
47//=============================================================================================================
48
49#include <Eigen/Dense>
50
51//=============================================================================================================
52// QT INCLUDES
53//=============================================================================================================
54
55#include <QVector>
56#include <QFuture>
57#include <QtConcurrent/QtConcurrent>
58
59//=============================================================================================================
60// USED NAMESPACES
61//=============================================================================================================
62
63using namespace Eigen;
64using namespace INVLIB;
65using namespace FIFFLIB;
66using namespace FWDLIB;
67
68//=============================================================================================================
69// DEFINE GLOBAL METHODS
70//=============================================================================================================
71
72//=============================================================================================================
73// DEFINE MEMBER METHODS
74//=============================================================================================================
75
76//=============================================================================================================
77
79 : m_sensors(sensorSet),
80 m_signalModel(InvSignalModel())
81{
82
83}
84
85//=============================================================================================================
86
88{
89 if(m_sensors != sensorSet) {
90 m_sensors = sensorSet;
91 }
92}
93
94//=============================================================================================================
95
96void InvHpiFit::fit(const MatrixXd& matProjectedData,
97 const MatrixXd& matProjectors,
98 const InvHpiModelParameters& hpiModelParameters,
99 const MatrixXd& matCoilsHead,
100 HpiFitResult& hpiFitResult)
101{
102 fit(matProjectedData,matProjectors,hpiModelParameters,matCoilsHead,false,hpiFitResult);
103}
104
105//=============================================================================================================
106
107void InvHpiFit::fit(const MatrixXd& matProjectedData,
108 const MatrixXd& matProjectors,
109 const InvHpiModelParameters& hpiModelParameters,
110 const MatrixXd& matCoilsHead,
111 const bool bOrderFrequencies,
112 HpiFitResult& hpiFitResult)
113{
114 if(matProjectedData.rows() != matProjectors.rows()) {
115 std::cout<< "InvHpiFit::fit - Projector and data dimensions do not match. Returning."<<std::endl;
116 return;
117 } else if(hpiModelParameters.iNHpiCoils()!= matCoilsHead.rows()) {
118 std::cout<< "InvHpiFit::fit - Number of coils and hpi digitizers do not match. Returning."<<std::endl;
119 return;
120 } else if(matProjectedData.rows()==0 || matProjectors.rows()==0) {
121 std::cout<< "InvHpiFit::fit - No data or Projectors passed. Returning."<<std::endl;
122 return;
123 } else if(m_sensors.ncoils() != matProjectedData.rows()) {
124 std::cout<< "InvHpiFit::fit - Number of channels in sensors and data do not match. Returning."<<std::endl;
125 return;
126 }
127
128 const MatrixXd matAmplitudes = computeAmplitudes(matProjectedData,
129 hpiModelParameters);
130
131 const MatrixXd matCoilsSeed = computeSeedPoints(matAmplitudes,
132 hpiFitResult.devHeadTrans,
133 hpiFitResult.errorDistances,
134 matCoilsHead);
135
136 CoilParam fittedCoilParams = dipfit(matCoilsSeed,
137 m_sensors,
138 matAmplitudes,
139 hpiModelParameters.iNHpiCoils(),
140 matProjectors,
141 500,
142 1e-9f);
143
144 if(bOrderFrequencies) {
145 const std::vector<int> vecOrder = findCoilOrder(fittedCoilParams.pos,
146 matCoilsHead);
147
148 fittedCoilParams.pos = order(vecOrder,fittedCoilParams.pos);
149 hpiFitResult.hpiFreqs = order(vecOrder,hpiModelParameters.vecHpiFreqs());
150 }
151
152 hpiFitResult.GoF = computeGoF(fittedCoilParams.dpfiterror);
153
154 hpiFitResult.fittedCoils = getFittedPointSet(fittedCoilParams.pos);
155
156 hpiFitResult.devHeadTrans = computeDeviceHeadTransformation(fittedCoilParams.pos,
157 matCoilsHead);
158
159 hpiFitResult.errorDistances = computeEstimationError(fittedCoilParams.pos,
160 matCoilsHead,
161 hpiFitResult.devHeadTrans);
162}
163
164//=============================================================================================================
165
166Eigen::MatrixXd InvHpiFit::computeAmplitudes(const Eigen::MatrixXd& matProjectedData,
167 const InvHpiModelParameters& hpiModelParameters)
168{
169 // fit model
170 MatrixXd matTopo = m_signalModel.fitData(hpiModelParameters,matProjectedData);
171 matTopo.transposeInPlace();
172
173 // split into sine and cosine amplitudes
174 const int iNumCoils = hpiModelParameters.iNHpiCoils();
175
176 MatrixXd matAmpSine(matProjectedData.cols(), iNumCoils);
177 MatrixXd matAmpCosine(matProjectedData.cols(), iNumCoils);
178
179 matAmpSine = matTopo.leftCols(iNumCoils);
180 matAmpCosine = matTopo.middleCols(iNumCoils,iNumCoils);
181
182 // Select sine or cosine component depending on their contributions to the amplitudes
183 for(int j = 0; j < iNumCoils; ++j) {
184 float fNS = 0.0;
185 float fNC = 0.0;
186 fNS = matAmpSine.col(j).array().square().sum();
187 fNC = matAmpCosine.col(j).array().square().sum();
188 if(fNC > fNS) {
189 matAmpSine.col(j) = matAmpCosine.col(j);
190 }
191 }
192
193 return matAmpSine;
194}
195
196//=============================================================================================================
197
198Eigen::MatrixXd InvHpiFit::computeSeedPoints(const Eigen::MatrixXd& matAmplitudes,
199 const FIFFLIB::FiffCoordTrans& transDevHead,
200 const QVector<double>& vecError,
201 const Eigen::MatrixXd& matCoilsHead)
202{
203 const int iNumCoils = matCoilsHead.rows();
204 MatrixXd matCoilsSeed = MatrixXd::Zero(iNumCoils,3);
205
206 const double dError = std::accumulate(vecError.begin(), vecError.end(), .0) / vecError.size();
207
208 if(transDevHead.trans != MatrixXd::Identity(4,4).cast<float>() && dError < 0.010) {
209 // if good last fit, use old trafo
210 matCoilsSeed = transDevHead.apply_inverse_trans(matCoilsHead.cast<float>()).cast<double>();
211 } else {
212 // if not, find max amplitudes in channels
213 VectorXi vecChIdcs(iNumCoils);
214
215 for (int j = 0; j < iNumCoils; j++) {
216 int iChIdx = 0;
217 VectorXd::Index indMax;
218 matAmplitudes.col(j).maxCoeff(&indMax);
219 if(indMax < m_sensors.ncoils()) {
220 iChIdx = indMax;
221 }
222 vecChIdcs(j) = iChIdx;
223 }
224 // and go 3 cm inwards from max channels
225 for (int j = 0; j < vecChIdcs.rows(); ++j) {
226 if(vecChIdcs(j) < m_sensors.ncoils()) {
227 Vector3d r0 = m_sensors.r0(vecChIdcs(j));
228 Vector3d ez = m_sensors.ez(vecChIdcs(j));
229 matCoilsSeed.row(j) = (-1 * ez * 0.03 + r0).transpose();
230 }
231 }
232 }
233 return matCoilsSeed;
234}
235
236//=============================================================================================================
237
238CoilParam InvHpiFit::dipfit(const MatrixXd matCoilsSeed,
239 const InvSensorSet& sensors,
240 const MatrixXd& matData,
241 const int iNumCoils,
242 const MatrixXd& matProjectors,
243 const int iMaxIterations,
244 const float fAbortError)
245{
246 //Do this in conncurrent mode
247 //Generate QList structure which can be handled by the QConcurrent framework
248 QList<InvHpiFitData> lCoilData;
249
250 for(qint32 i = 0; i < iNumCoils; ++i) {
251 InvHpiFitData coilData;
252 coilData.m_coilPos = matCoilsSeed.row(i);
253 coilData.m_sensorData = matData.col(i);
254 coilData.m_sensors = sensors;
255 coilData.m_matProjector = matProjectors;
256 coilData.m_iMaxIterations = iMaxIterations;
257 coilData.m_fAbortError = fAbortError;
258
259 lCoilData.append(coilData);
260 }
261 //Do the concurrent filtering
262 CoilParam coil(iNumCoils);
263
264 if(!lCoilData.isEmpty()) {
265 // //Do sequential
266 // for(int l = 0; l < lCoilData.size(); ++l) {
267 // doDipfitConcurrent(lCoilData[l]);
268 // }
269
270 //Do concurrent
271 QFuture<void> future = QtConcurrent::map(lCoilData,
273 future.waitForFinished();
274
275 //Transform results to final coil information
276 for(qint32 i = 0; i < lCoilData.size(); ++i) {
277 coil.pos.row(i) = lCoilData.at(i).m_coilPos;
278 coil.mom = lCoilData.at(i).m_errorInfo.moment.transpose();
279 coil.dpfiterror(i) = lCoilData.at(i).m_errorInfo.error;
280 coil.dpfitnumitr(i) = lCoilData.at(i).m_errorInfo.numIterations;
281
282 //std::cout<<std::endl<< "InvHpiFit::dipfit - Itr steps for coil " << i << " =" <<coil.dpfitnumitr(i);
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 // maximum 10 mm mean error
301 const double dErrorMin = 0.010;
302 double dErrorActual = 0.0;
303 double dErrorBest = dErrorMin;
304
305 MatrixXd matTrans(4,4);
306 std::vector<int> vecOrderBest = vecOrder;
307
308 bool bSuccess = false;
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 bSuccess = true;
321 }
322 } while (std::next_permutation(vecOrder.begin(), vecOrder.end()));
323 return vecOrderBest;
324}
325
326//=============================================================================================================
327
328double InvHpiFit::objectTrans(const MatrixXd& matHeadCoil,
329 const MatrixXd& matCoil,
330 const MatrixXd& matTrans)
331{
332 // Compute the fiducial registration error - the lower, the better.
333 const int iNumCoils = matHeadCoil.rows();
334 MatrixXd matTemp = matCoil;
335
336 // homogeneous coordinates
337 matTemp.conservativeResize(matCoil.rows(),matCoil.cols()+1);
338 matTemp.block(0,3,iNumCoils,1).setOnes();
339 matTemp.transposeInPlace();
340
341 // apply transformation
342 MatrixXd matTestPos = matTrans * matTemp;
343
344 // remove
345 MatrixXd matDiff = matTestPos.block(0,0,3,iNumCoils) - matHeadCoil.transpose();
346 VectorXd vecError = matDiff.colwise().norm();
347
348 // compute error
349 double dError = matDiff.colwise().norm().mean();;
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 = 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 = 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,dNormf;
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 // Calculate GOF
447 dNormf = (matNH.transpose()-matBT.transpose()).norm();
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}
575
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-...
#define M_PI
return FiffCoordTrans(from_frame, to_frame, R, moveVec)
Eigen::JacobiSVD< Eigen::Matrix3f > svd(S, Eigen::ComputeFullU|Eigen::ComputeFullV)
Container for the FIFF_DIG_POINT records of a measurement (a parsed FIFFB_ISOTRAK block).
#define FIFFV_POINT_EEG
Noise / data covariance matrix as stored under FIFFB_MNE_COV, with channel names, kind,...
HPI (Head Position Indicator) fitting — estimates the MEG dewar-to-head transform from coil-current s...
Sinusoidal HPI signal model — builds and inverts the regressor matrix that extracts coil amplitudes f...
Pre-processing front-end for HPI fitting — re-shapes raw MEG data, projectors and digitised coils int...
Immutable configuration for the HPI signal model — coil drive frequencies, sample rate,...
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 ...
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:83
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:85
Eigen::MatrixXd pos
Definition inv_hpi_fit.h:86
Eigen::VectorXd dpfiterror
Definition inv_hpi_fit.h:88
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.