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")
138 QMap<QString, int> chOrdinal;
139 for (
const auto& ch : info.
chs) {
141 chOrdinal.insert(ch.ch_name,
static_cast<int>(chOrdinal.size()) + 1);
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")
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;
163QJsonObject readJsonFile(
const QString& sFilePath)
165 QFile file(sFilePath);
166 if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
168 QJsonParseError error;
169 QJsonDocument doc = QJsonDocument::fromJson(file.readAll(), &error);
171 if (error.error != QJsonParseError::NoError)
177bool writeJsonFile(
const QString& sFilePath,
const QJsonObject& json)
179 QFile file(sFilePath);
180 if (!file.open(QIODevice::WriteOnly | QIODevice::Text))
182 file.write(QJsonDocument(json).toJson(QJsonDocument::Indented));
188void readSidecarJson(
const QString& sFilePath,
192 QJsonObject json = readJsonFile(sFilePath);
197 double plf = json.value(QStringLiteral(
"PowerLineFrequency")).toDouble();
199 info.
linefreq =
static_cast<float>(plf);
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;
207 data.
ieegReference = json.value(QStringLiteral(
"iEEGReference")).toString();
208 data.
taskDescription = json.value(QStringLiteral(
"TaskDescription")).toString();
209 data.
manufacturer = json.value(QStringLiteral(
"Manufacturer")).toString();
211 data.
softwareVersions = json.value(QStringLiteral(
"SoftwareVersions")).toString();
212 data.
recordingType = json.value(QStringLiteral(
"RecordingType")).toString();
220QString fiffUnitToBidsString(
int unit,
int unitMul)
225 return QStringLiteral(
"\u00B5V");
227 return QStringLiteral(
"mV");
229 return QStringLiteral(
"nV");
231 return QStringLiteral(
"V");
237 return QStringLiteral(
"fT");
239 return QStringLiteral(
"pT");
241 return QStringLiteral(
"T");
244 return QStringLiteral(
"n/a");
248QString channelTypeDescription(
int kind)
252 return QStringLiteral(
"ElectroEncephaloGram");
254 return QStringLiteral(
"Electrocorticography");
256 return QStringLiteral(
"StereoElectroEncephaloGram");
258 return QStringLiteral(
"DeepBrainStimulation");
260 return QStringLiteral(
"MagnetoEncephaloGram");
262 return QStringLiteral(
"Trigger");
264 return QStringLiteral(
"ElectroOculoGram");
266 return QStringLiteral(
"ElectroCardioGram");
268 return QStringLiteral(
"ElectroMyoGram");
270 return QStringLiteral(
"Miscellaneous");
272 return QStringLiteral(
"Respiration");
274 return QStringLiteral(
"n/a");
279QList<BidsChannel> buildChannelRecords(
const FiffInfo& info)
281 QList<BidsChannel> records;
283 QSet<QString> badsSet(info.
bads.begin(), info.
bads.end());
285 for (
int i = 0; i < info.
chs.size(); ++i) {
290 rec.
type = kindMap.contains(ch.
kind) ? kindMap[ch.
kind] : QStringLiteral(
"MISC");
294 ? QString::number(
static_cast<double>(info.
highpass),
'g', 10)
295 : QStringLiteral(
"n/a");
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");
309QList<BidsElectrode> buildElectrodeRecords(
const FiffInfo& info)
311 QList<BidsElectrode> records;
313 QMap<int, const FiffDigPoint*> digByIdent;
314 for (
const auto& dp : info.
dig) {
316 digByIdent[dp.
ident] = &dp;
321 for (
int i = 0; i < info.
chs.size(); ++i) {
334 bool hasPosition =
false;
336 if (digByIdent.contains(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);
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);
353 rec.
x = QStringLiteral(
"n/a");
354 rec.
y = QStringLiteral(
"n/a");
355 rec.
z = QStringLiteral(
"n/a");
359 rec.
size = QStringLiteral(
"n/a");
360 rec.
type = QStringLiteral(
"n/a");
361 rec.
material = QStringLiteral(
"n/a");
370QJsonObject buildIeegSidecarJson(
const BidsRawData& data,
377 json[QStringLiteral(
"TaskName")] = bidsPath.
task();
378 json[QStringLiteral(
"SamplingFrequency")] =
static_cast<double>(info.
sfreq);
379 json[QStringLiteral(
"PowerLineFrequency")] =
static_cast<double>(info.
linefreq);
385 json[QStringLiteral(
"iEEGReference")] = QStringLiteral(
"n/a");
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) {
422 json[QStringLiteral(
"ECOGChannelCount")] = ecog;
423 json[QStringLiteral(
"SEEGChannelCount")] = seeg;
425 json[QStringLiteral(
"DBSChannelCount")] = dbs;
427 json[QStringLiteral(
"EEGChannelCount")] = eeg;
429 json[QStringLiteral(
"EOGChannelCount")] = eog;
431 json[QStringLiteral(
"ECGChannelCount")] = ecg;
433 json[QStringLiteral(
"EMGChannelCount")] = emg;
435 json[QStringLiteral(
"MiscChannelCount")] = misc;
437 json[QStringLiteral(
"TriggerChannelCount")] = trig;
443 json[QStringLiteral(
"RecordingType")] = QStringLiteral(
"continuous");
448 json[QStringLiteral(
"RecordingDuration")] = dur;
455 json[QStringLiteral(
"Manufacturer")] = data.
manufacturer;
465bool copyFile(
const QString& src,
const QString& dst,
bool overwrite)
467 if (!QFileInfo::exists(src)) {
468 qWarning() <<
"[BidsRawData::write] Source file does not exist:" << src;
471 if (QFileInfo::exists(dst)) {
473 qWarning() <<
"[BidsRawData::write] Target file already exists:" << dst;
478 return QFile::copy(src, dst);
482bool copyBrainVisionFiles(
const QString& srcVhdr,
const BIDSPath& bidsPath,
bool overwrite)
484 QFileInfo srcInfo(srcVhdr);
485 QString srcDir = srcInfo.absolutePath();
487 QFile vhdrFile(srcVhdr);
488 if (!vhdrFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
489 qWarning() <<
"[BidsRawData::write] Cannot open .vhdr file:" << srcVhdr;
493 QString dataFileName;
494 QString markerFileName;
495 QByteArray vhdrContent = vhdrFile.readAll();
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();
507 QString dstBase = bidsPath.
basename();
508 dstBase = dstBase.left(dstBase.lastIndexOf(
'.'));
510 QString dstVhdr = bidsPath.
filePath();
511 QString newDataFile = dstBase + QStringLiteral(
".eeg");
512 QString newMarkerFile = dstBase + QStringLiteral(
".vmrk");
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());
522 if (QFileInfo::exists(dstVhdr) && !overwrite) {
523 qWarning() <<
"[BidsRawData::write] Target file already exists:" << dstVhdr;
526 if (QFileInfo::exists(dstVhdr))
527 QFile::remove(dstVhdr);
529 QFile dstVhdrFile(dstVhdr);
530 if (!dstVhdrFile.open(QIODevice::WriteOnly | QIODevice::Text)) {
531 qWarning() <<
"[BidsRawData::write] Cannot write .vhdr file:" << dstVhdr;
534 dstVhdrFile.write(vhdrStr.toUtf8());
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;
546 if (!markerFileName.isEmpty()) {
547 QString srcMarker = QDir(srcDir).absoluteFilePath(markerFileName);
548 QString dstMarker = dstDir + QFileInfo(newMarkerFile).fileName();
550 if (QFileInfo::exists(srcMarker)) {
551 QFile markerFile(srcMarker);
552 if (markerFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
553 QString markerContent = QString::fromUtf8(markerFile.readAll());
556 markerContent.replace(
"DataFile=" + dataFileName,
557 "DataFile=" + QFileInfo(newDataFile).fileName());
559 if (QFileInfo::exists(dstMarker)) {
561 qWarning() <<
"[BidsRawData::write] Target marker file already exists:" << dstMarker;
564 QFile::remove(dstMarker);
567 QFile dstMarkerFile(dstMarker);
568 if (dstMarkerFile.open(QIODevice::WriteOnly | QIODevice::Text)) {
569 dstMarkerFile.write(markerContent.toUtf8());
570 dstMarkerFile.close();
580bool copyRawDataFile(
const QString& sourcePath,
const BIDSPath& bidsPath,
bool overwrite)
582 if (sourcePath.isEmpty())
585 QString ext = bidsPath.
extension().toLower();
586 if (ext ==
".vhdr" || ext ==
".ahdr")
587 return copyBrainVisionFiles(sourcePath, bidsPath, overwrite);
589 return copyFile(sourcePath, bidsPath.
filePath(), overwrite);
600,
events(std::move(other.events))
604,
reader(std::move(other.reader))
611, m_bIsValid(other.m_bIsValid)
613 other.m_bIsValid =
false;
620 if (
this != &other) {
622 events = std::move(other.events);
626 reader = std::move(other.reader);
633 m_bIsValid = other.m_bIsValid;
634 other.m_bIsValid =
false;
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>();
684 if (bidsPath.
root().isEmpty()) {
685 qWarning() <<
"[BidsRawData::read] BIDSPath root is not set";
688 if (bidsPath.
subject().isEmpty()) {
689 qWarning() <<
"[BidsRawData::read] BIDSPath subject is not set";
693 qWarning() <<
"[BidsRawData::read] BIDSPath extension is not set";
700 QString rawFilePath = bidsPath.
filePath();
702 if (!QFileInfo::exists(rawFilePath)) {
704 if (ext.toLower() ==
".edf") {
707 if (QFileInfo::exists(altPath.
filePath()))
712 if (!QFileInfo::exists(rawFilePath)) {
713 qWarning() <<
"[BidsRawData::read] Raw data file not found:" << rawFilePath;
722 qWarning() <<
"[BidsRawData::read] Unsupported file extension:" << bidsPath.
extension();
726 if (!result.
reader->open(rawFilePath)) {
727 qWarning() <<
"[BidsRawData::read] Failed to open raw file:" << rawFilePath;
734 result.
raw = result.
reader->toFiffRawData();
740 if (QFileInfo::exists(channelsPath.
filePath())) {
742 applyChannelsTsv(result.
raw.
info, channels);
749 if (QFileInfo::exists(coordsysPath.
filePath()))
756 if (QFileInfo::exists(electrodesPath.
filePath())) {
766 if (QFileInfo::exists(eventsPath.
filePath())) {
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);
777 for (
const auto& ev : result.
events) {
778 if (!ev.trialType.isEmpty() && ev.trialType !=
"n/a")
779 result.
eventIdMap.insert(ev.trialType, ev.value);
787 if (QFileInfo::exists(sidecarPath.
filePath()))
793 result.m_bIsValid =
true;
802 const QString& sourcePath,
810 if (bidsPath.
root().isEmpty()) {
811 qWarning() <<
"[BidsRawData::write] BIDSPath root is not set";
814 if (bidsPath.
subject().isEmpty()) {
815 qWarning() <<
"[BidsRawData::write] BIDSPath subject is not set";
818 if (bidsPath.
task().isEmpty()) {
819 qWarning() <<
"[BidsRawData::write] BIDSPath task is not set";
822 if (bidsPath.
datatype().isEmpty()) {
823 qWarning() <<
"[BidsRawData::write] BIDSPath datatype is not set";
826 if (
raw.info.isEmpty()) {
827 qWarning() <<
"[BidsRawData::write] FiffRawData info is empty";
835 qWarning() <<
"[BidsRawData::write] Failed to create directory:" << bidsPath.
directory();
842 if (options.
copyData && !sourcePath.isEmpty()) {
843 if (!copyRawDataFile(sourcePath, bidsPath, options.
overwrite)) {
844 qWarning() <<
"[BidsRawData::write] Failed to copy raw data file";
855 qWarning() <<
"[BidsRawData::write] channels.tsv already exists:" << channelsPath.
filePath();
859 QList<BidsChannel> channelRecords = buildChannelRecords(
raw.info);
861 qWarning() <<
"[BidsRawData::write] Failed to write channels.tsv";
870 QList<BidsElectrode> electrodeRecords = buildElectrodeRecords(
raw.info);
872 if (!electrodeRecords.isEmpty()) {
876 qWarning() <<
"[BidsRawData::write] Failed to write electrodes.tsv";
883 if (cs.
system.isEmpty()) {
884 if (
raw.info.dig.isEmpty()) {
885 cs.
system = QStringLiteral(
"Other");
886 cs.
units = QStringLiteral(
"n/a");
888 int coordFrame =
raw.info.dig.first().coord_frame;
890 cs.
system = frameMap.contains(coordFrame)
891 ? frameMap[coordFrame]
892 : QStringLiteral(
"Other");
893 cs.
units = QStringLiteral(
"m");
897 cs.
description = QStringLiteral(
"Coordinate system derived from recording data");
900 qWarning() <<
"[BidsRawData::write] Failed to write coordsystem.json";
911 qWarning() <<
"[BidsRawData::write] events.tsv already exists:" << eventsPath.
filePath();
915 QList<BidsEvent> eventsToWrite =
events;
918 QMap<int, QString> valueToType;
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"));
927 qWarning() <<
"[BidsRawData::write] Failed to write events.tsv";
936 qWarning() <<
"[BidsRawData::write] Sidecar JSON already exists:" << sidecarPath.
filePath();
940 QJsonObject sidecarJson = buildIeegSidecarJson(*
this, bidsPath);
941 if (!writeJsonFile(sidecarPath.
filePath(), sidecarJson)) {
942 qWarning() <<
"[BidsRawData::write] Failed to write sidecar JSON";
951 QString descPath = bidsPath.
root() + QDir::separator() + QStringLiteral(
"dataset_description.json");
953 if (!QFileInfo::exists(descPath)) {
956 ? QStringLiteral(
"[Unspecified]")
962 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.
#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
Continuous FIFF raw recording: FiffInfo plus a random-access directory of FIFF_DATA_BUFFER tags.