40#include <QRegularExpression>
41#include <QXmlStreamReader>
77: m_qListDigPoint(p_FiffDigPointSet.m_qListDigPoint)
84: m_qListDigPoint(pointList)
99 throw std::runtime_error(
"Could not read the FiffDigPointSet");
102 qInfo(
"[FiffDigPointSet::FiffDigPointSet] %i digitizer Points read from file.", this->
size());
118 bool open_here =
false;
120 if (!p_pStream->device()->isOpen()) {
121 QString t_sFileName = p_pStream->streamName();
123 if (!p_pStream->open())
126 qInfo(
"Opening header data %s...\n", t_sFileName.toUtf8().constData());
142 QList<FiffDirNode::SPtr> isotrak = p_pStream->dirtree()->dir_tree_find(
FIFFB_ISOTRAK);
147 if (isotrak.size() == 1) {
148 for (k = 0; k < isotrak[0]->nent(); ++k) {
149 kind = isotrak[0]->dir[k]->kind;
150 pos = isotrak[0]->dir[k]->pos;
152 p_pStream->read_tag(t_pTag, pos);
153 p_Dig.m_qListDigPoint.append(t_pTag->toDigPoint());
156 p_pStream->read_tag(t_pTag, pos);
157 coord_frame = *t_pTag->toInt();
161 for (k = 0; k < p_Dig.
size(); ++k) {
162 p_Dig[k].coord_frame = coord_frame;
180double unitScale(
const QString& unit)
182 if (unit == QLatin1String(
"m"))
184 if (unit == QLatin1String(
"cm"))
186 if (unit == QLatin1String(
"mm"))
188 qWarning() <<
"[FiffDigPointSet] unknown unit" << unit <<
"(use m, cm or mm)";
193bool parseNumbers(QStringView text, QList<double>& values)
195 static const QRegularExpression whitespace(QStringLiteral(
"\\s+"));
196 for (
const QStringView token : text.split(whitespace, Qt::SkipEmptyParts)) {
198 values.append(token.toDouble(&ok));
206QList<Vector3d> readIsotrakElp(
const QString& text)
208 static const QRegularExpression coordinates(
209 QStringLiteral(
"(-?\\d+\\.?\\d*e?-?\\d*)\\s+(-?\\d+\\.?\\d*e?-?\\d*)\\s+(-?\\d+\\.?\\d*e?-?\\d*)\\s*$"),
210 QRegularExpression::MultilineOption);
211 QList<Vector3d> points;
212 for (QRegularExpressionMatchIterator it = coordinates.globalMatch(text); it.hasNext();) {
213 const QRegularExpressionMatch match = it.next();
214 points.append(Vector3d(match.captured(1).toDouble(), match.captured(2).toDouble(), match.captured(3).toDouble()));
220QList<Vector3d> readIsotrakHsp(
const QString& text)
222 const QStringList lines = text.split(QLatin1Char(
'\n'));
224 while (line < lines.size() && !lines[line].toLower().contains(QLatin1String(
"position of fiducials")))
226 QList<Vector3d> points;
227 for (
int i = 1; i <= 3; ++i) {
228 QList<double> values;
229 if (line + i >= lines.size() || !parseNumbers(QString(lines[line + i]).remove(QLatin1String(
"%F")), values) || values.size() != 3)
231 points.append(Vector3d(values[0], values[1], values[2]));
234 const qsizetype header = line + 5;
235 if (header >= lines.size())
238 if (!parseNumbers(lines[header], shape) || shape.size() != 2 || shape[1] != 3.0)
240 QList<double> values;
241 if (!parseNumbers(lines.mid(header + 1).join(QLatin1Char(
'\n')), values) || values.size() != 3 * shape[0])
243 for (qsizetype i = 0; i + 2 < values.size(); i += 3)
244 points.append(Vector3d(values[i], values[i + 1], values[i + 2]));
251 std::optional<Vector3d> nasion, lpa, rpa;
252 QList<Vector3d> hpi, extra;
254 QList<Vector3d> chPos;
257 void addElectrode(
const QString& name,
const Vector3d& r)
259 const qsizetype index = chNames.indexOf(name);
261 qWarning() <<
"[FiffDigPointSet] duplicate electrode" << name <<
"- the last position is used";
265 chNames.append(name);
272FiffDigPointSet makeDigMontage(
const Montage& montage, QStringList* chNames)
274 QList<FiffDigPoint> points;
275 const auto append = [&points](
int kind,
int ident,
const Vector3d& r) {
280 for (
int c = 0; c < 3; ++c)
281 point.
r[c] =
static_cast<float>(r[c]);
282 points.append(point);
290 for (qsizetype i = 0; i < montage.hpi.size(); ++i)
292 for (qsizetype i = 0; i < montage.extra.size(); ++i)
296 for (
const QString& name : montage.chNames) {
298 idents.append(name.right(3).trimmed().toInt(&ok));
304 for (qsizetype i = 0; i < montage.chPos.size(); ++i)
305 append(
FIFFV_POINT_EEG, idents.isEmpty() ?
static_cast<int>(i) + 1 : idents[i], montage.chPos[i]);
307 *chNames = montage.chNames;
312std::optional<QString> readText(
const QString& path,
const char* reader)
315 if (!file.open(QIODevice::ReadOnly)) {
316 qWarning() <<
"[FiffDigPointSet::" << reader <<
"] cannot read" << path;
319 return QString::fromUtf8(file.readAll());
323QList<QHash<QString, QString>> readXmlRecords(
const QString& path,
const QString& element)
326 QList<QHash<QString, QString>> records;
327 if (!file.open(QIODevice::ReadOnly))
329 QXmlStreamReader xml(&file);
330 while (!xml.atEnd()) {
331 if (xml.readNext() != QXmlStreamReader::StartElement || xml.name() != element)
333 QHash<QString, QString> record;
334 while (xml.readNextStartElement()) {
335 const QString key = xml.name().toString();
336 record.insert(key, xml.readElementText(QXmlStreamReader::SkipChildElements));
338 records.append(record);
340 if (xml.hasError()) {
341 qWarning() <<
"[FiffDigPointSet]" << path << xml.errorString();
348bool recordPosition(
const QHash<QString, QString>& record,
const QStringList& keys,
double scale, Vector3d& r)
350 for (
int c = 0; c < 3; ++c) {
352 r[c] = scale * record.value(keys[c]).toDouble(&ok);
365 const QString extension = QLatin1Char(
'.') + QFileInfo(path).suffix();
366 const double scale = unitScale(unit);
367 if (scale == 0.0 || !QStringList{
".hsp",
".elp",
".eeg"}.contains(extension)) {
368 qWarning() <<
"[FiffDigPointSet::readPolhemusIsotrak] not an Isotrak file:" << path;
371 const std::optional<QString> text = readText(path,
"readPolhemusIsotrak");
374 QList<Vector3d> points = extension == QLatin1String(
".elp") ? readIsotrakElp(*text) : readIsotrakHsp(*text);
375 const qsizetype count = points.size() - 3;
376 if (count < 0 || (!chNames.isEmpty() && chNames.size() != count)) {
377 qWarning() <<
"[FiffDigPointSet::readPolhemusIsotrak]" << path <<
"holds" << qMax(count, qsizetype(0)) <<
"points besides the fiducials, but"
378 << chNames.size() <<
"names were given";
381 for (Vector3d& point : points)
385 montage.nasion = points[0];
386 montage.lpa = points[1];
387 montage.rpa = points[2];
388 const QList<Vector3d> rest = points.mid(3);
389 if (!chNames.isEmpty()) {
390 for (qsizetype i = 0; i < count; ++i)
391 montage.addElectrode(chNames[i], rest[i]);
392 }
else if (extension == QLatin1String(
".elp")) {
395 montage.extra = rest;
397 dig = makeDigMontage(montage,
nullptr);
405 const double scale = unitScale(unit);
406 if (scale == 0.0 || QFileInfo(path).suffix() != QLatin1String(
"txt")) {
407 qWarning() <<
"[FiffDigPointSet::readPolhemusFastscan] not a FastSCAN file:" << path;
410 const std::optional<QString> text = readText(path,
"readPolhemusFastscan");
413 const QStringList lines = text->split(QLatin1Char(
'\n'));
416 for (qsizetype i = 0; i < lines.size() && lines[i].startsWith(QLatin1Char(
'%')); ++i)
418 if (requireHeader && !header.contains(QLatin1String(
"FastSCAN"))) {
419 qWarning() <<
"[FiffDigPointSet::readPolhemusFastscan]" << path <<
"does not contain a valid Polhemus FastSCAN header";
423 QList<double> values;
424 for (
const QString& line : lines) {
426 if (!parseNumbers(line.left(line.indexOf(QLatin1Char(
'%'))), row) || (!row.isEmpty() && row.size() != 3)) {
427 qWarning() <<
"[FiffDigPointSet::readPolhemusFastscan]" << path <<
"has a row that is not three numbers:" << line;
432 points.resize(values.size() / 3, 3);
433 for (qsizetype i = 0; i < values.size(); ++i)
434 points(i / 3, i % 3) = scale * values[i];
443 for (
const QHash<QString, QString>& electrode : readXmlRecords(path, QStringLiteral(
"CapTrakElectrode"))) {
445 if (!recordPosition(electrode, {
"X",
"Y",
"Z"}, 1e-3, r)) {
446 qWarning() <<
"[FiffDigPointSet::readCaptrak] electrode without a position in" << path;
449 const QString name = electrode.value(QStringLiteral(
"Name"));
450 if (name == QLatin1String(
"Nasion"))
452 else if (name == QLatin1String(
"LPA"))
454 else if (name == QLatin1String(
"RPA"))
457 montage.addElectrode(name, r);
459 if (!montage.nasion || !montage.lpa || !montage.rpa) {
460 qWarning() <<
"[FiffDigPointSet::readCaptrak]" << path <<
"lacks the Nasion, LPA or RPA electrode";
463 dig = makeDigMontage(montage, chNames);
472 for (
const QHash<QString, QString>& sensor : readXmlRecords(path, QStringLiteral(
"sensor"))) {
475 const int number = sensor.value(QStringLiteral(
"number")).toInt(&ok);
476 if (!ok || !recordPosition(sensor, {
"x",
"y",
"z"}, 1e-2, r)) {
477 qWarning() <<
"[FiffDigPointSet::readEgi] sensor without a number or position in" << path;
480 const QString name = sensor.value(QStringLiteral(
"name"));
481 switch (sensor.value(QStringLiteral(
"type")).toInt()) {
483 montage.addElectrode(QStringLiteral(
"EEG %1").arg(number, 3, 10, QLatin1Char(
'0')), r);
486 montage.addElectrode(QStringLiteral(
"EEG %1").arg(montage.chNames.size() + 1, 3, 10, QLatin1Char(
'0')), r);
489 if (name == QLatin1String(
"Nasion"))
491 else if (name == QLatin1String(
"Left periauricular point"))
493 else if (name == QLatin1String(
"Right periauricular point"))
497 qWarning() <<
"[FiffDigPointSet::readEgi] skipping sensor" << number <<
"of unknown type";
500 if (!montage.nasion || !montage.lpa || !montage.rpa) {
501 qWarning() <<
"[FiffDigPointSet::readEgi]" << path <<
"lacks the nasion or a periauricular point";
504 dig = makeDigMontage(montage, chNames);
512 QStringList* chNames,
513 const QString& nasion,
517 const std::optional<QString> text = readText(path,
"readLocalite");
520 const QStringList lines = text->split(QLatin1Char(
'\n'));
522 for (qsizetype i = 1; i < lines.size(); ++i) {
523 if (lines[i].trimmed().
isEmpty())
525 const QStringList fields = lines[i].split(QLatin1Char(
','));
527 bool ok = fields.size() == 5;
528 for (
int c = 0; ok && c < 3; ++c)
529 r[c] = fields[c + 2].toDouble(&ok) / 1000.0;
531 qWarning() <<
"[FiffDigPointSet::readLocalite]" << path <<
"line" << i + 1 <<
"is not '#,name,x,y,z':" << lines[i];
534 montage.addElectrode(fields[1], r);
537 const auto takeFiducial = [&montage](
const QString& name, std::optional<Vector3d>& fiducial) {
540 const qsizetype index = montage.chNames.indexOf(name);
543 fiducial = montage.chPos.takeAt(index);
544 montage.chNames.removeAt(index);
547 if (!takeFiducial(nasion, montage.nasion) || !takeFiducial(lpa, montage.lpa) || !takeFiducial(rpa, montage.rpa)) {
548 qWarning() <<
"[FiffDigPointSet::readLocalite]" << path <<
"has no point named" << nasion << lpa << rpa;
551 dig = makeDigMontage(montage, chNames);
559 static const QRegularExpression whitespace(QStringLiteral(
"\\s+"));
560 const std::optional<QString> text = readText(path,
"readNeuroscanDat");
563 const QStringList lines = text->split(QLatin1Char(
'\n'));
565 for (qsizetype i = 0; i < lines.size(); ++i) {
566 const QStringList items = lines[i].split(whitespace, Qt::SkipEmptyParts);
570 bool ok = items.size() == 5;
571 for (
int c = 0; ok && c < 3; ++c)
572 r[c] = items[c + 2].toDouble(&ok);
574 qWarning() <<
"[FiffDigPointSet::readNeuroscanDat]" << path <<
"line" << i <<
"is not 'name number x y z':" << lines[i];
578 const QString& number = items[1];
579 if (number == QLatin1String(
"78"))
581 else if (number == QLatin1String(
"76"))
583 else if (number == QLatin1String(
"82"))
585 else if (number != QLatin1String(
"67"))
586 montage.addElectrode(items[0], r);
588 dig = makeDigMontage(montage, chNames);
600 qInfo(
"Write Digitizer Points in %s...\n", t_pStream->streamName().toUtf8().constData());
602 t_pStream->end_file();
610 if (filePath.isEmpty()) {
612 *errorMessage = QStringLiteral(
"Output path is empty.");
616 const QFileInfo info(filePath);
617 if (!info.dir().exists()) {
619 *errorMessage = QStringLiteral(
"Destination directory '%1' does not exist.")
620 .arg(info.dir().absolutePath());
625 QFile file(filePath);
626 if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
628 *errorMessage = QStringLiteral(
"Cannot open '%1' for writing: %2")
629 .arg(filePath, file.errorString());
637 if (!QFileInfo::exists(filePath)) {
639 *errorMessage = QStringLiteral(
"FiffDigPointSet::write produced no file at '%1'.")
655 for (qint32 h = 0; h < m_qListDigPoint.size(); ++h) {
659 qInfo(
"\t%lld digitizer points written\n",
static_cast<long long>(m_qListDigPoint.size()));
669 if (idx >= m_qListDigPoint.length()) {
670 qWarning(
"Warning: Required DigPoint doesn't exist! Returning DigPoint '0'.");
673 return m_qListDigPoint[idx];
680 if (idx >= m_qListDigPoint.length()) {
681 qWarning(
"Warning: Required DigPoint doesn't exist! Returning DigPoint '0'.");
684 return m_qListDigPoint[idx];
693 for (
int i = 0; i < m_qListDigPoint.size(); ++i) {
694 if (includeTypes.contains(m_qListDigPoint[i].kind)) {
695 pickedSet << m_qListDigPoint[i];
706 this->m_qListDigPoint.append(dig);
714 this->m_qListDigPoint.append(*dig);
723 for (
int i = 0; i < m_qListDigPoint.size(); ++i) {
724 tempvec(0) = m_qListDigPoint.at(i).r[0];
725 tempvec(1) = m_qListDigPoint.at(i).r[1];
726 tempvec(2) = m_qListDigPoint.at(i).r[2];
729 tempvec = coordTrans.
invtrans * tempvec;
731 tempvec = coordTrans.
trans * tempvec;
733 m_qListDigPoint[i].r[0] = tempvec(0);
734 m_qListDigPoint[i].r[1] = tempvec(1);
735 m_qListDigPoint[i].r[2] = tempvec(2);
743 const FiffDigPoint* fid[3] = {
nullptr,
nullptr,
nullptr};
749 if (!fid[0] || !fid[1] || !fid[2] || fid[0]->coord_frame != fid[1]->coord_frame || fid[0]->coord_frame != fid[2]->coord_frame) {
750 qWarning() <<
"[FiffDigPointSet::transformToHead] LPA, nasion and RPA in one frame are required.";
760 if (p.coord_frame == frame) {
772 return m_qListDigPoint;
constexpr auto FIFFV_COORD_HEAD
constexpr auto FIFFV_POINT_EXTRA
constexpr auto FIFFV_POINT_CARDINAL
constexpr auto FIFF_MNE_COORD_FRAME
constexpr auto FIFFV_POINT_HPI
constexpr auto FIFFV_POINT_EEG
constexpr auto FIFFV_POINT_LPA
constexpr auto FIFFV_COORD_UNKNOWN
constexpr auto FIFFV_POINT_NASION
constexpr auto FIFFV_POINT_RPA
FIFF tag: the 16-byte tag header (kind, type, size, next) plus its decoded payload.
Container for the FIFF_DIG_POINT records of a measurement (a parsed FIFFB_ISOTRAK block).
constexpr auto FIFFB_ISOTRAK
constexpr auto FIFF_DIG_POINT
constexpr auto FIFFB_MEAS_INFO
constexpr auto FIFFB_MEAS
Recursive node of the parsed FIFF block tree (FIFFB_* hierarchy with directory entries and children).
Single digitization point (FIFF_DIG_POINT) with kind (cardinal/HPI/EEG/extra), identifier and 3D coor...
Primitive scalar typedefs and forward-compatible aliases backing the FIFF type system.
FIFF file I/O, in-memory data structures and high-level readers/writers.
Labelled 4x4 FIFF affine: source frame, destination frame, rotation, translation and cached inverse.
Eigen::Matrix< float, 4, 4, Eigen::DontAlign > invtrans
Eigen::MatrixX3f apply_inverse_trans(const Eigen::MatrixX3f &rr, bool do_move=true) const
static FiffCoordTrans fromCardinalPoints(int from, int to, const float *rL, const float *rN, const float *rR)
Eigen::Matrix< float, 4, 4, Eigen::DontAlign > trans
One digitizer point: kind (cardinal/HPI/EEG/extra), ident, 3D position in FIFFV_COORD_HEAD.
Collection of FiffDigPoint records as parsed from a FIFFB_ISOTRAK block.
static bool readPolhemusFastscan(const QString &path, Eigen::MatrixX3d &points, const QString &unit=QStringLiteral("mm"), bool requireHeader=true)
bool write(QIODevice &p_IODevice)
static bool readPolhemusIsotrak(const QString &path, FiffDigPointSet &dig, const QStringList &chNames=QStringList(), const QString &unit=QStringLiteral("m"))
static bool readLocalite(const QString &path, FiffDigPointSet &dig, QStringList *chNames=nullptr, const QString &nasion=QString(), const QString &lpa=QString(), const QString &rpa=QString())
const FiffDigPoint & operator[](qint32 idx) const
FiffDigPointSet & operator<<(const FiffDigPoint &dig)
static bool readEgi(const QString &path, FiffDigPointSet &dig, QStringList *chNames=nullptr)
FiffDigPointSet pickTypes(QList< int > includeTypes) const
static bool readCaptrak(const QString &path, FiffDigPointSet &dig, QStringList *chNames=nullptr)
void applyTransform(const FiffCoordTrans &coordTrans, bool bApplyInverse=false)
static bool readNeuroscanDat(const QString &path, FiffDigPointSet &dig, QStringList *chNames=nullptr)
void writeToStream(FiffStream *p_pStream)
static bool readFromStream(FiffStream::SPtr &p_Stream, FiffDigPointSet &p_Dig)
QList< FiffDigPoint > getList()
FIFF tag-stream reader/writer: wraps a QIODevice and exposes typed read_* / write_* methods for every...
fiff_long_t start_block(fiff_int_t kind)
QSharedPointer< FiffStream > SPtr
fiff_long_t write_dig_point(const FiffDigPoint &dig)
static FiffStream::SPtr start_file(QIODevice &p_IODevice)
fiff_long_t end_block(fiff_int_t kind, fiff_int_t next=FIFFV_NEXT_SEQ)
std::unique_ptr< FiffTag > UPtr