51#include <QRegularExpression>
73 QString nameUpper =
name.toUpper();
74 if(nameUpper.contains(
"ECOG"))
76 else if(nameUpper.contains(
"SEEG"))
78 else if(nameUpper.contains(
"EOG") || nameUpper ==
"HEOGL" || nameUpper ==
"HEOGR" || nameUpper ==
"VEOGB")
80 else if(nameUpper.contains(
"ECG") || nameUpper.contains(
"EKG"))
82 else if(nameUpper.contains(
"EMG"))
84 else if(nameUpper ==
"STI 014" || nameUpper.contains(
"STIM"))
96 QString unitUpper =
unit.toUpper();
97 if(unitUpper.contains(
"V") || unitUpper.isEmpty()) {
99 if(unitUpper.startsWith(
"N"))
101 else if(unitUpper.startsWith(QStringLiteral(
"\u00B5")) || unitUpper.startsWith(
"U"))
103 else if(unitUpper.startsWith(
"M"))
132 if(m_dataFile.isOpen()) {
141 m_sVhdrPath = sFilePath;
143 if(!parseHeader(sFilePath)) {
148 m_dataFile.setFileName(m_sDataPath);
149 if(!m_dataFile.open(QIODevice::ReadOnly)) {
150 qWarning() <<
"[BrainVisionReader::open] Could not open data file:" << m_sDataPath;
154 computeSampleCount();
157 if(!m_sMarkerPath.isEmpty()) {
158 parseMarkers(m_sMarkerPath);
167bool BrainVisionReader::parseHeader(
const QString& sVhdrPath)
169 QFile hdrFile(sVhdrPath);
170 if(!hdrFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
171 qWarning() <<
"[BrainVisionReader::parseHeader] Could not open header:" << sVhdrPath;
175 QFileInfo fi(sVhdrPath);
176 QString sDir = fi.absolutePath();
178 QTextStream in(&hdrFile);
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;
191 QString currentSection;
192 QMap<QString, QMap<QString, QString>> sections;
195 QString line = in.readLine().trimmed();
196 if(line.isEmpty() || line.startsWith(
';'))
199 if(line.startsWith(
'[') && line.endsWith(
']')) {
200 currentSection = line.mid(1, line.size() - 2);
202 if(currentSection.toLower() ==
"comment")
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;
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"];
224 if(commonInfos.isEmpty()) {
225 qWarning() <<
"[BrainVisionReader::parseHeader] Missing [Common Infos] section";
230 QString dataFileName = commonInfos.value(
"DataFile");
231 if(dataFileName.isEmpty()) {
232 qWarning() <<
"[BrainVisionReader::parseHeader] No DataFile specified";
235 m_sDataPath = QDir(sDir).absoluteFilePath(dataFileName);
238 QString markerFileName = commonInfos.value(
"MarkerFile");
239 if(!markerFileName.isEmpty()) {
240 m_sMarkerPath = QDir(sDir).absoluteFilePath(markerFileName);
244 QString orientation = commonInfos.value(
"DataOrientation",
"MULTIPLEXED").toUpper();
248 m_iNumChannels = commonInfos.value(
"NumberOfChannels",
"0").toInt();
249 if(m_iNumChannels <= 0) {
250 qWarning() <<
"[BrainVisionReader::parseHeader] Invalid channel count:" << m_iNumChannels;
255 float samplingInterval = commonInfos.value(
"SamplingInterval",
"0").toFloat();
256 if(samplingInterval > 0.0f) {
257 m_fSFreq = 1.0e6f / samplingInterval;
261 QMap<QString, QString> binaryInfos;
262 if(sections.contains(
"Binary Infos"))
263 binaryInfos = sections[
"Binary Infos"];
265 QString binaryFormat = binaryInfos.value(
"BinaryFormat",
"INT_16").toUpper();
266 if(binaryFormat ==
"INT_32")
268 else if(binaryFormat ==
"IEEE_FLOAT_32")
274 QMap<QString, QString> channelInfos;
275 if(sections.contains(
"Channel Infos"))
276 channelInfos = sections[
"Channel Infos"];
279 m_vChannels.reserve(m_iNumChannels);
281 for(
int i = 1; i <= m_iNumChannels; ++i) {
282 QString key = QStringLiteral(
"Ch%1").arg(i);
283 QString value = channelInfos.value(key);
285 BrainVisionChannelInfo ch;
288 if(!value.isEmpty()) {
290 QStringList parts = value.split(
',');
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())
298 if(parts.size() >= 4 && !parts[3].isEmpty())
301 ch.
unit = QStringLiteral(
"\u00B5V");
303 ch.
name = QStringLiteral(
"Ch%1").arg(i);
304 ch.
unit = QStringLiteral(
"\u00B5V");
307 m_vChannels.push_back(ch);
315bool BrainVisionReader::parseMarkers(
const QString& sVmrkPath)
317 QFile mrkFile(sVmrkPath);
318 if(!mrkFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
319 qWarning() <<
"[BrainVisionReader::parseMarkers] Could not open marker file:" << sVmrkPath;
323 QTextStream in(&mrkFile);
328 QString currentSection;
332 QString line = in.readLine().trimmed();
333 if(line.isEmpty() || line.startsWith(
';'))
336 if(line.startsWith(
'[') && line.endsWith(
']')) {
337 currentSection = line.mid(1, line.size() - 2);
341 if(currentSection ==
"Marker Infos") {
343 int eqPos = line.indexOf(
'=');
344 if(eqPos <= 0)
continue;
346 QString value = line.mid(eqPos + 1);
347 QStringList parts = value.split(
',');
348 if(parts.size() < 5)
continue;
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;
354 marker.
duration = parts[3].toLong();
355 marker.
channel = parts[4].toInt();
357 if(parts.size() >= 6 && !parts[5].isEmpty()) {
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()));
367 m_vMarkers.push_back(marker);
377void BrainVisionReader::computeSampleCount()
379 if(m_iNumChannels <= 0) {
384 qint64 fileSize = m_dataFile.size();
385 int bytesPerSample = 0;
386 switch(m_binaryFormat) {
392 m_lSampleCount = fileSize / (
static_cast<qint64
>(bytesPerSample) * m_iNumChannels);
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;
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;
416 info.
nchan = m_vChannels.size();
417 info.
sfreq = m_fSFreq;
419 for(
const auto& ch : m_vChannels) {
421 info.
chs.append(fiffCh);
433 qWarning() <<
"[BrainVisionReader::readRawSegment] File not open";
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;
445 int iNumSamples = iEndSampleIdx - iStartSampleIdx;
446 int bytesPerValue = 0;
447 switch(m_binaryFormat) {
453 MatrixXf result(m_iNumChannels, iNumSamples);
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);
461 QByteArray rawData = m_dataFile.read(totalBytes);
462 const char* pData = rawData.constData();
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;
469 switch(m_binaryFormat) {
471 rawValue =
static_cast<float>(qFromLittleEndian<qint16>(pData + offset));
474 rawValue =
static_cast<float>(qFromLittleEndian<qint32>(pData + offset));
477 rawValue = qFromLittleEndian<float>(pData + offset);
482 float cal = m_vChannels[ch].resolution;
483 float scale =
unitScale(m_vChannels[ch].unit);
484 result(ch, s) = rawValue * cal * scale;
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;
495 m_dataFile.seek(startByte);
496 QByteArray rawData = m_dataFile.read(readBytes);
497 const char* pData = rawData.constData();
499 float cal = m_vChannels[ch].resolution;
500 float scale =
unitScale(m_vChannels[ch].unit);
502 for(
int s = 0; s < iNumSamples; ++s) {
503 qint64 offset =
static_cast<qint64
>(s) * bytesPerValue;
504 float rawValue = 0.0f;
506 switch(m_binaryFormat) {
508 rawValue =
static_cast<float>(qFromLittleEndian<qint16>(pData + offset));
511 rawValue =
static_cast<float>(qFromLittleEndian<qint32>(pData + offset));
514 rawValue = qFromLittleEndian<float>(pData + offset);
518 result(ch, s) = rawValue * cal * scale;
530 return m_lSampleCount;
544 return m_iNumChannels;
557 for(
int i = 0; i < raw.
info.
chs.size(); ++i) {
558 cals[i] =
static_cast<double>(raw.
info.
chs[i].cal);
569 return QStringLiteral(
"BrainVision");
576 QString ext = sExtension.toLower();
577 return (ext ==
".vhdr" || ext ==
".ahdr");
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
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
FIFF measurement file information.
FIFF raw measurement data.