v2.0.0
Loading...
Searching...
No Matches
sourceestimateoverlay.cpp
Go to the documentation of this file.
1//=============================================================================================================
12
13//=============================================================================================================
14// INCLUDES
15//=============================================================================================================
16
19#include "core/rendertypes.h"
20
24
25#include <QFile>
26#include <QDebug>
27#include <cmath>
28
29namespace DISP3DLIB
30{
31
32//=============================================================================================================
33// DEFINE MEMBER METHODS
34//=============================================================================================================
35
39
40//=============================================================================================================
41
45
46//=============================================================================================================
47
48bool SourceEstimateOverlay::loadStc(const QString& path, int hemi)
49{
50 QFile file(path);
51 // Note: InvSourceEstimate::read() opens the file internally, don't open it here
52
54 if (!INVLIB::InvSourceEstimate::read(file, stc)) {
55 qWarning() << "SourceEstimateOverlay::loadStc - Failed to read STC file:" << path;
56 return false;
57 }
58
59 if (hemi == 0) {
60 m_stcLh = stc;
61 m_hasLh = true;
62 qDebug() << "SourceEstimateOverlay: Loaded LH with" << stc.data.rows() << "vertices,"
63 << stc.data.cols() << "time points";
64 } else {
65 m_stcRh = stc;
66 m_hasRh = true;
67 qDebug() << "SourceEstimateOverlay: Loaded RH with" << stc.data.rows() << "vertices,"
68 << stc.data.cols() << "time points";
69 }
70 invalidateColorCache();
71
72 // Auto-set thresholds based on data range
73 if (m_hasLh || m_hasRh) {
74 double minVal, maxVal;
75 getDataRange(minVal, maxVal);
76 m_threshMin = minVal;
77 m_threshMax = maxVal;
78 m_threshMid = (minVal + maxVal) / 2.0;
79 qDebug() << "SourceEstimateOverlay: Auto thresholds set to" << m_threshMin << m_threshMid << m_threshMax;
80 }
81
82 return true;
83}
84
85//=============================================================================================================
86
88{
89 return m_hasLh || m_hasRh;
90}
91
92//=============================================================================================================
93
95{
96 if (!surface)
97 return;
98
99 int hemi = surface->hemi();
100 const INVLIB::InvSourceEstimate* stc = nullptr;
101 QSharedPointer<Eigen::SparseMatrix<float>> interpMat;
102
103 if (hemi == 0 && m_hasLh) {
104 stc = &m_stcLh;
105 interpMat = m_interpolationMatLh;
106 } else if (hemi == 1 && m_hasRh) {
107 stc = &m_stcRh;
108 interpMat = m_interpolationMatRh;
109 } else {
110 return; // No data for this hemisphere
111 }
112
113 if (stc->isEmpty())
114 return;
115
116 // Clamp time index
117 int tIdx = qBound(0, timeIndex, static_cast<int>(stc->data.cols()) - 1);
118 const uint32_t vertexCount = surface->vertexCount();
119
120 // ── Color cache fast-path ──────────────────────────────────────
121 // Hot loop while scrubbing or auto-looping: avoid the sparse
122 // interpolation matvec and per-vertex colormap evaluation by
123 // returning the pre-computed buffer for this (hemi, vertexCount,
124 // timeIndex). The cache is invalidated whenever colormap,
125 // thresholds or source data change.
126 const ColorCacheKey cacheKey(hemi, static_cast<int>(vertexCount));
127 auto bucketIt = m_colorCache.find(cacheKey);
128 if (bucketIt != m_colorCache.end()) {
129 auto frameIt = bucketIt.value().constFind(tIdx);
130 if (frameIt != bucketIt.value().constEnd()) {
131 surface->applySourceEstimateColors(frameIt.value());
132 return;
133 }
134 }
135
136 // Get source data for this time point
137 Eigen::VectorXf sourceData = stc->data.col(tIdx).cwiseAbs().cast<float>();
138
139 // Create color array for all surface vertices
140 QVector<uint32_t> colors(vertexCount, 0xFF808080); // Default gray
141
142 // Determine if we have an interpolation matrix
143 Eigen::VectorXf interpolatedData;
144
145 if (interpMat && interpMat->rows() == static_cast<int>(vertexCount) &&
146 interpMat->cols() == sourceData.size()) {
147 // Use interpolation to spread values to all vertices
148 // Note: interpolateSignal returns by value, not QSharedPointer, so assignment matches
149 interpolatedData = DISP3DLIB::Interpolation::interpolateSignal(interpMat, QSharedPointer<Eigen::VectorXf>::create(sourceData));
150 } else {
151 // Fall back to sparse visualization (direct mapping)
152 interpolatedData = Eigen::VectorXf::Zero(vertexCount);
153 const Eigen::VectorXi& srcVertices = stc->vertices;
154 for (int i = 0; i < srcVertices.size() && i < sourceData.size(); ++i) {
155 int vertIdx = srcVertices(i);
156 if (vertIdx >= 0 && vertIdx < static_cast<int>(vertexCount)) {
157 interpolatedData(vertIdx) = sourceData(i);
158 }
159 }
160 }
161
162 // Convert interpolated values to colors
163 for (int i = 0; i < static_cast<int>(vertexCount); ++i) {
164 float value = interpolatedData(i);
165
166 // Normalize based on thresholds
167 double normalized = 0.0;
168 if (m_threshMax > m_threshMin) {
169 normalized = (value - m_threshMin) / (m_threshMax - m_threshMin);
170 normalized = qBound(0.0, normalized, 1.0);
171 }
172
173 // Calculate alpha based on threshold
174 uint8_t alpha = 255;
175 if (value < m_threshMin) {
176 alpha = 0; // Fully transparent below minimum
177 } else if (value < m_threshMid) {
178 // Fade in from min to mid
179 float range = m_threshMid - m_threshMin;
180 if (range > 0) {
181 alpha = static_cast<uint8_t>(255.0f * (value - m_threshMin) / range);
182 }
183 }
184
185 colors[i] = valueToColor(normalized, alpha);
186 }
187
188 // Store in cache before handing off to the surface so back-scrubs
189 // and auto-loop iterations are free.
190 m_colorCache[cacheKey].insert(tIdx, colors);
191
192 surface->applySourceEstimateColors(colors);
193}
194
195//=============================================================================================================
196
197void SourceEstimateOverlay::setColormap(const QString& name)
198{
199 if (m_colormap == name)
200 return;
201 m_colormap = name;
202 invalidateColorCache();
203}
204
205//=============================================================================================================
206
207void SourceEstimateOverlay::setThresholds(float min, float mid, float max)
208{
209 if (m_threshMin == min && m_threshMid == mid && m_threshMax == max)
210 return;
211 m_threshMin = min;
212 m_threshMid = mid;
213 m_threshMax = max;
214 invalidateColorCache();
215}
216
217//=============================================================================================================
218
220{
221 if (m_hasLh)
222 return m_stcLh.data.cols();
223 if (m_hasRh)
224 return m_stcRh.data.cols();
225 return 0;
226}
227
228//=============================================================================================================
229
231{
232 if (m_hasLh && idx < m_stcLh.times.size()) {
233 return m_stcLh.times(idx);
234 }
235 if (m_hasRh && idx < m_stcRh.times.size()) {
236 return m_stcRh.times(idx);
237 }
238 return 0.0f;
239}
240
241//=============================================================================================================
242
244{
245 if (m_hasLh)
246 return m_stcLh.tmin;
247 if (m_hasRh)
248 return m_stcRh.tmin;
249 return 0.0f;
250}
251
252//=============================================================================================================
253
255{
256 if (m_hasLh)
257 return m_stcLh.tstep;
258 if (m_hasRh)
259 return m_stcRh.tstep;
260 return 0.0f;
261}
262
263//=============================================================================================================
264
265void SourceEstimateOverlay::getDataRange(double& minVal, double& maxVal) const
266{
267 minVal = std::numeric_limits<double>::max();
268 maxVal = std::numeric_limits<double>::lowest();
269
270 // applyToSurface() colours |data|, so the range is that of the magnitudes
271 for (const INVLIB::InvSourceEstimate* stc : {m_hasLh ? &m_stcLh : nullptr, m_hasRh ? &m_stcRh : nullptr}) {
272 if (stc && stc->data.size() > 0) {
273 minVal = qMin(minVal, stc->data.cwiseAbs().minCoeff());
274 maxVal = qMax(maxVal, stc->data.cwiseAbs().maxCoeff());
275 }
276 }
277
278 // If no data, set defaults
279 if (minVal > maxVal) {
280 minVal = 0.0;
281 maxVal = 1.0;
282 }
283}
284
285//=============================================================================================================
286
287uint32_t SourceEstimateOverlay::valueToColor(double value, uint8_t alpha) const
288{
289 QRgb rgb = DISPLIB::ColorMap::valueToColor(value, m_colormap);
290
291 uint32_t r = qRed(rgb);
292 uint32_t g = qGreen(rgb);
293 uint32_t b = qBlue(rgb);
294
295 // Pack as ABGR (same format as BrainSurface uses)
296 return packABGR(r, g, b, static_cast<uint32_t>(alpha));
297}
298
299//=============================================================================================================
300
301void SourceEstimateOverlay::computeInterpolationMatrix(BrainSurface* surface, int hemi, double cancelDist)
302{
303 if (!surface)
304 return;
305
306 const INVLIB::InvSourceEstimate* stc = nullptr;
307 QSharedPointer<Eigen::SparseMatrix<float>>* pMatPtr = nullptr;
308
309 if (hemi == 0 && m_hasLh) {
310 stc = &m_stcLh;
311 pMatPtr = &m_interpolationMatLh;
312 } else if (hemi == 1 && m_hasRh) {
313 stc = &m_stcRh;
314 pMatPtr = &m_interpolationMatRh;
315 } else {
316 return;
317 }
318
319 if (stc->isEmpty())
320 return;
321
322 qDebug() << "SourceEstimateOverlay: Computing interpolation matrix for hemi" << hemi;
323
324 // Get vertices and neighbor information needed for Dijkstra (SCDC)
325 Eigen::MatrixX3f matVertices = surface->verticesAsMatrix();
326 std::vector<Eigen::VectorXi> vecNeighbors = surface->computeNeighbors();
327
328 // Source vertex subset from STC (already a VectorXi)
329 Eigen::VectorXi vecSourceVertices = stc->vertices;
330
331 qDebug() << "SourceEstimateOverlay: FsSurface has" << matVertices.rows() << "vertices,"
332 << vecSourceVertices.size() << "sources";
333
334 if (vecSourceVertices.size() == 0) {
335 qWarning() << "SourceEstimateOverlay: No source vertices found";
336 return;
337 }
338
339 // 1. Calculate Distance Table (Geodesic distance on surface)
340 // This uses Dijkstra's algorithm via GeometryInfo::scdc
341 // Note: This can be slow for many sources!
342 qDebug() << "SourceEstimateOverlay: Computing distance table (SCDC)...";
343 QSharedPointer<Eigen::MatrixXd> distTable = DISP3DLIB::GeometryInfo::scdc(
344 matVertices,
345 vecNeighbors,
346 vecSourceVertices,
347 cancelDist);
348
349 if (!distTable || distTable->rows() == 0) {
350 qWarning() << "SourceEstimateOverlay: Failed to compute distance table";
351 return;
352 }
353
354 // 2. Create Interpolation Matrix
355 qDebug() << "SourceEstimateOverlay: Creating interpolation matrix...";
357 vecSourceVertices,
358 distTable,
359 DISP3DLIB::Interpolation::cubic, // Use cubic interpolation function
360 cancelDist);
361
362 if (*pMatPtr && (*pMatPtr)->rows() > 0) {
363 qDebug() << "SourceEstimateOverlay: Interpolation matrix created:"
364 << (*pMatPtr)->rows() << "x" << (*pMatPtr)->cols();
365 } else {
366 qWarning() << "SourceEstimateOverlay: Failed to compute interpolation matrix";
367 }
368}
369
370//=============================================================================================================
371
373{
374 if (hemi == 0) {
375 m_stcLh = stc;
376 m_hasLh = true;
377 } else {
378 m_stcRh = stc;
379 m_hasRh = true;
380 }
381 invalidateColorCache();
382}
383
384//=============================================================================================================
385
386void SourceEstimateOverlay::setInterpolationMatrix(QSharedPointer<Eigen::SparseMatrix<float>> mat, int hemi)
387{
388 if (hemi == 0) {
389 m_interpolationMatLh = mat;
390 } else {
391 m_interpolationMatRh = mat;
392 }
393 invalidateColorCache();
394}
395
396//=============================================================================================================
397
399{
400 if (m_hasLh || m_hasRh) {
401 double minVal, maxVal;
402 getDataRange(minVal, maxVal);
403 m_threshMin = minVal;
404 m_threshMax = maxVal;
405 m_threshMid = (minVal + maxVal) / 2.0;
406 invalidateColorCache();
407 qDebug() << "SourceEstimateOverlay: Auto thresholds set to" << m_threshMin << m_threshMid << m_threshMax;
408 }
409}
410
411//=============================================================================================================
412
413Eigen::VectorXd SourceEstimateOverlay::sourceDataColumn(int timeIndex) const
414{
415 int nLh = m_hasLh ? m_stcLh.data.rows() : 0;
416 int nRh = m_hasRh ? m_stcRh.data.rows() : 0;
417
418 if (nLh == 0 && nRh == 0) {
419 return Eigen::VectorXd();
420 }
421
422 Eigen::VectorXd result(nLh + nRh);
423
424 if (m_hasLh) {
425 int tIdx = qBound(0, timeIndex, static_cast<int>(m_stcLh.data.cols()) - 1);
426 result.head(nLh) = m_stcLh.data.col(tIdx);
427 }
428
429 if (m_hasRh) {
430 int tIdx = qBound(0, timeIndex, static_cast<int>(m_stcRh.data.cols()) - 1);
431 result.segment(nLh, nRh) = m_stcRh.data.col(tIdx);
432 }
433
434 return result;
435}
436
437} // namespace DISP3DLIB
Static scalar-to-colour lookup helpers (Jet, Hot, Bone, Viridis, Cool, RedBlue, MNE) used by every pl...
Renderable cortical / BEM mesh with interleaved vertex attributes and Qt-RHI buffer management.
Colour-mapped source-time-course overlay that interpolates STC activation onto a cortical mesh and up...
Distance-based sparse interpolation weights and per-frame signal smoothing on triangulated meshes.
Surface-constrained geodesic distance and sensor-to-mesh projection helpers.
Lightweight render-related enums (ShaderMode, VisualizationMode) shared across disp3D.
3-D brain visualisation using the Qt RHI rendering backend.
uint32_t packABGR(uint32_t r, uint32_t g, uint32_t b, uint32_t a=0xFF)
Definition rendertypes.h:51
static QRgb valueToColor(double v, const QString &sMap)
Definition colormap.h:683
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.
static Eigen::VectorXf interpolateSignal(const QSharedPointer< Eigen::SparseMatrix< float > > matInterpolationMatrix, const QSharedPointer< Eigen::VectorXf > &vecMeasurementData)
interpolateSignal Interpolates sensor data using the weight matrix (shared pointer version).
static double cubic(const double dIn)
cubic Cubic hyperbola interpolation function.
static QSharedPointer< Eigen::SparseMatrix< float > > createInterpolationMat(const Eigen::VectorXi &vecProjectedSensors, const QSharedPointer< Eigen::MatrixXd > matDistanceTable, double(*interpolationFunction)(double), const double dCancelDist=FLOAT_INFINITY, const Eigen::VectorXi &vecExcludeIndex=Eigen::VectorXi())
createInterpolationMat Calculates the weight matrix for interpolation.
Renderable cortical surface mesh with per-vertex color, curvature data, and GPU buffer management.
Eigen::MatrixX3f verticesAsMatrix() const
uint32_t vertexCount() const
void applySourceEstimateColors(const QVector< uint32_t > &colors)
std::vector< Eigen::VectorXi > computeNeighbors() const
void setThresholds(float min, float mid, float max)
void applyToSurface(BrainSurface *surface, int timeIndex)
Eigen::VectorXd sourceDataColumn(int timeIndex) const
bool loadStc(const QString &path, int hemi)
void getDataRange(double &minVal, double &maxVal) const
void computeInterpolationMatrix(BrainSurface *surface, int hemi, double cancelDist=0.05)
void setInterpolationMatrix(QSharedPointer< Eigen::SparseMatrix< float > > mat, int hemi)
void setStcData(const INVLIB::InvSourceEstimate &stc, int hemi)
Source-space inverse-solution container with dense grid plus optional focal-dipole,...
static bool read(QIODevice &p_IODevice, InvSourceEstimate &p_stc)