v2.0.0
Loading...
Searching...
No Matches
bids_edf_reader.cpp
Go to the documentation of this file.
1//=============================================================================================================
36
37//=============================================================================================================
38// INCLUDES
39//=============================================================================================================
40
41#include "bids_edf_reader.h"
42
43#include <fiff/fiff_constants.h>
44
45//=============================================================================================================
46// QT INCLUDES
47//=============================================================================================================
48
49#include <QDebug>
50#include <QIODevice>
51
52//=============================================================================================================
53// USED NAMESPACES
54//=============================================================================================================
55
56using namespace BIDSLIB;
57using namespace FIFFLIB;
58using namespace Eigen;
59
60//=============================================================================================================
61// EDFChannelInfo
62//=============================================================================================================
63
65{
66 FiffChInfo info;
67
68 info.scanNo = channelNumber;
69 info.logNo = channelNumber;
70
71 QString sLabelUpper = label.toUpper();
72 if(!isMeasurement) {
73 info.kind = sLabelUpper.contains("STIM") ? FIFFV_STIM_CH : FIFFV_MISC_CH;
74 } else {
75 if(sLabelUpper.contains("ECOG"))
76 info.kind = FIFFV_ECOG_CH;
77 else if(sLabelUpper.contains("SEEG"))
78 info.kind = FIFFV_SEEG_CH;
79 else if(sLabelUpper.contains("EEG"))
80 info.kind = FIFFV_EEG_CH;
81 else if(sLabelUpper.contains("MEG"))
82 info.kind = FIFFV_MEG_CH;
83 else if(sLabelUpper.contains("ECG"))
84 info.kind = FIFFV_ECG_CH;
85 else if(sLabelUpper.contains("EOG"))
86 info.kind = FIFFV_EOG_CH;
87 else if(sLabelUpper.contains("EMG"))
88 info.kind = FIFFV_EMG_CH;
89 else
90 info.kind = FIFFV_MISC_CH;
91 }
92
93 QString sUnitUpper = physicalDimension.toUpper();
94 if(sUnitUpper.endsWith("V") || sUnitUpper.endsWith("VOLT")) {
95 info.unit = FIFF_UNIT_V;
96 if(sUnitUpper.startsWith("U") || sUnitUpper.startsWith("MICRO"))
98 else if(sUnitUpper.startsWith("M") || sUnitUpper.startsWith("MILLI"))
100 else if(sUnitUpper.startsWith("N") || sUnitUpper.startsWith("NANO"))
101 info.unit_mul = FIFF_UNITM_N;
102 else
104 } else {
105 info.unit = FIFF_UNIT_NONE;
107 }
108
109 info.cal = 1.0f;
110 info.range = 1.0f;
111 info.ch_name = label;
112
113 return info;
114}
115
116//=============================================================================================================
117// EDFReader
118//=============================================================================================================
119
120EDFReader::EDFReader(float fScaleFactor)
121 : m_fScaleFactor(fScaleFactor)
122{
123}
124
125//=============================================================================================================
126
128{
129 if(m_file.isOpen()) {
130 m_file.close();
131 }
132}
133
134//=============================================================================================================
135
136bool EDFReader::open(const QString& sFilePath)
137{
138 m_sFilePath = sFilePath;
139 m_file.setFileName(sFilePath);
140
141 if(!m_file.open(QIODevice::ReadOnly)) {
142 qWarning() << "[EDFReader::open] Could not open file:" << sFilePath;
143 return false;
144 }
145
146 parseHeader(&m_file);
147 m_bIsOpen = true;
148 return true;
149}
150
151//=============================================================================================================
152
153void EDFReader::parseHeader(QIODevice* pDev)
154{
155 if(pDev->pos() != 0) {
156 pDev->seek(0);
157 }
158
159 // General header fields
160 m_sVersionNo = QString::fromLatin1(pDev->read(EDF_VERSION)).trimmed();
161 m_sPatientId = QString::fromLatin1(pDev->read(LOCAL_PATIENT_INFO)).trimmed();
162 m_sRecordingId = QString::fromLatin1(pDev->read(LOCAL_RECORD_INFO)).trimmed();
163 m_startDateTime.setDate(QDate::fromString(QString::fromLatin1(pDev->read(STARTDATE)), "dd.MM.yy"));
164 m_startDateTime = m_startDateTime.addYears(100);
165 m_startDateTime.setTime(QTime::fromString(QString::fromLatin1(pDev->read(STARTTIME)), "hh.mm.ss"));
166 m_iNumBytesInHeader = QString::fromLatin1(pDev->read(NUM_BYTES_HEADER)).toInt();
167 pDev->read(HEADER_RESERVED);
168 m_iNumDataRecords = QString::fromLatin1(pDev->read(NUM_DATA_RECORDS)).toInt();
169 m_fDataRecordsDuration = QString::fromLatin1(pDev->read(DURATION_DATA_RECS)).toFloat();
170 m_iNumChannels = QString::fromLatin1(pDev->read(NUM_SIGNALS)).toInt();
171
172 // Per-channel fields (read in EDF-specified order: all labels, then all transducers, etc.)
173 QVector<QString> vLabels, vTransducers, vPhysDims, vPrefilterings;
174 QVector<float> vPhysMins, vPhysMaxs;
175 QVector<long> vDigMins, vDigMaxs, vSamplesPerRecord;
176
177 for(int i = 0; i < m_iNumChannels; ++i)
178 vLabels.push_back(QString::fromLatin1(pDev->read(SIG_LABEL)).trimmed());
179 for(int i = 0; i < m_iNumChannels; ++i)
180 vTransducers.push_back(QString::fromLatin1(pDev->read(SIG_TRANSDUCER)).trimmed());
181 for(int i = 0; i < m_iNumChannels; ++i)
182 vPhysDims.push_back(QString::fromLatin1(pDev->read(SIG_PHYS_DIM)).trimmed());
183 for(int i = 0; i < m_iNumChannels; ++i)
184 vPhysMins.push_back(QString::fromLatin1(pDev->read(SIG_PHYS_MIN)).toFloat());
185 for(int i = 0; i < m_iNumChannels; ++i)
186 vPhysMaxs.push_back(QString::fromLatin1(pDev->read(SIG_PHYS_MAX)).toFloat());
187 for(int i = 0; i < m_iNumChannels; ++i)
188 vDigMins.push_back(QString::fromLatin1(pDev->read(SIG_DIG_MIN)).toLong());
189 for(int i = 0; i < m_iNumChannels; ++i)
190 vDigMaxs.push_back(QString::fromLatin1(pDev->read(SIG_DIG_MAX)).toLong());
191 for(int i = 0; i < m_iNumChannels; ++i)
192 vPrefilterings.push_back(QString::fromLatin1(pDev->read(SIG_PREFILTERING)).trimmed());
193 for(int i = 0; i < m_iNumChannels; ++i)
194 vSamplesPerRecord.push_back(QString::fromLatin1(pDev->read(SIG_NUM_SAMPLES)).toLong());
195 for(int i = 0; i < m_iNumChannels; ++i)
196 pDev->read(SIG_RESERVED);
197
198 // Build channel info structs
199 m_vAllChannels.clear();
200 for(int i = 0; i < m_iNumChannels; ++i) {
202 ch.channelNumber = i;
203 ch.label = vLabels[i];
204 ch.transducerType = vTransducers[i];
205 ch.physicalDimension = vPhysDims[i];
206 ch.prefiltering = vPrefilterings[i];
207 ch.physicalMin = vPhysMins[i];
208 ch.physicalMax = vPhysMaxs[i];
209 ch.digitalMin = vDigMins[i];
210 ch.digitalMax = vDigMaxs[i];
211 ch.samplesPerRecord = vSamplesPerRecord[i];
212 ch.sampleCount = vSamplesPerRecord[i] * m_iNumDataRecords;
213 ch.frequency = (m_fDataRecordsDuration > 0.0f)
214 ? vSamplesPerRecord[i] / m_fDataRecordsDuration
215 : 0.0f;
216 ch.isMeasurement = false;
217 m_vAllChannels.push_back(ch);
218 }
219
220 // Verify header size consistency
221 if(pDev->pos() != m_iNumBytesInHeader) {
222 qWarning() << "[EDFReader::parseHeader] Header byte count mismatch: read"
223 << pDev->pos() << "expected" << m_iNumBytesInHeader;
224 }
225
226 // Calculate bytes per data record
227 m_iNumBytesPerDataRecord = 0;
228 for(const auto& ch : m_vAllChannels) {
229 m_iNumBytesPerDataRecord += ch.samplesPerRecord * 2; // 16-bit integers
230 }
231
232 // Identify measurement channels (those with the highest sample rate)
233 long iMaxSamplesPerRecord = -1;
234 for(const auto& ch : m_vAllChannels) {
235 if(ch.samplesPerRecord > iMaxSamplesPerRecord) {
236 iMaxSamplesPerRecord = ch.samplesPerRecord;
237 }
238 }
239
240 m_vMeasChannels.clear();
241 for(int i = 0; i < m_vAllChannels.size(); ++i) {
242 if(m_vAllChannels[i].samplesPerRecord == iMaxSamplesPerRecord) {
243 m_vAllChannels[i].isMeasurement = true;
244 m_vMeasChannels.push_back(m_vAllChannels[i]);
245 }
246 }
247}
248
249//=============================================================================================================
250
252{
253 FiffInfo info;
254 info.nchan = m_vMeasChannels.size();
255
256 for(const auto& ch : m_vMeasChannels) {
257 FiffChInfo fiffCh = ch.toFiffChInfo();
258 info.chs.append(fiffCh);
259 info.ch_names.append(fiffCh.ch_name);
260 }
261
262 info.sfreq = getFrequency();
263 return info;
264}
265
266//=============================================================================================================
267
268MatrixXf EDFReader::readRawSegment(int iStartSampleIdx, int iEndSampleIdx) const
269{
270 if(!m_bIsOpen) {
271 qWarning() << "[EDFReader::readRawSegment] File not open";
272 return MatrixXf();
273 }
274
275 long totalSamples = getSampleCount();
276 if(iStartSampleIdx < 0 || iStartSampleIdx >= totalSamples ||
277 iEndSampleIdx < 0 || iEndSampleIdx > totalSamples) {
278 qWarning() << "[EDFReader::readRawSegment] Index out of bounds:"
279 << iStartSampleIdx << "-" << iEndSampleIdx;
280 return MatrixXf();
281 }
282
283 int iNumSamples = iEndSampleIdx - iStartSampleIdx;
284 if(iNumSamples <= 0) {
285 return MatrixXf();
286 }
287
288 int iSamplesPerRecord = m_vMeasChannels.isEmpty() ? 0 : m_vMeasChannels[0].samplesPerRecord;
289 if(iSamplesPerRecord <= 0) {
290 return MatrixXf();
291 }
292
293 // Calculate which data records to read
294 int iFirstRecord = iStartSampleIdx / iSamplesPerRecord;
295 int iRelativeFirst = iStartSampleIdx % iSamplesPerRecord;
296 int iNumRecords = static_cast<int>(
297 std::ceil(static_cast<float>(iNumSamples + iRelativeFirst) / iSamplesPerRecord));
298
299 // Seek to first needed data record
300 m_file.seek(m_iNumBytesInHeader + static_cast<qint64>(iFirstRecord) * m_iNumBytesPerDataRecord);
301
302 // Read needed data records
303 QVector<QByteArray> vRecords;
304 vRecords.reserve(iNumRecords);
305 for(int i = 0; i < iNumRecords; ++i) {
306 vRecords.push_back(m_file.read(m_iNumBytesPerDataRecord));
307 }
308
309 // Demultiplex: channels are interleaved within each record
310 QVector<QVector<int>> vRawPatches(m_vAllChannels.size());
311 for(int iRec = 0; iRec < vRecords.size(); ++iRec) {
312 int iOffset = 0;
313 for(int iCh = 0; iCh < m_vAllChannels.size(); ++iCh) {
314 int nSamp = m_vAllChannels[iCh].samplesPerRecord;
315 QVector<int> patch(nSamp);
316 for(int s = 0; s < nSamp; ++s) {
317 int byteIdx = (iOffset + s) * 2;
318 // 16-bit little-endian signed integer
319 patch[s] = static_cast<int16_t>(
320 (static_cast<unsigned char>(vRecords[iRec].at(byteIdx + 1)) << 8) |
321 (static_cast<unsigned char>(vRecords[iRec].at(byteIdx))));
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 float digRange = static_cast<float>(ch.digitalMax - ch.digitalMin);
343 float physRange = ch.physicalMax - ch.physicalMin;
344
345 for(int s = 0; s < iNumSamples; ++s) {
346 int rawIdx = s + iRelativeFirst;
347 float physVal = static_cast<float>(vMeasPatches[iCh][rawIdx] - ch.digitalMin) / digRange
348 * physRange + ch.physicalMin;
349 if(ch.isMeasurement) {
350 physVal /= m_fScaleFactor;
351 }
352 result(iCh, s) = physVal;
353 }
354 }
355
356 return result;
357}
358
359//=============================================================================================================
360
362{
363 if(!m_vMeasChannels.isEmpty()) {
364 return m_vMeasChannels[0].sampleCount;
365 }
366 return 0;
367}
368
369//=============================================================================================================
370
372{
373 if(!m_vMeasChannels.isEmpty()) {
374 return m_vMeasChannels[0].frequency;
375 }
376 return 0.0f;
377}
378
379//=============================================================================================================
380
382{
383 return m_vMeasChannels.size();
384}
385
386//=============================================================================================================
387
389{
390 FiffRawData raw;
391 raw.info = getInfo();
392 raw.first_samp = 0;
394
395 RowVectorXd cals(raw.info.nchan);
396 for(int i = 0; i < raw.info.chs.size(); ++i) {
397 cals[i] = static_cast<double>(raw.info.chs[i].cal);
398 }
399 raw.cals = cals;
400
401 return raw;
402}
403
404//=============================================================================================================
405
407{
408 return QStringLiteral("EDF");
409}
410
411//=============================================================================================================
412
413bool EDFReader::supportsExtension(const QString& sExtension) const
414{
415 QString ext = sExtension.toLower();
416 return (ext == ".edf" || ext == ".bdf");
417}
418
419//=============================================================================================================
420
421QVector<EDFChannelInfo> EDFReader::getAllChannelInfos() const
422{
423 return m_vAllChannels;
424}
425
426//=============================================================================================================
427
428QVector<EDFChannelInfo> EDFReader::getMeasurementChannelInfos() const
429{
430 return m_vMeasChannels;
431}
Fiff constants.
#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 FIFF_UNITM_N
#define FIFF_UNITM_M
#define FIFFV_STIM_CH
#define FIFFV_EMG_CH
#define FIFFV_ECG_CH
#define FIFF_UNITM_MU
Contains the declaration of the EDFReader class. Refactored from the mne_edf2fiff tool into a reusabl...
BIDS dataset reading, writing, path construction, and sidecar metadata handling for iEEG/EEG/MEG.
FIFF file I/O and data structures (raw, epochs, evoked, covariance, forward).
Channel-level metadata from the EDF header.
FIFFLIB::FiffChInfo toFiffChInfo() const
FIFFLIB::FiffInfo getInfo() const override
Return measurement metadata as FiffInfo.
EDFReader(float fScaleFactor=1e6)
EDFReader Default constructor.
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.
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.
Channel info descriptor.
FIFF measurement file information.
Definition fiff_info.h:86
QList< FiffChInfo > chs
FIFF raw measurement data.
Eigen::RowVectorXd cals