v2.0.0
Loading...
Searching...
No Matches
bids_raw_data.cpp
Go to the documentation of this file.
1//=============================================================================================================
12
13//=============================================================================================================
14// INCLUDES
15//=============================================================================================================
16
17#include "bids_raw_data.h"
18#include "bids_channel.h"
20#include "bids_const.h"
23
24#include <fiff/fiff_constants.h>
25
26//=============================================================================================================
27// QT INCLUDES
28//=============================================================================================================
29
30#include <QDebug>
31#include <QFileInfo>
32#include <QDir>
33#include <QFile>
34#include <QJsonDocument>
35#include <QJsonObject>
36#include <cmath>
37
38//=============================================================================================================
39// USED NAMESPACES
40//=============================================================================================================
41
42using namespace BIDSLIB;
43using namespace FIFFLIB;
44using namespace Eigen;
45
46//=============================================================================================================
47// ANONYMOUS HELPERS — read side
48//=============================================================================================================
49
50namespace
51{
52
53//=========================================================================================================
54int bidsUnitToFiffUnit(const QString& sUnit)
55{
56 QString u = sUnit.toLower().trimmed();
57 if(u == "v" || u == "\u00B5v" || u == "uv" || u == "mv" || u == "nv")
58 return FIFF_UNIT_V;
59 if(u == "t" || u == "ft" || u == "pt")
60 return FIFF_UNIT_T;
61 return FIFF_UNIT_NONE;
62}
63
64//=========================================================================================================
65int bidsUnitToFiffUnitMul(const QString& sUnit)
66{
67 QString u = sUnit.toLower().trimmed();
68 if(u == "\u00B5v" || u == "uv" || u == "\u00B5s" || u == "us")
69 return FIFF_UNITM_MU;
70 if(u == "mv")
71 return FIFF_UNITM_M;
72 if(u == "nv")
73 return FIFF_UNITM_N;
74 if(u == "ft")
75 return FIFF_UNITM_F;
76 if(u == "pt")
77 return FIFF_UNITM_P;
78 return FIFF_UNITM_NONE;
79}
80
81//=========================================================================================================
82void applyChannelsTsv(FiffInfo& info, const QList<BidsChannel>& channels)
83{
84 if(channels.isEmpty())
85 return;
86
87 QMap<QString, int> bidsToFiff = bidsTypeToFiffKind();
88
89 QMap<QString, const BidsChannel*> channelMap;
90 for(const auto& ch : channels)
91 channelMap[ch.name] = &ch;
92
93 info.bads.clear();
94
95 for(int i = 0; i < info.chs.size(); ++i) {
96 FiffChInfo& fiffCh = info.chs[i];
97 auto it = channelMap.find(fiffCh.ch_name);
98 if(it == channelMap.end())
99 continue;
100 const BidsChannel* rec = it.value();
101
102 QString typeUpper = rec->type.toUpper();
103 if(bidsToFiff.contains(typeUpper))
104 fiffCh.kind = bidsToFiff[typeUpper];
105
106 if(!rec->units.isEmpty() && rec->units != "n/a") {
107 fiffCh.unit = bidsUnitToFiffUnit(rec->units);
108 fiffCh.unit_mul = bidsUnitToFiffUnitMul(rec->units);
109 }
110
111 if(rec->status.toLower() == "bad")
112 info.bads.append(fiffCh.ch_name);
113 }
114}
115
116//=========================================================================================================
117void applyElectrodePositions(FiffInfo& info,
118 const QList<BidsElectrode>& electrodes,
119 const QString& coordSystemName,
120 const QString& coordUnits)
121{
122 if(electrodes.isEmpty())
123 return;
124
125 QMap<QString, int> coordMap = bidsCoordToFiffFrame();
126 int coordFrame = FIFFV_COORD_UNKNOWN;
127 if(coordMap.contains(coordSystemName))
128 coordFrame = coordMap[coordSystemName];
129
130 float scaleFactor = 1.0f;
131 QString units = coordUnits.toLower();
132 if(units == "mm")
133 scaleFactor = 0.001f;
134 else if(units == "cm")
135 scaleFactor = 0.01f;
136
137 QSet<QString> chNames;
138 for(const auto& ch : info.chs)
139 chNames.insert(ch.ch_name);
140
141 info.dig.clear();
142 int ident = 1;
143 for(const auto& elec : electrodes) {
144 if(elec.x == "n/a" || elec.y == "n/a" || elec.z == "n/a")
145 continue;
146
147 FiffDigPoint dp;
148 dp.kind = chNames.contains(elec.name) ? FIFFV_POINT_EEG : FIFFV_POINT_EXTRA;
149 dp.ident = ident++;
150 dp.r[0] = elec.x.toFloat() * scaleFactor;
151 dp.r[1] = elec.y.toFloat() * scaleFactor;
152 dp.r[2] = elec.z.toFloat() * scaleFactor;
153 dp.coord_frame = coordFrame;
154
155 info.dig.append(dp);
156 }
157}
158
159//=========================================================================================================
160QJsonObject readJsonFile(const QString& sFilePath)
161{
162 QFile file(sFilePath);
163 if(!file.open(QIODevice::ReadOnly | QIODevice::Text))
164 return {};
165 QJsonParseError error;
166 QJsonDocument doc = QJsonDocument::fromJson(file.readAll(), &error);
167 file.close();
168 if(error.error != QJsonParseError::NoError)
169 return {};
170 return doc.object();
171}
172
173//=========================================================================================================
174bool writeJsonFile(const QString& sFilePath, const QJsonObject& json)
175{
176 QFile file(sFilePath);
177 if(!file.open(QIODevice::WriteOnly | QIODevice::Text))
178 return false;
179 file.write(QJsonDocument(json).toJson(QJsonDocument::Indented));
180 file.close();
181 return true;
182}
183
184//=========================================================================================================
185void readSidecarJson(const QString& sFilePath,
186 FiffInfo& info,
187 BidsRawData& data)
188{
189 QJsonObject json = readJsonFile(sFilePath);
190 if(json.isEmpty())
191 return;
192
193 // Apply to FiffInfo
194 double plf = json.value(QStringLiteral("PowerLineFrequency")).toDouble();
195 if(plf > 0.0)
196 info.linefreq = static_cast<float>(plf);
197
198 double sf = json.value(QStringLiteral("SamplingFrequency")).toDouble();
199 if(sf > 0.0 && std::abs(info.sfreq - static_cast<float>(sf)) > 0.5f)
200 qWarning() << "[BidsRawData::read] Sampling frequency mismatch: raw ="
201 << info.sfreq << "sidecar =" << sf;
202
203 // Store independent metadata fields on BidsRawData
204 data.ieegReference = json.value(QStringLiteral("iEEGReference")).toString();
205 data.taskDescription = json.value(QStringLiteral("TaskDescription")).toString();
206 data.manufacturer = json.value(QStringLiteral("Manufacturer")).toString();
207 data.manufacturerModelName = json.value(QStringLiteral("ManufacturerModelName")).toString();
208 data.softwareVersions = json.value(QStringLiteral("SoftwareVersions")).toString();
209 data.recordingType = json.value(QStringLiteral("RecordingType")).toString();
210}
211
212//=============================================================================================================
213// ANONYMOUS HELPERS — write side
214//=============================================================================================================
215
216//=========================================================================================================
217QString fiffUnitToBidsString(int unit, int unitMul)
218{
219 if(unit == FIFF_UNIT_V) {
220 switch(unitMul) {
221 case FIFF_UNITM_MU: return QStringLiteral("\u00B5V");
222 case FIFF_UNITM_M: return QStringLiteral("mV");
223 case FIFF_UNITM_N: return QStringLiteral("nV");
224 default: return QStringLiteral("V");
225 }
226 }
227 if(unit == FIFF_UNIT_T) {
228 switch(unitMul) {
229 case FIFF_UNITM_F: return QStringLiteral("fT");
230 case FIFF_UNITM_P: return QStringLiteral("pT");
231 default: return QStringLiteral("T");
232 }
233 }
234 return QStringLiteral("n/a");
235}
236
237//=========================================================================================================
238QString channelTypeDescription(int kind)
239{
240 switch(kind) {
241 case FIFFV_EEG_CH: return QStringLiteral("ElectroEncephaloGram");
242 case FIFFV_ECOG_CH: return QStringLiteral("Electrocorticography");
243 case FIFFV_SEEG_CH: return QStringLiteral("StereoElectroEncephaloGram");
244 case FIFFV_DBS_CH: return QStringLiteral("DeepBrainStimulation");
245 case FIFFV_MEG_CH: return QStringLiteral("MagnetoEncephaloGram");
246 case FIFFV_STIM_CH: return QStringLiteral("Trigger");
247 case FIFFV_EOG_CH: return QStringLiteral("ElectroOculoGram");
248 case FIFFV_ECG_CH: return QStringLiteral("ElectroCardioGram");
249 case FIFFV_EMG_CH: return QStringLiteral("ElectroMyoGram");
250 case FIFFV_MISC_CH: return QStringLiteral("Miscellaneous");
251 case FIFFV_RESP_CH: return QStringLiteral("Respiration");
252 default: return QStringLiteral("n/a");
253 }
254}
255
256//=========================================================================================================
257QList<BidsChannel> buildChannelRecords(const FiffInfo& info)
258{
259 QList<BidsChannel> records;
260 QMap<int, QString> kindMap = fiffKindToBidsType();
261 QSet<QString> badsSet(info.bads.begin(), info.bads.end());
262
263 for(int i = 0; i < info.chs.size(); ++i) {
264 const FiffChInfo& ch = info.chs[i];
265
266 BidsChannel rec;
267 rec.name = ch.ch_name;
268 rec.type = kindMap.contains(ch.kind) ? kindMap[ch.kind] : QStringLiteral("MISC");
269 rec.units = fiffUnitToBidsString(ch.unit, ch.unit_mul);
270 rec.samplingFreq = QString::number(static_cast<double>(info.sfreq), 'g', 10);
271 rec.lowCutoff = (info.highpass > 0.0f)
272 ? QString::number(static_cast<double>(info.highpass), 'g', 10)
273 : QStringLiteral("n/a");
274 rec.highCutoff = (info.lowpass > 0.0f)
275 ? QString::number(static_cast<double>(info.lowpass), 'g', 10)
276 : QStringLiteral("n/a");
277 rec.notch = QStringLiteral("n/a");
278 rec.status = badsSet.contains(ch.ch_name) ? QStringLiteral("bad") : QStringLiteral("good");
279 rec.description = channelTypeDescription(ch.kind);
280
281 records.append(rec);
282 }
283 return records;
284}
285
286//=========================================================================================================
287QList<BidsElectrode> buildElectrodeRecords(const FiffInfo& info)
288{
289 QList<BidsElectrode> records;
290
291 QMap<int, const FiffDigPoint*> digByIdent;
292 for(const auto& dp : info.dig) {
293 if(dp.kind == FIFFV_POINT_EEG || dp.kind == FIFFV_POINT_EXTRA)
294 digByIdent[dp.ident] = &dp;
295 }
296
297 int digIdx = 0;
298
299 for(int i = 0; i < info.chs.size(); ++i) {
300 const FiffChInfo& ch = info.chs[i];
301
302 if(ch.kind == FIFFV_STIM_CH)
303 continue;
304 if(ch.kind != FIFFV_EEG_CH && ch.kind != FIFFV_ECOG_CH &&
305 ch.kind != FIFFV_SEEG_CH && ch.kind != FIFFV_DBS_CH)
306 continue;
307
308 BidsElectrode rec;
309 rec.name = ch.ch_name;
310
311 ++digIdx;
312 bool hasPosition = false;
313
314 if(digByIdent.contains(digIdx)) {
315 const FiffDigPoint* dp = digByIdent[digIdx];
316 if(std::isfinite(dp->r[0]) && std::isfinite(dp->r[1]) && std::isfinite(dp->r[2])) {
317 rec.x = QString::number(static_cast<double>(dp->r[0]), 'g', 8);
318 rec.y = QString::number(static_cast<double>(dp->r[1]), 'g', 8);
319 rec.z = QString::number(static_cast<double>(dp->r[2]), 'g', 8);
320 hasPosition = true;
321 }
322 }
323
324 if(!hasPosition) {
325 const Eigen::Vector3f& r0 = ch.chpos.r0;
326 if(r0.squaredNorm() > 0.0f && std::isfinite(r0[0])) {
327 rec.x = QString::number(static_cast<double>(r0[0]), 'g', 8);
328 rec.y = QString::number(static_cast<double>(r0[1]), 'g', 8);
329 rec.z = QString::number(static_cast<double>(r0[2]), 'g', 8);
330 } else {
331 rec.x = QStringLiteral("n/a");
332 rec.y = QStringLiteral("n/a");
333 rec.z = QStringLiteral("n/a");
334 }
335 }
336
337 rec.size = QStringLiteral("n/a");
338 rec.type = QStringLiteral("n/a");
339 rec.material = QStringLiteral("n/a");
340 rec.impedance = QStringLiteral("n/a");
341
342 records.append(rec);
343 }
344 return records;
345}
346
347//=========================================================================================================
348QJsonObject buildIeegSidecarJson(const BidsRawData& data,
349 const BIDSPath& bidsPath)
350{
351 QJsonObject json;
352 const FiffInfo& info = data.raw.info;
353
354 // Required
355 json[QStringLiteral("TaskName")] = bidsPath.task();
356 json[QStringLiteral("SamplingFrequency")] = static_cast<double>(info.sfreq);
357 json[QStringLiteral("PowerLineFrequency")] = static_cast<double>(info.linefreq);
358
359 // Reference
360 if(!data.ieegReference.isEmpty())
361 json[QStringLiteral("iEEGReference")] = data.ieegReference;
362 else
363 json[QStringLiteral("iEEGReference")] = QStringLiteral("n/a");
364
365 // Channel counts — computed from FiffInfo
366 int ecog = 0, seeg = 0, dbs = 0, eeg = 0, eog = 0, ecg = 0, emg = 0, misc = 0, trig = 0;
367 for(const auto& ch : info.chs) {
368 switch(ch.kind) {
369 case FIFFV_ECOG_CH: ++ecog; break;
370 case FIFFV_SEEG_CH: ++seeg; break;
371 case FIFFV_DBS_CH: ++dbs; break;
372 case FIFFV_EEG_CH: ++eeg; break;
373 case FIFFV_EOG_CH: ++eog; break;
374 case FIFFV_ECG_CH: ++ecg; break;
375 case FIFFV_EMG_CH: ++emg; break;
376 case FIFFV_MISC_CH: ++misc; break;
377 case FIFFV_STIM_CH: ++trig; break;
378 default: break;
379 }
380 }
381 json[QStringLiteral("ECOGChannelCount")] = ecog;
382 json[QStringLiteral("SEEGChannelCount")] = seeg;
383 if(dbs > 0) json[QStringLiteral("DBSChannelCount")] = dbs;
384 if(eeg > 0) json[QStringLiteral("EEGChannelCount")] = eeg;
385 if(eog > 0) json[QStringLiteral("EOGChannelCount")] = eog;
386 if(ecg > 0) json[QStringLiteral("ECGChannelCount")] = ecg;
387 if(emg > 0) json[QStringLiteral("EMGChannelCount")] = emg;
388 if(misc > 0) json[QStringLiteral("MiscChannelCount")] = misc;
389 if(trig > 0) json[QStringLiteral("TriggerChannelCount")] = trig;
390
391 // Recording metadata
392 if(!data.recordingType.isEmpty())
393 json[QStringLiteral("RecordingType")] = data.recordingType;
394 else
395 json[QStringLiteral("RecordingType")] = QStringLiteral("continuous");
396
397 if(info.sfreq > 0.0f && data.raw.last_samp >= data.raw.first_samp) {
398 double dur = static_cast<double>(data.raw.last_samp - data.raw.first_samp + 1)
399 / static_cast<double>(info.sfreq);
400 json[QStringLiteral("RecordingDuration")] = dur;
401 }
402
403 // Optional strings from BidsRawData
404 if(!data.taskDescription.isEmpty())
405 json[QStringLiteral("TaskDescription")] = data.taskDescription;
406 if(!data.manufacturer.isEmpty())
407 json[QStringLiteral("Manufacturer")] = data.manufacturer;
408 if(!data.manufacturerModelName.isEmpty())
409 json[QStringLiteral("ManufacturerModelName")] = data.manufacturerModelName;
410 if(!data.softwareVersions.isEmpty())
411 json[QStringLiteral("SoftwareVersions")] = data.softwareVersions;
412
413 return json;
414}
415
416//=========================================================================================================
417bool copyFile(const QString& src, const QString& dst, bool overwrite)
418{
419 if(!QFileInfo::exists(src)) {
420 qWarning() << "[BidsRawData::write] Source file does not exist:" << src;
421 return false;
422 }
423 if(QFileInfo::exists(dst)) {
424 if(!overwrite) {
425 qWarning() << "[BidsRawData::write] Target file already exists:" << dst;
426 return false;
427 }
428 QFile::remove(dst);
429 }
430 return QFile::copy(src, dst);
431}
432
433//=========================================================================================================
434bool copyBrainVisionFiles(const QString& srcVhdr, const BIDSPath& bidsPath, bool overwrite)
435{
436 QFileInfo srcInfo(srcVhdr);
437 QString srcDir = srcInfo.absolutePath();
438
439 QFile vhdrFile(srcVhdr);
440 if(!vhdrFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
441 qWarning() << "[BidsRawData::write] Cannot open .vhdr file:" << srcVhdr;
442 return false;
443 }
444
445 QString dataFileName;
446 QString markerFileName;
447 QByteArray vhdrContent = vhdrFile.readAll();
448 vhdrFile.close();
449
450 for(const auto& line : vhdrContent.split('\n')) {
451 QString sLine = QString::fromUtf8(line).trimmed();
452 if(sLine.startsWith("DataFile=", Qt::CaseInsensitive))
453 dataFileName = sLine.mid(9).trimmed();
454 else if(sLine.startsWith("MarkerFile=", Qt::CaseInsensitive))
455 markerFileName = sLine.mid(11).trimmed();
456 }
457
458 QString dstDir = bidsPath.directory();
459 QString dstBase = bidsPath.basename();
460 dstBase = dstBase.left(dstBase.lastIndexOf('.'));
461
462 QString dstVhdr = bidsPath.filePath();
463 QString newDataFile = dstBase + QStringLiteral(".eeg");
464 QString newMarkerFile = dstBase + QStringLiteral(".vmrk");
465
466 QString vhdrStr = QString::fromUtf8(vhdrContent);
467 if(!dataFileName.isEmpty())
468 vhdrStr.replace("DataFile=" + dataFileName,
469 "DataFile=" + QFileInfo(newDataFile).fileName());
470 if(!markerFileName.isEmpty())
471 vhdrStr.replace("MarkerFile=" + markerFileName,
472 "MarkerFile=" + QFileInfo(newMarkerFile).fileName());
473
474 if(QFileInfo::exists(dstVhdr) && !overwrite) {
475 qWarning() << "[BidsRawData::write] Target file already exists:" << dstVhdr;
476 return false;
477 }
478 if(QFileInfo::exists(dstVhdr))
479 QFile::remove(dstVhdr);
480
481 QFile dstVhdrFile(dstVhdr);
482 if(!dstVhdrFile.open(QIODevice::WriteOnly | QIODevice::Text)) {
483 qWarning() << "[BidsRawData::write] Cannot write .vhdr file:" << dstVhdr;
484 return false;
485 }
486 dstVhdrFile.write(vhdrStr.toUtf8());
487 dstVhdrFile.close();
488
489 if(!dataFileName.isEmpty()) {
490 QString srcData = QDir(srcDir).absoluteFilePath(dataFileName);
491 QString dstData = dstDir + QFileInfo(newDataFile).fileName();
492 if(!copyFile(srcData, dstData, overwrite)) {
493 qWarning() << "[BidsRawData::write] Failed to copy data file:" << srcData;
494 return false;
495 }
496 }
497
498 if(!markerFileName.isEmpty()) {
499 QString srcMarker = QDir(srcDir).absoluteFilePath(markerFileName);
500 QString dstMarker = dstDir + QFileInfo(newMarkerFile).fileName();
501
502 if(QFileInfo::exists(srcMarker)) {
503 QFile markerFile(srcMarker);
504 if(markerFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
505 QString markerContent = QString::fromUtf8(markerFile.readAll());
506 markerFile.close();
507
508 markerContent.replace("DataFile=" + dataFileName,
509 "DataFile=" + QFileInfo(newDataFile).fileName());
510
511 if(QFileInfo::exists(dstMarker)) {
512 if(!overwrite) {
513 qWarning() << "[BidsRawData::write] Target marker file already exists:" << dstMarker;
514 return false;
515 }
516 QFile::remove(dstMarker);
517 }
518
519 QFile dstMarkerFile(dstMarker);
520 if(dstMarkerFile.open(QIODevice::WriteOnly | QIODevice::Text)) {
521 dstMarkerFile.write(markerContent.toUtf8());
522 dstMarkerFile.close();
523 }
524 }
525 }
526 }
527
528 return true;
529}
530
531//=========================================================================================================
532bool copyRawDataFile(const QString& sourcePath, const BIDSPath& bidsPath, bool overwrite)
533{
534 if(sourcePath.isEmpty())
535 return true;
536
537 QString ext = bidsPath.extension().toLower();
538 if(ext == ".vhdr" || ext == ".ahdr")
539 return copyBrainVisionFiles(sourcePath, bidsPath, overwrite);
540
541 return copyFile(sourcePath, bidsPath.filePath(), overwrite);
542}
543
544} // anonymous namespace
545
546//=============================================================================================================
547// MEMBER METHODS
548//=============================================================================================================
549
551 : raw(other.raw) // FiffRawData has only copy ctor
552 , events(std::move(other.events))
553 , eventIdMap(std::move(other.eventIdMap))
554 , electrodes(std::move(other.electrodes))
555 , coordinateSystem(std::move(other.coordinateSystem))
556 , reader(std::move(other.reader))
557 , ieegReference(std::move(other.ieegReference))
558 , taskDescription(std::move(other.taskDescription))
559 , manufacturer(std::move(other.manufacturer))
560 , manufacturerModelName(std::move(other.manufacturerModelName))
561 , softwareVersions(std::move(other.softwareVersions))
562 , recordingType(std::move(other.recordingType))
563 , m_bIsValid(other.m_bIsValid)
564{
565 other.m_bIsValid = false;
566}
567
568//=============================================================================================================
569
571{
572 if(this != &other) {
573 raw = other.raw; // FiffRawData has only copy ctor
574 events = std::move(other.events);
575 eventIdMap = std::move(other.eventIdMap);
576 electrodes = std::move(other.electrodes);
577 coordinateSystem = std::move(other.coordinateSystem);
578 reader = std::move(other.reader);
579 ieegReference = std::move(other.ieegReference);
580 taskDescription = std::move(other.taskDescription);
581 manufacturer = std::move(other.manufacturer);
582 manufacturerModelName = std::move(other.manufacturerModelName);
583 softwareVersions = std::move(other.softwareVersions);
584 recordingType = std::move(other.recordingType);
585 m_bIsValid = other.m_bIsValid;
586 other.m_bIsValid = false;
587 }
588 return *this;
589}
590
591//=============================================================================================================
592
594{
595 raw = FiffRawData();
596 events.clear();
597 eventIdMap.clear();
598 electrodes.clear();
599 reader.reset();
600
601 ieegReference.clear();
602 taskDescription.clear();
603 manufacturer.clear();
604 manufacturerModelName.clear();
605 softwareVersions.clear();
606 recordingType.clear();
607
609
610 m_bIsValid = false;
611}
612
613//=============================================================================================================
614
616{
617 QString ext = sExtension.toLower();
618 if(ext == ".vhdr" || ext == ".ahdr")
619 return std::make_unique<BrainVisionReader>();
620 if(ext == ".edf" || ext == ".bdf")
621 return std::make_unique<EDFReader>();
622 return nullptr;
623}
624
625//=============================================================================================================
626// BidsRawData::read()
627//=============================================================================================================
628
630{
631 BidsRawData result;
632
633 //=========================================================================================================
634 // Step 1 — Validate BIDSPath
635 //=========================================================================================================
636 if(bidsPath.root().isEmpty()) {
637 qWarning() << "[BidsRawData::read] BIDSPath root is not set";
638 return result;
639 }
640 if(bidsPath.subject().isEmpty()) {
641 qWarning() << "[BidsRawData::read] BIDSPath subject is not set";
642 return result;
643 }
644 if(bidsPath.extension().isEmpty()) {
645 qWarning() << "[BidsRawData::read] BIDSPath extension is not set";
646 return result;
647 }
648
649 //=========================================================================================================
650 // Step 2 — Resolve raw file path
651 //=========================================================================================================
652 QString rawFilePath = bidsPath.filePath();
653
654 if(!QFileInfo::exists(rawFilePath)) {
655 QString ext = bidsPath.extension();
656 if(ext.toLower() == ".edf") {
657 BIDSPath altPath(bidsPath);
658 altPath.setExtension(".EDF");
659 if(QFileInfo::exists(altPath.filePath()))
660 rawFilePath = altPath.filePath();
661 }
662 }
663
664 if(!QFileInfo::exists(rawFilePath)) {
665 qWarning() << "[BidsRawData::read] Raw data file not found:" << rawFilePath;
666 return result;
667 }
668
669 //=========================================================================================================
670 // Step 3 — Create and open the format reader
671 //=========================================================================================================
672 result.reader = createReader(bidsPath.extension());
673 if(!result.reader) {
674 qWarning() << "[BidsRawData::read] Unsupported file extension:" << bidsPath.extension();
675 return result;
676 }
677
678 if(!result.reader->open(rawFilePath)) {
679 qWarning() << "[BidsRawData::read] Failed to open raw file:" << rawFilePath;
680 return result;
681 }
682
683 //=========================================================================================================
684 // Step 4 — Build FiffRawData from the reader
685 //=========================================================================================================
686 result.raw = result.reader->toFiffRawData();
687
688 //=========================================================================================================
689 // Step 5 — Read and apply *_channels.tsv
690 //=========================================================================================================
691 BIDSPath channelsPath = bidsPath.channelsTsvPath();
692 if(QFileInfo::exists(channelsPath.filePath())) {
693 QList<BidsChannel> channels = BidsChannel::readTsv(channelsPath.filePath());
694 applyChannelsTsv(result.raw.info, channels);
695 }
696
697 //=========================================================================================================
698 // Step 6 — Read *_coordsystem.json (before electrodes, for scale/frame info)
699 //=========================================================================================================
700 BIDSPath coordsysPath = bidsPath.coordsystemJsonPath();
701 if(QFileInfo::exists(coordsysPath.filePath()))
703
704 //=========================================================================================================
705 // Step 7 — Read and apply *_electrodes.tsv
706 //=========================================================================================================
707 BIDSPath electrodesPath = bidsPath.electrodesTsvPath();
708 if(QFileInfo::exists(electrodesPath.filePath())) {
709 result.electrodes = BidsElectrode::readTsv(electrodesPath.filePath());
710 applyElectrodePositions(result.raw.info, result.electrodes,
712 }
713
714 //=========================================================================================================
715 // Step 8 — Read *_events.tsv
716 //=========================================================================================================
717 BIDSPath eventsPath = bidsPath.eventsTsvPath();
718 if(QFileInfo::exists(eventsPath.filePath())) {
719 result.events = BidsEvent::readTsv(eventsPath.filePath());
720
721 // Compute sample from onset*sfreq if sample column was absent
722 float sfreq = result.raw.info.sfreq;
723 for(auto& ev : result.events) {
724 if(ev.sample == 0 && ev.onset > 0.0f && sfreq > 0)
725 ev.sample = static_cast<int>(ev.onset * sfreq);
726 }
727
728 // Build eventIdMap from trial_type → value
729 for(const auto& ev : result.events) {
730 if(!ev.trialType.isEmpty() && ev.trialType != "n/a")
731 result.eventIdMap.insert(ev.trialType, ev.value);
732 }
733 }
734
735 //=========================================================================================================
736 // Step 9 — Read sidecar *_{datatype}.json
737 //=========================================================================================================
738 BIDSPath sidecarPath = bidsPath.sidecarJsonPath();
739 if(QFileInfo::exists(sidecarPath.filePath()))
740 readSidecarJson(sidecarPath.filePath(), result.raw.info, result);
741
742 //=========================================================================================================
743 // Done
744 //=========================================================================================================
745 result.m_bIsValid = true;
746 return result;
747}
748
749//=============================================================================================================
750// BidsRawData::write()
751//=============================================================================================================
752
754 const QString& sourcePath,
755 const WriteOptions& options) const
756{
757 BIDSPath result;
758
759 //=========================================================================================================
760 // Step 1 — Validate
761 //=========================================================================================================
762 if(bidsPath.root().isEmpty()) {
763 qWarning() << "[BidsRawData::write] BIDSPath root is not set";
764 return result;
765 }
766 if(bidsPath.subject().isEmpty()) {
767 qWarning() << "[BidsRawData::write] BIDSPath subject is not set";
768 return result;
769 }
770 if(bidsPath.task().isEmpty()) {
771 qWarning() << "[BidsRawData::write] BIDSPath task is not set";
772 return result;
773 }
774 if(bidsPath.datatype().isEmpty()) {
775 qWarning() << "[BidsRawData::write] BIDSPath datatype is not set";
776 return result;
777 }
778 if(raw.info.isEmpty()) {
779 qWarning() << "[BidsRawData::write] FiffRawData info is empty";
780 return result;
781 }
782
783 //=========================================================================================================
784 // Step 2 — Create directory structure
785 //=========================================================================================================
786 if(!bidsPath.mkdirs()) {
787 qWarning() << "[BidsRawData::write] Failed to create directory:" << bidsPath.directory();
788 return result;
789 }
790
791 //=========================================================================================================
792 // Step 3 — Copy raw data file
793 //=========================================================================================================
794 if(options.copyData && !sourcePath.isEmpty()) {
795 if(!copyRawDataFile(sourcePath, bidsPath, options.overwrite)) {
796 qWarning() << "[BidsRawData::write] Failed to copy raw data file";
797 return result;
798 }
799 }
800
801 //=========================================================================================================
802 // Step 4 — Write *_channels.tsv
803 //=========================================================================================================
804 {
805 BIDSPath channelsPath = bidsPath.channelsTsvPath();
806 if(!options.overwrite && QFileInfo::exists(channelsPath.filePath())) {
807 qWarning() << "[BidsRawData::write] channels.tsv already exists:" << channelsPath.filePath();
808 return result;
809 }
810
811 QList<BidsChannel> channelRecords = buildChannelRecords(raw.info);
812 if(!BidsChannel::writeTsv(channelsPath.filePath(), channelRecords)) {
813 qWarning() << "[BidsRawData::write] Failed to write channels.tsv";
814 return result;
815 }
816 }
817
818 //=========================================================================================================
819 // Step 5 — Write *_electrodes.tsv + *_coordsystem.json
820 //=========================================================================================================
821 {
822 QList<BidsElectrode> electrodeRecords = buildElectrodeRecords(raw.info);
823
824 if(!electrodeRecords.isEmpty()) {
825 BIDSPath electrodesPath = bidsPath.electrodesTsvPath();
826 if(options.overwrite || !QFileInfo::exists(electrodesPath.filePath())) {
827 if(!BidsElectrode::writeTsv(electrodesPath.filePath(), electrodeRecords))
828 qWarning() << "[BidsRawData::write] Failed to write electrodes.tsv";
829 }
830
831 BIDSPath coordsysPath = bidsPath.coordsystemJsonPath();
832 if(options.overwrite || !QFileInfo::exists(coordsysPath.filePath())) {
833 // Build coordinate system, deriving defaults from FiffInfo if needed
835 if(cs.system.isEmpty()) {
836 if(raw.info.dig.isEmpty()) {
837 cs.system = QStringLiteral("Other");
838 cs.units = QStringLiteral("n/a");
839 } else {
840 int coordFrame = raw.info.dig.first().coord_frame;
841 QMap<int, QString> frameMap = fiffFrameToBidsCoord();
842 cs.system = frameMap.contains(coordFrame)
843 ? frameMap[coordFrame]
844 : QStringLiteral("Other");
845 cs.units = QStringLiteral("m");
846 }
847 }
848 if(cs.description.isEmpty() && !raw.info.dig.isEmpty())
849 cs.description = QStringLiteral("Coordinate system derived from recording data");
850
851 if(!BidsCoordinateSystem::writeJson(coordsysPath.filePath(), cs))
852 qWarning() << "[BidsRawData::write] Failed to write coordsystem.json";
853 }
854 }
855 }
856
857 //=========================================================================================================
858 // Step 6 — Write *_events.tsv
859 //=========================================================================================================
860 if(!events.isEmpty()) {
861 BIDSPath eventsPath = bidsPath.eventsTsvPath();
862 if(!options.overwrite && QFileInfo::exists(eventsPath.filePath())) {
863 qWarning() << "[BidsRawData::write] events.tsv already exists:" << eventsPath.filePath();
864 return result;
865 }
866
867 QList<BidsEvent> eventsToWrite = events;
868
869 // Apply trial_type fallback from eventIdMap
870 QMap<int, QString> valueToType;
871 for(auto it = eventIdMap.constBegin(); it != eventIdMap.constEnd(); ++it)
872 valueToType[it.value()] = it.key();
873 for(auto& ev : eventsToWrite) {
874 if(ev.trialType.isEmpty() || ev.trialType == "n/a")
875 ev.trialType = valueToType.value(ev.value, QStringLiteral("n/a"));
876 }
877
878 if(!BidsEvent::writeTsv(eventsPath.filePath(), eventsToWrite))
879 qWarning() << "[BidsRawData::write] Failed to write events.tsv";
880 }
881
882 //=========================================================================================================
883 // Step 7 — Write *_{datatype}.json sidecar
884 //=========================================================================================================
885 {
886 BIDSPath sidecarPath = bidsPath.sidecarJsonPath();
887 if(!options.overwrite && QFileInfo::exists(sidecarPath.filePath())) {
888 qWarning() << "[BidsRawData::write] Sidecar JSON already exists:" << sidecarPath.filePath();
889 return result;
890 }
891
892 QJsonObject sidecarJson = buildIeegSidecarJson(*this, bidsPath);
893 if(!writeJsonFile(sidecarPath.filePath(), sidecarJson)) {
894 qWarning() << "[BidsRawData::write] Failed to write sidecar JSON";
895 return result;
896 }
897 }
898
899 //=========================================================================================================
900 // Step 8 — Write dataset_description.json (never overwrite)
901 //=========================================================================================================
902 {
903 QString descPath = bidsPath.root() + QDir::separator()
904 + QStringLiteral("dataset_description.json");
905
906 if(!QFileInfo::exists(descPath)) {
908 desc.name = options.datasetName.isEmpty()
909 ? QStringLiteral("[Unspecified]")
910 : options.datasetName;
911 desc.bidsVersion = QStringLiteral("1.9.0");
912 desc.datasetType = QStringLiteral("raw");
913
914 if(!BidsDatasetDescription::write(descPath, desc))
915 qWarning() << "[BidsRawData::write] Failed to write dataset_description.json";
916 }
917 }
918
919 //=========================================================================================================
920 // Done
921 //=========================================================================================================
922 result = bidsPath;
923 return result;
924}
BIDSLIB::AbstractFormatReader implementation for European Data Format (EDF / EDF+) files.
BIDSLIB::AbstractFormatReader implementation for the BrainVision .vhdr / .vmrk / ....
Centralised BIDS vocabulary: datatype / suffix / extension whitelists, FIFF↔BIDS channel-type and coo...
Reader/writer for dataset_description.json — the REQUIRED root sidecar of every BIDS dataset.
Central container for a BIDS raw recording — the BIDS-side analogue of FIFFLIB::FiffRawData,...
Reader/writer for the BIDS _channels.tsv sidecar — one record per recorded channel.
Symbolic FIFF tag, block, value, unit and channel-type constants shared across FIFFLIB.
#define FIFFV_POINT_EXTRA
#define FIFFV_EOG_CH
#define FIFFV_SEEG_CH
#define FIFFV_EEG_CH
#define FIFF_UNIT_NONE
#define FIFFV_RESP_CH
#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_F
#define FIFFV_POINT_EEG
#define FIFF_UNITM_M
#define FIFFV_STIM_CH
#define FIFF_UNITM_P
#define FIFFV_COORD_UNKNOWN
#define FIFF_UNIT_T
#define FIFFV_EMG_CH
#define FIFFV_ECG_CH
#define FIFFV_DBS_CH
#define FIFF_UNITM_MU
BIDS dataset reading, writing, path construction, and sidecar metadata handling for iEEG/EEG/MEG.
QMap< int, QString > fiffKindToBidsType()
Definition bids_const.h:108
QMap< QString, int > bidsCoordToFiffFrame()
Definition bids_const.h:157
QMap< QString, int > bidsTypeToFiffKind()
Definition bids_const.h:130
QMap< int, QString > fiffFrameToBidsCoord()
Definition bids_const.h:174
FIFF file I/O, in-memory data structures and high-level readers/writers.
Channel metadata record corresponding to one row in *_channels.tsv.
static QList< BidsChannel > readTsv(const QString &sFilePath)
Read a BIDS *_channels.tsv file.
static bool writeTsv(const QString &sFilePath, const QList< BidsChannel > &channels)
Write a BIDS *_channels.tsv file.
Coordinate system metadata from *_coordsystem.json.
static bool writeJson(const QString &sFilePath, const BidsCoordinateSystem &cs)
Write a BIDS *_coordsystem.json file.
static BidsCoordinateSystem readJson(const QString &sFilePath)
Read a BIDS *_coordsystem.json file.
Dataset-level metadata from dataset_description.json.
static bool write(const QString &sFilePath, const BidsDatasetDescription &desc)
Write a dataset_description.json file.
Electrode position record corresponding to one row in *_electrodes.tsv.
static bool writeTsv(const QString &sFilePath, const QList< BidsElectrode > &electrodes)
Write a BIDS *_electrodes.tsv file.
static QList< BidsElectrode > readTsv(const QString &sFilePath)
Read a BIDS *_electrodes.tsv file.
static QList< BidsEvent > readTsv(const QString &sFilePath)
Read a BIDS *_events.tsv file.
static bool writeTsv(const QString &sFilePath, const QList< BidsEvent > &events)
Write a BIDS *_events.tsv file.
BIDS-compliant path and filename construction.
Definition bids_path.h:93
QString subject() const
QString filePath() const
BIDSPath electrodesTsvPath() const
QString root() const
BIDSPath channelsTsvPath() const
QString directory() const
QString extension() const
QString basename() const
BIDSPath coordsystemJsonPath() const
QString task() const
BIDSPath eventsTsvPath() const
void setExtension(const QString &sExtension)
bool mkdirs() const
QString datatype() const
BIDSPath sidecarJsonPath() const
Central container for a BIDS raw dataset, bundling electrophysiological data with all associated side...
void clear()
Clears all data members and resets to invalid state.
static BidsRawData read(const BIDSPath &bidsPath)
Read a BIDS dataset from disk.
static AbstractFormatReader::UPtr createReader(const QString &sExtension)
Create the appropriate format reader for a given file extension.
FIFFLIB::FiffRawData raw
AbstractFormatReader::UPtr reader
QMap< QString, int > eventIdMap
QList< BidsElectrode > electrodes
BidsRawData & operator=(BidsRawData &&other) noexcept
QList< BidsEvent > events
BidsCoordinateSystem coordinateSystem
BIDSPath write(const BIDSPath &bidsPath, const QString &sourcePath, const WriteOptions &options) const
Write this dataset to a BIDS-compliant directory.
Options controlling how write() operates.
std::unique_ptr< AbstractFormatReader > UPtr
Per-channel FIFF descriptor: identifiers, kind, calibration, coil type, channel-frame coil position a...
Eigen::Vector3f r0
One digitizer point: kind (cardinal/HPI/EEG/extra), ident, 3D position in FIFFV_COORD_HEAD.
Full FIFF measurement info: per-channel descriptors, sampling and filter setup, projectors,...
Definition fiff_info.h:88
QList< FiffDigPoint > dig
Definition fiff_info.h:275
QList< FiffChInfo > chs