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 vecThreads[i] = QtConcurrent::run(std::bind(iterativeDijkstra,
85 returnMat,
86 std::cref(matVertices),
87 std::cref(vecNeighborVertices),
88 std::cref(vecVertSubset),
89 iBegin,
90 static_cast<qint32>(vecVertSubset.size()),
91 dCancelDist));
92 break;
93 } else {
94 vecThreads[i] = QtConcurrent::run(std::bind(iterativeDijkstra,
95 returnMat,
96 std::cref(matVertices),
97 std::cref(vecNeighborVertices),
98 std::cref(vecVertSubset),
99 iBegin,
100 iEnd,
101 dCancelDist));
102 iBegin += iSubArraySize;
103 iEnd += iSubArraySize;
104 }
105 }
106
107 for (QFuture<void>& f : vecThreads) {
108 f.waitForFinished();
109 }
110
111 return returnMat;
112}
113
114//=============================================================================================================
115
116QSharedPointer<SparseMatrix<float>> GeometryInfo::scdcInterpolationMat(
117 const MatrixX3f& matVertices,
118 const std::vector<VectorXi>& vecNeighborVertices,
119 const VectorXi& vecVertSubset,
120 double (*interpolationFunction)(double),
121 double dCancelDist,
122 std::function<void(int, int)> progressCallback,
123 const std::atomic<bool>* cancelledFlag)
124{
125 const qint32 nVerts = static_cast<qint32>(matVertices.rows());
126 const qint32 nSources = static_cast<qint32>(vecVertSubset.size());
127 const qint32 nAdj = static_cast<qint32>(vecNeighborVertices.size());
128
129 // Build a set of source vertex indices for O(1) lookup
130 // (vertices that ARE source vertices get weight=1 on themselves)
131 QSet<qint32> sourceVertexSet;
132 QHash<qint32, qint32> vertexToSourceIdx;
133 for (qint32 s = 0; s < nSources; ++s) {
134 sourceVertexSet.insert(vecVertSubset[s]);
135 vertexToSourceIdx.insert(vecVertSubset[s], s);
136 }
137
138 // For each source vertex, run Dijkstra from that source.
139 // Collect distances to all reachable mesh vertices within cancelDist.
140 // Store as: perVertex[meshVertexIdx] -> list of (sourceIdx, geodesicDist)
141 // We process source-by-source since each Dijkstra only touches a small neighborhood.
142
143 // Thread-local storage: each chunk produces triplets
144 struct VertexWeight
145 {
146 qint32 sourceIdx;
147 float dist;
148 };
149 struct WeightTriple
150 {
151 qint32 vertex;
152 qint32 sourceIdx;
153 float dist;
154 };
155
156 // Parallelize the per-source Dijkstra loop. Sources are independent;
157 // each worker keeps its own thread-local Dijkstra scratch buffers and
158 // emits a flat list of (vertex, source, dist) triples. A final
159 // single-threaded merge pass groups them per mesh vertex. This recovers
160 // the multi-core scaling that the legacy scdc() path used to provide.
161 int iCores = QThread::idealThreadCount();
162 if (iCores <= 0) {
163 iCores = 2;
164 }
165#ifdef __EMSCRIPTEN__
166 iCores = qMin(iCores, 2);
167#endif
168 iCores = qMin(iCores, qMax(1, static_cast<int>(nSources)));
169
170 std::atomic<bool> internalCancel(false);
171 const std::atomic<bool>* cancelView = cancelledFlag ? cancelledFlag : &internalCancel;
172
173 std::atomic<qint32> progressCounter(0);
174
175 std::vector<std::vector<WeightTriple>> perThreadTriples(iCores);
176
177 auto worker = [&](int threadIdx, qint32 sBegin, qint32 sEnd) {
178 std::vector<WeightTriple>& out = perThreadTriples[threadIdx];
179 // Heuristic reserve: average ~32 reachable vertices per source.
180 out.reserve(static_cast<size_t>(sEnd - sBegin) * 32);
181
182 // Thread-local Dijkstra scratch.
183 std::vector<float> vecMinDists(nAdj, FLOAT_INFINITY);
184 // Track which entries we touched so we only have to reset those
185 // between sources (avoids O(nVerts) fill() per source).
186 std::vector<qint32> touched;
187 touched.reserve(1024);
188
189 // Lazy-deletion priority queue: push (dist, vertex); skip stale entries
190 // when popping. ~3-5x faster than std::set for Dijkstra.
191 using QueueEntry = std::pair<float, qint32>;
192 std::priority_queue<QueueEntry, std::vector<QueueEntry>, std::greater<QueueEntry>> vertexQ;
193
194 const float fCancelDist = static_cast<float>(dCancelDist);
195
196 for (qint32 s = sBegin; s < sEnd; ++s) {
197 // Cooperative cancellation; only check every 16 sources to keep
198 // the atomic load out of the hot inner loop.
199 if (((s - sBegin) & 0xF) == 0 && cancelView->load(std::memory_order_relaxed)) {
200 return;
201 }
202
203 const qint32 iRoot = vecVertSubset[s];
204
205 // Reset only previously-touched slots.
206 for (qint32 idx : touched)
207 vecMinDists[idx] = FLOAT_INFINITY;
208 touched.clear();
209 while (!vertexQ.empty())
210 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])
223 continue;
224 if (fDist > fCancelDist)
225 continue;
226
227 const VectorXi& vecNeighbours = vecNeighborVertices[u];
228 const float ux = matVertices(u, 0);
229 const float uy = matVertices(u, 1);
230 const float uz = matVertices(u, 2);
231
232 for (Eigen::Index ne = 0; ne < vecNeighbours.size(); ++ne) {
233 const qint32 v = vecNeighbours[ne];
234 const float dx = ux - matVertices(v, 0);
235 const float dy = uy - matVertices(v, 1);
236 const float dz = uz - matVertices(v, 2);
237 const float fDistWithU = fDist + std::sqrt(dx * dx + dy * dy + dz * dz);
238
239 if (fDistWithU < vecMinDists[v]) {
240 if (vecMinDists[v] == FLOAT_INFINITY)
241 touched.push_back(v);
242 vecMinDists[v] = fDistWithU;
243 if (fDistWithU <= fCancelDist)
244 vertexQ.emplace(fDistWithU, v);
245 }
246 }
247 }
248
249 // Collect reachable vertices for this source.
250 for (qint32 idx : touched) {
251 const float d = vecMinDists[idx];
252 if (d < fCancelDist) {
253 out.push_back({idx, s, d});
254 }
255 }
256
257 if (progressCallback) {
258 const qint32 done = progressCounter.fetch_add(1, std::memory_order_relaxed) + 1;
259 if ((done % 100) == 0) {
260 progressCallback(done, nSources);
261 }
262 }
263 }
264 };
265
266 if (iCores <= 1) {
267 worker(0, 0, nSources);
268 } else {
269 QVector<QFuture<void>> vecThreads;
270 vecThreads.reserve(iCores);
271 const qint32 chunk = nSources / iCores;
272 qint32 sBegin = 0;
273 for (int t = 0; t < iCores; ++t) {
274 const qint32 sEnd = (t == iCores - 1) ? nSources : (sBegin + chunk);
275 vecThreads.push_back(QtConcurrent::run(worker, t, sBegin, sEnd));
276 sBegin = sEnd;
277 }
278 for (QFuture<void>& f : vecThreads)
279 f.waitForFinished();
280 }
281
282 if (cancelView->load(std::memory_order_relaxed))
283 return QSharedPointer<SparseMatrix<float>>();
284
285 // Merge thread-local triples into the per-vertex list.
286 std::vector<std::vector<VertexWeight>> perVertexWeights(nVerts);
287 {
288 // Pre-size each per-vertex bucket to avoid repeated reallocations.
289 std::vector<size_t> counts(nVerts, 0);
290 for (const auto& chunk : perThreadTriples) {
291 for (const auto& t : chunk)
292 ++counts[t.vertex];
293 }
294 for (qint32 v = 0; v < nVerts; ++v) {
295 if (counts[v])
296 perVertexWeights[v].reserve(counts[v]);
297 }
298 for (const auto& chunk : perThreadTriples) {
299 for (const auto& t : chunk) {
300 perVertexWeights[t.vertex].push_back({t.sourceIdx, t.dist});
301 }
302 }
303 }
304
305 // Build the sparse interpolation matrix from the collected distances
306
307 QVector<Eigen::Triplet<float>> vecTriplets;
308 // Estimate ~10-20 sources per vertex on average
309 vecTriplets.reserve(nVerts * 10);
310
311 for (qint32 r = 0; r < nVerts; ++r) {
312 if (sourceVertexSet.contains(r)) {
313 // Source vertex: identity mapping (weight = 1)
314 vecTriplets.push_back(Eigen::Triplet<float>(r, vertexToSourceIdx[r], 1.0f));
315 } else {
316 const auto& weights = perVertexWeights[r];
317 if (weights.empty())
318 continue;
319
320 // Apply interpolation function and normalize weights
321 float dWeightsSum = 0.0f;
322 QVector<QPair<qint32, float>> vecBelowThresh;
323 vecBelowThresh.reserve(static_cast<int>(weights.size()));
324
325 for (const auto& w : weights) {
326 const float dValueWeight = std::fabs(1.0f / static_cast<float>(interpolationFunction(w.dist)));
327 dWeightsSum += dValueWeight;
328 vecBelowThresh.push_back(qMakePair(w.sourceIdx, dValueWeight));
329 }
330
331 for (const auto& qp : vecBelowThresh) {
332 vecTriplets.push_back(Eigen::Triplet<float>(r, qp.first, qp.second / dWeightsSum));
333 }
334 }
335 }
336
337 auto interpMat = QSharedPointer<SparseMatrix<float>>::create(nVerts, nSources);
338 interpMat->setFromTriplets(vecTriplets.begin(), vecTriplets.end());
339
340 return interpMat;
341}
342
343//=============================================================================================================
344
345VectorXi GeometryInfo::projectSensors(const MatrixX3f& matVertices,
346 const MatrixX3f& matSensorPositions)
347{
348 const qint32 iNumSensors = static_cast<qint32>(matSensorPositions.rows());
349
350 qint32 iCores = QThread::idealThreadCount();
351 if (iCores <= 0) {
352 iCores = 2;
353 }
354#ifdef __EMSCRIPTEN__
355 iCores = qMin(iCores, (qint32)2);
356#endif
357
358 const qint32 iSubArraySize = int(double(iNumSensors) / double(iCores));
359
360 if (iSubArraySize <= 1) {
361 return nearestNeighbor(matVertices, matSensorPositions, 0, iNumSensors);
362 }
363
364 QVector<QFuture<VectorXi>> vecThreads(iCores);
365 qint32 iBeginOffset = 0;
366 qint32 iEndOffset = iBeginOffset + iSubArraySize;
367 for (qint32 i = 0; i < vecThreads.size(); ++i) {
368 if (i == vecThreads.size() - 1) {
369 vecThreads[i] = QtConcurrent::run(nearestNeighbor,
370 matVertices,
371 matSensorPositions,
372 iBeginOffset,
373 iNumSensors);
374 break;
375 } else {
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 const VectorXi& partial = vecThreads[i].result();
395 vecOutputArray.segment(iOffset, partial.size()) = partial;
396 iOffset += static_cast<qint32>(partial.size());
397 }
398
399 return vecOutputArray;
400}
401
402//=============================================================================================================
403
404VectorXi GeometryInfo::nearestNeighbor(const MatrixX3f& matVertices,
405 const MatrixX3f& matSensorPositions,
406 qint32 iBegin,
407 qint32 iEnd)
408{
409 VectorXi vecMappedSensors(iEnd - iBegin);
410
411 for (qint32 s = iBegin; s < iEnd; ++s) {
412 qint32 iChampionId = 0;
413 double iChampDist = std::numeric_limits<double>::max();
414 for (qint32 i = 0; i < matVertices.rows(); ++i) {
415 double dDist = sqrt(squared(matVertices(i, 0) - matSensorPositions(s, 0)) + squared(matVertices(i, 1) - matSensorPositions(s, 1)) + squared(matVertices(i, 2) - matSensorPositions(s, 2)));
416 if (dDist < iChampDist) {
417 iChampionId = i;
418 iChampDist = dDist;
419 }
420 }
421 vecMappedSensors[s - iBegin] = iChampionId;
422 }
423
424 return vecMappedSensors;
425}
426
427//=============================================================================================================
428
429void GeometryInfo::iterativeDijkstra(QSharedPointer<MatrixXd> matOutputDistMatrix,
430 const MatrixX3f& matVertices,
431 const std::vector<VectorXi>& vecNeighborVertices,
432 const VectorXi& vecVertSubset,
433 qint32 iBegin,
434 qint32 iEnd,
435 double dCancelDistance)
436{
437 const std::vector<VectorXi>& vecAdjacency = vecNeighborVertices;
438 qint32 n = static_cast<qint32>(vecAdjacency.size());
439 QVector<double> vecMinDists(n);
440 std::set<std::pair<double, qint32>> vertexQ;
441 const double INF = FLOAT_INFINITY;
442
443 for (qint32 i = iBegin; i < iEnd; ++i) {
444 if ((i - iBegin) > 0 && (i - iBegin) % 100 == 0) {
445 qDebug() << "GeometryInfo::iterativeDijkstra progress:" << (i - iBegin) << "/" << (iEnd - iBegin) << " (Thread range:" << iBegin << "-" << iEnd << ")";
446 }
447
448 qint32 iRoot = vecVertSubset[i];
449 vertexQ.clear();
450 vecMinDists.fill(INF);
451 vecMinDists[iRoot] = 0.0;
452 vertexQ.insert(std::make_pair(vecMinDists[iRoot], iRoot));
453
454 while (vertexQ.empty() == false) {
455 const double dDist = vertexQ.begin()->first;
456 const qint32 u = vertexQ.begin()->second;
457 vertexQ.erase(vertexQ.begin());
458
459 if (dDist <= dCancelDistance) {
460 const VectorXi& vecNeighbours = vecAdjacency[u];
461
462 for (Eigen::Index ne = 0; ne < vecNeighbours.size(); ++ne) {
463 qint32 v = vecNeighbours[ne];
464
465 const double dDistX = matVertices(u, 0) - matVertices(v, 0);
466 const double dDistY = matVertices(u, 1) - matVertices(v, 1);
467 const double dDistZ = matVertices(u, 2) - matVertices(v, 2);
468 const double dDistWithU = dDist + sqrt(dDistX * dDistX + dDistY * dDistY + dDistZ * dDistZ);
469
470 if (dDistWithU < vecMinDists[v]) {
471 vertexQ.erase(std::make_pair(vecMinDists[v], v));
472 vecMinDists[v] = dDistWithU;
473 vertexQ.insert(std::make_pair(vecMinDists[v], v));
474 }
475 }
476 }
477 }
478
479 for (qint32 m = 0; m < vecMinDists.size(); ++m) {
480 matOutputDistMatrix->coeffRef(m, i) = vecMinDists[m];
481 }
482 }
483}
484
485//=============================================================================================================
486
487VectorXi GeometryInfo::filterBadChannels(QSharedPointer<Eigen::MatrixXd> matDistanceTable,
488 const FIFFLIB::FiffInfo& fiffInfo,
489 qint32 iSensorType)
490{
491 std::vector<int> vecBadColumns;
492 QVector<const FiffChInfo*> vecSensors;
493 for (const FiffChInfo& s : fiffInfo.chs) {
494 if (s.kind == iSensorType && (s.unit == FIFF_UNIT_T || s.unit == FIFF_UNIT_V)) {
495 vecSensors.push_back(&s);
496 }
497 }
498
499 for (const QString& b : fiffInfo.bads) {
500 for (int col = 0; col < vecSensors.size(); ++col) {
501 if (vecSensors[col]->ch_name == b) {
502 vecBadColumns.push_back(col);
503 for (int row = 0; row < matDistanceTable->rows(); ++row) {
504 matDistanceTable->coeffRef(row, col) = FLOAT_INFINITY;
505 }
506 break;
507 }
508 }
509 }
510
511 return Eigen::Map<VectorXi>(vecBadColumns.data(), static_cast<Eigen::Index>(vecBadColumns.size()));
512}
#define FIFF_UNIT_V
#define FIFF_UNIT_T
Full FIFF measurement metadata: everything from FIFFB_MEAS / FIFFB_MEAS_INFO needed to interpret a re...
Surface-constrained geodesic distance and sensor-to-mesh projection helpers.
#define FLOAT_INFINITY
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:90
QList< FiffChInfo > chs