56#include <QJsonDocument>
76int bidsUnitToFiffUnit(
const QString& sUnit)
78 QString u = sUnit.toLower().trimmed();
79 if(u ==
"v" || u ==
"\u00B5v" || u ==
"uv" || u ==
"mv" || u ==
"nv")
81 if(u ==
"t" || u ==
"ft" || u ==
"pt")
87int bidsUnitToFiffUnitMul(
const QString& sUnit)
89 QString u = sUnit.toLower().trimmed();
90 if(u ==
"\u00B5v" || u ==
"uv" || u ==
"\u00B5s" || u ==
"us")
104void applyChannelsTsv(
FiffInfo& info,
const QList<BidsChannel>& channels)
106 if(channels.isEmpty())
111 QMap<QString, const BidsChannel*> channelMap;
112 for(
const auto& ch : channels)
113 channelMap[ch.name] = &ch;
117 for(
int i = 0; i < info.
chs.size(); ++i) {
119 auto it = channelMap.find(fiffCh.
ch_name);
120 if(it == channelMap.end())
124 QString typeUpper = rec->
type.toUpper();
125 if(bidsToFiff.contains(typeUpper))
126 fiffCh.
kind = bidsToFiff[typeUpper];
128 if(!rec->
units.isEmpty() && rec->
units !=
"n/a") {
129 fiffCh.
unit = bidsUnitToFiffUnit(rec->
units);
133 if(rec->
status.toLower() ==
"bad")
139void applyElectrodePositions(
FiffInfo& info,
140 const QList<BidsElectrode>& electrodes,
141 const QString& coordSystemName,
142 const QString& coordUnits)
144 if(electrodes.isEmpty())
149 if(coordMap.contains(coordSystemName))
150 coordFrame = coordMap[coordSystemName];
152 float scaleFactor = 1.0f;
153 QString units = coordUnits.toLower();
155 scaleFactor = 0.001f;
156 else if(units ==
"cm")
159 QSet<QString> chNames;
160 for(
const auto& ch : info.
chs)
161 chNames.insert(ch.ch_name);
165 for(
const auto& elec : electrodes) {
166 if(elec.x ==
"n/a" || elec.y ==
"n/a" || elec.z ==
"n/a")
172 dp.
r[0] = elec.x.toFloat() * scaleFactor;
173 dp.
r[1] = elec.y.toFloat() * scaleFactor;
174 dp.
r[2] = elec.z.toFloat() * scaleFactor;
182QJsonObject readJsonFile(
const QString& sFilePath)
184 QFile file(sFilePath);
185 if(!file.open(QIODevice::ReadOnly | QIODevice::Text))
187 QJsonParseError error;
188 QJsonDocument doc = QJsonDocument::fromJson(file.readAll(), &error);
190 if(error.error != QJsonParseError::NoError)
196bool writeJsonFile(
const QString& sFilePath,
const QJsonObject& json)
198 QFile file(sFilePath);
199 if(!file.open(QIODevice::WriteOnly | QIODevice::Text))
201 file.write(QJsonDocument(json).toJson(QJsonDocument::Indented));
207void readSidecarJson(
const QString& sFilePath,
211 QJsonObject json = readJsonFile(sFilePath);
216 double plf = json.value(QStringLiteral(
"PowerLineFrequency")).toDouble();
218 info.
linefreq =
static_cast<float>(plf);
220 double sf = json.value(QStringLiteral(
"SamplingFrequency")).toDouble();
221 if(sf > 0.0 && std::abs(info.
sfreq -
static_cast<float>(sf)) > 0.5f)
222 qWarning() <<
"[BidsRawData::read] Sampling frequency mismatch: raw ="
223 << info.
sfreq <<
"sidecar =" << sf;
226 data.
ieegReference = json.value(QStringLiteral(
"iEEGReference")).toString();
227 data.
taskDescription = json.value(QStringLiteral(
"TaskDescription")).toString();
228 data.
manufacturer = json.value(QStringLiteral(
"Manufacturer")).toString();
230 data.
softwareVersions = json.value(QStringLiteral(
"SoftwareVersions")).toString();
231 data.
recordingType = json.value(QStringLiteral(
"RecordingType")).toString();
239QString fiffUnitToBidsString(
int unit,
int unitMul)
246 default:
return QStringLiteral(
"V");
253 default:
return QStringLiteral(
"T");
256 return QStringLiteral(
"n/a");
260QString channelTypeDescription(
int kind)
263 case FIFFV_EEG_CH:
return QStringLiteral(
"ElectroEncephaloGram");
264 case FIFFV_ECOG_CH:
return QStringLiteral(
"Electrocorticography");
265 case FIFFV_SEEG_CH:
return QStringLiteral(
"StereoElectroEncephaloGram");
266 case FIFFV_DBS_CH:
return QStringLiteral(
"DeepBrainStimulation");
267 case FIFFV_MEG_CH:
return QStringLiteral(
"MagnetoEncephaloGram");
269 case FIFFV_EOG_CH:
return QStringLiteral(
"ElectroOculoGram");
270 case FIFFV_ECG_CH:
return QStringLiteral(
"ElectroCardioGram");
271 case FIFFV_EMG_CH:
return QStringLiteral(
"ElectroMyoGram");
274 default:
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) {
403 json[QStringLiteral(
"ECOGChannelCount")] = ecog;
404 json[QStringLiteral(
"SEEGChannelCount")] = seeg;
405 if(dbs > 0) json[QStringLiteral(
"DBSChannelCount")] = dbs;
406 if(eeg > 0) json[QStringLiteral(
"EEGChannelCount")] = eeg;
407 if(eog > 0) json[QStringLiteral(
"EOGChannelCount")] = eog;
408 if(ecg > 0) json[QStringLiteral(
"ECGChannelCount")] = ecg;
409 if(emg > 0) json[QStringLiteral(
"EMGChannelCount")] = emg;
410 if(misc > 0) json[QStringLiteral(
"MiscChannelCount")] = misc;
411 if(trig > 0) json[QStringLiteral(
"TriggerChannelCount")] = trig;
417 json[QStringLiteral(
"RecordingType")] = QStringLiteral(
"continuous");
421 /
static_cast<double>(info.
sfreq);
422 json[QStringLiteral(
"RecordingDuration")] = dur;
429 json[QStringLiteral(
"Manufacturer")] = data.
manufacturer;
439bool copyFile(
const QString& src,
const QString& dst,
bool overwrite)
441 if(!QFileInfo::exists(src)) {
442 qWarning() <<
"[BidsRawData::write] Source file does not exist:" << src;
445 if(QFileInfo::exists(dst)) {
447 qWarning() <<
"[BidsRawData::write] Target file already exists:" << dst;
452 return QFile::copy(src, dst);
456bool copyBrainVisionFiles(
const QString& srcVhdr,
const BIDSPath& bidsPath,
bool overwrite)
458 QFileInfo srcInfo(srcVhdr);
459 QString srcDir = srcInfo.absolutePath();
461 QFile vhdrFile(srcVhdr);
462 if(!vhdrFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
463 qWarning() <<
"[BidsRawData::write] Cannot open .vhdr file:" << srcVhdr;
467 QString dataFileName;
468 QString markerFileName;
469 QByteArray vhdrContent = vhdrFile.readAll();
472 for(
const auto& line : vhdrContent.split(
'\n')) {
473 QString sLine = QString::fromUtf8(line).trimmed();
474 if(sLine.startsWith(
"DataFile=", Qt::CaseInsensitive))
475 dataFileName = sLine.mid(9).trimmed();
476 else if(sLine.startsWith(
"MarkerFile=", Qt::CaseInsensitive))
477 markerFileName = sLine.mid(11).trimmed();
481 QString dstBase = bidsPath.
basename();
482 dstBase = dstBase.left(dstBase.lastIndexOf(
'.'));
484 QString dstVhdr = bidsPath.
filePath();
485 QString newDataFile = dstBase + QStringLiteral(
".eeg");
486 QString newMarkerFile = dstBase + QStringLiteral(
".vmrk");
488 QString vhdrStr = QString::fromUtf8(vhdrContent);
489 if(!dataFileName.isEmpty())
490 vhdrStr.replace(
"DataFile=" + dataFileName,
491 "DataFile=" + QFileInfo(newDataFile).fileName());
492 if(!markerFileName.isEmpty())
493 vhdrStr.replace(
"MarkerFile=" + markerFileName,
494 "MarkerFile=" + QFileInfo(newMarkerFile).fileName());
496 if(QFileInfo::exists(dstVhdr) && !overwrite) {
497 qWarning() <<
"[BidsRawData::write] Target file already exists:" << dstVhdr;
500 if(QFileInfo::exists(dstVhdr))
501 QFile::remove(dstVhdr);
503 QFile dstVhdrFile(dstVhdr);
504 if(!dstVhdrFile.open(QIODevice::WriteOnly | QIODevice::Text)) {
505 qWarning() <<
"[BidsRawData::write] Cannot write .vhdr file:" << dstVhdr;
508 dstVhdrFile.write(vhdrStr.toUtf8());
511 if(!dataFileName.isEmpty()) {
512 QString srcData = QDir(srcDir).absoluteFilePath(dataFileName);
513 QString dstData = dstDir + QFileInfo(newDataFile).fileName();
514 if(!copyFile(srcData, dstData, overwrite)) {
515 qWarning() <<
"[BidsRawData::write] Failed to copy data file:" << srcData;
520 if(!markerFileName.isEmpty()) {
521 QString srcMarker = QDir(srcDir).absoluteFilePath(markerFileName);
522 QString dstMarker = dstDir + QFileInfo(newMarkerFile).fileName();
524 if(QFileInfo::exists(srcMarker)) {
525 QFile markerFile(srcMarker);
526 if(markerFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
527 QString markerContent = QString::fromUtf8(markerFile.readAll());
530 markerContent.replace(
"DataFile=" + dataFileName,
531 "DataFile=" + QFileInfo(newDataFile).fileName());
533 if(QFileInfo::exists(dstMarker)) {
535 qWarning() <<
"[BidsRawData::write] Target marker file already exists:" << dstMarker;
538 QFile::remove(dstMarker);
541 QFile dstMarkerFile(dstMarker);
542 if(dstMarkerFile.open(QIODevice::WriteOnly | QIODevice::Text)) {
543 dstMarkerFile.write(markerContent.toUtf8());
544 dstMarkerFile.close();
554bool copyRawDataFile(
const QString& sourcePath,
const BIDSPath& bidsPath,
bool overwrite)
556 if(sourcePath.isEmpty())
559 QString ext = bidsPath.
extension().toLower();
560 if(ext ==
".vhdr" || ext ==
".ahdr")
561 return copyBrainVisionFiles(sourcePath, bidsPath, overwrite);
563 return copyFile(sourcePath, bidsPath.
filePath(), overwrite);
574 ,
events(std::move(other.events))
578 ,
reader(std::move(other.reader))
585 , m_bIsValid(other.m_bIsValid)
587 other.m_bIsValid =
false;
596 events = std::move(other.events);
600 reader = std::move(other.reader);
607 m_bIsValid = other.m_bIsValid;
608 other.m_bIsValid =
false;
639 QString ext = sExtension.toLower();
640 if(ext ==
".vhdr" || ext ==
".ahdr")
641 return std::make_unique<BrainVisionReader>();
642 if(ext ==
".edf" || ext ==
".bdf")
643 return std::make_unique<EDFReader>();
658 if(bidsPath.
root().isEmpty()) {
659 qWarning() <<
"[BidsRawData::read] BIDSPath root is not set";
662 if(bidsPath.
subject().isEmpty()) {
663 qWarning() <<
"[BidsRawData::read] BIDSPath subject is not set";
667 qWarning() <<
"[BidsRawData::read] BIDSPath extension is not set";
674 QString rawFilePath = bidsPath.
filePath();
676 if(!QFileInfo::exists(rawFilePath)) {
678 if(ext.toLower() ==
".edf") {
681 if(QFileInfo::exists(altPath.
filePath()))
686 if(!QFileInfo::exists(rawFilePath)) {
687 qWarning() <<
"[BidsRawData::read] Raw data file not found:" << rawFilePath;
696 qWarning() <<
"[BidsRawData::read] Unsupported file extension:" << bidsPath.
extension();
700 if(!result.
reader->open(rawFilePath)) {
701 qWarning() <<
"[BidsRawData::read] Failed to open raw file:" << rawFilePath;
708 result.
raw = result.
reader->toFiffRawData();
714 if(QFileInfo::exists(channelsPath.
filePath())) {
716 applyChannelsTsv(result.
raw.
info, channels);
723 if(QFileInfo::exists(coordsysPath.
filePath()))
730 if(QFileInfo::exists(electrodesPath.
filePath())) {
740 if(QFileInfo::exists(eventsPath.
filePath())) {
745 for(
auto& ev : result.
events) {
746 if(ev.sample == 0 && ev.onset > 0.0f && sfreq > 0)
747 ev.sample =
static_cast<int>(ev.onset * sfreq);
751 for(
const auto& ev : result.
events) {
752 if(!ev.trialType.isEmpty() && ev.trialType !=
"n/a")
753 result.
eventIdMap.insert(ev.trialType, ev.value);
761 if(QFileInfo::exists(sidecarPath.
filePath()))
767 result.m_bIsValid =
true;
776 const QString& sourcePath,
784 if(bidsPath.
root().isEmpty()) {
785 qWarning() <<
"[BidsRawData::write] BIDSPath root is not set";
788 if(bidsPath.
subject().isEmpty()) {
789 qWarning() <<
"[BidsRawData::write] BIDSPath subject is not set";
792 if(bidsPath.
task().isEmpty()) {
793 qWarning() <<
"[BidsRawData::write] BIDSPath task is not set";
797 qWarning() <<
"[BidsRawData::write] BIDSPath datatype is not set";
800 if(
raw.info.isEmpty()) {
801 qWarning() <<
"[BidsRawData::write] FiffRawData info is empty";
809 qWarning() <<
"[BidsRawData::write] Failed to create directory:" << bidsPath.
directory();
816 if(options.
copyData && !sourcePath.isEmpty()) {
817 if(!copyRawDataFile(sourcePath, bidsPath, options.
overwrite)) {
818 qWarning() <<
"[BidsRawData::write] Failed to copy raw data file";
829 qWarning() <<
"[BidsRawData::write] channels.tsv already exists:" << channelsPath.
filePath();
833 QList<BidsChannel> channelRecords = buildChannelRecords(
raw.info);
835 qWarning() <<
"[BidsRawData::write] Failed to write channels.tsv";
844 QList<BidsElectrode> electrodeRecords = buildElectrodeRecords(
raw.info);
846 if(!electrodeRecords.isEmpty()) {
850 qWarning() <<
"[BidsRawData::write] Failed to write electrodes.tsv";
858 if(
raw.info.dig.isEmpty()) {
859 cs.
system = QStringLiteral(
"Other");
860 cs.
units = QStringLiteral(
"n/a");
862 int coordFrame =
raw.info.dig.first().coord_frame;
864 cs.
system = frameMap.contains(coordFrame)
865 ? frameMap[coordFrame]
866 : QStringLiteral(
"Other");
867 cs.
units = QStringLiteral(
"m");
871 cs.
description = QStringLiteral(
"Coordinate system derived from recording data");
874 qWarning() <<
"[BidsRawData::write] Failed to write coordsystem.json";
885 qWarning() <<
"[BidsRawData::write] events.tsv already exists:" << eventsPath.
filePath();
889 QList<BidsEvent> eventsToWrite =
events;
892 QMap<int, QString> valueToType;
894 valueToType[it.value()] = it.key();
895 for(
auto& ev : eventsToWrite) {
896 if(ev.trialType.isEmpty() || ev.trialType ==
"n/a")
897 ev.trialType = valueToType.value(ev.value, QStringLiteral(
"n/a"));
901 qWarning() <<
"[BidsRawData::write] Failed to write events.tsv";
910 qWarning() <<
"[BidsRawData::write] Sidecar JSON already exists:" << sidecarPath.
filePath();
914 QJsonObject sidecarJson = buildIeegSidecarJson(*
this, bidsPath);
915 if(!writeJsonFile(sidecarPath.
filePath(), sidecarJson)) {
916 qWarning() <<
"[BidsRawData::write] Failed to write sidecar JSON";
925 QString descPath = bidsPath.
root() + QDir::separator()
926 + QStringLiteral(
"dataset_description.json");
928 if(!QFileInfo::exists(descPath)) {
931 ? QStringLiteral(
"[Unspecified]")
937 qWarning() <<
"[BidsRawData::write] Failed to write dataset_description.json";
#define FIFFV_POINT_EXTRA
#define FIFFV_COORD_UNKNOWN
Contains the declaration of the BrainVisionReader class.
Contains the declaration of the EDFReader class. Refactored from the mne_edf2fiff tool into a reusabl...
BIDS constants, channel type mappings, and allowed values.
BidsRawData class declaration — central container for a BIDS raw dataset with integrated read/write c...
BidsDatasetDescription struct — dataset_description.json I/O.
BidsChannel struct — channel metadata from *_channels.tsv.
BIDS dataset reading, writing, path construction, and sidecar metadata handling for iEEG/EEG/MEG.
QMap< int, QString > fiffKindToBidsType()
QMap< QString, int > bidsCoordToFiffFrame()
QMap< QString, int > bidsTypeToFiffKind()
QMap< int, QString > fiffFrameToBidsCoord()
FIFF file I/O and data structures (raw, epochs, evoked, covariance, forward).
Channel metadata record corresponding to one row in *_channels.tsv.
static QList< BidsChannel > readTsv(const QString &sFilePath)
Read a BIDS *_channels.tsv file.
static bool writeTsv(const QString &sFilePath, const QList< BidsChannel > &channels)
Write a BIDS *_channels.tsv file.
Coordinate system metadata from *_coordsystem.json.
static bool writeJson(const QString &sFilePath, const BidsCoordinateSystem &cs)
Write a BIDS *_coordsystem.json file.
static BidsCoordinateSystem readJson(const QString &sFilePath)
Read a BIDS *_coordsystem.json file.
Dataset-level metadata from dataset_description.json.
static bool write(const QString &sFilePath, const BidsDatasetDescription &desc)
Write a dataset_description.json file.
Electrode position record corresponding to one row in *_electrodes.tsv.
static bool writeTsv(const QString &sFilePath, const QList< BidsElectrode > &electrodes)
Write a BIDS *_electrodes.tsv file.
static QList< BidsElectrode > readTsv(const QString &sFilePath)
Read a BIDS *_electrodes.tsv file.
static QList< BidsEvent > readTsv(const QString &sFilePath)
Read a BIDS *_events.tsv file.
static bool writeTsv(const QString &sFilePath, const QList< BidsEvent > &events)
Write a BIDS *_events.tsv file.
BIDS-compliant path and filename construction.
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
Digitization point description.
FIFF measurement file information.
QList< FiffDigPoint > dig
FIFF raw measurement data.