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 // Check if unit suggests a voltage measurement (EEG)
67 if(scale > 0.0f)
68 info.kind = FIFFV_EEG_CH;
69 else
70 info.kind = FIFFV_MISC_CH;
71 }
72
73 // Map unit
74 QString unitUpper = unit.toUpper();
75 if(unitUpper.contains("V") || unitUpper.isEmpty()) {
76 info.unit = FIFF_UNIT_V;
77 if(unitUpper.startsWith("N"))
79 else if(unitUpper.startsWith(QStringLiteral("\u00B5")) || unitUpper.startsWith("U"))
81 else if(unitUpper.startsWith("M"))
83 else
85 } else {
86 info.unit = FIFF_UNIT_NONE;
88 }
89
90 // Calibration: resolution is the scaling factor from raw → physical units
91 info.cal = resolution;
92 info.range = 1.0f;
93 info.ch_name = name;
94
95 return info;
96}
97
98//=============================================================================================================
99// BrainVisionReader
100//=============================================================================================================
101
105
106//=============================================================================================================
107
109{
110 if(m_dataFile.isOpen()) {
111 m_dataFile.close();
112 }
113}
114
115//=============================================================================================================
116
117bool BrainVisionReader::open(const QString& sFilePath)
118{
119 m_sVhdrPath = sFilePath;
120
121 if(!parseHeader(sFilePath)) {
122 return false;
123 }
124
125 // Open the binary data file
126 m_dataFile.setFileName(m_sDataPath);
127 if(!m_dataFile.open(QIODevice::ReadOnly)) {
128 qWarning() << "[BrainVisionReader::open] Could not open data file:" << m_sDataPath;
129 return false;
130 }
131
132 computeSampleCount();
133
134 // Parse markers if marker file exists
135 if(!m_sMarkerPath.isEmpty()) {
136 parseMarkers(m_sMarkerPath);
137 }
138
139 m_bIsOpen = true;
140 return true;
141}
142
143//=============================================================================================================
144
145bool BrainVisionReader::parseHeader(const QString& sVhdrPath)
146{
147 QFile hdrFile(sVhdrPath);
148 if(!hdrFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
149 qWarning() << "[BrainVisionReader::parseHeader] Could not open header:" << sVhdrPath;
150 return false;
151 }
152
153 QFileInfo fi(sVhdrPath);
154 QString sDir = fi.absolutePath();
155
156 QTextStream in(&hdrFile);
157
158 // Validate version line
159 QString firstLine = in.readLine().trimmed();
160 QRegularExpression versionRe(
161 QStringLiteral("Brain ?Vision( Core| V-Amp)? Data( Exchange)? Header File,? Version [12]\\.0"),
162 QRegularExpression::CaseInsensitiveOption);
163 if(!versionRe.match(firstLine).hasMatch()) {
164 qWarning() << "[BrainVisionReader::parseHeader] Unrecognized header version:" << firstLine;
165 return false;
166 }
167
168 // Simple INI-style parser (sections + key=value)
169 QString currentSection;
170 QMap<QString, QMap<QString, QString>> sections;
171
172 while(!in.atEnd()) {
173 QString line = in.readLine().trimmed();
174 if(line.isEmpty() || line.startsWith(';'))
175 continue;
176
177 if(line.startsWith('[') && line.endsWith(']')) {
178 currentSection = line.mid(1, line.size() - 2);
179 // Stop parsing at [Comment] section — it's free-form text
180 if(currentSection.toLower() == "comment")
181 break;
182 continue;
183 }
184
185 int eqPos = line.indexOf('=');
186 if(eqPos > 0 && !currentSection.isEmpty()) {
187 QString key = line.left(eqPos).trimmed();
188 QString value = line.mid(eqPos + 1).trimmed();
189 sections[currentSection][key] = value;
190 }
191 }
192
193 hdrFile.close();
194
195 // Extract Common Infos — handle both "Common Infos" and "Common infos" (NeurOne variant)
196 QMap<QString, QString> commonInfos;
197 if(sections.contains("Common Infos"))
198 commonInfos = sections["Common Infos"];
199 else if(sections.contains("Common infos"))
200 commonInfos = sections["Common infos"];
201
202 if(commonInfos.isEmpty()) {
203 qWarning() << "[BrainVisionReader::parseHeader] Missing [Common Infos] section";
204 return false;
205 }
206
207 // Data file path (relative to .vhdr location)
208 QString dataFileName = commonInfos.value("DataFile");
209 if(dataFileName.isEmpty()) {
210 qWarning() << "[BrainVisionReader::parseHeader] No DataFile specified";
211 return false;
212 }
213 m_sDataPath = QDir(sDir).absoluteFilePath(dataFileName);
214
215 // Marker file path
216 QString markerFileName = commonInfos.value("MarkerFile");
217 if(!markerFileName.isEmpty()) {
218 m_sMarkerPath = QDir(sDir).absoluteFilePath(markerFileName);
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) continue;
323
324 QString value = line.mid(eqPos + 1);
325 QStringList parts = value.split(',');
326 if(parts.size() < 5) continue;
327
328 BrainVisionMarker marker;
329 marker.type = parts[0].replace(QStringLiteral("\\1"), QStringLiteral(","));
330 marker.description = parts[1].replace(QStringLiteral("\\1"), QStringLiteral(","));
331 marker.position = parts[2].toLong() - 1; // Convert 1-indexed to 0-indexed
332 marker.duration = parts[3].toLong();
333 marker.channel = parts[4].toInt();
334
335 if(parts.size() >= 6 && !parts[5].isEmpty()) {
336 // Date format: YYYYMMDDHHMMSSffffff (20 chars)
337 QString dateStr = parts[5];
338 if(dateStr.size() >= 14) {
339 marker.date = QDateTime(
340 QDate(dateStr.mid(0, 4).toInt(), dateStr.mid(4, 2).toInt(), dateStr.mid(6, 2).toInt()),
341 QTime(dateStr.mid(8, 2).toInt(), dateStr.mid(10, 2).toInt(), dateStr.mid(12, 2).toInt()));
342 }
343 }
344
345 m_vMarkers.push_back(marker);
346 }
347 }
348
349 mrkFile.close();
350 return true;
351}
352
353//=============================================================================================================
354
355void BrainVisionReader::computeSampleCount()
356{
357 if(m_iNumChannels <= 0) {
358 m_lSampleCount = 0;
359 return;
360 }
361
362 qint64 fileSize = m_dataFile.size();
363 int bytesPerSample = 0;
364 switch(m_binaryFormat) {
365 case BVBinaryFormat::INT_16: bytesPerSample = 2; break;
366 case BVBinaryFormat::INT_32: bytesPerSample = 4; break;
367 case BVBinaryFormat::IEEE_FLOAT_32: bytesPerSample = 4; break;
368 }
369
370 m_lSampleCount = fileSize / (static_cast<qint64>(bytesPerSample) * m_iNumChannels);
371}
372
373//=============================================================================================================
374
375float BrainVisionReader::unitScale(const QString& sUnit)
376{
377 QString u = sUnit.toLower();
378 if(u == "v") return 1.0f;
379 if(u == "\u00B5v" || u == "uv" || u == "µv") return 1.0e-6f;
380 if(u == "mv") return 1.0e-3f;
381 if(u == "nv") return 1.0e-9f;
382 if(u == "c" || u == "\u00B0c") return 1.0f; // temperature
383 if(u == "\u00B5s" || u == "us") return 1.0e-6f;
384 if(u == "s") return 1.0f;
385 if(u == "n/a") return 1.0f;
386 return 0.0f; // unknown unit
387}
388
389//=============================================================================================================
390
392{
393 FiffInfo info;
394 info.nchan = m_vChannels.size();
395 info.sfreq = m_fSFreq;
396
397 for(const auto& ch : m_vChannels) {
398 FiffChInfo fiffCh = ch.toFiffChInfo();
399 info.chs.append(fiffCh);
400 info.ch_names.append(fiffCh.ch_name);
401 }
402
403 return info;
404}
405
406//=============================================================================================================
407
408Eigen::MatrixXf BrainVisionReader::readRawSegment(int iStartSampleIdx, int iEndSampleIdx) const
409{
410 if(!m_bIsOpen) {
411 qWarning() << "[BrainVisionReader::readRawSegment] File not open";
412 return MatrixXf();
413 }
414
415 if(iStartSampleIdx < 0 || iStartSampleIdx >= m_lSampleCount ||
416 iEndSampleIdx < 0 || iEndSampleIdx > m_lSampleCount ||
417 iEndSampleIdx <= iStartSampleIdx) {
418 qWarning() << "[BrainVisionReader::readRawSegment] Invalid range:"
419 << iStartSampleIdx << "-" << iEndSampleIdx;
420 return MatrixXf();
421 }
422
423 int iNumSamples = iEndSampleIdx - iStartSampleIdx;
424 int bytesPerValue = 0;
425 switch(m_binaryFormat) {
426 case BVBinaryFormat::INT_16: bytesPerValue = 2; break;
427 case BVBinaryFormat::INT_32: bytesPerValue = 4; break;
428 case BVBinaryFormat::IEEE_FLOAT_32: bytesPerValue = 4; break;
429 }
430
431 MatrixXf result(m_iNumChannels, iNumSamples);
432
433 if(m_orientation == BVOrientation::MULTIPLEXED) {
434 // Data layout: [ch1_t1, ch2_t1, ..., chN_t1, ch1_t2, ...]
435 qint64 startByte = static_cast<qint64>(iStartSampleIdx) * m_iNumChannels * bytesPerValue;
436 qint64 totalBytes = static_cast<qint64>(iNumSamples) * m_iNumChannels * bytesPerValue;
437 m_dataFile.seek(startByte);
438
439 QByteArray rawData = m_dataFile.read(totalBytes);
440 const char* pData = rawData.constData();
441
442 for(int s = 0; s < iNumSamples; ++s) {
443 for(int ch = 0; ch < m_iNumChannels; ++ch) {
444 qint64 offset = (static_cast<qint64>(s) * m_iNumChannels + ch) * bytesPerValue;
445 float rawValue = 0.0f;
446
447 switch(m_binaryFormat) {
449 rawValue = static_cast<float>(qFromLittleEndian<qint16>(pData + offset));
450 break;
452 rawValue = static_cast<float>(qFromLittleEndian<qint32>(pData + offset));
453 break;
455 rawValue = qFromLittleEndian<float>(pData + offset);
456 break;
457 }
458
459 // Apply channel resolution (cal) and unit scaling
460 float cal = m_vChannels[ch].resolution;
461 float scale = unitScale(m_vChannels[ch].unit);
462 result(ch, s) = rawValue * cal * scale;
463 }
464 }
465 } else {
466 // VECTORIZED: Each channel contiguous
467 // Layout: [ch1_t1, ch1_t2, ..., ch1_tM, ch2_t1, ...]
468 for(int ch = 0; ch < m_iNumChannels; ++ch) {
469 qint64 channelOffset = static_cast<qint64>(ch) * m_lSampleCount * bytesPerValue;
470 qint64 startByte = channelOffset + static_cast<qint64>(iStartSampleIdx) * bytesPerValue;
471 qint64 readBytes = static_cast<qint64>(iNumSamples) * bytesPerValue;
472
473 m_dataFile.seek(startByte);
474 QByteArray rawData = m_dataFile.read(readBytes);
475 const char* pData = rawData.constData();
476
477 float cal = m_vChannels[ch].resolution;
478 float scale = unitScale(m_vChannels[ch].unit);
479
480 for(int s = 0; s < iNumSamples; ++s) {
481 qint64 offset = static_cast<qint64>(s) * bytesPerValue;
482 float rawValue = 0.0f;
483
484 switch(m_binaryFormat) {
486 rawValue = static_cast<float>(qFromLittleEndian<qint16>(pData + offset));
487 break;
489 rawValue = static_cast<float>(qFromLittleEndian<qint32>(pData + offset));
490 break;
492 rawValue = qFromLittleEndian<float>(pData + offset);
493 break;
494 }
495
496 result(ch, s) = rawValue * cal * scale;
497 }
498 }
499 }
500
501 return result;
502}
503
504//=============================================================================================================
505
507{
508 return m_lSampleCount;
509}
510
511//=============================================================================================================
512
514{
515 return m_fSFreq;
516}
517
518//=============================================================================================================
519
521{
522 return m_iNumChannels;
523}
524
525//=============================================================================================================
526
528{
529 FiffRawData raw;
530 raw.info = getInfo();
531 raw.first_samp = 0;
532 raw.last_samp = m_lSampleCount;
533
534 RowVectorXd cals(raw.info.nchan);
535 for(int i = 0; i < raw.info.chs.size(); ++i) {
536 cals[i] = static_cast<double>(raw.info.chs[i].cal);
537 }
538 raw.cals = cals;
539
540 return raw;
541}
542
543//=============================================================================================================
544
546{
547 return QStringLiteral("BrainVision");
548}
549
550//=============================================================================================================
551
552bool BrainVisionReader::supportsExtension(const QString& sExtension) const
553{
554 QString ext = sExtension.toLower();
555 return (ext == ".vhdr" || ext == ".ahdr");
556}
557
558//=============================================================================================================
559
560QVector<BrainVisionMarker> BrainVisionReader::getMarkers() const
561{
562 return m_vMarkers;
563}
564
565//=============================================================================================================
566
567QVector<BrainVisionChannelInfo> BrainVisionReader::getChannelInfos() const
568{
569 return m_vChannels;
570}
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.
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:88
QList< FiffChInfo > chs
fiff_int_t first_samp
Eigen::RowVectorXd cals
fiff_int_t last_samp
FiffInfo info