v2.0.0
Loading...
Searching...
No Matches
ieeg_implantation.cpp
Go to the documentation of this file.
1//=============================================================================================================
12
13//=============================================================================================================
14// INCLUDES
15//=============================================================================================================
16
17#include "ieeg_implantation.h"
18
20#include <bids/bids_electrode.h>
21
22//=============================================================================================================
23// QT INCLUDES
24//=============================================================================================================
25
26#include <QDir>
27#include <QFileInfo>
28#include <QHash>
29#include <QRegularExpression>
30
31//=============================================================================================================
32// STL INCLUDES
33//=============================================================================================================
34
35#include <algorithm>
36#include <limits>
37
38//=============================================================================================================
39// USED NAMESPACES
40//=============================================================================================================
41
42using namespace IEEGLIB;
43
44namespace
45{
46
47// "LA10" -> ("LA", 10); "A'2" -> ("A'", 2); a name without a trailing number has no number
48QRegularExpressionMatch splitName(const QString& name)
49{
50 static const QRegularExpression trailingNumber(QStringLiteral("^(.*?)[\\s_-]*(\\d+)$"));
51 return trailingNumber.match(name);
52}
53
54void setError(QString* error, const QString& message)
55{
56 if (error)
57 *error = message;
58}
59
60} // namespace
61
62//=============================================================================================================
63// DEFINE MEMBER METHODS
64//=============================================================================================================
65
66IeegImplantation::IeegImplantation(const QVector<IeegContact>& contacts, const QString& coordinateSystem)
67: m_contacts(contacts)
68, m_coordinateSystem(coordinateSystem)
69{
70 QHash<QString, int> leadIndex;
71 QVector<QVector<QPair<int, int>>> members; // per lead: (number along the lead, contact index)
72 for (int i = 0; i < m_contacts.size(); ++i) {
73 IeegContact& contact = m_contacts[i];
74 const QRegularExpressionMatch m = splitName(contact.name);
75 if (contact.lead.isEmpty())
76 contact.lead = (m.hasMatch() && !m.captured(1).isEmpty()) ? m.captured(1) : contact.name;
77 if (!leadIndex.contains(contact.lead)) {
78 leadIndex.insert(contact.lead, int(m_leads.size()));
79 IeegLead lead;
80 lead.name = contact.lead;
81 m_leads.append(lead);
82 members.append(QVector<QPair<int, int>>());
83 }
84 members[leadIndex.value(contact.lead)].append({m.hasMatch() ? m.captured(2).toInt() : 0, i});
85 }
86
87 for (int l = 0; l < m_leads.size(); ++l) {
88 QVector<QPair<int, int>>& lead = members[l];
89 std::stable_sort(lead.begin(), lead.end(), [](const auto& a, const auto& b) { return a.first < b.first; });
90 float spacing = std::numeric_limits<float>::max();
91 for (int k = 0; k < lead.size(); ++k) {
92 m_leads[l].contacts.append(lead[k].second);
93 if (k > 0)
94 spacing = std::min(spacing, (m_contacts[lead[k].second].position - m_contacts[lead[k - 1].second].position).norm());
95 }
96 m_leads[l].type = spacing < kMaxDepthSpacing ? IeegLeadType::Depth : IeegLeadType::Strip;
97 }
98}
99
100//=============================================================================================================
101
102IeegImplantation IeegImplantation::readBids(const QString& electrodesTsv, QString* error)
103{
104 const QFileInfo info(electrodesTsv);
105 // The coordsystem sidecar sharing the longest name prefix, so sessions are never mixed
106 QString coordPath;
107 int bestPrefix = -1;
108 const QStringList candidates = info.dir().entryList({QStringLiteral("*_coordsystem.json")}, QDir::Files);
109 for (const QString& candidate : candidates) {
110 int prefix = 0;
111 while (prefix < candidate.size() && prefix < info.fileName().size() && candidate[prefix] == info.fileName()[prefix])
112 ++prefix;
113 if (prefix > bestPrefix) {
114 bestPrefix = prefix;
115 coordPath = info.dir().filePath(candidate);
116 }
117 }
118 if (coordPath.isEmpty()) {
119 setError(error, QStringLiteral("no *_coordsystem.json next to %1, so the units are unknown").arg(electrodesTsv));
120 return {};
121 }
123 const float scale = coordSystem.units == QLatin1String("m") ? 1.0f
124 : coordSystem.units == QLatin1String("cm") ? 0.01f
125 : coordSystem.units == QLatin1String("mm") ? 0.001f
126 : 0.0f;
127 if (scale == 0.0f) {
128 setError(error, QStringLiteral("unsupported iEEGCoordinateUnits \"%1\" in %2").arg(coordSystem.units, coordPath));
129 return {};
130 }
131
132 QVector<IeegContact> contacts;
133 QHash<QString, QString> declaredTypes;
134 QHash<QString, QString> declaredDimensions;
135 for (const BIDSLIB::BidsElectrode& e : BIDSLIB::BidsElectrode::readTsv(electrodesTsv)) {
136 bool okX = false, okY = false, okZ = false;
137 const Eigen::Vector3f position(e.x.toFloat(&okX), e.y.toFloat(&okY), e.z.toFloat(&okZ));
138 if (!okX || !okY || !okZ)
139 continue;
140 IeegContact contact;
141 contact.name = e.name;
142 contact.position = position * scale;
143 contact.lead = e.group;
144 contacts.append(contact);
145 // Declarations are per lead; contacts without a group take their lead from the name later
146 const QString key = e.group.isEmpty() ? e.name : e.group;
147 if (!e.type.isEmpty())
148 declaredTypes.insert(key, e.type.toLower());
149 if (!e.dimension.isEmpty())
150 declaredDimensions.insert(key, e.dimension);
151 }
152 if (contacts.isEmpty()) {
153 setError(error, QStringLiteral("no contacts with coordinates in %1").arg(electrodesTsv));
154 return {};
155 }
156
157 IeegImplantation implantation(contacts, coordSystem.system);
158 static const QRegularExpression dimension(QStringLiteral("^\\[?\\s*(\\d+)\\s*x\\s*(\\d+)\\s*\\]?$"));
159 for (int l = 0; l < implantation.m_leads.size(); ++l) {
160 const IeegLead lead = implantation.m_leads[l];
161 // A declaration on any contact of the lead applies to the lead
162 QString type = declaredTypes.value(lead.name);
163 QString shape = declaredDimensions.value(lead.name);
164 for (const int c : std::as_const(lead.contacts)) {
165 const QString& name = implantation.m_contacts[c].name;
166 if (type.isEmpty())
167 type = declaredTypes.value(name);
168 if (shape.isEmpty())
169 shape = declaredDimensions.value(name);
170 }
171 const QRegularExpressionMatch m = dimension.match(shape);
172 const int rows = m.hasMatch() ? m.captured(1).toInt() : 0;
173 const int cols = m.hasMatch() ? m.captured(2).toInt() : 0;
174 IeegLeadType leadType = (rows > 1 && cols > 1 && qint64(rows) * cols == lead.contacts.size()) ? IeegLeadType::Grid : lead.type;
175 if (type.contains(QLatin1String("depth")) || type.contains(QLatin1String("seeg")))
176 leadType = IeegLeadType::Depth;
177 else if (type.contains(QLatin1String("strip")))
178 leadType = IeegLeadType::Strip;
179 else if (type.contains(QLatin1String("grid")))
180 leadType = IeegLeadType::Grid;
181 implantation.setLeadType(lead.name, leadType, rows, cols);
182 }
183 return implantation;
184}
185
186//=============================================================================================================
187
188bool IeegImplantation::setLeadType(const QString& lead, IeegLeadType type, int rows, int cols)
189{
190 for (IeegLead& l : m_leads) {
191 if (l.name != lead)
192 continue;
193 l.type = type;
194 const bool shapeFits = rows > 0 && cols > 0 && qint64(rows) * cols == l.contacts.size();
195 l.rows = shapeFits ? rows : 0;
196 l.cols = shapeFits ? cols : 0;
197 return true;
198 }
199 return false;
200}
201
202//=============================================================================================================
203
204int IeegImplantation::indexOf(const QString& name) const
205{
206 for (int i = 0; i < m_contacts.size(); ++i) {
207 if (m_contacts[i].name == name)
208 return i;
209 }
210 return -1;
211}
212
213//=============================================================================================================
214
216{
217 return m_coordinateSystem.compare(QLatin1String("ACPC"), Qt::CaseInsensitive) == 0 || m_coordinateSystem.compare(QLatin1String("ScanRAS"), Qt::CaseInsensitive) == 0;
218}
219
220//=============================================================================================================
221
222IeegImplantation IeegImplantation::transformed(const Eigen::Matrix4f& trans, const QString& frame) const
223{
224 IeegImplantation result = *this;
225 for (IeegContact& contact : result.m_contacts)
226 contact.position = trans.topLeftCorner<3, 3>() * contact.position + trans.topRightCorner<3, 1>();
227 result.m_coordinateSystem = frame;
228 return result;
229}
230
231//=============================================================================================================
232
233Eigen::MatrixX3f IeegImplantation::positions() const
234{
235 Eigen::MatrixX3f rr(m_contacts.size(), 3);
236 for (int i = 0; i < m_contacts.size(); ++i)
237 rr.row(i) = m_contacts[i].position.transpose();
238 return rr;
239}
Reader/writer for the BIDS _electrodes.tsv sidecar plus a bridge into FIFF digitizer-point space.
Reader/writer for the BIDS _coordsystem.json sidecar describing the spatial reference frame of an _el...
sEEG / ECoG implantation: contacts grouped into depth leads, strips and grids.
Intracranial EEG (sEEG, ECoG): implantation, bipolar referencing, cortical mapping.
IeegLeadType
Kind of lead a contact sits on.
Coordinate system metadata from *_coordsystem.json.
static BidsCoordinateSystem readJson(const QString &sFilePath)
Read a BIDS *_coordsystem.json file.
Electrode position record corresponding to one row in *_electrodes.tsv.
static QList< BidsElectrode > readTsv(const QString &sFilePath)
Read a BIDS *_electrodes.tsv file.
One recording contact.
Eigen::Vector3f position
One lead: a depth shaft, a strip or a grid.
QVector< int > contacts
static constexpr float kMaxDepthSpacing
static IeegImplantation readBids(const QString &electrodesTsv, QString *error=nullptr)
Eigen::MatrixX3f positions() const
bool setLeadType(const QString &lead, IeegLeadType type, int rows=0, int cols=0)
const QVector< IeegContact > & contacts() const
IeegImplantation transformed(const Eigen::Matrix4f &trans, const QString &frame) const
int indexOf(const QString &name) const