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