v2.0.0
Loading...
Searching...
No Matches
channeldatamodel.cpp
Go to the documentation of this file.
1//=============================================================================================================
13
14//=============================================================================================================
15// INCLUDES
16//=============================================================================================================
17
18#include "channeldatamodel.h"
19#include <fiff/fiff_info.h>
20#include <fiff/fiff_constants.h>
21
22//=============================================================================================================
23// QT INCLUDES
24//=============================================================================================================
25
26#include <QtMath>
27#include <QWriteLocker>
28#include <QReadLocker>
29
30//=============================================================================================================
31// USED NAMESPACES
32//=============================================================================================================
33
34using namespace DISPLIB;
35using namespace FIFFLIB;
36using namespace Eigen;
37
38//=============================================================================================================
39// Default amplitude scales (physical units) per channel kind.
40// Keys match FIFF channel kind constants.
41//=============================================================================================================
42
43namespace
44{
45constexpr float kScaleMEGGrad = 400e-13f; // T/m
46constexpr float kScaleMEGMag = 1.2e-12f; // T
47constexpr float kScaleEEG = 30e-6f; // V
48constexpr float kScaleEOG = 150e-6f; // V
49constexpr float kScaleEMG = 1e-3f; // V
50constexpr float kScaleECG = 1e-3f; // V
51constexpr float kScaleSTIM = 5.0f; // AU
52constexpr float kScaleMISC = 1.0f; // AU
53constexpr float kScaleFallback = 1.0f;
54
55// Internal pseudo-kind keys for separate MEG grad/mag scales
56constexpr qint32 kMEGGradKind = -1;
57constexpr qint32 kMEGMagKind = -2;
58}
59
60//=============================================================================================================
61// DEFINE MEMBER METHODS
62//=============================================================================================================
63
65: QObject(parent)
66{
67 // Default scales
68 m_scaleMap[FIFFV_MEG_CH] = kScaleMEGGrad;
69 m_scaleMap[FIFFV_EEG_CH] = kScaleEEG;
70 m_scaleMap[FIFFV_EOG_CH] = kScaleEOG;
71 m_scaleMap[FIFFV_EMG_CH] = kScaleEMG;
72 m_scaleMap[FIFFV_ECG_CH] = kScaleECG;
73 m_scaleMap[FIFFV_STIM_CH] = kScaleSTIM;
74 m_scaleMap[FIFFV_MISC_CH] = kScaleMISC;
75}
76
77//=============================================================================================================
78
79void ChannelDataModel::init(QSharedPointer<FiffInfo> pFiffInfo)
80{
81 QWriteLocker lk(&m_lock);
82 m_pFiffInfo = pFiffInfo;
83
84 int nCh = (pFiffInfo ? pFiffInfo->nchan : 0) + m_virtualDisplayInfo.size();
85 m_channelData.resize(nCh);
86 for (auto& ch : m_channelData)
87 ch.clear();
88 m_firstSample = 0;
89
90 lk.unlock();
91 rebuildDisplayInfo();
92 emit metaChanged();
93}
94
95//=============================================================================================================
96
97void ChannelDataModel::setData(const MatrixXd& data, int firstSample)
98{
99 if (data.rows() == 0 || data.cols() == 0)
100 return;
101
102 QWriteLocker lk(&m_lock);
103 int nCh = static_cast<int>(data.rows());
104 m_channelData.resize(nCh);
105 for (int ch = 0; ch < nCh; ++ch) {
106 m_channelData[ch].resize(static_cast<int>(data.cols()));
107 for (int s = 0; s < static_cast<int>(data.cols()); ++s)
108 m_channelData[ch][s] = static_cast<float>(data(ch, s));
109 }
110 m_firstSample = firstSample;
111 lk.unlock();
112
113 emit dataChanged();
114}
115
116//=============================================================================================================
117
118void ChannelDataModel::appendData(const MatrixXd& data)
119{
120 if (data.rows() == 0 || data.cols() == 0)
121 return;
122
123 QWriteLocker lk(&m_lock);
124 int nCh = static_cast<int>(data.rows());
125 int nNew = static_cast<int>(data.cols());
126
127 if (m_channelData.size() != nCh)
128 m_channelData.resize(nCh);
129
130 for (int ch = 0; ch < nCh; ++ch) {
131 auto& buf = m_channelData[ch];
132 int oldSize = buf.size();
133 buf.resize(oldSize + nNew);
134 for (int s = 0; s < nNew; ++s)
135 buf[oldSize + s] = static_cast<float>(data(ch, s));
136
137 // Drop oldest samples when capacity exceeded
138 if (m_maxStoredSamples > 0 && buf.size() > m_maxStoredSamples) {
139 int drop = buf.size() - m_maxStoredSamples;
140 buf.remove(0, drop);
141 if (ch == 0)
142 m_firstSample += drop;
143 }
144 }
145 lk.unlock();
146
147 emit dataChanged();
148}
149
150//=============================================================================================================
151
153{
154 {
155 QWriteLocker lk(&m_lock);
156 for (auto& ch : m_channelData)
157 ch.clear();
158 m_firstSample = 0;
159 }
160 emit dataChanged();
161}
162
163//=============================================================================================================
164
165void ChannelDataModel::setScaleMap(const QMap<qint32, float>& scaleMap)
166{
167 {
168 QWriteLocker lk(&m_lock);
169 m_scaleMap = scaleMap;
170 }
171 rebuildDisplayInfo();
172 emit metaChanged();
173}
174
175//=============================================================================================================
176
177void ChannelDataModel::setScaleMapFromStrings(const QMap<QString, double>& scaleMap)
178{
179 QMap<qint32, float> intMap;
180 if (scaleMap.contains(QStringLiteral("MEG_grad")))
181 intMap[kMEGGradKind] = static_cast<float>(scaleMap.value(QStringLiteral("MEG_grad")));
182 if (scaleMap.contains(QStringLiteral("MEG_mag")))
183 intMap[kMEGMagKind] = static_cast<float>(scaleMap.value(QStringLiteral("MEG_mag")));
184 if (scaleMap.contains(QStringLiteral("MEG_EEG")))
185 intMap[FIFFV_EEG_CH] = static_cast<float>(scaleMap.value(QStringLiteral("MEG_EEG")));
186 if (scaleMap.contains(QStringLiteral("MEG_EOG")))
187 intMap[FIFFV_EOG_CH] = static_cast<float>(scaleMap.value(QStringLiteral("MEG_EOG")));
188 if (scaleMap.contains(QStringLiteral("MEG_EMG")))
189 intMap[FIFFV_EMG_CH] = static_cast<float>(scaleMap.value(QStringLiteral("MEG_EMG")));
190 if (scaleMap.contains(QStringLiteral("MEG_ECG")))
191 intMap[FIFFV_ECG_CH] = static_cast<float>(scaleMap.value(QStringLiteral("MEG_ECG")));
192 if (scaleMap.contains(QStringLiteral("MEG_MISC")))
193 intMap[FIFFV_MISC_CH] = static_cast<float>(scaleMap.value(QStringLiteral("MEG_MISC")));
194 if (scaleMap.contains(QStringLiteral("MEG_STIM")))
195 intMap[FIFFV_STIM_CH] = static_cast<float>(scaleMap.value(QStringLiteral("MEG_STIM")));
196 setScaleMap(intMap);
197}
198
199//=============================================================================================================
200
201void ChannelDataModel::setVirtualChannels(const QVector<ChannelDisplayInfo>& virtualChannels)
202{
203 {
204 QWriteLocker lk(&m_lock);
205 m_virtualDisplayInfo = virtualChannels;
206
207 const int realChannelCount = m_pFiffInfo ? m_pFiffInfo->nchan : 0;
208 const int totalChannelCount = realChannelCount + m_virtualDisplayInfo.size();
209 m_channelData.resize(totalChannelCount);
210 }
211
212 rebuildDisplayInfo();
213 emit metaChanged();
214 emit dataChanged();
215}
216
217//=============================================================================================================
218
219void ChannelDataModel::setSignalColor(const QColor& color)
220{
221 {
222 QWriteLocker lk(&m_lock);
223 m_signalColor = color;
224 }
225 rebuildDisplayInfo();
226 emit metaChanged();
227}
228
229//=============================================================================================================
230
232{
233 QWriteLocker lk(&m_lock);
234 m_maxStoredSamples = (n > 0) ? n : 0;
235}
236
237//=============================================================================================================
238
243
244//=============================================================================================================
245
247{
248 {
249 QWriteLocker lk(&m_lock);
250 m_detrendMode = mode;
251 }
252 emit dataChanged();
253}
254
255//=============================================================================================================
256
257void ChannelDataModel::setChannelBad(int channelIdx, bool bad)
258{
259 QWriteLocker lk(&m_lock);
260 if (channelIdx >= 0 && channelIdx < m_displayInfo.size())
261 m_displayInfo[channelIdx].bad = bad;
262
263 if (m_pFiffInfo && channelIdx >= 0 && channelIdx < m_pFiffInfo->nchan) {
264 const QString channelName = m_pFiffInfo->ch_names.value(channelIdx);
265 const int badIndex = m_pFiffInfo->bads.indexOf(channelName);
266
267 if (bad) {
268 if (badIndex < 0)
269 m_pFiffInfo->bads.append(channelName);
270 } else if (badIndex >= 0) {
271 m_pFiffInfo->bads.removeAt(badIndex);
272 }
273 }
274
275 lk.unlock();
276 emit metaChanged();
277}
278
279//=============================================================================================================
280
282{
283 QReadLocker lk(&m_lock);
284 const int realChannelCount = m_pFiffInfo ? m_pFiffInfo->nchan : 0;
285 return qMax(m_channelData.size(), realChannelCount + m_virtualDisplayInfo.size());
286}
287
288//=============================================================================================================
289
291{
292 QReadLocker lk(&m_lock);
293 return m_firstSample;
294}
295
296//=============================================================================================================
297
299{
300 QReadLocker lk(&m_lock);
301 return m_channelData.isEmpty() ? 0 : m_channelData[0].size();
302}
303
304//=============================================================================================================
305
307{
308 QReadLocker lk(&m_lock);
309 if (channelIdx >= 0 && channelIdx < m_displayInfo.size())
310 return m_displayInfo[channelIdx];
311 return {};
312}
313
314//=============================================================================================================
315
316float ChannelDataModel::channelRms(int channelIdx, int first, int last) const
317{
318 QReadLocker lk(&m_lock);
319 if (channelIdx < 0 || channelIdx >= m_channelData.size())
320 return 0.f;
321 const QVector<float>& src = m_channelData[channelIdx];
322 int bufFirst = qBound(0, first - m_firstSample, src.size());
323 int bufLast = qBound(0, last - m_firstSample, src.size());
324 if (bufLast <= bufFirst)
325 return 0.f;
326 // Cap at 1000 samples: use the last kMax samples of the window for speed
327 constexpr int kMax = 1000;
328 if (bufLast - bufFirst > kMax)
329 bufFirst = bufLast - kMax;
330 double sum = 0.0;
331 for (int i = bufFirst; i < bufLast; ++i)
332 sum += static_cast<double>(src[i]) * src[i];
333 return static_cast<float>(qSqrt(sum / (bufLast - bufFirst)));
334}
335
336//=============================================================================================================
337
338float ChannelDataModel::sampleValueAt(int channelIdx, int sample) const
339{
340 QReadLocker lk(&m_lock);
341 if (channelIdx < 0 || channelIdx >= m_channelData.size())
342 return 0.f;
343 const QVector<float>& src = m_channelData[channelIdx];
344 int bufIdx = sample - m_firstSample;
345 if (bufIdx < 0 || bufIdx >= src.size())
346 return 0.f;
347 return src[bufIdx];
348}
349
350//=============================================================================================================
351
352QVector<float> ChannelDataModel::decimatedVertices(int channelIdx,
353 int firstSample,
354 int lastSample,
355 int pixelWidth,
356 int& vboFirstSample) const
357{
358 QReadLocker lk(&m_lock);
359
360 vboFirstSample = firstSample; // may be updated below after clamping
361
362 if (channelIdx < 0 || channelIdx >= m_channelData.size() || pixelWidth <= 0 || firstSample >= lastSample) {
363 return {};
364 }
365
366 const QVector<float>& src = m_channelData[channelIdx];
367 // Map absolute sample indices to buffer indices
368 int bufFirst = firstSample - m_firstSample;
369 int bufLast = lastSample - m_firstSample;
370
371 // Clamp to available data
372 bufFirst = qBound(0, bufFirst, src.size());
373 bufLast = qBound(0, bufLast, src.size());
374
375 // Update to the actual absolute sample index at vertex 0 after clamping.
376 // When firstSample < m_firstSample (tile extends before buffer start),
377 // bufFirst is clamped to 0 so vboFirstSample must reflect m_firstSample,
378 // not the original (possibly negative) firstSample.
379 vboFirstSample = m_firstSample + bufFirst;
380
381 if (bufLast <= bufFirst)
382 return {};
383
384 int nSamples = bufLast - bufFirst;
385
386 // ── Detrending: compute offset / trend over the window ────────────
387 float dcOffset = 0.f;
388 float linearSlope = 0.f;
389 float linearIntercept = 0.f;
390 const bool useLinear = (m_detrendMode == DetrendMode::Linear);
391 const bool useMean = (m_detrendMode == DetrendMode::Mean);
392
393 if (useMean) {
394 double sum = 0.0;
395 for (int i = bufFirst; i < bufLast; ++i)
396 sum += src[i];
397 dcOffset = static_cast<float>(sum / nSamples);
398 } else if (useLinear) {
399 // Least-squares fit: y = slope * t + intercept, where t = 0..nSamples-1
400 double sumX = 0.0, sumY = 0.0, sumXX = 0.0, sumXY = 0.0;
401 for (int i = 0; i < nSamples; ++i) {
402 double x = static_cast<double>(i);
403 double y = static_cast<double>(src[bufFirst + i]);
404 sumX += x;
405 sumY += y;
406 sumXX += x * x;
407 sumXY += x * y;
408 }
409 double denom = nSamples * sumXX - sumX * sumX;
410 if (qAbs(denom) > 1e-30) {
411 linearSlope = static_cast<float>((nSamples * sumXY - sumX * sumY) / denom);
412 linearIntercept = static_cast<float>((sumY - linearSlope * sumX) / nSamples);
413 }
414 }
415
416 if (nSamples <= pixelWidth * 2) {
417 // ── Raw path: one vertex per sample ────────────────────────────
418 QVector<float> result;
419 result.reserve(nSamples * 2);
420 for (int i = 0; i < nSamples; ++i) {
421 float trend = useMean ? dcOffset
422 : useLinear ? (linearSlope * i + linearIntercept)
423 : 0.f;
424 result.append(static_cast<float>(i));
425 result.append(src[bufFirst + i] - trend);
426 }
427 return result;
428 }
429
430 // ── Decimation path: min/max envelope, 2 vertices per pixel ─────────
431 // Interleaved as (x_at_col, max), (x_at_col, min) per column.
432 // Connected as LINE_STRIP this draws: vertical spike at each column,
433 // diagonals between columns — the classic oscilloscope envelope look.
434
435 QVector<float> result;
436 result.reserve(pixelWidth * 4); // 2 vertices × 2 floats
437
438 float spp = static_cast<float>(nSamples) / pixelWidth; // samples per pixel
439
440 for (int px = 0; px < pixelWidth; ++px) {
441 int sBegin = bufFirst + static_cast<int>(px * spp);
442 int sEnd = bufFirst + static_cast<int>((px + 1) * spp);
443 sEnd = qMin(sEnd, bufLast);
444 if (sBegin >= sEnd)
445 sBegin = qMax(sEnd - 1, bufFirst);
446
447 float minV = src[sBegin];
448 float maxV = src[sBegin];
449 for (int s = sBegin + 1; s < sEnd; ++s) {
450 if (src[s] < minV)
451 minV = src[s];
452 if (src[s] > maxV)
453 maxV = src[s];
454 }
455
456 // Subtract trend at the center of this pixel bin
457 float tCenter = static_cast<float>(sBegin - bufFirst) + (sEnd - sBegin) * 0.5f;
458 float trend = useMean ? dcOffset
459 : useLinear ? (linearSlope * tCenter + linearIntercept)
460 : 0.f;
461 minV -= trend;
462 maxV -= trend;
463
464 float xOffset = px * spp;
465
466 // Emit max first, then min: draws ascending spike first
467 result.append(xOffset);
468 result.append(maxV);
469 result.append(xOffset);
470 result.append(minV);
471 }
472
473 return result;
474}
475
476//=============================================================================================================
477// Private
478//=============================================================================================================
479
480void ChannelDataModel::rebuildDisplayInfo()
481{
482 QWriteLocker lk(&m_lock);
483 const int realChannelCount = m_pFiffInfo ? m_pFiffInfo->nchan : 0;
484 const int nCh = qMax(m_channelData.size(), realChannelCount + m_virtualDisplayInfo.size());
485 m_displayInfo.resize(nCh);
486 for (int ch = 0; ch < nCh; ++ch) {
487 if (ch >= realChannelCount && ch - realChannelCount < m_virtualDisplayInfo.size()) {
488 ChannelDisplayInfo info = m_virtualDisplayInfo.at(ch - realChannelCount);
489 if (info.name.isEmpty())
490 info.name = QString("Virtual %1").arg(ch - realChannelCount + 1);
491 if (info.typeLabel.isEmpty())
492 info.typeLabel = QStringLiteral("MISC");
493 if (!info.color.isValid())
494 info.color = m_signalColor;
495 if (info.amplitudeMax <= 0.f)
496 info.amplitudeMax = kScaleFallback;
497 m_displayInfo[ch] = info;
498 m_displayInfo[ch].isVirtualChannel = true;
499 continue;
500 }
501
502 m_displayInfo[ch].amplitudeMax = amplitudeMaxForChannel(ch);
503 m_displayInfo[ch].color = colorForChannel(ch);
504 if (m_pFiffInfo && ch < realChannelCount)
505 m_displayInfo[ch].name = m_pFiffInfo->ch_names[ch];
506 else
507 m_displayInfo[ch].name = QString("CH %1").arg(ch + 1);
508 m_displayInfo[ch].typeLabel = typeLabelForChannel(ch);
509 m_displayInfo[ch].bad = (m_pFiffInfo && ch < realChannelCount)
510 ? m_pFiffInfo->bads.contains(m_displayInfo[ch].name)
511 : false;
512 m_displayInfo[ch].isVirtualChannel = false;
513 }
514}
515
516//=============================================================================================================
517
518float ChannelDataModel::amplitudeMaxForChannel(int ch) const
519{
520 if (!m_pFiffInfo || ch >= m_pFiffInfo->nchan)
521 return kScaleFallback;
522
523 const auto& info = m_pFiffInfo->chs[ch];
524 qint32 kind = info.kind;
525
526 // MEG: distinguish gradiometer vs. magnetometer by unit
527 if (kind == FIFFV_MEG_CH) {
528 if (info.unit == FIFF_UNIT_T_M)
529 return m_scaleMap.value(kMEGGradKind,
530 m_scaleMap.value(FIFFV_MEG_CH, kScaleMEGGrad));
531 else
532 return m_scaleMap.value(kMEGMagKind,
533 m_scaleMap.value(FIFFV_MEG_CH, kScaleMEGMag));
534 }
535 if (m_scaleMap.contains(kind))
536 return m_scaleMap.value(kind);
537 return kScaleFallback;
538}
539
540//=============================================================================================================
541
542QColor ChannelDataModel::colorForChannel(int ch) const
543{
544 if (!m_pFiffInfo || ch >= m_pFiffInfo->nchan)
545 return m_signalColor;
546
547 // Dark colours chosen to be readable on a light (near-white) background
548 switch (m_pFiffInfo->chs[ch].kind) {
549 case FIFFV_MEG_CH:
550 return QColor(20, 90, 180); // dark blue for MEG
551 case FIFFV_EEG_CH:
552 return QColor(170, 55, 10); // dark orange for EEG
553 case FIFFV_EOG_CH:
554 return QColor(130, 0, 130); // dark purple for EOG
555 case FIFFV_ECG_CH:
556 return QColor(190, 15, 45); // dark crimson for ECG
557 case FIFFV_EMG_CH:
558 return QColor(20, 110, 20); // dark green for EMG
559 case FIFFV_STIM_CH:
560 return QColor(180, 100, 0); // dark amber for STIM
561 default:
562 return m_signalColor;
563 }
564}
565
566//=============================================================================================================
567
568QString ChannelDataModel::typeLabelForChannel(int ch) const
569{
570 if (!m_pFiffInfo || ch >= m_pFiffInfo->nchan)
571 return QStringLiteral("MISC");
572 switch (m_pFiffInfo->chs[ch].kind) {
573 case FIFFV_MEG_CH:
574 if (m_pFiffInfo->chs[ch].unit == FIFF_UNIT_T_M)
575 return QStringLiteral("MEG grad");
576 return QStringLiteral("MEG mag");
577 case FIFFV_EEG_CH:
578 return QStringLiteral("EEG");
579 case FIFFV_EOG_CH:
580 return QStringLiteral("EOG");
581 case FIFFV_ECG_CH:
582 return QStringLiteral("ECG");
583 case FIFFV_EMG_CH:
584 return QStringLiteral("EMG");
585 case FIFFV_STIM_CH:
586 return QStringLiteral("STIM");
587 default:
588 return QStringLiteral("MISC");
589 }
590}
591
592//=============================================================================================================
593
595{
596 QReadLocker lk(&m_lock);
597 return m_pFiffInfo ? static_cast<float>(m_pFiffInfo->sfreq) : 0.f;
598}
Symbolic FIFF tag, block, value, unit and channel-type constants shared across FIFFLIB.
#define FIFFV_EOG_CH
#define FIFFV_EEG_CH
#define FIFFV_MISC_CH
#define FIFFV_MEG_CH
#define FIFFV_STIM_CH
#define FIFFV_EMG_CH
#define FIFFV_ECG_CH
#define FIFF_UNIT_T_M
Full FIFF measurement metadata: everything from FIFFB_MEAS / FIFFB_MEAS_INFO needed to interpret a re...
Circular-buffer Qt model exposing a rolling window of the live FIFF stream as a table.
FIFF file I/O, in-memory data structures and high-level readers/writers.
2-D display widgets and visualisation helpers (charts, topography, colour maps).
DetrendMode
Channel display metadata (read-only from the renderer's perspective).
Channel display metadata (read-only from the renderer's perspective).
void appendData(const Eigen::MatrixXd &data)
void setDetrendMode(DetrendMode mode)
void setScaleMap(const QMap< qint32, float > &scaleMap)
float channelRms(int channelIdx, int firstSample, int lastSample) const
float sampleValueAt(int channelIdx, int sample) const
void setScaleMapFromStrings(const QMap< QString, double > &scaleMap)
void setVirtualChannels(const QVector< ChannelDisplayInfo > &virtualChannels)
void setChannelBad(int channelIdx, bool bad)
void init(QSharedPointer< FIFFLIB::FiffInfo > pFiffInfo)
void setSignalColor(const QColor &color)
void setData(const Eigen::MatrixXd &data, int firstSample=0)
ChannelDisplayInfo channelInfo(int channelIdx) const
QVector< float > decimatedVertices(int channelIdx, int firstSample, int lastSample, int pixelWidth, int &vboFirstSample) const
ChannelDataModel(QObject *parent=nullptr)