v2.0.0
Loading...
Searching...
No Matches
geometryinfo.cpp
Go to the documentation of this file.
1//=============================================================================================================
12
13//=============================================================================================================
14// INCLUDES
15//=============================================================================================================
16
17#include "geometryinfo.h"
18
19#include <fiff/fiff_info.h>
20
21//=============================================================================================================
22// STL INCLUDES
23//=============================================================================================================
24
25#include <atomic>
26#include <cmath>
27#include <fstream>
28#include <queue>
29#include <set>
30#include <utility>
31#include <vector>
32
33//=============================================================================================================
34// QT INCLUDES
35//=============================================================================================================
36
37#include <QThread>
38#include <QtConcurrent/QtConcurrent>
39
40//=============================================================================================================
41// USED NAMESPACES
42//=============================================================================================================
43
44using namespace DISP3DLIB;
45using namespace Eigen;
46using namespace FIFFLIB;
47
48//=============================================================================================================
49// DEFINE MEMBER METHODS
50//=============================================================================================================
51
52QSharedPointer<MatrixXd> GeometryInfo::scdc(const MatrixX3f &matVertices,
53 const std::vector<VectorXi> &vecNeighborVertices,
54 VectorXi &vecVertSubset,
55 double dCancelDist)
56{
57 // create matrix and check for empty subset:
58 qint32 iCols = static_cast<qint32>(vecVertSubset.size());
59 if(vecVertSubset.size() == 0) {
60 qDebug() << "[WARNING] SCDC received empty subset, calculating full distance table, make sure you have enough memory !";
61 vecVertSubset = VectorXi::LinSpaced(matVertices.rows(), 0, static_cast<int>(matVertices.rows()) - 1);
62 iCols = static_cast<qint32>(matVertices.rows());
63 }
64
65 QSharedPointer<MatrixXd> returnMat = QSharedPointer<MatrixXd>::create(matVertices.rows(), iCols);
66
67 // distribute calculation on cores
68 int iCores = QThread::idealThreadCount();
69 if (iCores <= 0) {
70 iCores = 2;
71 }
72#ifdef __EMSCRIPTEN__
73 // Cap parallel threads to avoid exhausting the Emscripten pthread pool.
74 iCores = qMin(iCores, 2);
75#endif
76
77 qint32 iSubArraySize = int(double(vecVertSubset.size()) / double(iCores));
78 QVector<QFuture<void> > vecThreads(iCores);
79 qint32 iBegin = 0;
80 qint32 iEnd = iSubArraySize;
81
82 for (int i = 0; i < vecThreads.size(); ++i) {
83 if(i == vecThreads.size()-1)
84 {
85 vecThreads[i] = QtConcurrent::run(std::bind(iterativeDijkstra,
86 returnMat,
87 std::cref(matVertices),
88 std::cref(vecNeighborVertices),
89 std::cref(vecVertSubset),
90 iBegin,
91 static_cast<qint32>(vecVertSubset.size()),
92 dCancelDist));
93 break;
94 }
95 else
96 {
97 vecThreads[i] = QtConcurrent::run(std::bind(iterativeDijkstra,
98 returnMat,
99 std::cref(matVertices),
100 std::cref(vecNeighborVertices),
101 std::cref(vecVertSubset),
102 iBegin,
103 iEnd,
104 dCancelDist));
105 iBegin += iSubArraySize;
106 iEnd += iSubArraySize;
107 }
108 }
109
110 for (QFuture<void>& f : vecThreads) {
111 f.waitForFinished();
112 }
113
114 return returnMat;
115}
116
117//=============================================================================================================
118
119QSharedPointer<SparseMatrix<float>> GeometryInfo::scdcInterpolationMat(
120 const MatrixX3f &matVertices,
121 const std::vector<VectorXi> &vecNeighborVertices,
122 const VectorXi &vecVertSubset,
123 double (*interpolationFunction)(double),
124 double dCancelDist,
125 std::function<void(int, int)> progressCallback,
126 const std::atomic<bool> *cancelledFlag)
127{
128 const qint32 nVerts = static_cast<qint32>(matVertices.rows());
129 const qint32 nSources = static_cast<qint32>(vecVertSubset.size());
130 const qint32 nAdj = static_cast<qint32>(vecNeighborVertices.size());
131
132 // Build a set of source vertex indices for O(1) lookup
133 // (vertices that ARE source vertices get weight=1 on themselves)
134 QSet<qint32> sourceVertexSet;
135 QHash<qint32, qint32> vertexToSourceIdx;
136 for (qint32 s = 0; s < nSources; ++s) {
137 sourceVertexSet.insert(vecVertSubset[s]);
138 vertexToSourceIdx.insert(vecVertSubset[s], s);
139 }
140
141 // For each source vertex, run Dijkstra from that source.
142 // Collect distances to all reachable mesh vertices within cancelDist.
143 // Store as: perVertex[meshVertexIdx] -> list of (sourceIdx, geodesicDist)
144 // We process source-by-source since each Dijkstra only touches a small neighborhood.
145
146 // Thread-local storage: each chunk produces triplets
147 struct VertexWeight {
148 qint32 sourceIdx;
149 float dist;
150 };
151 struct WeightTriple {
152 qint32 vertex;
153 qint32 sourceIdx;
154 float dist;
155 };
156
157 // Parallelize the per-source Dijkstra loop. Sources are independent;
158 // each worker keeps its own thread-local Dijkstra scratch buffers and
159 // emits a flat list of (vertex, source, dist) triples. A final
160 // single-threaded merge pass groups them per mesh vertex. This recovers
161 // the multi-core scaling that the legacy scdc() path used to provide.
162 int iCores = QThread::idealThreadCount();
163 if (iCores <= 0) {
164 iCores = 2;
165 }
166#ifdef __EMSCRIPTEN__
167 iCores = qMin(iCores, 2);
168#endif
169 iCores = qMin(iCores, qMax(1, static_cast<int>(nSources)));
170
171 std::atomic<bool> internalCancel(false);
172 const std::atomic<bool> *cancelView = cancelledFlag ? cancelledFlag : &internalCancel;
173
174 std::atomic<qint32> progressCounter(0);
175
176 std::vector<std::vector<WeightTriple>> perThreadTriples(iCores);
177
178 auto worker = [&](int threadIdx, qint32 sBegin, qint32 sEnd) {
179 std::vector<WeightTriple> &out = perThreadTriples[threadIdx];
180 // Heuristic reserve: average ~32 reachable vertices per source.
181 out.reserve(static_cast<size_t>(sEnd - sBegin) * 32);
182
183 // Thread-local Dijkstra scratch.
184 std::vector<float> vecMinDists(nAdj, FLOAT_INFINITY);
185 // Track which entries we touched so we only have to reset those
186 // between sources (avoids O(nVerts) fill() per source).
187 std::vector<qint32> touched;
188 touched.reserve(1024);
189
190 // Lazy-deletion priority queue: push (dist, vertex); skip stale entries
191 // when popping. ~3-5x faster than std::set for Dijkstra.
192 using QueueEntry = std::pair<float, qint32>;
193 std::priority_queue<QueueEntry, std::vector<QueueEntry>, std::greater<QueueEntry>> vertexQ;
194
195 const float fCancelDist = static_cast<float>(dCancelDist);
196
197 for (qint32 s = sBegin; s < sEnd; ++s) {
198 // Cooperative cancellation; only check every 16 sources to keep
199 // the atomic load out of the hot inner loop.
200 if (((s - sBegin) & 0xF) == 0
201 && cancelView->load(std::memory_order_relaxed)) {
202 return;
203 }
204
205 const qint32 iRoot = vecVertSubset[s];
206
207 // Reset only previously-touched slots.
208 for (qint32 idx : touched) vecMinDists[idx] = FLOAT_INFINITY;
209 touched.clear();
210 while (!vertexQ.empty()) vertexQ.pop();
211
212 vecMinDists[iRoot] = 0.0f;
213 touched.push_back(iRoot);
214 vertexQ.emplace(0.0f, iRoot);
215
216 while (!vertexQ.empty()) {
217 const float fDist = vertexQ.top().first;
218 const qint32 u = vertexQ.top().second;
219 vertexQ.pop();
220
221 // Stale entry from lazy deletion?
222 if (fDist > vecMinDists[u]) continue;
223 if (fDist > fCancelDist) continue;
224
225 const VectorXi &vecNeighbours = vecNeighborVertices[u];
226 const float ux = matVertices(u, 0);
227 const float uy = matVertices(u, 1);
228 const float uz = matVertices(u, 2);
229
230 for (Eigen::Index ne = 0; ne < vecNeighbours.size(); ++ne) {
231 const qint32 v = vecNeighbours[ne];
232 const float dx = ux - matVertices(v, 0);
233 const float dy = uy - matVertices(v, 1);
234 const float dz = uz - matVertices(v, 2);
235 const float fDistWithU = fDist + std::sqrt(dx*dx + dy*dy + dz*dz);
236
237 if (fDistWithU < vecMinDists[v]) {
238 if (vecMinDists[v] == FLOAT_INFINITY)
239 touched.push_back(v);
240 vecMinDists[v] = fDistWithU;
241 if (fDistWithU <= fCancelDist)
242 vertexQ.emplace(fDistWithU, v);
243 }
244 }
245 }
246
247 // Collect reachable vertices for this source.
248 for (qint32 idx : touched) {
249 const float d = vecMinDists[idx];
250 if (d < fCancelDist) {
251 out.push_back({idx, s, d});
252 }
253 }
254
255 if (progressCallback) {
256 const qint32 done = progressCounter.fetch_add(1, std::memory_order_relaxed) + 1;
257 if ((done % 100) == 0) {
258 progressCallback(done, nSources);
259 }
260 }
261 }
262 };
263
264 if (iCores <= 1) {
265 worker(0, 0, nSources);
266 } else {
267 QVector<QFuture<void>> vecThreads;
268 vecThreads.reserve(iCores);
269 const qint32 chunk = nSources / iCores;
270 qint32 sBegin = 0;
271 for (int t = 0; t < iCores; ++t) {
272 const qint32 sEnd = (t == iCores - 1) ? nSources : (sBegin + chunk);
273 vecThreads.push_back(QtConcurrent::run(worker, t, sBegin, sEnd));
274 sBegin = sEnd;
275 }
276 for (QFuture<void> &f : vecThreads) f.waitForFinished();
277 }
278
279 if (cancelView->load(std::memory_order_relaxed))
280 return QSharedPointer<SparseMatrix<float>>();
281
282 // Merge thread-local triples into the per-vertex list.
283 std::vector<std::vector<VertexWeight>> perVertexWeights(nVerts);
284 {
285 // Pre-size each per-vertex bucket to avoid repeated reallocations.
286 std::vector<size_t> counts(nVerts, 0);
287 for (const auto &chunk : perThreadTriples) {
288 for (const auto &t : chunk) ++counts[t.vertex];
289 }
290 for (qint32 v = 0; v < nVerts; ++v) {
291 if (counts[v]) perVertexWeights[v].reserve(counts[v]);
292 }
293 for (const auto &chunk : perThreadTriples) {
294 for (const auto &t : chunk) {
295 perVertexWeights[t.vertex].push_back({t.sourceIdx, t.dist});
296 }
297 }
298 }
299
300 // Build the sparse interpolation matrix from the collected distances
301
302 QVector<Eigen::Triplet<float>> vecTriplets;
303 // Estimate ~10-20 sources per vertex on average
304 vecTriplets.reserve(nVerts * 10);
305
306 for (qint32 r = 0; r < nVerts; ++r) {
307 if (sourceVertexSet.contains(r)) {
308 // Source vertex: identity mapping (weight = 1)
309 vecTriplets.push_back(Eigen::Triplet<float>(r, vertexToSourceIdx[r], 1.0f));
310 } else {
311 const auto& weights = perVertexWeights[r];
312 if (weights.empty()) continue;
313
314 // Apply interpolation function and normalize weights
315 float dWeightsSum = 0.0f;
316 QVector<QPair<qint32, float>> vecBelowThresh;
317 vecBelowThresh.reserve(static_cast<int>(weights.size()));
318
319 for (const auto& w : weights) {
320 const float dValueWeight = std::fabs(1.0f / static_cast<float>(interpolationFunction(w.dist)));
321 dWeightsSum += dValueWeight;
322 vecBelowThresh.push_back(qMakePair(w.sourceIdx, dValueWeight));
323 }
324
325 for (const auto& qp : vecBelowThresh) {
326 vecTriplets.push_back(Eigen::Triplet<float>(r, qp.first, qp.second / dWeightsSum));
327 }
328 }
329 }
330
331 auto interpMat = QSharedPointer<SparseMatrix<float>>::create(nVerts, nSources);
332 interpMat->setFromTriplets(vecTriplets.begin(), vecTriplets.end());
333
334 return interpMat;
335}
336
337//=============================================================================================================
338
339VectorXi GeometryInfo::projectSensors(const MatrixX3f &matVertices,
340 const MatrixX3f &matSensorPositions)
341{
342 const qint32 iNumSensors = static_cast<qint32>(matSensorPositions.rows());
343
344 qint32 iCores = QThread::idealThreadCount();
345 if (iCores <= 0)
346 {
347 iCores = 2;
348 }
349#ifdef __EMSCRIPTEN__
350 iCores = qMin(iCores, (qint32)2);
351#endif
352
353 const qint32 iSubArraySize = int(double(iNumSensors) / double(iCores));
354
355 if(iSubArraySize <= 1)
356 {
357 return nearestNeighbor(matVertices, matSensorPositions, 0, iNumSensors);
358 }
359
360 QVector<QFuture<VectorXi> > vecThreads(iCores);
361 qint32 iBeginOffset = 0;
362 qint32 iEndOffset = iBeginOffset + iSubArraySize;
363 for(qint32 i = 0; i < vecThreads.size(); ++i)
364 {
365 if(i == vecThreads.size()-1)
366 {
367 vecThreads[i] = QtConcurrent::run(nearestNeighbor,
368 matVertices,
369 matSensorPositions,
370 iBeginOffset,
371 iNumSensors);
372 break;
373 }
374 else
375 {
376 vecThreads[i] = QtConcurrent::run(nearestNeighbor,
377 matVertices,
378 matSensorPositions,
379 iBeginOffset,
380 iEndOffset);
381 iBeginOffset = iEndOffset;
382 iEndOffset += iSubArraySize;
383 }
384 }
385
386 for (QFuture<VectorXi>& f : vecThreads) {
387 f.waitForFinished();
388 }
389
390 // concatenate partial results
391 VectorXi vecOutputArray(iNumSensors);
392 qint32 iOffset = 0;
393 for(qint32 i = 0; i < vecThreads.size(); ++i)
394 {
395 const VectorXi& partial = vecThreads[i].result();
396 vecOutputArray.segment(iOffset, partial.size()) = partial;
397 iOffset += static_cast<qint32>(partial.size());
398 }
399
400 return vecOutputArray;
401}
402
403//=============================================================================================================
404
405VectorXi GeometryInfo::nearestNeighbor(const MatrixX3f &matVertices,
406 const MatrixX3f &matSensorPositions,
407 qint32 iBegin,
408 qint32 iEnd)
409{
410 VectorXi vecMappedSensors(iEnd - iBegin);
411
412 for(qint32 s = iBegin; s < iEnd; ++s)
413 {
414 qint32 iChampionId = 0;
415 double iChampDist = std::numeric_limits<double>::max();
416 for(qint32 i = 0; i < matVertices.rows(); ++i)
417 {
418 double dDist = sqrt(squared(matVertices(i, 0) - matSensorPositions(s, 0))
419 + squared(matVertices(i, 1) - matSensorPositions(s, 1))
420 + squared(matVertices(i, 2) - matSensorPositions(s, 2)));
421 if(dDist < iChampDist)
422 {
423 iChampionId = i;
424 iChampDist = dDist;
425 }
426 }
427 vecMappedSensors[s - iBegin] = iChampionId;
428 }
429
430 return vecMappedSensors;
431}
432
433//=============================================================================================================
434
435void GeometryInfo::iterativeDijkstra(QSharedPointer<MatrixXd> matOutputDistMatrix,
436 const MatrixX3f &matVertices,
437 const std::vector<VectorXi> &vecNeighborVertices,
438 const VectorXi &vecVertSubset,
439 qint32 iBegin,
440 qint32 iEnd,
441 double dCancelDistance) {
442 const std::vector<VectorXi> &vecAdjacency = vecNeighborVertices;
443 qint32 n = static_cast<qint32>(vecAdjacency.size());
444 QVector<double> vecMinDists(n);
445 std::set< std::pair< double, qint32> > vertexQ;
446 const double INF = FLOAT_INFINITY;
447
448 for (qint32 i = iBegin; i < iEnd; ++i) {
449 if ((i - iBegin) > 0 && (i - iBegin) % 100 == 0) {
450 qDebug() << "GeometryInfo::iterativeDijkstra progress:" << (i - iBegin) << "/" << (iEnd - iBegin) << " (Thread range:" << iBegin << "-" << iEnd << ")";
451 }
452
453 qint32 iRoot = vecVertSubset[i];
454 vertexQ.clear();
455 vecMinDists.fill(INF);
456 vecMinDists[iRoot] = 0.0;
457 vertexQ.insert(std::make_pair(vecMinDists[iRoot], iRoot));
458
459 while (vertexQ.empty() == false) {
460 const double dDist = vertexQ.begin()->first;
461 const qint32 u = vertexQ.begin()->second;
462 vertexQ.erase(vertexQ.begin());
463
464 if (dDist <= dCancelDistance) {
465 const VectorXi& vecNeighbours = vecAdjacency[u];
466
467 for (Eigen::Index ne = 0; ne < vecNeighbours.size(); ++ne) {
468 qint32 v = vecNeighbours[ne];
469
470 const double dDistX = matVertices(u, 0) - matVertices(v, 0);
471 const double dDistY = matVertices(u, 1) - matVertices(v, 1);
472 const double dDistZ = matVertices(u, 2) - matVertices(v, 2);
473 const double dDistWithU = dDist + sqrt(dDistX * dDistX + dDistY * dDistY + dDistZ * dDistZ);
474
475 if (dDistWithU < vecMinDists[v]) {
476 vertexQ.erase(std::make_pair(vecMinDists[v], v));
477 vecMinDists[v] = dDistWithU;
478 vertexQ.insert(std::make_pair(vecMinDists[v], v));
479 }
480 }
481 }
482 }
483
484 for (qint32 m = 0; m < vecMinDists.size(); ++m) {
485 matOutputDistMatrix->coeffRef(m , i) = vecMinDists[m];
486 }
487 }
488}
489
490//=============================================================================================================
491
492VectorXi GeometryInfo::filterBadChannels(QSharedPointer<Eigen::MatrixXd> matDistanceTable,
493 const FIFFLIB::FiffInfo& fiffInfo,
494 qint32 iSensorType) {
495 std::vector<int> vecBadColumns;
496 QVector<const FiffChInfo*> vecSensors;
497 for(const FiffChInfo& s : fiffInfo.chs){
498 if(s.kind == iSensorType && (s.unit == FIFF_UNIT_T || s.unit == FIFF_UNIT_V)){
499 vecSensors.push_back(&s);
500 }
501 }
502
503 for(const QString& b : fiffInfo.bads){
504 for(int col = 0; col < vecSensors.size(); ++col){
505 if(vecSensors[col]->ch_name == b){
506 vecBadColumns.push_back(col);
507 for(int row = 0; row < matDistanceTable->rows(); ++row){
508 matDistanceTable->coeffRef(row, col) = FLOAT_INFINITY;
509 }
510 break;
511 }
512 }
513 }
514
515 return Eigen::Map<VectorXi>(vecBadColumns.data(), static_cast<Eigen::Index>(vecBadColumns.size()));
516}
Surface-constrained geodesic distance and sensor-to-mesh projection helpers.
#define FLOAT_INFINITY
#define FIFF_UNIT_V
#define FIFF_UNIT_T
Full FIFF measurement metadata: everything from FIFFB_MEAS / FIFFB_MEAS_INFO needed to interpret a re...
FIFF file I/O, in-memory data structures and high-level readers/writers.
3-D brain visualisation using the Qt RHI rendering backend.
static QSharedPointer< Eigen::SparseMatrix< float > > scdcInterpolationMat(const Eigen::MatrixX3f &matVertices, const std::vector< Eigen::VectorXi > &vecNeighborVertices, const Eigen::VectorXi &vecVertSubset, double(*interpolationFunction)(double), double dCancelDist, std::function< void(int, int)> progressCallback=nullptr, const std::atomic< bool > *cancelledFlag=nullptr)
scdcInterpolationMat Computes geodesic distances (SCDC) and builds the sparse interpolation matrix in...
static Eigen::VectorXi nearestNeighbor(const Eigen::MatrixX3f &matVertices, const Eigen::MatrixX3f &matSensorPositions, qint32 iBegin, qint32 iEnd)
static Eigen::VectorXi filterBadChannels(QSharedPointer< Eigen::MatrixXd > matDistanceTable, const FIFFLIB::FiffInfo &fiffInfo, qint32 iSensorType)
filterBadChannels Filters bad channels from distance table.
static void iterativeDijkstra(QSharedPointer< Eigen::MatrixXd > matOutputDistMatrix, const Eigen::MatrixX3f &matVertices, const std::vector< Eigen::VectorXi > &vecNeighborVertices, const Eigen::VectorXi &vecVertSubset, qint32 iBegin, qint32 iEnd, double dCancelDistance)
static double squared(double dBase)
static Eigen::VectorXi projectSensors(const Eigen::MatrixX3f &matVertices, const Eigen::MatrixX3f &matSensorPositions)
projectSensors Calculates the nearest neighbor vertex to each sensor.
static QSharedPointer< Eigen::MatrixXd > scdc(const Eigen::MatrixX3f &matVertices, const std::vector< Eigen::VectorXi > &vecNeighborVertices, Eigen::VectorXi &vecVertSubset, double dCancelDist=FLOAT_INFINITY)
scdc Calculates surface constrained distances on a mesh.
Per-channel FIFF descriptor: identifiers, kind, calibration, coil type, channel-frame coil position a...
Full FIFF measurement info: per-channel descriptors, sampling and filter setup, projectors,...
Definition fiff_info.h:88
QList< FiffChInfo > chs