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 constexpr float kScaleMEGGrad = 400e-13f; // T/m
45 constexpr float kScaleMEGMag = 1.2e-12f; // T
46 constexpr float kScaleEEG = 30e-6f; // V
47 constexpr float kScaleEOG = 150e-6f; // V
48 constexpr float kScaleEMG = 1e-3f; // V
49 constexpr float kScaleECG = 1e-3f; // V
50 constexpr float kScaleSTIM = 5.0f; // AU
51 constexpr float kScaleMISC = 1.0f; // AU
52 constexpr float kScaleFallback = 1.0f;
53
54 // Internal pseudo-kind keys for separate MEG grad/mag scales
55 constexpr qint32 kMEGGradKind = -1;
56 constexpr qint32 kMEGMagKind = -2;
57}
58
59//=============================================================================================================
60// DEFINE MEMBER METHODS
61//=============================================================================================================
62
64 : QObject(parent)
65{
66 // Default scales
67 m_scaleMap[FIFFV_MEG_CH] = kScaleMEGGrad;
68 m_scaleMap[FIFFV_EEG_CH] = kScaleEEG;
69 m_scaleMap[FIFFV_EOG_CH] = kScaleEOG;
70 m_scaleMap[FIFFV_EMG_CH] = kScaleEMG;
71 m_scaleMap[FIFFV_ECG_CH] = kScaleECG;
72 m_scaleMap[FIFFV_STIM_CH] = kScaleSTIM;
73 m_scaleMap[FIFFV_MISC_CH] = kScaleMISC;
74}
75
76//=============================================================================================================
77
78void ChannelDataModel::init(QSharedPointer<FiffInfo> pFiffInfo)
79{
80 QWriteLocker lk(&m_lock);
81 m_pFiffInfo = pFiffInfo;
82
83 int nCh = (pFiffInfo ? pFiffInfo->nchan : 0) + m_virtualDisplayInfo.size();
84 m_channelData.resize(nCh);
85 for (auto &ch : m_channelData)
86 ch.clear();
87 m_firstSample = 0;
88
89 lk.unlock();
90 rebuildDisplayInfo();
91 emit metaChanged();
92}
93
94//=============================================================================================================
95
96void ChannelDataModel::setData(const MatrixXd &data, int firstSample)
97{
98 if (data.rows() == 0 || data.cols() == 0)
99 return;
100
101 QWriteLocker lk(&m_lock);
102 int nCh = static_cast<int>(data.rows());
103 m_channelData.resize(nCh);
104 for (int ch = 0; ch < nCh; ++ch) {
105 m_channelData[ch].resize(static_cast<int>(data.cols()));
106 for (int s = 0; s < static_cast<int>(data.cols()); ++s)
107 m_channelData[ch][s] = static_cast<float>(data(ch, s));
108 }
109 m_firstSample = firstSample;
110 lk.unlock();
111
112 emit dataChanged();
113}
114
115//=============================================================================================================
116
117void ChannelDataModel::appendData(const MatrixXd &data)
118{
119 if (data.rows() == 0 || data.cols() == 0)
120 return;
121
122 QWriteLocker lk(&m_lock);
123 int nCh = static_cast<int>(data.rows());
124 int nNew = static_cast<int>(data.cols());
125
126 if (m_channelData.size() != nCh)
127 m_channelData.resize(nCh);
128
129 for (int ch = 0; ch < nCh; ++ch) {
130 auto &buf = m_channelData[ch];
131 int oldSize = buf.size();
132 buf.resize(oldSize + nNew);
133 for (int s = 0; s < nNew; ++s)
134 buf[oldSize + s] = static_cast<float>(data(ch, s));
135
136 // Drop oldest samples when capacity exceeded
137 if (m_maxStoredSamples > 0 && buf.size() > m_maxStoredSamples) {
138 int drop = buf.size() - m_maxStoredSamples;
139 buf.remove(0, drop);
140 if (ch == 0)
141 m_firstSample += drop;
142 }
143 }
144 lk.unlock();
145
146 emit dataChanged();
147}
148
149//=============================================================================================================
150
152{
153 {
154 QWriteLocker lk(&m_lock);
155 for (auto &ch : m_channelData)
156 ch.clear();
157 m_firstSample = 0;
158 }
159 emit dataChanged();
160}
161
162//=============================================================================================================
163
164void ChannelDataModel::setScaleMap(const QMap<qint32, float> &scaleMap)
165{
166 {
167 QWriteLocker lk(&m_lock);
168 m_scaleMap = scaleMap;
169 }
170 rebuildDisplayInfo();
171 emit metaChanged();
172}
173
174//=============================================================================================================
175
176void ChannelDataModel::setScaleMapFromStrings(const QMap<QString, double> &scaleMap)
177{
178 QMap<qint32, float> intMap;
179 if (scaleMap.contains(QStringLiteral("MEG_grad")))
180 intMap[kMEGGradKind] = static_cast<float>(scaleMap.value(QStringLiteral("MEG_grad")));
181 if (scaleMap.contains(QStringLiteral("MEG_mag")))
182 intMap[kMEGMagKind] = static_cast<float>(scaleMap.value(QStringLiteral("MEG_mag")));
183 if (scaleMap.contains(QStringLiteral("MEG_EEG")))
184 intMap[FIFFV_EEG_CH] = static_cast<float>(scaleMap.value(QStringLiteral("MEG_EEG")));
185 if (scaleMap.contains(QStringLiteral("MEG_EOG")))
186 intMap[FIFFV_EOG_CH] = static_cast<float>(scaleMap.value(QStringLiteral("MEG_EOG")));
187 if (scaleMap.contains(QStringLiteral("MEG_EMG")))
188 intMap[FIFFV_EMG_CH] = static_cast<float>(scaleMap.value(QStringLiteral("MEG_EMG")));
189 if (scaleMap.contains(QStringLiteral("MEG_ECG")))
190 intMap[FIFFV_ECG_CH] = static_cast<float>(scaleMap.value(QStringLiteral("MEG_ECG")));
191 if (scaleMap.contains(QStringLiteral("MEG_MISC")))
192 intMap[FIFFV_MISC_CH] = static_cast<float>(scaleMap.value(QStringLiteral("MEG_MISC")));
193 if (scaleMap.contains(QStringLiteral("MEG_STIM")))
194 intMap[FIFFV_STIM_CH] = static_cast<float>(scaleMap.value(QStringLiteral("MEG_STIM")));
195 setScaleMap(intMap);
196}
197
198//=============================================================================================================
199
200void ChannelDataModel::setVirtualChannels(const QVector<ChannelDisplayInfo> &virtualChannels)
201{
202 {
203 QWriteLocker lk(&m_lock);
204 m_virtualDisplayInfo = virtualChannels;
205
206 const int realChannelCount = m_pFiffInfo ? m_pFiffInfo->nchan : 0;
207 const int totalChannelCount = realChannelCount + m_virtualDisplayInfo.size();
208 m_channelData.resize(totalChannelCount);
209 }
210
211 rebuildDisplayInfo();
212 emit metaChanged();
213 emit dataChanged();
214}
215
216//=============================================================================================================
217
218void ChannelDataModel::setSignalColor(const QColor &color)
219{
220 {
221 QWriteLocker lk(&m_lock);
222 m_signalColor = color;
223 }
224 rebuildDisplayInfo();
225 emit metaChanged();
226}
227
228//=============================================================================================================
229
231{
232 QWriteLocker lk(&m_lock);
233 m_maxStoredSamples = (n > 0) ? n : 0;
234}
235
236//=============================================================================================================
237
242
243//=============================================================================================================
244
246{
247 {
248 QWriteLocker lk(&m_lock);
249 m_detrendMode = mode;
250 }
251 emit dataChanged();
252}
253
254//=============================================================================================================
255
256void ChannelDataModel::setChannelBad(int channelIdx, bool bad)
257{
258 QWriteLocker lk(&m_lock);
259 if (channelIdx >= 0 && channelIdx < m_displayInfo.size())
260 m_displayInfo[channelIdx].bad = bad;
261
262 if (m_pFiffInfo && channelIdx >= 0 && channelIdx < m_pFiffInfo->nchan) {
263 const QString channelName = m_pFiffInfo->ch_names.value(channelIdx);
264 const int badIndex = m_pFiffInfo->bads.indexOf(channelName);
265
266 if (bad) {
267 if (badIndex < 0)
268 m_pFiffInfo->bads.append(channelName);
269 } else if (badIndex >= 0) {
270 m_pFiffInfo->bads.removeAt(badIndex);
271 }
272 }
273
274 lk.unlock();
275 emit metaChanged();
276}
277
278//=============================================================================================================
279
281{
282 QReadLocker lk(&m_lock);
283 const int realChannelCount = m_pFiffInfo ? m_pFiffInfo->nchan : 0;
284 return qMax(m_channelData.size(), realChannelCount + m_virtualDisplayInfo.size());
285}
286
287//=============================================================================================================
288
290{
291 QReadLocker lk(&m_lock);
292 return m_firstSample;
293}
294
295//=============================================================================================================
296
298{
299 QReadLocker lk(&m_lock);
300 return m_channelData.isEmpty() ? 0 : m_channelData[0].size();
301}
302
303//=============================================================================================================
304
306{
307 QReadLocker lk(&m_lock);
308 if (channelIdx >= 0 && channelIdx < m_displayInfo.size())
309 return m_displayInfo[channelIdx];
310 return {};
311}
312
313//=============================================================================================================
314
315float ChannelDataModel::channelRms(int channelIdx, int first, int last) const
316{
317 QReadLocker lk(&m_lock);
318 if (channelIdx < 0 || channelIdx >= m_channelData.size())
319 return 0.f;
320 const QVector<float> &src = m_channelData[channelIdx];
321 int bufFirst = qBound(0, first - m_firstSample, src.size());
322 int bufLast = qBound(0, last - m_firstSample, src.size());
323 if (bufLast <= bufFirst)
324 return 0.f;
325 // Cap at 1000 samples: use the last kMax samples of the window for speed
326 constexpr int kMax = 1000;
327 if (bufLast - bufFirst > kMax)
328 bufFirst = bufLast - kMax;
329 double sum = 0.0;
330 for (int i = bufFirst; i < bufLast; ++i)
331 sum += static_cast<double>(src[i]) * src[i];
332 return static_cast<float>(qSqrt(sum / (bufLast - bufFirst)));
333}
334
335//=============================================================================================================
336
337float ChannelDataModel::sampleValueAt(int channelIdx, int sample) const
338{
339 QReadLocker lk(&m_lock);
340 if (channelIdx < 0 || channelIdx >= m_channelData.size())
341 return 0.f;
342 const QVector<float> &src = m_channelData[channelIdx];
343 int bufIdx = sample - m_firstSample;
344 if (bufIdx < 0 || bufIdx >= src.size())
345 return 0.f;
346 return src[bufIdx];
347}
348
349//=============================================================================================================
350
351QVector<float> ChannelDataModel::decimatedVertices(int channelIdx,
352 int firstSample,
353 int lastSample,
354 int pixelWidth,
355 int &vboFirstSample) const
356{
357 QReadLocker lk(&m_lock);
358
359 vboFirstSample = firstSample; // may be updated below after clamping
360
361 if (channelIdx < 0 || channelIdx >= m_channelData.size()
362 || 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) sBegin = qMax(sEnd - 1, bufFirst);
445
446 float minV = src[sBegin];
447 float maxV = src[sBegin];
448 for (int s = sBegin + 1; s < sEnd; ++s) {
449 if (src[s] < minV) minV = src[s];
450 if (src[s] > maxV) maxV = src[s];
451 }
452
453 // Subtract trend at the center of this pixel bin
454 float tCenter = static_cast<float>(sBegin - bufFirst) + (sEnd - sBegin) * 0.5f;
455 float trend = useMean ? dcOffset
456 : useLinear ? (linearSlope * tCenter + linearIntercept)
457 : 0.f;
458 minV -= trend;
459 maxV -= trend;
460
461 float xOffset = px * spp;
462
463 // Emit max first, then min: draws ascending spike first
464 result.append(xOffset); result.append(maxV);
465 result.append(xOffset); result.append(minV);
466 }
467
468 return result;
469}
470
471//=============================================================================================================
472// Private
473//=============================================================================================================
474
475void ChannelDataModel::rebuildDisplayInfo()
476{
477 QWriteLocker lk(&m_lock);
478 const int realChannelCount = m_pFiffInfo ? m_pFiffInfo->nchan : 0;
479 const int nCh = qMax(m_channelData.size(), realChannelCount + m_virtualDisplayInfo.size());
480 m_displayInfo.resize(nCh);
481 for (int ch = 0; ch < nCh; ++ch) {
482 if (ch >= realChannelCount && ch - realChannelCount < m_virtualDisplayInfo.size()) {
483 ChannelDisplayInfo info = m_virtualDisplayInfo.at(ch - realChannelCount);
484 if (info.name.isEmpty())
485 info.name = QString("Virtual %1").arg(ch - realChannelCount + 1);
486 if (info.typeLabel.isEmpty())
487 info.typeLabel = QStringLiteral("MISC");
488 if (!info.color.isValid())
489 info.color = m_signalColor;
490 if (info.amplitudeMax <= 0.f)
491 info.amplitudeMax = kScaleFallback;
492 m_displayInfo[ch] = info;
493 m_displayInfo[ch].isVirtualChannel = true;
494 continue;
495 }
496
497 m_displayInfo[ch].amplitudeMax = amplitudeMaxForChannel(ch);
498 m_displayInfo[ch].color = colorForChannel(ch);
499 if (m_pFiffInfo && ch < realChannelCount)
500 m_displayInfo[ch].name = m_pFiffInfo->ch_names[ch];
501 else
502 m_displayInfo[ch].name = QString("CH %1").arg(ch + 1);
503 m_displayInfo[ch].typeLabel = typeLabelForChannel(ch);
504 m_displayInfo[ch].bad = (m_pFiffInfo && ch < realChannelCount)
505 ? m_pFiffInfo->bads.contains(m_displayInfo[ch].name)
506 : false;
507 m_displayInfo[ch].isVirtualChannel = false;
508 }
509}
510
511//=============================================================================================================
512
513float ChannelDataModel::amplitudeMaxForChannel(int ch) const
514{
515 if (!m_pFiffInfo || ch >= m_pFiffInfo->nchan)
516 return kScaleFallback;
517
518 const auto &info = m_pFiffInfo->chs[ch];
519 qint32 kind = info.kind;
520
521 // MEG: distinguish gradiometer vs. magnetometer by unit
522 if (kind == FIFFV_MEG_CH) {
523 if (info.unit == FIFF_UNIT_T_M)
524 return m_scaleMap.value(kMEGGradKind,
525 m_scaleMap.value(FIFFV_MEG_CH, kScaleMEGGrad));
526 else
527 return m_scaleMap.value(kMEGMagKind,
528 m_scaleMap.value(FIFFV_MEG_CH, kScaleMEGMag));
529 }
530 if (m_scaleMap.contains(kind))
531 return m_scaleMap.value(kind);
532 return kScaleFallback;
533}
534
535//=============================================================================================================
536
537QColor ChannelDataModel::colorForChannel(int ch) const
538{
539 if (!m_pFiffInfo || ch >= m_pFiffInfo->nchan)
540 return m_signalColor;
541
542 // Dark colours chosen to be readable on a light (near-white) background
543 switch (m_pFiffInfo->chs[ch].kind) {
544 case FIFFV_MEG_CH: return QColor(20, 90, 180); // dark blue for MEG
545 case FIFFV_EEG_CH: return QColor(170, 55, 10); // dark orange for EEG
546 case FIFFV_EOG_CH: return QColor(130, 0, 130); // dark purple for EOG
547 case FIFFV_ECG_CH: return QColor(190, 15, 45); // dark crimson for ECG
548 case FIFFV_EMG_CH: return QColor(20, 110, 20); // dark green for EMG
549 case FIFFV_STIM_CH: return QColor(180, 100, 0); // dark amber for STIM
550 default: return m_signalColor;
551 }
552}
553
554//=============================================================================================================
555
556QString ChannelDataModel::typeLabelForChannel(int ch) const
557{
558 if (!m_pFiffInfo || ch >= m_pFiffInfo->nchan)
559 return QStringLiteral("MISC");
560 switch (m_pFiffInfo->chs[ch].kind) {
561 case FIFFV_MEG_CH:
562 if (m_pFiffInfo->chs[ch].unit == FIFF_UNIT_T_M)
563 return QStringLiteral("MEG grad");
564 return QStringLiteral("MEG mag");
565 case FIFFV_EEG_CH: return QStringLiteral("EEG");
566 case FIFFV_EOG_CH: return QStringLiteral("EOG");
567 case FIFFV_ECG_CH: return QStringLiteral("ECG");
568 case FIFFV_EMG_CH: return QStringLiteral("EMG");
569 case FIFFV_STIM_CH: return QStringLiteral("STIM");
570 default: return QStringLiteral("MISC");
571 }
572}
573
574//=============================================================================================================
575
577{
578 QReadLocker lk(&m_lock);
579 return m_pFiffInfo ? static_cast<float>(m_pFiffInfo->sfreq) : 0.f;
580}
Circular-buffer Qt model exposing a rolling window of the live FIFF stream as a table.
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...
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)