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