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 // Ident = 1-based ordinal among electrode channels, the order buildElectrodeRecords() reads them back in
138 QMap<QString, int> chOrdinal;
139 for (const auto& ch : info.chs) {
140 if (ch.kind == FIFFV_EEG_CH || ch.kind == FIFFV_ECOG_CH || ch.kind == FIFFV_SEEG_CH || ch.kind == FIFFV_DBS_CH)
141 chOrdinal.insert(ch.ch_name, static_cast<int>(chOrdinal.size()) + 1);
142 }
143
144 info.dig.clear();
145 int extraIdent = static_cast<int>(chOrdinal.size()) + 1;
146 for (const auto& elec : electrodes) {
147 if (elec.x == "n/a" || elec.y == "n/a" || elec.z == "n/a")
148 continue;
149
150 FiffDigPoint dp;
151 dp.kind = chOrdinal.contains(elec.name) ? FIFFV_POINT_EEG : FIFFV_POINT_EXTRA;
152 dp.ident = chOrdinal.contains(elec.name) ? chOrdinal.value(elec.name) : extraIdent++;
153 dp.r[0] = elec.x.toFloat() * scaleFactor;
154 dp.r[1] = elec.y.toFloat() * scaleFactor;
155 dp.r[2] = elec.z.toFloat() * scaleFactor;
156 dp.coord_frame = coordFrame;
157
158 info.dig.append(dp);
159 }
160}
161
162//=========================================================================================================
163QJsonObject readJsonFile(const QString& sFilePath)
164{
165 QFile file(sFilePath);
166 if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
167 return {};
168 QJsonParseError error;
169 QJsonDocument doc = QJsonDocument::fromJson(file.readAll(), &error);
170 file.close();
171 if (error.error != QJsonParseError::NoError)
172 return {};
173 return doc.object();
174}
175
176//=========================================================================================================
177bool writeJsonFile(const QString& sFilePath, const QJsonObject& json)
178{
179 QFile file(sFilePath);
180 if (!file.open(QIODevice::WriteOnly | QIODevice::Text))
181 return false;
182 file.write(QJsonDocument(json).toJson(QJsonDocument::Indented));
183 file.close();
184 return true;
185}
186
187//=========================================================================================================
188void readSidecarJson(const QString& sFilePath,
189 FiffInfo& info,
190 BidsRawData& data)
191{
192 QJsonObject json = readJsonFile(sFilePath);
193 if (json.isEmpty())
194 return;
195
196 // Apply to FiffInfo
197 double plf = json.value(QStringLiteral("PowerLineFrequency")).toDouble();
198 if (plf > 0.0)
199 info.linefreq = static_cast<float>(plf);
200
201 double sf = json.value(QStringLiteral("SamplingFrequency")).toDouble();
202 if (sf > 0.0 && std::abs(info.sfreq - static_cast<float>(sf)) > 0.5f)
203 qWarning() << "[BidsRawData::read] Sampling frequency mismatch: raw ="
204 << info.sfreq << "sidecar =" << sf;
205
206 // Store independent metadata fields on BidsRawData
207 data.ieegReference = json.value(QStringLiteral("iEEGReference")).toString();
208 data.taskDescription = json.value(QStringLiteral("TaskDescription")).toString();
209 data.manufacturer = json.value(QStringLiteral("Manufacturer")).toString();
210 data.manufacturerModelName = json.value(QStringLiteral("ManufacturerModelName")).toString();
211 data.softwareVersions = json.value(QStringLiteral("SoftwareVersions")).toString();
212 data.recordingType = json.value(QStringLiteral("RecordingType")).toString();
213}
214
215//=============================================================================================================
216// ANONYMOUS HELPERS — write side
217//=============================================================================================================
218
219//=========================================================================================================
220QString fiffUnitToBidsString(int unit, int unitMul)
221{
222 if (unit == FIFF_UNIT_V) {
223 switch (unitMul) {
224 case FIFF_UNITM_MU:
225 return QStringLiteral("\u00B5V");
226 case FIFF_UNITM_M:
227 return QStringLiteral("mV");
228 case FIFF_UNITM_N:
229 return QStringLiteral("nV");
230 default:
231 return QStringLiteral("V");
232 }
233 }
234 if (unit == FIFF_UNIT_T) {
235 switch (unitMul) {
236 case FIFF_UNITM_F:
237 return QStringLiteral("fT");
238 case FIFF_UNITM_P:
239 return QStringLiteral("pT");
240 default:
241 return QStringLiteral("T");
242 }
243 }
244 return QStringLiteral("n/a");
245}
246
247//=========================================================================================================
248QString channelTypeDescription(int kind)
249{
250 switch (kind) {
251 case FIFFV_EEG_CH:
252 return QStringLiteral("ElectroEncephaloGram");
253 case FIFFV_ECOG_CH:
254 return QStringLiteral("Electrocorticography");
255 case FIFFV_SEEG_CH:
256 return QStringLiteral("StereoElectroEncephaloGram");
257 case FIFFV_DBS_CH:
258 return QStringLiteral("DeepBrainStimulation");
259 case FIFFV_MEG_CH:
260 return QStringLiteral("MagnetoEncephaloGram");
261 case FIFFV_STIM_CH:
262 return QStringLiteral("Trigger");
263 case FIFFV_EOG_CH:
264 return QStringLiteral("ElectroOculoGram");
265 case FIFFV_ECG_CH:
266 return QStringLiteral("ElectroCardioGram");
267 case FIFFV_EMG_CH:
268 return QStringLiteral("ElectroMyoGram");
269 case FIFFV_MISC_CH:
270 return QStringLiteral("Miscellaneous");
271 case FIFFV_RESP_CH:
272 return QStringLiteral("Respiration");
273 default:
274 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:
392 ++ecog;
393 break;
394 case FIFFV_SEEG_CH:
395 ++seeg;
396 break;
397 case FIFFV_DBS_CH:
398 ++dbs;
399 break;
400 case FIFFV_EEG_CH:
401 ++eeg;
402 break;
403 case FIFFV_EOG_CH:
404 ++eog;
405 break;
406 case FIFFV_ECG_CH:
407 ++ecg;
408 break;
409 case FIFFV_EMG_CH:
410 ++emg;
411 break;
412 case FIFFV_MISC_CH:
413 ++misc;
414 break;
415 case FIFFV_STIM_CH:
416 ++trig;
417 break;
418 default:
419 break;
420 }
421 }
422 json[QStringLiteral("ECOGChannelCount")] = ecog;
423 json[QStringLiteral("SEEGChannelCount")] = seeg;
424 if (dbs > 0)
425 json[QStringLiteral("DBSChannelCount")] = dbs;
426 if (eeg > 0)
427 json[QStringLiteral("EEGChannelCount")] = eeg;
428 if (eog > 0)
429 json[QStringLiteral("EOGChannelCount")] = eog;
430 if (ecg > 0)
431 json[QStringLiteral("ECGChannelCount")] = ecg;
432 if (emg > 0)
433 json[QStringLiteral("EMGChannelCount")] = emg;
434 if (misc > 0)
435 json[QStringLiteral("MiscChannelCount")] = misc;
436 if (trig > 0)
437 json[QStringLiteral("TriggerChannelCount")] = trig;
438
439 // Recording metadata
440 if (!data.recordingType.isEmpty())
441 json[QStringLiteral("RecordingType")] = data.recordingType;
442 else
443 json[QStringLiteral("RecordingType")] = QStringLiteral("continuous");
444
445 // mne-bids writes raw.times[-1]
446 if (info.sfreq > 0.0f && data.raw.last_samp >= data.raw.first_samp) {
447 double dur = static_cast<double>(data.raw.last_samp - data.raw.first_samp) / static_cast<double>(info.sfreq);
448 json[QStringLiteral("RecordingDuration")] = dur;
449 }
450
451 // Optional strings from BidsRawData
452 if (!data.taskDescription.isEmpty())
453 json[QStringLiteral("TaskDescription")] = data.taskDescription;
454 if (!data.manufacturer.isEmpty())
455 json[QStringLiteral("Manufacturer")] = data.manufacturer;
456 if (!data.manufacturerModelName.isEmpty())
457 json[QStringLiteral("ManufacturerModelName")] = data.manufacturerModelName;
458 if (!data.softwareVersions.isEmpty())
459 json[QStringLiteral("SoftwareVersions")] = data.softwareVersions;
460
461 return json;
462}
463
464//=========================================================================================================
465bool copyFile(const QString& src, const QString& dst, bool overwrite)
466{
467 if (!QFileInfo::exists(src)) {
468 qWarning() << "[BidsRawData::write] Source file does not exist:" << src;
469 return false;
470 }
471 if (QFileInfo::exists(dst)) {
472 if (!overwrite) {
473 qWarning() << "[BidsRawData::write] Target file already exists:" << dst;
474 return false;
475 }
476 QFile::remove(dst);
477 }
478 return QFile::copy(src, dst);
479}
480
481//=========================================================================================================
482bool copyBrainVisionFiles(const QString& srcVhdr, const BIDSPath& bidsPath, bool overwrite)
483{
484 QFileInfo srcInfo(srcVhdr);
485 QString srcDir = srcInfo.absolutePath();
486
487 QFile vhdrFile(srcVhdr);
488 if (!vhdrFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
489 qWarning() << "[BidsRawData::write] Cannot open .vhdr file:" << srcVhdr;
490 return false;
491 }
492
493 QString dataFileName;
494 QString markerFileName;
495 QByteArray vhdrContent = vhdrFile.readAll();
496 vhdrFile.close();
497
498 for (const auto& line : vhdrContent.split('\n')) {
499 QString sLine = QString::fromUtf8(line).trimmed();
500 if (sLine.startsWith("DataFile=", Qt::CaseInsensitive))
501 dataFileName = sLine.mid(9).trimmed();
502 else if (sLine.startsWith("MarkerFile=", Qt::CaseInsensitive))
503 markerFileName = sLine.mid(11).trimmed();
504 }
505
506 QString dstDir = bidsPath.directory();
507 QString dstBase = bidsPath.basename();
508 dstBase = dstBase.left(dstBase.lastIndexOf('.'));
509
510 QString dstVhdr = bidsPath.filePath();
511 QString newDataFile = dstBase + QStringLiteral(".eeg");
512 QString newMarkerFile = dstBase + QStringLiteral(".vmrk");
513
514 QString vhdrStr = QString::fromUtf8(vhdrContent);
515 if (!dataFileName.isEmpty())
516 vhdrStr.replace("DataFile=" + dataFileName,
517 "DataFile=" + QFileInfo(newDataFile).fileName());
518 if (!markerFileName.isEmpty())
519 vhdrStr.replace("MarkerFile=" + markerFileName,
520 "MarkerFile=" + QFileInfo(newMarkerFile).fileName());
521
522 if (QFileInfo::exists(dstVhdr) && !overwrite) {
523 qWarning() << "[BidsRawData::write] Target file already exists:" << dstVhdr;
524 return false;
525 }
526 if (QFileInfo::exists(dstVhdr))
527 QFile::remove(dstVhdr);
528
529 QFile dstVhdrFile(dstVhdr);
530 if (!dstVhdrFile.open(QIODevice::WriteOnly | QIODevice::Text)) {
531 qWarning() << "[BidsRawData::write] Cannot write .vhdr file:" << dstVhdr;
532 return false;
533 }
534 dstVhdrFile.write(vhdrStr.toUtf8());
535 dstVhdrFile.close();
536
537 if (!dataFileName.isEmpty()) {
538 QString srcData = QDir(srcDir).absoluteFilePath(dataFileName);
539 QString dstData = dstDir + QFileInfo(newDataFile).fileName();
540 if (!copyFile(srcData, dstData, overwrite)) {
541 qWarning() << "[BidsRawData::write] Failed to copy data file:" << srcData;
542 return false;
543 }
544 }
545
546 if (!markerFileName.isEmpty()) {
547 QString srcMarker = QDir(srcDir).absoluteFilePath(markerFileName);
548 QString dstMarker = dstDir + QFileInfo(newMarkerFile).fileName();
549
550 if (QFileInfo::exists(srcMarker)) {
551 QFile markerFile(srcMarker);
552 if (markerFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
553 QString markerContent = QString::fromUtf8(markerFile.readAll());
554 markerFile.close();
555
556 markerContent.replace("DataFile=" + dataFileName,
557 "DataFile=" + QFileInfo(newDataFile).fileName());
558
559 if (QFileInfo::exists(dstMarker)) {
560 if (!overwrite) {
561 qWarning() << "[BidsRawData::write] Target marker file already exists:" << dstMarker;
562 return false;
563 }
564 QFile::remove(dstMarker);
565 }
566
567 QFile dstMarkerFile(dstMarker);
568 if (dstMarkerFile.open(QIODevice::WriteOnly | QIODevice::Text)) {
569 dstMarkerFile.write(markerContent.toUtf8());
570 dstMarkerFile.close();
571 }
572 }
573 }
574 }
575
576 return true;
577}
578
579//=========================================================================================================
580bool copyRawDataFile(const QString& sourcePath, const BIDSPath& bidsPath, bool overwrite)
581{
582 if (sourcePath.isEmpty())
583 return true;
584
585 QString ext = bidsPath.extension().toLower();
586 if (ext == ".vhdr" || ext == ".ahdr")
587 return copyBrainVisionFiles(sourcePath, bidsPath, overwrite);
588
589 return copyFile(sourcePath, bidsPath.filePath(), overwrite);
590}
591
592} // anonymous namespace
593
594//=============================================================================================================
595// MEMBER METHODS
596//=============================================================================================================
597
599: raw(other.raw) // FiffRawData has only copy ctor
600, events(std::move(other.events))
601, eventIdMap(std::move(other.eventIdMap))
602, electrodes(std::move(other.electrodes))
603, coordinateSystem(std::move(other.coordinateSystem))
604, reader(std::move(other.reader))
605, ieegReference(std::move(other.ieegReference))
606, taskDescription(std::move(other.taskDescription))
607, manufacturer(std::move(other.manufacturer))
608, manufacturerModelName(std::move(other.manufacturerModelName))
609, softwareVersions(std::move(other.softwareVersions))
610, recordingType(std::move(other.recordingType))
611, m_bIsValid(other.m_bIsValid)
612{
613 other.m_bIsValid = false;
614}
615
616//=============================================================================================================
617
619{
620 if (this != &other) {
621 raw = other.raw; // FiffRawData has only copy ctor
622 events = std::move(other.events);
623 eventIdMap = std::move(other.eventIdMap);
624 electrodes = std::move(other.electrodes);
625 coordinateSystem = std::move(other.coordinateSystem);
626 reader = std::move(other.reader);
627 ieegReference = std::move(other.ieegReference);
628 taskDescription = std::move(other.taskDescription);
629 manufacturer = std::move(other.manufacturer);
630 manufacturerModelName = std::move(other.manufacturerModelName);
631 softwareVersions = std::move(other.softwareVersions);
632 recordingType = std::move(other.recordingType);
633 m_bIsValid = other.m_bIsValid;
634 other.m_bIsValid = false;
635 }
636 return *this;
637}
638
639//=============================================================================================================
640
642{
643 raw = FiffRawData();
644 events.clear();
645 eventIdMap.clear();
646 electrodes.clear();
647 reader.reset();
648
649 ieegReference.clear();
650 taskDescription.clear();
651 manufacturer.clear();
652 manufacturerModelName.clear();
653 softwareVersions.clear();
654 recordingType.clear();
655
657
658 m_bIsValid = false;
659}
660
661//=============================================================================================================
662
664{
665 QString ext = sExtension.toLower();
666 if (ext == ".vhdr" || ext == ".ahdr")
667 return std::make_unique<BrainVisionReader>();
668 if (ext == ".edf" || ext == ".bdf")
669 return std::make_unique<EDFReader>();
670 return nullptr;
671}
672
673//=============================================================================================================
674// BidsRawData::read()
675//=============================================================================================================
676
678{
679 BidsRawData result;
680
681 //=========================================================================================================
682 // Step 1 — Validate BIDSPath
683 //=========================================================================================================
684 if (bidsPath.root().isEmpty()) {
685 qWarning() << "[BidsRawData::read] BIDSPath root is not set";
686 return result;
687 }
688 if (bidsPath.subject().isEmpty()) {
689 qWarning() << "[BidsRawData::read] BIDSPath subject is not set";
690 return result;
691 }
692 if (bidsPath.extension().isEmpty()) {
693 qWarning() << "[BidsRawData::read] BIDSPath extension is not set";
694 return result;
695 }
696
697 //=========================================================================================================
698 // Step 2 — Resolve raw file path
699 //=========================================================================================================
700 QString rawFilePath = bidsPath.filePath();
701
702 if (!QFileInfo::exists(rawFilePath)) {
703 QString ext = bidsPath.extension();
704 if (ext.toLower() == ".edf") {
705 BIDSPath altPath(bidsPath);
706 altPath.setExtension(".EDF");
707 if (QFileInfo::exists(altPath.filePath()))
708 rawFilePath = altPath.filePath();
709 }
710 }
711
712 if (!QFileInfo::exists(rawFilePath)) {
713 qWarning() << "[BidsRawData::read] Raw data file not found:" << rawFilePath;
714 return result;
715 }
716
717 //=========================================================================================================
718 // Step 3 — Create and open the format reader
719 //=========================================================================================================
720 result.reader = createReader(bidsPath.extension());
721 if (!result.reader) {
722 qWarning() << "[BidsRawData::read] Unsupported file extension:" << bidsPath.extension();
723 return result;
724 }
725
726 if (!result.reader->open(rawFilePath)) {
727 qWarning() << "[BidsRawData::read] Failed to open raw file:" << rawFilePath;
728 return result;
729 }
730
731 //=========================================================================================================
732 // Step 4 — Build FiffRawData from the reader
733 //=========================================================================================================
734 result.raw = result.reader->toFiffRawData();
735
736 //=========================================================================================================
737 // Step 5 — Read and apply *_channels.tsv
738 //=========================================================================================================
739 BIDSPath channelsPath = bidsPath.channelsTsvPath();
740 if (QFileInfo::exists(channelsPath.filePath())) {
741 QList<BidsChannel> channels = BidsChannel::readTsv(channelsPath.filePath());
742 applyChannelsTsv(result.raw.info, channels);
743 }
744
745 //=========================================================================================================
746 // Step 6 — Read *_coordsystem.json (before electrodes, for scale/frame info)
747 //=========================================================================================================
748 BIDSPath coordsysPath = bidsPath.coordsystemJsonPath();
749 if (QFileInfo::exists(coordsysPath.filePath()))
751
752 //=========================================================================================================
753 // Step 7 — Read and apply *_electrodes.tsv
754 //=========================================================================================================
755 BIDSPath electrodesPath = bidsPath.electrodesTsvPath();
756 if (QFileInfo::exists(electrodesPath.filePath())) {
757 result.electrodes = BidsElectrode::readTsv(electrodesPath.filePath());
758 applyElectrodePositions(result.raw.info, result.electrodes,
760 }
761
762 //=========================================================================================================
763 // Step 8 — Read *_events.tsv
764 //=========================================================================================================
765 BIDSPath eventsPath = bidsPath.eventsTsvPath();
766 if (QFileInfo::exists(eventsPath.filePath())) {
767 result.events = BidsEvent::readTsv(eventsPath.filePath());
768
769 // Compute sample from onset*sfreq if sample column was absent
770 float sfreq = result.raw.info.sfreq;
771 for (auto& ev : result.events) {
772 if (ev.sample == 0 && ev.onset > 0.0f && sfreq > 0)
773 ev.sample = static_cast<int>(ev.onset * sfreq);
774 }
775
776 // Build eventIdMap from trial_type → value
777 for (const auto& ev : result.events) {
778 if (!ev.trialType.isEmpty() && ev.trialType != "n/a")
779 result.eventIdMap.insert(ev.trialType, ev.value);
780 }
781 }
782
783 //=========================================================================================================
784 // Step 9 — Read sidecar *_{datatype}.json
785 //=========================================================================================================
786 BIDSPath sidecarPath = bidsPath.sidecarJsonPath();
787 if (QFileInfo::exists(sidecarPath.filePath()))
788 readSidecarJson(sidecarPath.filePath(), result.raw.info, result);
789
790 //=========================================================================================================
791 // Done
792 //=========================================================================================================
793 result.m_bIsValid = true;
794 return result;
795}
796
797//=============================================================================================================
798// BidsRawData::write()
799//=============================================================================================================
800
802 const QString& sourcePath,
803 const WriteOptions& options) const
804{
805 BIDSPath result;
806
807 //=========================================================================================================
808 // Step 1 — Validate
809 //=========================================================================================================
810 if (bidsPath.root().isEmpty()) {
811 qWarning() << "[BidsRawData::write] BIDSPath root is not set";
812 return result;
813 }
814 if (bidsPath.subject().isEmpty()) {
815 qWarning() << "[BidsRawData::write] BIDSPath subject is not set";
816 return result;
817 }
818 if (bidsPath.task().isEmpty()) {
819 qWarning() << "[BidsRawData::write] BIDSPath task is not set";
820 return result;
821 }
822 if (bidsPath.datatype().isEmpty()) {
823 qWarning() << "[BidsRawData::write] BIDSPath datatype is not set";
824 return result;
825 }
826 if (raw.info.isEmpty()) {
827 qWarning() << "[BidsRawData::write] FiffRawData info is empty";
828 return result;
829 }
830
831 //=========================================================================================================
832 // Step 2 — Create directory structure
833 //=========================================================================================================
834 if (!bidsPath.mkdirs()) {
835 qWarning() << "[BidsRawData::write] Failed to create directory:" << bidsPath.directory();
836 return result;
837 }
838
839 //=========================================================================================================
840 // Step 3 — Copy raw data file
841 //=========================================================================================================
842 if (options.copyData && !sourcePath.isEmpty()) {
843 if (!copyRawDataFile(sourcePath, bidsPath, options.overwrite)) {
844 qWarning() << "[BidsRawData::write] Failed to copy raw data file";
845 return result;
846 }
847 }
848
849 //=========================================================================================================
850 // Step 4 — Write *_channels.tsv
851 //=========================================================================================================
852 {
853 BIDSPath channelsPath = bidsPath.channelsTsvPath();
854 if (!options.overwrite && QFileInfo::exists(channelsPath.filePath())) {
855 qWarning() << "[BidsRawData::write] channels.tsv already exists:" << channelsPath.filePath();
856 return result;
857 }
858
859 QList<BidsChannel> channelRecords = buildChannelRecords(raw.info);
860 if (!BidsChannel::writeTsv(channelsPath.filePath(), channelRecords)) {
861 qWarning() << "[BidsRawData::write] Failed to write channels.tsv";
862 return result;
863 }
864 }
865
866 //=========================================================================================================
867 // Step 5 — Write *_electrodes.tsv + *_coordsystem.json
868 //=========================================================================================================
869 {
870 QList<BidsElectrode> electrodeRecords = buildElectrodeRecords(raw.info);
871
872 if (!electrodeRecords.isEmpty()) {
873 BIDSPath electrodesPath = bidsPath.electrodesTsvPath();
874 if (options.overwrite || !QFileInfo::exists(electrodesPath.filePath())) {
875 if (!BidsElectrode::writeTsv(electrodesPath.filePath(), electrodeRecords))
876 qWarning() << "[BidsRawData::write] Failed to write electrodes.tsv";
877 }
878
879 BIDSPath coordsysPath = bidsPath.coordsystemJsonPath();
880 if (options.overwrite || !QFileInfo::exists(coordsysPath.filePath())) {
881 // Build coordinate system, deriving defaults from FiffInfo if needed
883 if (cs.system.isEmpty()) {
884 if (raw.info.dig.isEmpty()) {
885 cs.system = QStringLiteral("Other");
886 cs.units = QStringLiteral("n/a");
887 } else {
888 int coordFrame = raw.info.dig.first().coord_frame;
889 QMap<int, QString> frameMap = fiffFrameToBidsCoord();
890 cs.system = frameMap.contains(coordFrame)
891 ? frameMap[coordFrame]
892 : QStringLiteral("Other");
893 cs.units = QStringLiteral("m");
894 }
895 }
896 if (cs.description.isEmpty() && !raw.info.dig.isEmpty())
897 cs.description = QStringLiteral("Coordinate system derived from recording data");
898
899 if (!BidsCoordinateSystem::writeJson(coordsysPath.filePath(), cs))
900 qWarning() << "[BidsRawData::write] Failed to write coordsystem.json";
901 }
902 }
903 }
904
905 //=========================================================================================================
906 // Step 6 — Write *_events.tsv
907 //=========================================================================================================
908 if (!events.isEmpty()) {
909 BIDSPath eventsPath = bidsPath.eventsTsvPath();
910 if (!options.overwrite && QFileInfo::exists(eventsPath.filePath())) {
911 qWarning() << "[BidsRawData::write] events.tsv already exists:" << eventsPath.filePath();
912 return result;
913 }
914
915 QList<BidsEvent> eventsToWrite = events;
916
917 // Apply trial_type fallback from eventIdMap
918 QMap<int, QString> valueToType;
919 for (auto it = eventIdMap.constBegin(); it != eventIdMap.constEnd(); ++it)
920 valueToType[it.value()] = it.key();
921 for (auto& ev : eventsToWrite) {
922 if (ev.trialType.isEmpty() || ev.trialType == "n/a")
923 ev.trialType = valueToType.value(ev.value, QStringLiteral("n/a"));
924 }
925
926 if (!BidsEvent::writeTsv(eventsPath.filePath(), eventsToWrite))
927 qWarning() << "[BidsRawData::write] Failed to write events.tsv";
928 }
929
930 //=========================================================================================================
931 // Step 7 — Write *_{datatype}.json sidecar
932 //=========================================================================================================
933 {
934 BIDSPath sidecarPath = bidsPath.sidecarJsonPath();
935 if (!options.overwrite && QFileInfo::exists(sidecarPath.filePath())) {
936 qWarning() << "[BidsRawData::write] Sidecar JSON already exists:" << sidecarPath.filePath();
937 return result;
938 }
939
940 QJsonObject sidecarJson = buildIeegSidecarJson(*this, bidsPath);
941 if (!writeJsonFile(sidecarPath.filePath(), sidecarJson)) {
942 qWarning() << "[BidsRawData::write] Failed to write sidecar JSON";
943 return result;
944 }
945 }
946
947 //=========================================================================================================
948 // Step 8 — Write dataset_description.json (never overwrite)
949 //=========================================================================================================
950 {
951 QString descPath = bidsPath.root() + QDir::separator() + QStringLiteral("dataset_description.json");
952
953 if (!QFileInfo::exists(descPath)) {
955 desc.name = options.datasetName.isEmpty()
956 ? QStringLiteral("[Unspecified]")
957 : options.datasetName;
958 desc.bidsVersion = QStringLiteral("1.9.0");
959 desc.datasetType = QStringLiteral("raw");
960
961 if (!BidsDatasetDescription::write(descPath, desc))
962 qWarning() << "[BidsRawData::write] Failed to write dataset_description.json";
963 }
964 }
965
966 //=========================================================================================================
967 // Done
968 //=========================================================================================================
969 result = bidsPath;
970 return result;
971}
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.
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.
BIDSLIB::AbstractFormatReader implementation for European Data Format (EDF / EDF+) and BioSemi BDF fi...
BIDSLIB::AbstractFormatReader implementation for the BrainVision .vhdr / .vmrk / ....
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:126
QMap< QString, int > bidsCoordToFiffFrame()
Definition bids_const.h:179
QMap< QString, int > bidsTypeToFiffKind()
Definition bids_const.h:150
QMap< int, QString > fiffFrameToBidsCoord()
Definition bids_const.h:198
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:74
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:90
QList< FiffDigPoint > dig
Definition fiff_info.h:290
QList< FiffChInfo > chs
Continuous FIFF raw recording: FiffInfo plus a random-access directory of FIFF_DATA_BUFFER tags.