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