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