v2.0.0
Loading...
Searching...
No Matches
bids_brain_vision_reader.cpp
Go to the documentation of this file.
1//=============================================================================================================
12
13//=============================================================================================================
14// INCLUDES
15//=============================================================================================================
16
18
19#include <fiff/fiff_constants.h>
20
21//=============================================================================================================
22// QT INCLUDES
23//=============================================================================================================
24
25#include <QDebug>
26#include <QFileInfo>
27#include <QDir>
28#include <QTextStream>
29#include <QRegularExpression>
30#include <QtEndian>
31
32//=============================================================================================================
33// USED NAMESPACES
34//=============================================================================================================
35
36using namespace BIDSLIB;
37using namespace FIFFLIB;
38using namespace Eigen;
39
40//=============================================================================================================
41// BrainVisionChannelInfo
42//=============================================================================================================
43
45{
46 FiffChInfo info;
47 info.scanNo = channelNumber;
48 info.logNo = channelNumber;
49
50 // Detect channel type from name
51 QString nameUpper = name.toUpper();
52 if (nameUpper.contains("ECOG"))
53 info.kind = FIFFV_ECOG_CH;
54 else if (nameUpper.contains("SEEG"))
55 info.kind = FIFFV_SEEG_CH;
56 else if (nameUpper.contains("EOG") || nameUpper == "HEOGL" || nameUpper == "HEOGR" || nameUpper == "VEOGB")
57 info.kind = FIFFV_EOG_CH;
58 else if (nameUpper.contains("ECG") || nameUpper.contains("EKG"))
59 info.kind = FIFFV_ECG_CH;
60 else if (nameUpper.contains("EMG"))
61 info.kind = FIFFV_EMG_CH;
62 else if (nameUpper == "STI 014" || nameUpper.contains("STIM"))
63 info.kind = FIFFV_STIM_CH;
64 else {
65 // Like mne read_raw_brainvision: voltage channels are EEG, anything else (ARU, µS, °C, ...) is misc
67 }
68
69 // Map unit
71 info.unit = FIFF_UNIT_V;
72 if (unit == QLatin1String("nV"))
74 else if (unit == QStringLiteral("\u00B5V") || unit == QLatin1String("uV"))
76 else if (unit == QLatin1String("mV"))
78 else
80 } else {
81 info.unit = FIFF_UNIT_NONE;
83 }
84
85 // Calibration: resolution is the scaling factor from raw → physical units
86 info.cal = resolution;
87 info.range = 1.0f;
88 info.ch_name = name;
89
90 return info;
91}
92
93//=============================================================================================================
94// BrainVisionReader
95//=============================================================================================================
96
100
101//=============================================================================================================
102
104{
105 if (m_dataFile.isOpen()) {
106 m_dataFile.close();
107 }
108}
109
110//=============================================================================================================
111
112bool BrainVisionReader::open(const QString& sFilePath)
113{
114 m_sVhdrPath = sFilePath;
115
116 if (!parseHeader(sFilePath)) {
117 return false;
118 }
119
120 // Open the binary data file
121 m_dataFile.setFileName(m_sDataPath);
122 if (!m_dataFile.open(QIODevice::ReadOnly)) {
123 qWarning() << "[BrainVisionReader::open] Could not open data file:" << m_sDataPath;
124 return false;
125 }
126
127 computeSampleCount();
128
129 // Parse markers if marker file exists
130 if (!m_sMarkerPath.isEmpty()) {
131 parseMarkers(m_sMarkerPath);
132 }
133
134 m_bIsOpen = true;
135 return true;
136}
137
138//=============================================================================================================
139
140bool BrainVisionReader::parseHeader(const QString& sVhdrPath)
141{
142 QFile hdrFile(sVhdrPath);
143 if (!hdrFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
144 qWarning() << "[BrainVisionReader::parseHeader] Could not open header:" << sVhdrPath;
145 return false;
146 }
147
148 QFileInfo fi(sVhdrPath);
149 QString sDir = fi.absolutePath();
150
151 QTextStream in(&hdrFile);
152
153 // Validate version line
154 QString firstLine = in.readLine().trimmed();
155 QRegularExpression versionRe(
156 QStringLiteral("Brain ?Vision( Core| V-Amp)? Data( Exchange)? Header File,? Version [12]\\.0"),
157 QRegularExpression::CaseInsensitiveOption);
158 if (!versionRe.match(firstLine).hasMatch()) {
159 qWarning() << "[BrainVisionReader::parseHeader] Unrecognized header version:" << firstLine;
160 return false;
161 }
162
163 // Simple INI-style parser (sections + key=value)
164 QString currentSection;
165 QMap<QString, QMap<QString, QString>> sections;
166
167 while (!in.atEnd()) {
168 QString line = in.readLine().trimmed();
169 if (line.isEmpty() || line.startsWith(';'))
170 continue;
171
172 if (line.startsWith('[') && line.endsWith(']')) {
173 currentSection = line.mid(1, line.size() - 2);
174 // Stop parsing at [Comment] section — it's free-form text
175 if (currentSection.toLower() == "comment")
176 break;
177 continue;
178 }
179
180 int eqPos = line.indexOf('=');
181 if (eqPos > 0 && !currentSection.isEmpty()) {
182 QString key = line.left(eqPos).trimmed();
183 QString value = line.mid(eqPos + 1).trimmed();
184 sections[currentSection][key] = value;
185 }
186 }
187
188 hdrFile.close();
189
190 // Extract Common Infos — handle both "Common Infos" and "Common infos" (NeurOne variant)
191 QMap<QString, QString> commonInfos;
192 if (sections.contains("Common Infos"))
193 commonInfos = sections["Common Infos"];
194 else if (sections.contains("Common infos"))
195 commonInfos = sections["Common infos"];
196
197 if (commonInfos.isEmpty()) {
198 qWarning() << "[BrainVisionReader::parseHeader] Missing [Common Infos] section";
199 return false;
200 }
201
202 // Data file path (relative to .vhdr location)
203 QString dataFileName = commonInfos.value("DataFile");
204 if (dataFileName.isEmpty()) {
205 qWarning() << "[BrainVisionReader::parseHeader] No DataFile specified";
206 return false;
207 }
208 m_sDataPath = QDir(sDir).absoluteFilePath(dataFileName);
209
210 // Marker file path
211 QString markerFileName = commonInfos.value("MarkerFile");
212 if (!markerFileName.isEmpty()) {
213 m_sMarkerPath = QDir(sDir).absoluteFilePath(markerFileName);
214 }
215
216 if (commonInfos.value("DataFormat", "BINARY").toUpper() != QLatin1String("BINARY")) {
217 qWarning() << "[BrainVisionReader::parseHeader] Only BINARY data is supported, not" << commonInfos.value("DataFormat");
218 return false;
219 }
220
221 // Data orientation (default: MULTIPLEXED)
222 QString orientation = commonInfos.value("DataOrientation", "MULTIPLEXED").toUpper();
223 m_orientation = (orientation == "VECTORIZED") ? BVOrientation::VECTORIZED : BVOrientation::MULTIPLEXED;
224
225 // Number of channels
226 m_iNumChannels = commonInfos.value("NumberOfChannels", "0").toInt();
227 if (m_iNumChannels <= 0) {
228 qWarning() << "[BrainVisionReader::parseHeader] Invalid channel count:" << m_iNumChannels;
229 return false;
230 }
231
232 // Sampling interval in microseconds → frequency in Hz
233 float samplingInterval = commonInfos.value("SamplingInterval", "0").toFloat();
234 if (samplingInterval > 0.0f) {
235 m_fSFreq = 1.0e6f / samplingInterval;
236 }
237
238 // Binary format
239 QMap<QString, QString> binaryInfos;
240 if (sections.contains("Binary Infos"))
241 binaryInfos = sections["Binary Infos"];
242
243 QString binaryFormat = binaryInfos.value("BinaryFormat", "INT_16").toUpper();
244 if (binaryFormat == "INT_32")
245 m_binaryFormat = BVBinaryFormat::INT_32;
246 else if (binaryFormat == "IEEE_FLOAT_32")
247 m_binaryFormat = BVBinaryFormat::IEEE_FLOAT_32;
248 else
249 m_binaryFormat = BVBinaryFormat::INT_16;
250
251 // Channel Infos — Format: Ch<N>=<Name>,<Reference>,<Resolution>,<Unit>
252 QMap<QString, QString> channelInfos;
253 if (sections.contains("Channel Infos"))
254 channelInfos = sections["Channel Infos"];
255
256 m_vChannels.clear();
257 m_vChannels.reserve(m_iNumChannels);
258
259 for (int i = 1; i <= m_iNumChannels; ++i) {
260 QString key = QStringLiteral("Ch%1").arg(i);
261 QString value = channelInfos.value(key);
262
263 BrainVisionChannelInfo ch;
264 ch.channelNumber = i - 1; // 0-based internally
265
266 if (!value.isEmpty()) {
267 // Decode \1 back to comma for name parsing
268 QStringList parts = value.split(',');
269
270 if (parts.size() >= 1)
271 ch.name = parts[0].replace(QStringLiteral("\\1"), QStringLiteral(","));
272 if (parts.size() >= 2)
273 ch.reference = parts[1].replace(QStringLiteral("\\1"), QStringLiteral(","));
274 if (parts.size() >= 3 && !parts[2].isEmpty())
275 ch.resolution = parts[2].toFloat();
276 if (parts.size() >= 4 && !parts[3].isEmpty())
277 ch.unit = parts[3];
278 else
279 ch.unit = QStringLiteral("\u00B5V"); // Default: µV
280 } else {
281 ch.name = QStringLiteral("Ch%1").arg(i);
282 ch.unit = QStringLiteral("\u00B5V");
283 }
284
285 m_vChannels.push_back(ch);
286 }
287
288 return true;
289}
290
291//=============================================================================================================
292
293bool BrainVisionReader::parseMarkers(const QString& sVmrkPath)
294{
295 QFile mrkFile(sVmrkPath);
296 if (!mrkFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
297 qWarning() << "[BrainVisionReader::parseMarkers] Could not open marker file:" << sVmrkPath;
298 return false;
299 }
300
301 QTextStream in(&mrkFile);
302
303 // Skip version line
304 in.readLine();
305
306 QString currentSection;
307 m_vMarkers.clear();
308
309 while (!in.atEnd()) {
310 QString line = in.readLine().trimmed();
311 if (line.isEmpty() || line.startsWith(';'))
312 continue;
313
314 if (line.startsWith('[') && line.endsWith(']')) {
315 currentSection = line.mid(1, line.size() - 2);
316 continue;
317 }
318
319 if (currentSection == "Marker Infos") {
320 // Format: Mk<N>=<Type>,<Description>,<Position>,<Duration>,<Channel>[,<Date>]
321 int eqPos = line.indexOf('=');
322 if (eqPos <= 0)
323 continue;
324
325 QString value = line.mid(eqPos + 1);
326 QStringList parts = value.split(',');
327 if (parts.size() < 5)
328 continue;
329
330 BrainVisionMarker marker;
331 marker.type = parts[0].replace(QStringLiteral("\\1"), QStringLiteral(","));
332 marker.description = parts[1].replace(QStringLiteral("\\1"), QStringLiteral(","));
333 marker.position = parts[2].toLong() - 1; // Convert 1-indexed to 0-indexed
334 marker.duration = parts[3].toLong();
335 marker.channel = parts[4].toInt();
336
337 if (parts.size() >= 6 && !parts[5].isEmpty()) {
338 // Date format: YYYYMMDDHHMMSSffffff (20 chars)
339 QString dateStr = parts[5];
340 if (dateStr.size() >= 14) {
341 marker.date = QDateTime(
342 QDate(dateStr.mid(0, 4).toInt(), dateStr.mid(4, 2).toInt(), dateStr.mid(6, 2).toInt()),
343 QTime(dateStr.mid(8, 2).toInt(), dateStr.mid(10, 2).toInt(), dateStr.mid(12, 2).toInt()));
344 }
345 }
346
347 m_vMarkers.push_back(marker);
348 }
349 }
350
351 mrkFile.close();
352 return true;
353}
354
355//=============================================================================================================
356
357void BrainVisionReader::computeSampleCount()
358{
359 if (m_iNumChannels <= 0) {
360 m_lSampleCount = 0;
361 return;
362 }
363
364 qint64 fileSize = m_dataFile.size();
365 int bytesPerSample = 0;
366 switch (m_binaryFormat) {
368 bytesPerSample = 2;
369 break;
371 bytesPerSample = 4;
372 break;
374 bytesPerSample = 4;
375 break;
376 }
377
378 m_lSampleCount = fileSize / (static_cast<qint64>(bytesPerSample) * m_iNumChannels);
379}
380
381//=============================================================================================================
382
383float BrainVisionReader::unitScale(const QString& sUnit)
384{
385 // mne.io.brainvision _unit_dict; units it does not know are taken as-is (factor 1)
386 if (sUnit == QStringLiteral("\u00B5V") || sUnit == QLatin1String("uV") || sUnit == QStringLiteral("\u00B5S") || sUnit == QLatin1String("uS"))
387 return 1.0e-6f;
388 if (sUnit == QLatin1String("mV"))
389 return 1.0e-3f;
390 if (sUnit == QLatin1String("nV"))
391 return 1.0e-9f;
392 return 1.0f;
393}
394
395//=============================================================================================================
396
397bool BrainVisionReader::isVoltageUnit(const QString& sUnit)
398{
399 return sUnit == QLatin1String("V") || sUnit == QLatin1String("mV") || sUnit == QStringLiteral("\u00B5V") || sUnit == QLatin1String("uV") || sUnit == QLatin1String("nV");
400}
401
402//=============================================================================================================
403
405{
406 FiffInfo info;
407 info.nchan = m_vChannels.size();
408 info.sfreq = m_fSFreq;
409
410 for (const auto& ch : m_vChannels) {
411 FiffChInfo fiffCh = ch.toFiffChInfo();
412 info.chs.append(fiffCh);
413 info.ch_names.append(fiffCh.ch_name);
414 }
415
416 return info;
417}
418
419//=============================================================================================================
420
421Eigen::MatrixXf BrainVisionReader::readRawSegment(int iStartSampleIdx, int iEndSampleIdx) const
422{
423 if (!m_bIsOpen) {
424 qWarning() << "[BrainVisionReader::readRawSegment] File not open";
425 return MatrixXf();
426 }
427
428 if (iStartSampleIdx < 0 || iStartSampleIdx >= m_lSampleCount ||
429 iEndSampleIdx < 0 || iEndSampleIdx > m_lSampleCount ||
430 iEndSampleIdx <= iStartSampleIdx) {
431 qWarning() << "[BrainVisionReader::readRawSegment] Invalid range:"
432 << iStartSampleIdx << "-" << iEndSampleIdx;
433 return MatrixXf();
434 }
435
436 int iNumSamples = iEndSampleIdx - iStartSampleIdx;
437 int bytesPerValue = 0;
438 switch (m_binaryFormat) {
440 bytesPerValue = 2;
441 break;
443 bytesPerValue = 4;
444 break;
446 bytesPerValue = 4;
447 break;
448 }
449
450 MatrixXf result(m_iNumChannels, iNumSamples);
451
452 if (m_orientation == BVOrientation::MULTIPLEXED) {
453 // Data layout: [ch1_t1, ch2_t1, ..., chN_t1, ch1_t2, ...]
454 qint64 startByte = static_cast<qint64>(iStartSampleIdx) * m_iNumChannels * bytesPerValue;
455 qint64 totalBytes = static_cast<qint64>(iNumSamples) * m_iNumChannels * bytesPerValue;
456 m_dataFile.seek(startByte);
457
458 QByteArray rawData = m_dataFile.read(totalBytes);
459 const char* pData = rawData.constData();
460
461 for (int s = 0; s < iNumSamples; ++s) {
462 for (int ch = 0; ch < m_iNumChannels; ++ch) {
463 qint64 offset = (static_cast<qint64>(s) * m_iNumChannels + ch) * bytesPerValue;
464 float rawValue = 0.0f;
465
466 switch (m_binaryFormat) {
468 rawValue = static_cast<float>(qFromLittleEndian<qint16>(pData + offset));
469 break;
471 rawValue = static_cast<float>(qFromLittleEndian<qint32>(pData + offset));
472 break;
474 rawValue = qFromLittleEndian<float>(pData + offset);
475 break;
476 }
477
478 // Apply channel resolution (cal) and unit scaling
479 float cal = m_vChannels[ch].resolution;
480 float scale = unitScale(m_vChannels[ch].unit);
481 result(ch, s) = rawValue * cal * scale;
482 }
483 }
484 } else {
485 // VECTORIZED: Each channel contiguous
486 // Layout: [ch1_t1, ch1_t2, ..., ch1_tM, ch2_t1, ...]
487 for (int ch = 0; ch < m_iNumChannels; ++ch) {
488 qint64 channelOffset = static_cast<qint64>(ch) * m_lSampleCount * bytesPerValue;
489 qint64 startByte = channelOffset + static_cast<qint64>(iStartSampleIdx) * bytesPerValue;
490 qint64 readBytes = static_cast<qint64>(iNumSamples) * bytesPerValue;
491
492 m_dataFile.seek(startByte);
493 QByteArray rawData = m_dataFile.read(readBytes);
494 const char* pData = rawData.constData();
495
496 float cal = m_vChannels[ch].resolution;
497 float scale = unitScale(m_vChannels[ch].unit);
498
499 for (int s = 0; s < iNumSamples; ++s) {
500 qint64 offset = static_cast<qint64>(s) * bytesPerValue;
501 float rawValue = 0.0f;
502
503 switch (m_binaryFormat) {
505 rawValue = static_cast<float>(qFromLittleEndian<qint16>(pData + offset));
506 break;
508 rawValue = static_cast<float>(qFromLittleEndian<qint32>(pData + offset));
509 break;
511 rawValue = qFromLittleEndian<float>(pData + offset);
512 break;
513 }
514
515 result(ch, s) = rawValue * cal * scale;
516 }
517 }
518 }
519
520 return result;
521}
522
523//=============================================================================================================
524
526{
527 return m_lSampleCount;
528}
529
530//=============================================================================================================
531
533{
534 return m_fSFreq;
535}
536
537//=============================================================================================================
538
540{
541 return m_iNumChannels;
542}
543
544//=============================================================================================================
545
547{
548 FiffRawData raw;
549 raw.info = getInfo();
550 raw.first_samp = 0;
551 raw.last_samp = static_cast<int>(m_lSampleCount) - 1;
552
553 RowVectorXd cals(raw.info.nchan);
554 for (int i = 0; i < raw.info.chs.size(); ++i) {
555 cals[i] = static_cast<double>(raw.info.chs[i].cal);
556 }
557 raw.cals = cals;
558
559 return raw;
560}
561
562//=============================================================================================================
563
565{
566 return QStringLiteral("BrainVision");
567}
568
569//=============================================================================================================
570
571bool BrainVisionReader::supportsExtension(const QString& sExtension) const
572{
573 QString ext = sExtension.toLower();
574 return (ext == ".vhdr" || ext == ".ahdr");
575}
576
577//=============================================================================================================
578
579QVector<BrainVisionMarker> BrainVisionReader::getMarkers() const
580{
581 return m_vMarkers;
582}
583
584//=============================================================================================================
585
586QVector<BrainVisionChannelInfo> BrainVisionReader::getChannelInfos() const
587{
588 return m_vChannels;
589}
BIDSLIB::AbstractFormatReader implementation for the BrainVision .vhdr / .vmrk / ....
Symbolic FIFF tag, block, value, unit and channel-type constants shared across FIFFLIB.
#define FIFFV_EOG_CH
#define FIFFV_SEEG_CH
#define FIFFV_EEG_CH
#define FIFF_UNIT_NONE
#define FIFFV_MISC_CH
#define FIFF_UNIT_V
#define FIFF_UNITM_NONE
#define FIFFV_ECOG_CH
#define FIFF_UNITM_N
#define FIFF_UNITM_M
#define FIFFV_STIM_CH
#define FIFFV_EMG_CH
#define FIFFV_ECG_CH
#define FIFF_UNITM_MU
BIDS dataset reading, writing, path construction, and sidecar metadata handling for iEEG/EEG/MEG.
BVOrientation
Data orientation enumeration.
FIFF file I/O, in-memory data structures and high-level readers/writers.
FIFFLIB::FiffChInfo toFiffChInfo() const
FIFFLIB::FiffRawData toFiffRawData() const override
Convert the entire dataset to a FiffRawData structure.
long getSampleCount() const override
Return total number of samples across the recording.
Eigen::MatrixXf readRawSegment(int iStartSampleIdx, int iEndSampleIdx) const override
Read a segment of raw data.
static bool isVoltageUnit(const QString &sUnit)
QString formatName() const override
Return a descriptive name for the format (e.g. "EDF", "BrainVision").
int getChannelCount() const override
Return the number of measurement channels.
bool supportsExtension(const QString &sExtension) const override
Check whether this reader can handle the given file extension.
QVector< BrainVisionChannelInfo > getChannelInfos() const
Return all channel infos.
float getFrequency() const override
Return the sampling frequency in Hz.
FIFFLIB::FiffInfo getInfo() const override
Return measurement metadata as FiffInfo.
bool open(const QString &sFilePath) override
Open and parse the file header. Must be called before reading data.
static float unitScale(const QString &sUnit)
QVector< BrainVisionMarker > getMarkers() const
Return all parsed markers from the .vmrk file.
Per-channel FIFF descriptor: identifiers, kind, calibration, coil type, channel-frame coil position a...
Full FIFF measurement info: per-channel descriptors, sampling and filter setup, projectors,...
Definition fiff_info.h:90
QList< FiffChInfo > chs
Continuous FIFF raw recording: FiffInfo plus a random-access directory of FIFF_DATA_BUFFER tags.
Eigen::RowVectorXd cals