34#include <QJsonDocument>
54int bidsUnitToFiffUnit(
const QString& sUnit)
56 QString u = sUnit.toLower().trimmed();
57 if(u ==
"v" || u ==
"\u00B5v" || u ==
"uv" || u ==
"mv" || u ==
"nv")
59 if(u ==
"t" || u ==
"ft" || u ==
"pt")
65int bidsUnitToFiffUnitMul(
const QString& sUnit)
67 QString u = sUnit.toLower().trimmed();
68 if(u ==
"\u00B5v" || u ==
"uv" || u ==
"\u00B5s" || u ==
"us")
82void applyChannelsTsv(
FiffInfo& info,
const QList<BidsChannel>& channels)
84 if(channels.isEmpty())
89 QMap<QString, const BidsChannel*> channelMap;
90 for(
const auto& ch : channels)
91 channelMap[ch.name] = &ch;
95 for(
int i = 0; i < info.
chs.size(); ++i) {
97 auto it = channelMap.find(fiffCh.
ch_name);
98 if(it == channelMap.end())
102 QString typeUpper = rec->
type.toUpper();
103 if(bidsToFiff.contains(typeUpper))
104 fiffCh.
kind = bidsToFiff[typeUpper];
106 if(!rec->
units.isEmpty() && rec->
units !=
"n/a") {
107 fiffCh.
unit = bidsUnitToFiffUnit(rec->
units);
111 if(rec->
status.toLower() ==
"bad")
117void applyElectrodePositions(
FiffInfo& info,
118 const QList<BidsElectrode>& electrodes,
119 const QString& coordSystemName,
120 const QString& coordUnits)
122 if(electrodes.isEmpty())
127 if(coordMap.contains(coordSystemName))
128 coordFrame = coordMap[coordSystemName];
130 float scaleFactor = 1.0f;
131 QString units = coordUnits.toLower();
133 scaleFactor = 0.001f;
134 else if(units ==
"cm")
137 QSet<QString> chNames;
138 for(
const auto& ch : info.
chs)
139 chNames.insert(ch.ch_name);
143 for(
const auto& elec : electrodes) {
144 if(elec.x ==
"n/a" || elec.y ==
"n/a" || elec.z ==
"n/a")
150 dp.
r[0] = elec.x.toFloat() * scaleFactor;
151 dp.
r[1] = elec.y.toFloat() * scaleFactor;
152 dp.
r[2] = elec.z.toFloat() * scaleFactor;
160QJsonObject readJsonFile(
const QString& sFilePath)
162 QFile file(sFilePath);
163 if(!file.open(QIODevice::ReadOnly | QIODevice::Text))
165 QJsonParseError error;
166 QJsonDocument doc = QJsonDocument::fromJson(file.readAll(), &error);
168 if(error.error != QJsonParseError::NoError)
174bool writeJsonFile(
const QString& sFilePath,
const QJsonObject& json)
176 QFile file(sFilePath);
177 if(!file.open(QIODevice::WriteOnly | QIODevice::Text))
179 file.write(QJsonDocument(json).toJson(QJsonDocument::Indented));
185void readSidecarJson(
const QString& sFilePath,
189 QJsonObject json = readJsonFile(sFilePath);
194 double plf = json.value(QStringLiteral(
"PowerLineFrequency")).toDouble();
196 info.
linefreq =
static_cast<float>(plf);
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;
204 data.
ieegReference = json.value(QStringLiteral(
"iEEGReference")).toString();
205 data.
taskDescription = json.value(QStringLiteral(
"TaskDescription")).toString();
206 data.
manufacturer = json.value(QStringLiteral(
"Manufacturer")).toString();
208 data.
softwareVersions = json.value(QStringLiteral(
"SoftwareVersions")).toString();
209 data.
recordingType = json.value(QStringLiteral(
"RecordingType")).toString();
217QString fiffUnitToBidsString(
int unit,
int unitMul)
224 default:
return QStringLiteral(
"V");
231 default:
return QStringLiteral(
"T");
234 return QStringLiteral(
"n/a");
238QString channelTypeDescription(
int 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");
247 case FIFFV_EOG_CH:
return QStringLiteral(
"ElectroOculoGram");
248 case FIFFV_ECG_CH:
return QStringLiteral(
"ElectroCardioGram");
249 case FIFFV_EMG_CH:
return QStringLiteral(
"ElectroMyoGram");
252 default:
return QStringLiteral(
"n/a");
257QList<BidsChannel> buildChannelRecords(
const FiffInfo& info)
259 QList<BidsChannel> records;
261 QSet<QString> badsSet(info.
bads.begin(), info.
bads.end());
263 for(
int i = 0; i < info.
chs.size(); ++i) {
268 rec.
type = kindMap.contains(ch.
kind) ? kindMap[ch.
kind] : QStringLiteral(
"MISC");
272 ? QString::number(
static_cast<double>(info.
highpass),
'g', 10)
273 : QStringLiteral(
"n/a");
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");
287QList<BidsElectrode> buildElectrodeRecords(
const FiffInfo& info)
289 QList<BidsElectrode> records;
291 QMap<int, const FiffDigPoint*> digByIdent;
292 for(
const auto& dp : info.
dig) {
294 digByIdent[dp.
ident] = &dp;
299 for(
int i = 0; i < info.
chs.size(); ++i) {
312 bool hasPosition =
false;
314 if(digByIdent.contains(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);
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);
331 rec.
x = QStringLiteral(
"n/a");
332 rec.
y = QStringLiteral(
"n/a");
333 rec.
z = QStringLiteral(
"n/a");
337 rec.
size = QStringLiteral(
"n/a");
338 rec.
type = QStringLiteral(
"n/a");
339 rec.
material = QStringLiteral(
"n/a");
348QJsonObject buildIeegSidecarJson(
const BidsRawData& data,
355 json[QStringLiteral(
"TaskName")] = bidsPath.
task();
356 json[QStringLiteral(
"SamplingFrequency")] =
static_cast<double>(info.
sfreq);
357 json[QStringLiteral(
"PowerLineFrequency")] =
static_cast<double>(info.
linefreq);
363 json[QStringLiteral(
"iEEGReference")] = QStringLiteral(
"n/a");
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) {
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;
395 json[QStringLiteral(
"RecordingType")] = QStringLiteral(
"continuous");
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;
407 json[QStringLiteral(
"Manufacturer")] = data.
manufacturer;
417bool copyFile(
const QString& src,
const QString& dst,
bool overwrite)
419 if(!QFileInfo::exists(src)) {
420 qWarning() <<
"[BidsRawData::write] Source file does not exist:" << src;
423 if(QFileInfo::exists(dst)) {
425 qWarning() <<
"[BidsRawData::write] Target file already exists:" << dst;
430 return QFile::copy(src, dst);
434bool copyBrainVisionFiles(
const QString& srcVhdr,
const BIDSPath& bidsPath,
bool overwrite)
436 QFileInfo srcInfo(srcVhdr);
437 QString srcDir = srcInfo.absolutePath();
439 QFile vhdrFile(srcVhdr);
440 if(!vhdrFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
441 qWarning() <<
"[BidsRawData::write] Cannot open .vhdr file:" << srcVhdr;
445 QString dataFileName;
446 QString markerFileName;
447 QByteArray vhdrContent = vhdrFile.readAll();
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();
459 QString dstBase = bidsPath.
basename();
460 dstBase = dstBase.left(dstBase.lastIndexOf(
'.'));
462 QString dstVhdr = bidsPath.
filePath();
463 QString newDataFile = dstBase + QStringLiteral(
".eeg");
464 QString newMarkerFile = dstBase + QStringLiteral(
".vmrk");
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());
474 if(QFileInfo::exists(dstVhdr) && !overwrite) {
475 qWarning() <<
"[BidsRawData::write] Target file already exists:" << dstVhdr;
478 if(QFileInfo::exists(dstVhdr))
479 QFile::remove(dstVhdr);
481 QFile dstVhdrFile(dstVhdr);
482 if(!dstVhdrFile.open(QIODevice::WriteOnly | QIODevice::Text)) {
483 qWarning() <<
"[BidsRawData::write] Cannot write .vhdr file:" << dstVhdr;
486 dstVhdrFile.write(vhdrStr.toUtf8());
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;
498 if(!markerFileName.isEmpty()) {
499 QString srcMarker = QDir(srcDir).absoluteFilePath(markerFileName);
500 QString dstMarker = dstDir + QFileInfo(newMarkerFile).fileName();
502 if(QFileInfo::exists(srcMarker)) {
503 QFile markerFile(srcMarker);
504 if(markerFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
505 QString markerContent = QString::fromUtf8(markerFile.readAll());
508 markerContent.replace(
"DataFile=" + dataFileName,
509 "DataFile=" + QFileInfo(newDataFile).fileName());
511 if(QFileInfo::exists(dstMarker)) {
513 qWarning() <<
"[BidsRawData::write] Target marker file already exists:" << dstMarker;
516 QFile::remove(dstMarker);
519 QFile dstMarkerFile(dstMarker);
520 if(dstMarkerFile.open(QIODevice::WriteOnly | QIODevice::Text)) {
521 dstMarkerFile.write(markerContent.toUtf8());
522 dstMarkerFile.close();
532bool copyRawDataFile(
const QString& sourcePath,
const BIDSPath& bidsPath,
bool overwrite)
534 if(sourcePath.isEmpty())
537 QString ext = bidsPath.
extension().toLower();
538 if(ext ==
".vhdr" || ext ==
".ahdr")
539 return copyBrainVisionFiles(sourcePath, bidsPath, overwrite);
541 return copyFile(sourcePath, bidsPath.
filePath(), overwrite);
552 ,
events(std::move(other.events))
556 ,
reader(std::move(other.reader))
563 , m_bIsValid(other.m_bIsValid)
565 other.m_bIsValid =
false;
574 events = std::move(other.events);
578 reader = std::move(other.reader);
585 m_bIsValid = other.m_bIsValid;
586 other.m_bIsValid =
false;
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>();
636 if(bidsPath.
root().isEmpty()) {
637 qWarning() <<
"[BidsRawData::read] BIDSPath root is not set";
640 if(bidsPath.
subject().isEmpty()) {
641 qWarning() <<
"[BidsRawData::read] BIDSPath subject is not set";
645 qWarning() <<
"[BidsRawData::read] BIDSPath extension is not set";
652 QString rawFilePath = bidsPath.
filePath();
654 if(!QFileInfo::exists(rawFilePath)) {
656 if(ext.toLower() ==
".edf") {
659 if(QFileInfo::exists(altPath.
filePath()))
664 if(!QFileInfo::exists(rawFilePath)) {
665 qWarning() <<
"[BidsRawData::read] Raw data file not found:" << rawFilePath;
674 qWarning() <<
"[BidsRawData::read] Unsupported file extension:" << bidsPath.
extension();
678 if(!result.
reader->open(rawFilePath)) {
679 qWarning() <<
"[BidsRawData::read] Failed to open raw file:" << rawFilePath;
686 result.
raw = result.
reader->toFiffRawData();
692 if(QFileInfo::exists(channelsPath.
filePath())) {
694 applyChannelsTsv(result.
raw.info, channels);
701 if(QFileInfo::exists(coordsysPath.
filePath()))
708 if(QFileInfo::exists(electrodesPath.
filePath())) {
718 if(QFileInfo::exists(eventsPath.
filePath())) {
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);
729 for(
const auto& ev : result.
events) {
730 if(!ev.trialType.isEmpty() && ev.trialType !=
"n/a")
731 result.
eventIdMap.insert(ev.trialType, ev.value);
739 if(QFileInfo::exists(sidecarPath.
filePath()))
740 readSidecarJson(sidecarPath.
filePath(), result.
raw.info, result);
745 result.m_bIsValid =
true;
754 const QString& sourcePath,
762 if(bidsPath.
root().isEmpty()) {
763 qWarning() <<
"[BidsRawData::write] BIDSPath root is not set";
766 if(bidsPath.
subject().isEmpty()) {
767 qWarning() <<
"[BidsRawData::write] BIDSPath subject is not set";
770 if(bidsPath.
task().isEmpty()) {
771 qWarning() <<
"[BidsRawData::write] BIDSPath task is not set";
775 qWarning() <<
"[BidsRawData::write] BIDSPath datatype is not set";
778 if(
raw.info.isEmpty()) {
779 qWarning() <<
"[BidsRawData::write] FiffRawData info is empty";
787 qWarning() <<
"[BidsRawData::write] Failed to create directory:" << bidsPath.
directory();
794 if(options.
copyData && !sourcePath.isEmpty()) {
795 if(!copyRawDataFile(sourcePath, bidsPath, options.
overwrite)) {
796 qWarning() <<
"[BidsRawData::write] Failed to copy raw data file";
807 qWarning() <<
"[BidsRawData::write] channels.tsv already exists:" << channelsPath.
filePath();
811 QList<BidsChannel> channelRecords = buildChannelRecords(
raw.info);
813 qWarning() <<
"[BidsRawData::write] Failed to write channels.tsv";
822 QList<BidsElectrode> electrodeRecords = buildElectrodeRecords(
raw.info);
824 if(!electrodeRecords.isEmpty()) {
828 qWarning() <<
"[BidsRawData::write] Failed to write electrodes.tsv";
836 if(
raw.info.dig.isEmpty()) {
837 cs.
system = QStringLiteral(
"Other");
838 cs.
units = QStringLiteral(
"n/a");
840 int coordFrame =
raw.info.dig.first().coord_frame;
842 cs.
system = frameMap.contains(coordFrame)
843 ? frameMap[coordFrame]
844 : QStringLiteral(
"Other");
845 cs.
units = QStringLiteral(
"m");
849 cs.
description = QStringLiteral(
"Coordinate system derived from recording data");
852 qWarning() <<
"[BidsRawData::write] Failed to write coordsystem.json";
863 qWarning() <<
"[BidsRawData::write] events.tsv already exists:" << eventsPath.
filePath();
867 QList<BidsEvent> eventsToWrite =
events;
870 QMap<int, QString> valueToType;
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"));
879 qWarning() <<
"[BidsRawData::write] Failed to write events.tsv";
888 qWarning() <<
"[BidsRawData::write] Sidecar JSON already exists:" << sidecarPath.
filePath();
892 QJsonObject sidecarJson = buildIeegSidecarJson(*
this, bidsPath);
893 if(!writeJsonFile(sidecarPath.
filePath(), sidecarJson)) {
894 qWarning() <<
"[BidsRawData::write] Failed to write sidecar JSON";
903 QString descPath = bidsPath.
root() + QDir::separator()
904 + QStringLiteral(
"dataset_description.json");
906 if(!QFileInfo::exists(descPath)) {
909 ? QStringLiteral(
"[Unspecified]")
915 qWarning() <<
"[BidsRawData::write] Failed to write dataset_description.json";
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_COORD_UNKNOWN
BIDS dataset reading, writing, path construction, and sidecar metadata handling for iEEG/EEG/MEG.
QMap< int, QString > fiffKindToBidsType()
QMap< QString, int > bidsCoordToFiffFrame()
QMap< QString, int > bidsTypeToFiffKind()
QMap< int, QString > fiffFrameToBidsCoord()
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.
BIDSPath electrodesTsvPath() const
BIDSPath channelsTsvPath() const
QString directory() const
QString extension() const
BIDSPath coordsystemJsonPath() const
BIDSPath eventsTsvPath() const
void setExtension(const QString &sExtension)
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.
AbstractFormatReader::UPtr reader
QMap< QString, int > eventIdMap
QList< BidsElectrode > electrodes
BidsRawData & operator=(BidsRawData &&other) noexcept
QList< BidsEvent > events
QString manufacturerModelName
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...
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,...
QList< FiffDigPoint > dig