45bool isAutoStimLabel(
const QString& label)
47 return label.compare(QLatin1String(
"status"), Qt::CaseInsensitive) == 0 || label.compare(QLatin1String(
"trigger"), Qt::CaseInsensitive) == 0;
61 QString sLabelUpper =
label.toUpper();
64 }
else if (isAutoStimLabel(
label)) {
67 if (sLabelUpper.contains(
"ECOG"))
69 else if (sLabelUpper.contains(
"SEEG"))
71 else if (sLabelUpper.contains(
"EEG"))
73 else if (sLabelUpper.contains(
"MEG"))
75 else if (sLabelUpper.contains(
"ECG"))
77 else if (sLabelUpper.contains(
"EOG"))
79 else if (sLabelUpper.contains(
"EMG"))
103 if (isAutoStimLabel(
label))
106 if (d == QStringLiteral(
"\u03BCV") || d == QStringLiteral(
"\u00B5V") || d == QLatin1String(
"\x83\xCAV") || d == QLatin1String(
"uV"))
108 if (d == QLatin1String(
"mV"))
123 if (m_file.isOpen()) {
132 m_sFilePath = sFilePath;
133 m_file.setFileName(sFilePath);
135 if (!m_file.open(QIODevice::ReadOnly)) {
136 qWarning() <<
"[EDFReader::open] Could not open file:" << sFilePath;
141 m_iBytesPerSample = QFileInfo(sFilePath).suffix().compare(QLatin1String(
"bdf"), Qt::CaseInsensitive) == 0 ? 3 : 2;
142 parseHeader(&m_file);
149void EDFReader::parseHeader(QIODevice* pDev)
151 if (pDev->pos() != 0) {
156 m_sVersionNo = QString::fromLatin1(pDev->read(EDF_VERSION)).trimmed();
157 m_sPatientId = QString::fromLatin1(pDev->read(LOCAL_PATIENT_INFO)).trimmed();
158 m_sRecordingId = QString::fromLatin1(pDev->read(LOCAL_RECORD_INFO)).trimmed();
159 m_startDateTime.setDate(QDate::fromString(QString::fromLatin1(pDev->read(STARTDATE)),
"dd.MM.yy"));
160 m_startDateTime = m_startDateTime.addYears(100);
161 m_startDateTime.setTime(QTime::fromString(QString::fromLatin1(pDev->read(STARTTIME)),
"hh.mm.ss"));
162 m_iNumBytesInHeader = QString::fromLatin1(pDev->read(NUM_BYTES_HEADER)).trimmed().toInt();
163 pDev->read(HEADER_RESERVED);
164 m_iNumDataRecords = QString::fromLatin1(pDev->read(NUM_DATA_RECORDS)).trimmed().toInt();
165 m_fDataRecordsDuration = QString::fromLatin1(pDev->read(DURATION_DATA_RECS)).trimmed().toFloat();
166 m_iNumChannels = QString::fromLatin1(pDev->read(NUM_SIGNALS)).trimmed().toInt();
169 QVector<QString> vLabels, vTransducers, vPhysDims, vPrefilterings;
170 QVector<float> vPhysMins, vPhysMaxs;
171 QVector<long> vDigMins, vDigMaxs, vSamplesPerRecord;
173 for (
int i = 0; i < m_iNumChannels; ++i)
174 vLabels.push_back(QString::fromLatin1(pDev->read(SIG_LABEL)).trimmed());
175 for (
int i = 0; i < m_iNumChannels; ++i)
176 vTransducers.push_back(QString::fromLatin1(pDev->read(SIG_TRANSDUCER)).trimmed());
177 for (
int i = 0; i < m_iNumChannels; ++i)
178 vPhysDims.push_back(QString::fromLatin1(pDev->read(SIG_PHYS_DIM)).trimmed());
179 for (
int i = 0; i < m_iNumChannels; ++i)
180 vPhysMins.push_back(QString::fromLatin1(pDev->read(SIG_PHYS_MIN)).trimmed().toFloat());
181 for (
int i = 0; i < m_iNumChannels; ++i)
182 vPhysMaxs.push_back(QString::fromLatin1(pDev->read(SIG_PHYS_MAX)).trimmed().toFloat());
183 for (
int i = 0; i < m_iNumChannels; ++i)
184 vDigMins.push_back(QString::fromLatin1(pDev->read(SIG_DIG_MIN)).trimmed().toLong());
185 for (
int i = 0; i < m_iNumChannels; ++i)
186 vDigMaxs.push_back(QString::fromLatin1(pDev->read(SIG_DIG_MAX)).trimmed().toLong());
187 for (
int i = 0; i < m_iNumChannels; ++i)
188 vPrefilterings.push_back(QString::fromLatin1(pDev->read(SIG_PREFILTERING)).trimmed());
189 for (
int i = 0; i < m_iNumChannels; ++i)
190 vSamplesPerRecord.push_back(QString::fromLatin1(pDev->read(SIG_NUM_SAMPLES)).trimmed().toLong());
191 for (
int i = 0; i < m_iNumChannels; ++i)
192 pDev->read(SIG_RESERVED);
195 m_vAllChannels.clear();
196 for (
int i = 0; i < m_iNumChannels; ++i) {
199 ch.
label = vLabels[i];
208 ch.
sampleCount = vSamplesPerRecord[i] * m_iNumDataRecords;
209 ch.
frequency = (m_fDataRecordsDuration > 0.0f)
210 ? vSamplesPerRecord[i] / m_fDataRecordsDuration
213 m_vAllChannels.push_back(ch);
217 if (pDev->pos() != m_iNumBytesInHeader) {
218 qWarning() <<
"[EDFReader::parseHeader] Header byte count mismatch: read"
219 << pDev->pos() <<
"expected" << m_iNumBytesInHeader;
223 m_iNumBytesPerDataRecord = 0;
224 for (
const auto& ch : m_vAllChannels) {
229 long iMaxSamplesPerRecord = -1;
230 for (
const auto& ch : m_vAllChannels) {
236 m_vMeasChannels.clear();
237 for (
int i = 0; i < m_vAllChannels.size(); ++i) {
238 if (m_vAllChannels[i].samplesPerRecord == iMaxSamplesPerRecord) {
239 m_vAllChannels[i].isMeasurement =
true;
240 m_vMeasChannels.push_back(m_vAllChannels[i]);
250 info.
nchan = m_vMeasChannels.size();
252 for (
const auto& ch : m_vMeasChannels) {
254 info.
chs.append(fiffCh);
267 qWarning() <<
"[EDFReader::readRawSegment] File not open";
272 if (iStartSampleIdx < 0 || iStartSampleIdx >= totalSamples ||
273 iEndSampleIdx < 0 || iEndSampleIdx > totalSamples) {
274 qWarning() <<
"[EDFReader::readRawSegment] Index out of bounds:"
275 << iStartSampleIdx <<
"-" << iEndSampleIdx;
279 int iNumSamples = iEndSampleIdx - iStartSampleIdx;
280 if (iNumSamples <= 0) {
284 int iSamplesPerRecord = m_vMeasChannels.isEmpty() ? 0 : m_vMeasChannels[0].samplesPerRecord;
285 if (iSamplesPerRecord <= 0) {
290 int iFirstRecord = iStartSampleIdx / iSamplesPerRecord;
291 int iRelativeFirst = iStartSampleIdx % iSamplesPerRecord;
292 int iNumRecords =
static_cast<int>(
293 std::ceil(
static_cast<float>(iNumSamples + iRelativeFirst) / iSamplesPerRecord));
296 m_file.seek(m_iNumBytesInHeader +
static_cast<qint64
>(iFirstRecord) * m_iNumBytesPerDataRecord);
299 QVector<QByteArray> vRecords;
300 vRecords.reserve(iNumRecords);
301 for (
int i = 0; i < iNumRecords; ++i) {
302 vRecords.push_back(m_file.read(m_iNumBytesPerDataRecord));
306 QVector<QVector<int>> vRawPatches(m_vAllChannels.size());
307 for (
int iRec = 0; iRec < vRecords.size(); ++iRec) {
309 for (
int iCh = 0; iCh < m_vAllChannels.size(); ++iCh) {
310 int nSamp = m_vAllChannels[iCh].samplesPerRecord;
311 QVector<int> patch(nSamp);
312 for (
int s = 0; s < nSamp; ++s) {
314 const auto* bytes =
reinterpret_cast<const unsigned char*
>(vRecords[iRec].constData()) + (iOffset + s) * m_iBytesPerSample;
315 int value = bytes[0] | (bytes[1] << 8);
316 if (m_iBytesPerSample == 3) {
317 value |= bytes[2] << 16;
318 patch[s] = (value & 0x800000) ? value - 0x1000000 : value;
320 patch[s] =
static_cast<int16_t
>(value);
324 vRawPatches[iCh] += patch;
329 QVector<QVector<int>> vMeasPatches;
330 vMeasPatches.reserve(m_vMeasChannels.size());
331 for (
int iCh = 0; iCh < m_vAllChannels.size(); ++iCh) {
332 if (m_vAllChannels[iCh].isMeasurement) {
333 vMeasPatches.push_back(vRawPatches[iCh]);
338 MatrixXf result(vMeasPatches.size(), iNumSamples);
340 for (
int iCh = 0; iCh < vMeasPatches.size(); ++iCh) {
344 const bool stim = isAutoStimLabel(ch.
label);
346 for (
int s = 0; s < iNumSamples; ++s) {
347 const int raw = vMeasPatches[iCh][s + iRelativeFirst];
350 const double value = (m_iBytesPerSample == 3) ? raw : (raw - ch.
digitalMin) / digRange * physRange + ch.
physicalMin;
351 result(iCh, s) =
static_cast<float>(
static_cast<int>(value) & 0x1FFFF);
365 if (!m_vMeasChannels.isEmpty()) {
366 return m_vMeasChannels[0].sampleCount;
375 if (!m_vMeasChannels.isEmpty()) {
376 return m_vMeasChannels[0].frequency;
385 return m_vMeasChannels.size();
398 for (
int i = 0; i < raw.
info.
chs.size(); ++i) {
399 cals[i] =
static_cast<double>(raw.
info.
chs[i].cal);
410 return QStringLiteral(
"EDF");
417 QString ext = sExtension.toLower();
418 return (ext ==
".edf" || ext ==
".bdf");
425 return m_vAllChannels;
432 return m_vMeasChannels;
BIDSLIB::AbstractFormatReader implementation for European Data Format (EDF / EDF+) and BioSemi BDF fi...
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.
FIFF file I/O, in-memory data structures and high-level readers/writers.
Channel-level metadata from the EDF header.
QString physicalDimension
FIFFLIB::FiffChInfo toFiffChInfo() const
FIFFLIB::FiffInfo getInfo() const override
Return measurement metadata as FiffInfo.
Eigen::MatrixXf readRawSegment(int iStartSampleIdx, int iEndSampleIdx) const override
Read a segment of raw data.
bool open(const QString &sFilePath) override
Open and parse the file header. Must be called before reading data.
bool supportsExtension(const QString &sExtension) const override
Check whether this reader can handle the given file extension.
QVector< EDFChannelInfo > getAllChannelInfos() const
Return all channel infos (measurement + extra).
float getFrequency() const override
Return the sampling frequency in Hz.
FIFFLIB::FiffRawData toFiffRawData() const override
Convert the entire dataset to a FiffRawData structure.
QVector< EDFChannelInfo > getMeasurementChannelInfos() const
Return measurement channel infos only.
EDFReader()
EDFReader Default constructor.
long getSampleCount() const override
Return total number of samples across the recording.
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.
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.