v2.0.0
Loading...
Searching...
No Matches
bids_edf_reader.cpp
Go to the documentation of this file.
1//=============================================================================================================
12
13//=============================================================================================================
14// INCLUDES
15//=============================================================================================================
16
17#include "bids_edf_reader.h"
18
19#include <fiff/fiff_constants.h>
20
21//=============================================================================================================
22// QT INCLUDES
23//=============================================================================================================
24
25#include <QDebug>
26#include <QFileInfo>
27#include <QIODevice>
28
29//=============================================================================================================
30// USED NAMESPACES
31//=============================================================================================================
32
33using namespace BIDSLIB;
34using namespace FIFFLIB;
35using namespace Eigen;
36
37//=============================================================================================================
38// EDFChannelInfo
39//=============================================================================================================
40
41namespace
42{
43
44// mne read_raw_edf stim_channel="auto"
45bool isAutoStimLabel(const QString& label)
46{
47 return label.compare(QLatin1String("status"), Qt::CaseInsensitive) == 0 || label.compare(QLatin1String("trigger"), Qt::CaseInsensitive) == 0;
48}
49
50} // namespace
51
52//=============================================================================================================
53
55{
56 FiffChInfo info;
57
58 info.scanNo = channelNumber;
59 info.logNo = channelNumber;
60
61 QString sLabelUpper = label.toUpper();
62 if (!isMeasurement) {
63 info.kind = sLabelUpper.contains("STIM") ? FIFFV_STIM_CH : FIFFV_MISC_CH;
64 } else if (isAutoStimLabel(label)) {
65 info.kind = FIFFV_STIM_CH;
66 } else {
67 if (sLabelUpper.contains("ECOG"))
68 info.kind = FIFFV_ECOG_CH;
69 else if (sLabelUpper.contains("SEEG"))
70 info.kind = FIFFV_SEEG_CH;
71 else if (sLabelUpper.contains("EEG"))
72 info.kind = FIFFV_EEG_CH;
73 else if (sLabelUpper.contains("MEG"))
74 info.kind = FIFFV_MEG_CH;
75 else if (sLabelUpper.contains("ECG"))
76 info.kind = FIFFV_ECG_CH;
77 else if (sLabelUpper.contains("EOG"))
78 info.kind = FIFFV_EOG_CH;
79 else if (sLabelUpper.contains("EMG"))
80 info.kind = FIFFV_EMG_CH;
81 else
82 info.kind = FIFFV_EEG_CH;
83 }
84
85 // Samples are converted to SI by toSi(), so voltages are stored in V
86 info.unit = (info.kind != FIFFV_STIM_CH && physicalDimension.endsWith(QLatin1Char('V'))) ? FIFF_UNIT_V : FIFF_UNIT_NONE;
88
89 info.cal = 1.0f;
90 info.range = 1.0f;
91 info.ch_name = label;
92
93 return info;
94}
95
96//=============================================================================================================
97// EDFReader
98//=============================================================================================================
99
101{
102 // mne read_raw_edf: µV (micro sign, Greek mu, Shift-JIS mu) and uV are 1e-6, mV 1e-3, anything else 1
103 if (isAutoStimLabel(label))
104 return 1.0f;
105 const QString& d = physicalDimension;
106 if (d == QStringLiteral("\u03BCV") || d == QStringLiteral("\u00B5V") || d == QLatin1String("\x83\xCAV") || d == QLatin1String("uV"))
107 return 1.0e-6f;
108 if (d == QLatin1String("mV"))
109 return 1.0e-3f;
110 return 1.0f;
111}
112
113//=============================================================================================================
114
118
119//=============================================================================================================
120
122{
123 if (m_file.isOpen()) {
124 m_file.close();
125 }
126}
127
128//=============================================================================================================
129
130bool EDFReader::open(const QString& sFilePath)
131{
132 m_sFilePath = sFilePath;
133 m_file.setFileName(sFilePath);
134
135 if (!m_file.open(QIODevice::ReadOnly)) {
136 qWarning() << "[EDFReader::open] Could not open file:" << sFilePath;
137 return false;
138 }
139
140 // Like mne, the extension decides between EDF (16 bit) and BDF (24 bit)
141 m_iBytesPerSample = QFileInfo(sFilePath).suffix().compare(QLatin1String("bdf"), Qt::CaseInsensitive) == 0 ? 3 : 2;
142 parseHeader(&m_file);
143 m_bIsOpen = true;
144 return true;
145}
146
147//=============================================================================================================
148
149void EDFReader::parseHeader(QIODevice* pDev)
150{
151 if (pDev->pos() != 0) {
152 pDev->seek(0);
153 }
154
155 // General header fields
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();
167
168 // Per-channel fields (read in EDF-specified order: all labels, then all transducers, etc.)
169 QVector<QString> vLabels, vTransducers, vPhysDims, vPrefilterings;
170 QVector<float> vPhysMins, vPhysMaxs;
171 QVector<long> vDigMins, vDigMaxs, vSamplesPerRecord;
172
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);
193
194 // Build channel info structs
195 m_vAllChannels.clear();
196 for (int i = 0; i < m_iNumChannels; ++i) {
198 ch.channelNumber = i;
199 ch.label = vLabels[i];
200 ch.transducerType = vTransducers[i];
201 ch.physicalDimension = vPhysDims[i];
202 ch.prefiltering = vPrefilterings[i];
203 ch.physicalMin = vPhysMins[i];
204 ch.physicalMax = vPhysMaxs[i];
205 ch.digitalMin = vDigMins[i];
206 ch.digitalMax = vDigMaxs[i];
207 ch.samplesPerRecord = vSamplesPerRecord[i];
208 ch.sampleCount = vSamplesPerRecord[i] * m_iNumDataRecords;
209 ch.frequency = (m_fDataRecordsDuration > 0.0f)
210 ? vSamplesPerRecord[i] / m_fDataRecordsDuration
211 : 0.0f;
212 ch.isMeasurement = false;
213 m_vAllChannels.push_back(ch);
214 }
215
216 // Verify header size consistency
217 if (pDev->pos() != m_iNumBytesInHeader) {
218 qWarning() << "[EDFReader::parseHeader] Header byte count mismatch: read"
219 << pDev->pos() << "expected" << m_iNumBytesInHeader;
220 }
221
222 // Calculate bytes per data record
223 m_iNumBytesPerDataRecord = 0;
224 for (const auto& ch : m_vAllChannels) {
225 m_iNumBytesPerDataRecord += ch.samplesPerRecord * m_iBytesPerSample;
226 }
227
228 // Identify measurement channels (those with the highest sample rate)
229 long iMaxSamplesPerRecord = -1;
230 for (const auto& ch : m_vAllChannels) {
231 if (ch.samplesPerRecord > iMaxSamplesPerRecord) {
232 iMaxSamplesPerRecord = ch.samplesPerRecord;
233 }
234 }
235
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]);
241 }
242 }
243}
244
245//=============================================================================================================
246
248{
249 FiffInfo info;
250 info.nchan = m_vMeasChannels.size();
251
252 for (const auto& ch : m_vMeasChannels) {
253 FiffChInfo fiffCh = ch.toFiffChInfo();
254 info.chs.append(fiffCh);
255 info.ch_names.append(fiffCh.ch_name);
256 }
257
258 info.sfreq = getFrequency();
259 return info;
260}
261
262//=============================================================================================================
263
264MatrixXf EDFReader::readRawSegment(int iStartSampleIdx, int iEndSampleIdx) const
265{
266 if (!m_bIsOpen) {
267 qWarning() << "[EDFReader::readRawSegment] File not open";
268 return MatrixXf();
269 }
270
271 long totalSamples = getSampleCount();
272 if (iStartSampleIdx < 0 || iStartSampleIdx >= totalSamples ||
273 iEndSampleIdx < 0 || iEndSampleIdx > totalSamples) {
274 qWarning() << "[EDFReader::readRawSegment] Index out of bounds:"
275 << iStartSampleIdx << "-" << iEndSampleIdx;
276 return MatrixXf();
277 }
278
279 int iNumSamples = iEndSampleIdx - iStartSampleIdx;
280 if (iNumSamples <= 0) {
281 return MatrixXf();
282 }
283
284 int iSamplesPerRecord = m_vMeasChannels.isEmpty() ? 0 : m_vMeasChannels[0].samplesPerRecord;
285 if (iSamplesPerRecord <= 0) {
286 return MatrixXf();
287 }
288
289 // Calculate which data records to read
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));
294
295 // Seek to first needed data record
296 m_file.seek(m_iNumBytesInHeader + static_cast<qint64>(iFirstRecord) * m_iNumBytesPerDataRecord);
297
298 // Read needed data records
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));
303 }
304
305 // Demultiplex: channels are interleaved within each record
306 QVector<QVector<int>> vRawPatches(m_vAllChannels.size());
307 for (int iRec = 0; iRec < vRecords.size(); ++iRec) {
308 int iOffset = 0;
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) {
313 // Little-endian signed integers: 16 bit (EDF) or 24 bit (BDF)
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;
319 } else {
320 patch[s] = static_cast<int16_t>(value);
321 }
322 }
323 iOffset += nSamp;
324 vRawPatches[iCh] += patch;
325 }
326 }
327
328 // Filter to measurement channels only
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]);
334 }
335 }
336
337 // Scale and copy to result matrix
338 MatrixXf result(vMeasPatches.size(), iNumSamples);
339
340 for (int iCh = 0; iCh < vMeasPatches.size(); ++iCh) {
341 const EDFChannelInfo& ch = m_vMeasChannels[iCh];
342 const double digRange = static_cast<double>(ch.digitalMax - ch.digitalMin);
343 const double physRange = static_cast<double>(ch.physicalMax) - ch.physicalMin;
344 const bool stim = isAutoStimLabel(ch.label);
345
346 for (int s = 0; s < iNumSamples; ++s) {
347 const int raw = vMeasPatches[iCh][s + iRelativeFirst];
348 // mne read_raw_bdf keeps the raw stim value; both readers keep its low 17 bits
349 if (stim) {
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);
352 } else {
353 result(iCh, s) = static_cast<float>(((raw - ch.digitalMin) / digRange * physRange + ch.physicalMin) * ch.toSi());
354 }
355 }
356 }
357
358 return result;
359}
360
361//=============================================================================================================
362
364{
365 if (!m_vMeasChannels.isEmpty()) {
366 return m_vMeasChannels[0].sampleCount;
367 }
368 return 0;
369}
370
371//=============================================================================================================
372
374{
375 if (!m_vMeasChannels.isEmpty()) {
376 return m_vMeasChannels[0].frequency;
377 }
378 return 0.0f;
379}
380
381//=============================================================================================================
382
384{
385 return m_vMeasChannels.size();
386}
387
388//=============================================================================================================
389
391{
392 FiffRawData raw;
393 raw.info = getInfo();
394 raw.first_samp = 0;
395 raw.last_samp = static_cast<int>(getSampleCount()) - 1;
396
397 RowVectorXd cals(raw.info.nchan);
398 for (int i = 0; i < raw.info.chs.size(); ++i) {
399 cals[i] = static_cast<double>(raw.info.chs[i].cal);
400 }
401 raw.cals = cals;
402
403 return raw;
404}
405
406//=============================================================================================================
407
409{
410 return QStringLiteral("EDF");
411}
412
413//=============================================================================================================
414
415bool EDFReader::supportsExtension(const QString& sExtension) const
416{
417 QString ext = sExtension.toLower();
418 return (ext == ".edf" || ext == ".bdf");
419}
420
421//=============================================================================================================
422
423QVector<EDFChannelInfo> EDFReader::getAllChannelInfos() const
424{
425 return m_vAllChannels;
426}
427
428//=============================================================================================================
429
430QVector<EDFChannelInfo> EDFReader::getMeasurementChannelInfos() const
431{
432 return m_vMeasChannels;
433}
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.
#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 FIFFV_MEG_CH
#define FIFF_UNITM_NONE
#define FIFFV_ECOG_CH
#define FIFFV_STIM_CH
#define FIFFV_EMG_CH
#define FIFFV_ECG_CH
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.
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,...
Definition fiff_info.h:90
QList< FiffChInfo > chs
Continuous FIFF raw recording: FiffInfo plus a random-access directory of FIFF_DATA_BUFFER tags.
Eigen::RowVectorXd cals