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