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"))
72 if (
unit == QLatin1String(
"nV"))
74 else if (
unit == QStringLiteral(
"\u00B5V") ||
unit == QLatin1String(
"uV"))
76 else if (
unit == QLatin1String(
"mV"))
105 if (m_dataFile.isOpen()) {
114 m_sVhdrPath = sFilePath;
116 if (!parseHeader(sFilePath)) {
121 m_dataFile.setFileName(m_sDataPath);
122 if (!m_dataFile.open(QIODevice::ReadOnly)) {
123 qWarning() <<
"[BrainVisionReader::open] Could not open data file:" << m_sDataPath;
127 computeSampleCount();
130 if (!m_sMarkerPath.isEmpty()) {
131 parseMarkers(m_sMarkerPath);
140bool BrainVisionReader::parseHeader(
const QString& sVhdrPath)
142 QFile hdrFile(sVhdrPath);
143 if (!hdrFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
144 qWarning() <<
"[BrainVisionReader::parseHeader] Could not open header:" << sVhdrPath;
148 QFileInfo fi(sVhdrPath);
149 QString sDir = fi.absolutePath();
151 QTextStream in(&hdrFile);
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;
164 QString currentSection;
165 QMap<QString, QMap<QString, QString>> sections;
167 while (!in.atEnd()) {
168 QString line = in.readLine().trimmed();
169 if (line.isEmpty() || line.startsWith(
';'))
172 if (line.startsWith(
'[') && line.endsWith(
']')) {
173 currentSection = line.mid(1, line.size() - 2);
175 if (currentSection.toLower() ==
"comment")
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;
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"];
197 if (commonInfos.isEmpty()) {
198 qWarning() <<
"[BrainVisionReader::parseHeader] Missing [Common Infos] section";
203 QString dataFileName = commonInfos.value(
"DataFile");
204 if (dataFileName.isEmpty()) {
205 qWarning() <<
"[BrainVisionReader::parseHeader] No DataFile specified";
208 m_sDataPath = QDir(sDir).absoluteFilePath(dataFileName);
211 QString markerFileName = commonInfos.value(
"MarkerFile");
212 if (!markerFileName.isEmpty()) {
213 m_sMarkerPath = QDir(sDir).absoluteFilePath(markerFileName);
216 if (commonInfos.value(
"DataFormat",
"BINARY").toUpper() != QLatin1String(
"BINARY")) {
217 qWarning() <<
"[BrainVisionReader::parseHeader] Only BINARY data is supported, not" << commonInfos.value(
"DataFormat");
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;
309 while (!in.atEnd()) {
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(
'=');
325 QString value = line.mid(eqPos + 1);
326 QStringList parts = value.split(
',');
327 if (parts.size() < 5)
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;
334 marker.
duration = parts[3].toLong();
335 marker.
channel = parts[4].toInt();
337 if (parts.size() >= 6 && !parts[5].isEmpty()) {
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()));
347 m_vMarkers.push_back(marker);
357void BrainVisionReader::computeSampleCount()
359 if (m_iNumChannels <= 0) {
364 qint64 fileSize = m_dataFile.size();
365 int bytesPerSample = 0;
366 switch (m_binaryFormat) {
378 m_lSampleCount = fileSize / (
static_cast<qint64
>(bytesPerSample) * m_iNumChannels);
386 if (sUnit == QStringLiteral(
"\u00B5V") || sUnit == QLatin1String(
"uV") || sUnit == QStringLiteral(
"\u00B5S") || sUnit == QLatin1String(
"uS"))
388 if (sUnit == QLatin1String(
"mV"))
390 if (sUnit == QLatin1String(
"nV"))
399 return sUnit == QLatin1String(
"V") || sUnit == QLatin1String(
"mV") || sUnit == QStringLiteral(
"\u00B5V") || sUnit == QLatin1String(
"uV") || sUnit == QLatin1String(
"nV");
407 info.
nchan = m_vChannels.size();
408 info.
sfreq = m_fSFreq;
410 for (
const auto& ch : m_vChannels) {
412 info.
chs.append(fiffCh);
424 qWarning() <<
"[BrainVisionReader::readRawSegment] File not open";
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;
436 int iNumSamples = iEndSampleIdx - iStartSampleIdx;
437 int bytesPerValue = 0;
438 switch (m_binaryFormat) {
450 MatrixXf result(m_iNumChannels, iNumSamples);
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);
458 QByteArray rawData = m_dataFile.read(totalBytes);
459 const char* pData = rawData.constData();
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;
466 switch (m_binaryFormat) {
468 rawValue =
static_cast<float>(qFromLittleEndian<qint16>(pData + offset));
471 rawValue =
static_cast<float>(qFromLittleEndian<qint32>(pData + offset));
474 rawValue = qFromLittleEndian<float>(pData + offset);
479 float cal = m_vChannels[ch].resolution;
480 float scale =
unitScale(m_vChannels[ch].unit);
481 result(ch, s) = rawValue * cal * scale;
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;
492 m_dataFile.seek(startByte);
493 QByteArray rawData = m_dataFile.read(readBytes);
494 const char* pData = rawData.constData();
496 float cal = m_vChannels[ch].resolution;
497 float scale =
unitScale(m_vChannels[ch].unit);
499 for (
int s = 0; s < iNumSamples; ++s) {
500 qint64 offset =
static_cast<qint64
>(s) * bytesPerValue;
501 float rawValue = 0.0f;
503 switch (m_binaryFormat) {
505 rawValue =
static_cast<float>(qFromLittleEndian<qint16>(pData + offset));
508 rawValue =
static_cast<float>(qFromLittleEndian<qint32>(pData + offset));
511 rawValue = qFromLittleEndian<float>(pData + offset);
515 result(ch, s) = rawValue * cal * scale;
527 return m_lSampleCount;
541 return m_iNumChannels;
551 raw.
last_samp =
static_cast<int>(m_lSampleCount) - 1;
554 for (
int i = 0; i < raw.
info.
chs.size(); ++i) {
555 cals[i] =
static_cast<double>(raw.
info.
chs[i].cal);
566 return QStringLiteral(
"BrainVision");
573 QString ext = sExtension.toLower();
574 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.
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.
~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,...
Continuous FIFF raw recording: FiffInfo plus a random-access directory of FIFF_DATA_BUFFER tags.