34#include <QJsonDocument>
61const BidsUnit bidsUnits[] = {
80void bidsUnitToFiff(
const QString& sUnit,
int& unit,
int& unitMul)
82 const QString u = sUnit.trimmed();
83 for (
const BidsUnit& e : bidsUnits) {
84 if (u == QStringView(e.bids)) {
93void applyChannelsTsv(
FiffInfo& info,
const QList<BidsChannel>& channels)
95 if (channels.isEmpty())
100 QMap<QString, const BidsChannel*> channelMap;
101 for (
const auto& ch : channels)
102 channelMap[ch.name] = &ch;
106 for (
int i = 0; i < info.
chs.size(); ++i) {
108 auto it = channelMap.find(fiffCh.
ch_name);
109 if (it == channelMap.end())
113 QString typeUpper = rec->
type.toUpper();
114 if (bidsToFiff.contains(typeUpper))
115 fiffCh.
kind = bidsToFiff[typeUpper];
119 if (rec->
status.toLower() ==
"bad")
125void applyElectrodePositions(
FiffInfo& info,
126 const QList<BidsElectrode>& electrodes,
127 const QString& coordSystemName,
128 const QString& coordUnits)
130 if (electrodes.isEmpty())
135 if (coordMap.contains(coordSystemName))
136 coordFrame = coordMap[coordSystemName];
138 float scaleFactor = 1.0f;
139 QString units = coordUnits.toLower();
141 scaleFactor = 0.001f;
142 else if (units ==
"cm")
146 QMap<QString, int> chOrdinal;
147 for (
const auto& ch : info.
chs) {
149 chOrdinal.insert(ch.ch_name,
static_cast<int>(chOrdinal.size()) + 1);
153 int extraIdent =
static_cast<int>(chOrdinal.size()) + 1;
154 for (
const auto& elec : electrodes) {
155 if (elec.x ==
"n/a" || elec.y ==
"n/a" || elec.z ==
"n/a")
160 dp.
ident = chOrdinal.contains(elec.name) ? chOrdinal.value(elec.name) : extraIdent++;
161 dp.
r[0] = elec.x.toFloat() * scaleFactor;
162 dp.
r[1] = elec.y.toFloat() * scaleFactor;
163 dp.
r[2] = elec.z.toFloat() * scaleFactor;
171QJsonObject readJsonFile(
const QString& sFilePath)
173 QFile file(sFilePath);
174 if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
176 QJsonParseError error;
177 QJsonDocument doc = QJsonDocument::fromJson(file.readAll(), &error);
179 if (error.error != QJsonParseError::NoError)
185bool writeJsonFile(
const QString& sFilePath,
const QJsonObject& json)
187 QFile file(sFilePath);
188 if (!file.open(QIODevice::WriteOnly | QIODevice::Text))
190 file.write(QJsonDocument(json).toJson(QJsonDocument::Indented));
196void readSidecarJson(
const QString& sFilePath,
200 QJsonObject json = readJsonFile(sFilePath);
205 double plf = json.value(QStringLiteral(
"PowerLineFrequency")).toDouble();
207 info.
linefreq =
static_cast<float>(plf);
209 double sf = json.value(QStringLiteral(
"SamplingFrequency")).toDouble();
210 if (sf > 0.0 && std::abs(info.
sfreq -
static_cast<float>(sf)) > 0.5f)
211 qWarning() <<
"[BidsRawData::read] Sampling frequency mismatch: raw ="
212 << info.
sfreq <<
"sidecar =" << sf;
215 data.
ieegReference = json.value(QStringLiteral(
"iEEGReference")).toString();
216 data.
taskDescription = json.value(QStringLiteral(
"TaskDescription")).toString();
217 data.
manufacturer = json.value(QStringLiteral(
"Manufacturer")).toString();
219 data.
softwareVersions = json.value(QStringLiteral(
"SoftwareVersions")).toString();
220 data.
recordingType = json.value(QStringLiteral(
"RecordingType")).toString();
228QString fiffUnitToBidsString(
int unit,
int unitMul)
230 for (
const BidsUnit& e : bidsUnits) {
231 if (e.unit == unit && e.mul == unitMul && e.unit !=
FIFF_UNIT_NONE)
232 return QString::fromUtf16(e.bids);
235 for (
const BidsUnit& e : bidsUnits) {
237 return QString::fromUtf16(e.bids);
239 return QStringLiteral(
"n/a");
243QString channelTypeDescription(
int kind)
247 return QStringLiteral(
"ElectroEncephaloGram");
249 return QStringLiteral(
"Electrocorticography");
251 return QStringLiteral(
"StereoElectroEncephaloGram");
253 return QStringLiteral(
"DeepBrainStimulation");
255 return QStringLiteral(
"MagnetoEncephaloGram");
257 return QStringLiteral(
"Trigger");
259 return QStringLiteral(
"ElectroOculoGram");
261 return QStringLiteral(
"ElectroCardioGram");
263 return QStringLiteral(
"ElectroMyoGram");
265 return QStringLiteral(
"Miscellaneous");
267 return QStringLiteral(
"Respiration");
269 return QStringLiteral(
"n/a");
274QList<BidsChannel> buildChannelRecords(
const FiffInfo& info)
276 QList<BidsChannel> records;
278 QSet<QString> badsSet(info.
bads.begin(), info.
bads.end());
280 for (
int i = 0; i < info.
chs.size(); ++i) {
285 rec.
type = kindMap.contains(ch.
kind) ? kindMap[ch.
kind] : QStringLiteral(
"MISC");
289 ? QString::number(
static_cast<double>(info.
highpass),
'g', 10)
290 : QStringLiteral(
"n/a");
292 ? QString::number(
static_cast<double>(info.
lowpass),
'g', 10)
293 : QStringLiteral(
"n/a");
294 rec.
notch = QStringLiteral(
"n/a");
295 rec.
status = badsSet.contains(ch.
ch_name) ? QStringLiteral(
"bad") : QStringLiteral(
"good");
304QList<BidsElectrode> buildElectrodeRecords(
const FiffInfo& info)
306 QList<BidsElectrode> records;
308 QMap<int, const FiffDigPoint*> digByIdent;
309 for (
const auto& dp : info.
dig) {
311 digByIdent[dp.
ident] = &dp;
316 for (
int i = 0; i < info.
chs.size(); ++i) {
329 bool hasPosition =
false;
331 if (digByIdent.contains(digIdx)) {
333 if (std::isfinite(dp->
r[0]) && std::isfinite(dp->
r[1]) && std::isfinite(dp->
r[2])) {
334 rec.
x = QString::number(
static_cast<double>(dp->
r[0]),
'g', 8);
335 rec.
y = QString::number(
static_cast<double>(dp->
r[1]),
'g', 8);
336 rec.
z = QString::number(
static_cast<double>(dp->
r[2]),
'g', 8);
342 const Eigen::Vector3f& r0 = ch.
chpos.
r0;
343 if (r0.squaredNorm() > 0.0f && std::isfinite(r0[0])) {
344 rec.
x = QString::number(
static_cast<double>(r0[0]),
'g', 8);
345 rec.
y = QString::number(
static_cast<double>(r0[1]),
'g', 8);
346 rec.
z = QString::number(
static_cast<double>(r0[2]),
'g', 8);
348 rec.
x = QStringLiteral(
"n/a");
349 rec.
y = QStringLiteral(
"n/a");
350 rec.
z = QStringLiteral(
"n/a");
354 rec.
size = QStringLiteral(
"n/a");
355 rec.
type = QStringLiteral(
"n/a");
356 rec.
material = QStringLiteral(
"n/a");
365QJsonObject buildIeegSidecarJson(
const BidsRawData& data,
372 json[QStringLiteral(
"TaskName")] = bidsPath.
task();
373 json[QStringLiteral(
"SamplingFrequency")] =
static_cast<double>(info.
sfreq);
374 json[QStringLiteral(
"PowerLineFrequency")] =
static_cast<double>(info.
linefreq);
380 json[QStringLiteral(
"iEEGReference")] = QStringLiteral(
"n/a");
383 int ecog = 0, seeg = 0, dbs = 0, eeg = 0, eog = 0, ecg = 0, emg = 0, misc = 0, trig = 0;
384 for (
const auto& ch : info.
chs) {
417 json[QStringLiteral(
"ECOGChannelCount")] = ecog;
418 json[QStringLiteral(
"SEEGChannelCount")] = seeg;
420 json[QStringLiteral(
"DBSChannelCount")] = dbs;
422 json[QStringLiteral(
"EEGChannelCount")] = eeg;
424 json[QStringLiteral(
"EOGChannelCount")] = eog;
426 json[QStringLiteral(
"ECGChannelCount")] = ecg;
428 json[QStringLiteral(
"EMGChannelCount")] = emg;
430 json[QStringLiteral(
"MiscChannelCount")] = misc;
432 json[QStringLiteral(
"TriggerChannelCount")] = trig;
438 json[QStringLiteral(
"RecordingType")] = QStringLiteral(
"continuous");
443 json[QStringLiteral(
"RecordingDuration")] = dur;
450 json[QStringLiteral(
"Manufacturer")] = data.
manufacturer;
460bool copyFile(
const QString& src,
const QString& dst,
bool overwrite)
462 if (!QFileInfo::exists(src)) {
463 qWarning() <<
"[BidsRawData::write] Source file does not exist:" << src;
466 if (QFileInfo::exists(dst)) {
468 qWarning() <<
"[BidsRawData::write] Target file already exists:" << dst;
473 return QFile::copy(src, dst);
477bool copyBrainVisionFiles(
const QString& srcVhdr,
const BIDSPath& bidsPath,
bool overwrite)
479 QFileInfo srcInfo(srcVhdr);
480 QString srcDir = srcInfo.absolutePath();
482 QFile vhdrFile(srcVhdr);
483 if (!vhdrFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
484 qWarning() <<
"[BidsRawData::write] Cannot open .vhdr file:" << srcVhdr;
488 QString dataFileName;
489 QString markerFileName;
490 QByteArray vhdrContent = vhdrFile.readAll();
493 for (
const auto& line : vhdrContent.split(
'\n')) {
494 QString sLine = QString::fromUtf8(line).trimmed();
495 if (sLine.startsWith(
"DataFile=", Qt::CaseInsensitive))
496 dataFileName = sLine.mid(9).trimmed();
497 else if (sLine.startsWith(
"MarkerFile=", Qt::CaseInsensitive))
498 markerFileName = sLine.mid(11).trimmed();
502 QString dstBase = bidsPath.
basename();
503 dstBase = dstBase.left(dstBase.lastIndexOf(
'.'));
505 QString dstVhdr = bidsPath.
filePath();
506 QString newDataFile = dstBase + QStringLiteral(
".eeg");
507 QString newMarkerFile = dstBase + QStringLiteral(
".vmrk");
509 QString vhdrStr = QString::fromUtf8(vhdrContent);
510 if (!dataFileName.isEmpty())
511 vhdrStr.replace(
"DataFile=" + dataFileName,
512 "DataFile=" + QFileInfo(newDataFile).fileName());
513 if (!markerFileName.isEmpty())
514 vhdrStr.replace(
"MarkerFile=" + markerFileName,
515 "MarkerFile=" + QFileInfo(newMarkerFile).fileName());
517 if (QFileInfo::exists(dstVhdr) && !overwrite) {
518 qWarning() <<
"[BidsRawData::write] Target file already exists:" << dstVhdr;
521 if (QFileInfo::exists(dstVhdr))
522 QFile::remove(dstVhdr);
524 QFile dstVhdrFile(dstVhdr);
525 if (!dstVhdrFile.open(QIODevice::WriteOnly | QIODevice::Text)) {
526 qWarning() <<
"[BidsRawData::write] Cannot write .vhdr file:" << dstVhdr;
529 dstVhdrFile.write(vhdrStr.toUtf8());
532 if (!dataFileName.isEmpty()) {
533 QString srcData = QDir(srcDir).absoluteFilePath(dataFileName);
534 QString dstData = dstDir + QFileInfo(newDataFile).fileName();
535 if (!copyFile(srcData, dstData, overwrite)) {
536 qWarning() <<
"[BidsRawData::write] Failed to copy data file:" << srcData;
541 if (!markerFileName.isEmpty()) {
542 QString srcMarker = QDir(srcDir).absoluteFilePath(markerFileName);
543 QString dstMarker = dstDir + QFileInfo(newMarkerFile).fileName();
545 if (QFileInfo::exists(srcMarker)) {
546 QFile markerFile(srcMarker);
547 if (markerFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
548 QString markerContent = QString::fromUtf8(markerFile.readAll());
551 markerContent.replace(
"DataFile=" + dataFileName,
552 "DataFile=" + QFileInfo(newDataFile).fileName());
554 if (QFileInfo::exists(dstMarker)) {
556 qWarning() <<
"[BidsRawData::write] Target marker file already exists:" << dstMarker;
559 QFile::remove(dstMarker);
562 QFile dstMarkerFile(dstMarker);
563 if (dstMarkerFile.open(QIODevice::WriteOnly | QIODevice::Text)) {
564 dstMarkerFile.write(markerContent.toUtf8());
565 dstMarkerFile.close();
575bool copyRawDataFile(
const QString& sourcePath,
const BIDSPath& bidsPath,
bool overwrite)
577 if (sourcePath.isEmpty())
580 QString ext = bidsPath.
extension().toLower();
581 if (ext ==
".vhdr" || ext ==
".ahdr")
582 return copyBrainVisionFiles(sourcePath, bidsPath, overwrite);
584 return copyFile(sourcePath, bidsPath.
filePath(), overwrite);
595,
events(std::move(other.events))
599,
reader(std::move(other.reader))
606, m_bIsValid(other.m_bIsValid)
608 other.m_bIsValid =
false;
615 if (
this != &other) {
617 events = std::move(other.events);
621 reader = std::move(other.reader);
628 m_bIsValid = other.m_bIsValid;
629 other.m_bIsValid =
false;
660 QString ext = sExtension.toLower();
661 if (ext ==
".vhdr" || ext ==
".ahdr")
662 return std::make_unique<BrainVisionReader>();
663 if (ext ==
".edf" || ext ==
".bdf")
664 return std::make_unique<EDFReader>();
679 if (bidsPath.
root().isEmpty()) {
680 qWarning() <<
"[BidsRawData::read] BIDSPath root is not set";
683 if (bidsPath.
subject().isEmpty()) {
684 qWarning() <<
"[BidsRawData::read] BIDSPath subject is not set";
688 qWarning() <<
"[BidsRawData::read] BIDSPath extension is not set";
695 QString rawFilePath = bidsPath.
filePath();
697 if (!QFileInfo::exists(rawFilePath)) {
699 if (ext.toLower() ==
".edf") {
702 if (QFileInfo::exists(altPath.
filePath()))
707 if (!QFileInfo::exists(rawFilePath)) {
708 qWarning() <<
"[BidsRawData::read] Raw data file not found:" << rawFilePath;
717 qWarning() <<
"[BidsRawData::read] Unsupported file extension:" << bidsPath.
extension();
721 if (!result.
reader->open(rawFilePath)) {
722 qWarning() <<
"[BidsRawData::read] Failed to open raw file:" << rawFilePath;
729 result.
raw = result.
reader->toFiffRawData();
735 if (QFileInfo::exists(channelsPath.
filePath())) {
737 applyChannelsTsv(result.
raw.
info, channels);
744 if (QFileInfo::exists(coordsysPath.
filePath()))
751 if (QFileInfo::exists(electrodesPath.
filePath())) {
761 if (QFileInfo::exists(eventsPath.
filePath())) {
766 for (
auto& ev : result.
events) {
767 if (ev.sample == 0 && ev.onset > 0.0f && sfreq > 0)
768 ev.sample =
static_cast<int>(ev.onset * sfreq);
772 for (
const auto& ev : result.
events) {
773 if (!ev.trialType.isEmpty() && ev.trialType !=
"n/a")
774 result.
eventIdMap.insert(ev.trialType, ev.value);
782 if (QFileInfo::exists(sidecarPath.
filePath()))
788 result.m_bIsValid =
true;
797 const QString& sourcePath,
805 if (bidsPath.
root().isEmpty()) {
806 qWarning() <<
"[BidsRawData::write] BIDSPath root is not set";
809 if (bidsPath.
subject().isEmpty()) {
810 qWarning() <<
"[BidsRawData::write] BIDSPath subject is not set";
813 if (bidsPath.
task().isEmpty()) {
814 qWarning() <<
"[BidsRawData::write] BIDSPath task is not set";
817 if (bidsPath.
datatype().isEmpty()) {
818 qWarning() <<
"[BidsRawData::write] BIDSPath datatype is not set";
821 if (
raw.info.isEmpty()) {
822 qWarning() <<
"[BidsRawData::write] FiffRawData info is empty";
830 qWarning() <<
"[BidsRawData::write] Failed to create directory:" << bidsPath.
directory();
837 if (options.
copyData && !sourcePath.isEmpty()) {
838 if (!copyRawDataFile(sourcePath, bidsPath, options.
overwrite)) {
839 qWarning() <<
"[BidsRawData::write] Failed to copy raw data file";
850 qWarning() <<
"[BidsRawData::write] channels.tsv already exists:" << channelsPath.
filePath();
854 QList<BidsChannel> channelRecords = buildChannelRecords(
raw.info);
856 qWarning() <<
"[BidsRawData::write] Failed to write channels.tsv";
865 QList<BidsElectrode> electrodeRecords = buildElectrodeRecords(
raw.info);
867 if (!electrodeRecords.isEmpty()) {
871 qWarning() <<
"[BidsRawData::write] Failed to write electrodes.tsv";
878 if (cs.
system.isEmpty()) {
879 if (
raw.info.dig.isEmpty()) {
880 cs.
system = QStringLiteral(
"Other");
881 cs.
units = QStringLiteral(
"n/a");
883 int coordFrame =
raw.info.dig.first().coord_frame;
885 cs.
system = frameMap.contains(coordFrame)
886 ? frameMap[coordFrame]
887 : QStringLiteral(
"Other");
888 cs.
units = QStringLiteral(
"m");
892 cs.
description = QStringLiteral(
"Coordinate system derived from recording data");
895 qWarning() <<
"[BidsRawData::write] Failed to write coordsystem.json";
906 qWarning() <<
"[BidsRawData::write] events.tsv already exists:" << eventsPath.
filePath();
910 QList<BidsEvent> eventsToWrite =
events;
913 QMap<int, QString> valueToType;
915 valueToType[it.value()] = it.key();
916 for (
auto& ev : eventsToWrite) {
917 if (ev.trialType.isEmpty() || ev.trialType ==
"n/a")
918 ev.trialType = valueToType.value(ev.value, QStringLiteral(
"n/a"));
922 qWarning() <<
"[BidsRawData::write] Failed to write events.tsv";
931 qWarning() <<
"[BidsRawData::write] Sidecar JSON already exists:" << sidecarPath.
filePath();
935 QJsonObject sidecarJson = buildIeegSidecarJson(*
this, bidsPath);
936 if (!writeJsonFile(sidecarPath.
filePath(), sidecarJson)) {
937 qWarning() <<
"[BidsRawData::write] Failed to write sidecar JSON";
946 QString descPath = bidsPath.
root() + QDir::separator() + QStringLiteral(
"dataset_description.json");
948 if (!QFileInfo::exists(descPath)) {
951 ? QStringLiteral(
"[Unspecified]")
957 qWarning() <<
"[BidsRawData::write] Failed to write dataset_description.json";
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.
constexpr auto FIFF_UNIT_T_M
constexpr auto FIFFV_POINT_EXTRA
constexpr auto FIFFV_ECG_CH
constexpr auto FIFF_UNIT_CEL
constexpr auto FIFFV_DBS_CH
constexpr auto FIFF_UNITM_F
constexpr auto FIFFV_MISC_CH
constexpr auto FIFF_UNITM_NONE
constexpr auto FIFFV_SEEG_CH
constexpr auto FIFFV_POINT_EEG
constexpr auto FIFFV_ECOG_CH
constexpr auto FIFFV_STIM_CH
constexpr auto FIFF_UNIT_RAD
constexpr auto FIFF_UNIT_T
constexpr auto FIFFV_COORD_UNKNOWN
constexpr auto FIFF_UNIT_MHO
constexpr auto FIFF_UNITM_MU
constexpr auto FIFFV_EMG_CH
constexpr auto FIFF_UNIT_MOL
constexpr auto FIFF_UNIT_V
constexpr auto FIFF_UNITM_P
constexpr auto FIFF_UNITM_M
constexpr auto FIFF_UNIT_NONE
constexpr auto FIFFV_EOG_CH
constexpr auto FIFFV_RESP_CH
constexpr auto FIFF_UNITM_N
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
Continuous FIFF raw recording: FiffInfo plus a random-access directory of FIFF_DATA_BUFFER tags.