29#include <QRegularExpression>
51 QString nameUpper =
name.toUpper();
52 if(nameUpper.contains(
"ECOG"))
54 else if(nameUpper.contains(
"SEEG"))
56 else if(nameUpper.contains(
"EOG") || nameUpper ==
"HEOGL" || nameUpper ==
"HEOGR" || nameUpper ==
"VEOGB")
58 else if(nameUpper.contains(
"ECG") || nameUpper.contains(
"EKG"))
60 else if(nameUpper.contains(
"EMG"))
62 else if(nameUpper ==
"STI 014" || nameUpper.contains(
"STIM"))
74 QString unitUpper =
unit.toUpper();
75 if(unitUpper.contains(
"V") || unitUpper.isEmpty()) {
77 if(unitUpper.startsWith(
"N"))
79 else if(unitUpper.startsWith(QStringLiteral(
"\u00B5")) || unitUpper.startsWith(
"U"))
81 else if(unitUpper.startsWith(
"M"))
110 if(m_dataFile.isOpen()) {
119 m_sVhdrPath = sFilePath;
121 if(!parseHeader(sFilePath)) {
126 m_dataFile.setFileName(m_sDataPath);
127 if(!m_dataFile.open(QIODevice::ReadOnly)) {
128 qWarning() <<
"[BrainVisionReader::open] Could not open data file:" << m_sDataPath;
132 computeSampleCount();
135 if(!m_sMarkerPath.isEmpty()) {
136 parseMarkers(m_sMarkerPath);
145bool BrainVisionReader::parseHeader(
const QString& sVhdrPath)
147 QFile hdrFile(sVhdrPath);
148 if(!hdrFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
149 qWarning() <<
"[BrainVisionReader::parseHeader] Could not open header:" << sVhdrPath;
153 QFileInfo fi(sVhdrPath);
154 QString sDir = fi.absolutePath();
156 QTextStream in(&hdrFile);
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;
169 QString currentSection;
170 QMap<QString, QMap<QString, QString>> sections;
173 QString line = in.readLine().trimmed();
174 if(line.isEmpty() || line.startsWith(
';'))
177 if(line.startsWith(
'[') && line.endsWith(
']')) {
178 currentSection = line.mid(1, line.size() - 2);
180 if(currentSection.toLower() ==
"comment")
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;
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"];
202 if(commonInfos.isEmpty()) {
203 qWarning() <<
"[BrainVisionReader::parseHeader] Missing [Common Infos] section";
208 QString dataFileName = commonInfos.value(
"DataFile");
209 if(dataFileName.isEmpty()) {
210 qWarning() <<
"[BrainVisionReader::parseHeader] No DataFile specified";
213 m_sDataPath = QDir(sDir).absoluteFilePath(dataFileName);
216 QString markerFileName = commonInfos.value(
"MarkerFile");
217 if(!markerFileName.isEmpty()) {
218 m_sMarkerPath = QDir(sDir).absoluteFilePath(markerFileName);
222 QString orientation = commonInfos.value(
"DataOrientation",
"MULTIPLEXED").toUpper();
226 m_iNumChannels = commonInfos.value(
"NumberOfChannels",
"0").toInt();
227 if(m_iNumChannels <= 0) {
228 qWarning() <<
"[BrainVisionReader::parseHeader] Invalid channel count:" << m_iNumChannels;
233 float samplingInterval = commonInfos.value(
"SamplingInterval",
"0").toFloat();
234 if(samplingInterval > 0.0f) {
235 m_fSFreq = 1.0e6f / samplingInterval;
239 QMap<QString, QString> binaryInfos;
240 if(sections.contains(
"Binary Infos"))
241 binaryInfos = sections[
"Binary Infos"];
243 QString binaryFormat = binaryInfos.value(
"BinaryFormat",
"INT_16").toUpper();
244 if(binaryFormat ==
"INT_32")
246 else if(binaryFormat ==
"IEEE_FLOAT_32")
252 QMap<QString, QString> channelInfos;
253 if(sections.contains(
"Channel Infos"))
254 channelInfos = sections[
"Channel Infos"];
257 m_vChannels.reserve(m_iNumChannels);
259 for(
int i = 1; i <= m_iNumChannels; ++i) {
260 QString key = QStringLiteral(
"Ch%1").arg(i);
261 QString value = channelInfos.value(key);
263 BrainVisionChannelInfo ch;
266 if(!value.isEmpty()) {
268 QStringList parts = value.split(
',');
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())
276 if(parts.size() >= 4 && !parts[3].isEmpty())
279 ch.
unit = QStringLiteral(
"\u00B5V");
281 ch.
name = QStringLiteral(
"Ch%1").arg(i);
282 ch.
unit = QStringLiteral(
"\u00B5V");
285 m_vChannels.push_back(ch);
293bool BrainVisionReader::parseMarkers(
const QString& sVmrkPath)
295 QFile mrkFile(sVmrkPath);
296 if(!mrkFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
297 qWarning() <<
"[BrainVisionReader::parseMarkers] Could not open marker file:" << sVmrkPath;
301 QTextStream in(&mrkFile);
306 QString currentSection;
310 QString line = in.readLine().trimmed();
311 if(line.isEmpty() || line.startsWith(
';'))
314 if(line.startsWith(
'[') && line.endsWith(
']')) {
315 currentSection = line.mid(1, line.size() - 2);
319 if(currentSection ==
"Marker Infos") {
321 int eqPos = line.indexOf(
'=');
322 if(eqPos <= 0)
continue;
324 QString value = line.mid(eqPos + 1);
325 QStringList parts = value.split(
',');
326 if(parts.size() < 5)
continue;
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;
332 marker.
duration = parts[3].toLong();
333 marker.
channel = parts[4].toInt();
335 if(parts.size() >= 6 && !parts[5].isEmpty()) {
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()));
345 m_vMarkers.push_back(marker);
355void BrainVisionReader::computeSampleCount()
357 if(m_iNumChannels <= 0) {
362 qint64 fileSize = m_dataFile.size();
363 int bytesPerSample = 0;
364 switch(m_binaryFormat) {
370 m_lSampleCount = fileSize / (
static_cast<qint64
>(bytesPerSample) * m_iNumChannels);
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;
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;
394 info.
nchan = m_vChannels.size();
395 info.
sfreq = m_fSFreq;
397 for(
const auto& ch : m_vChannels) {
399 info.
chs.append(fiffCh);
411 qWarning() <<
"[BrainVisionReader::readRawSegment] File not open";
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;
423 int iNumSamples = iEndSampleIdx - iStartSampleIdx;
424 int bytesPerValue = 0;
425 switch(m_binaryFormat) {
431 MatrixXf result(m_iNumChannels, iNumSamples);
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);
439 QByteArray rawData = m_dataFile.read(totalBytes);
440 const char* pData = rawData.constData();
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;
447 switch(m_binaryFormat) {
449 rawValue =
static_cast<float>(qFromLittleEndian<qint16>(pData + offset));
452 rawValue =
static_cast<float>(qFromLittleEndian<qint32>(pData + offset));
455 rawValue = qFromLittleEndian<float>(pData + offset);
460 float cal = m_vChannels[ch].resolution;
461 float scale =
unitScale(m_vChannels[ch].unit);
462 result(ch, s) = rawValue * cal * scale;
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;
473 m_dataFile.seek(startByte);
474 QByteArray rawData = m_dataFile.read(readBytes);
475 const char* pData = rawData.constData();
477 float cal = m_vChannels[ch].resolution;
478 float scale =
unitScale(m_vChannels[ch].unit);
480 for(
int s = 0; s < iNumSamples; ++s) {
481 qint64 offset =
static_cast<qint64
>(s) * bytesPerValue;
482 float rawValue = 0.0f;
484 switch(m_binaryFormat) {
486 rawValue =
static_cast<float>(qFromLittleEndian<qint16>(pData + offset));
489 rawValue =
static_cast<float>(qFromLittleEndian<qint32>(pData + offset));
492 rawValue = qFromLittleEndian<float>(pData + offset);
496 result(ch, s) = rawValue * cal * scale;
508 return m_lSampleCount;
522 return m_iNumChannels;
535 for(
int i = 0; i < raw.
info.
chs.size(); ++i) {
536 cals[i] =
static_cast<double>(raw.
info.
chs[i].cal);
547 return QStringLiteral(
"BrainVision");
554 QString ext = sExtension.toLower();
555 return (ext ==
".vhdr" || ext ==
".ahdr");
BIDSLIB::AbstractFormatReader implementation for the BrainVision .vhdr / .vmrk / ....
Symbolic FIFF tag, block, value, unit and channel-type constants shared across FIFFLIB.
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
BrainVisionReader()
Default constructor.
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.
~BrainVisionReader() override
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,...