v2.0.0
Loading...
Searching...
No Matches
electrodeobject.cpp
Go to the documentation of this file.
1//=============================================================================================================
12
13//=============================================================================================================
14// INCLUDES
15//=============================================================================================================
16
17#include "electrodeobject.h"
18
21
22#include <QMatrix4x4>
23#include <QQuaternion>
24#include <QtMath>
25
26#include <array>
27#include <cstring>
28#include <limits>
29#include <map>
30#include <vector>
31
32//=============================================================================================================
33// USED NAMESPACES
34//=============================================================================================================
35
36using namespace DISP3DLIB;
37
38namespace
39{
40
41struct MeshVertex
42{
43 float x, y, z, nx, ny, nz;
44};
45
46template<typename T>
47QByteArray toBytes(const std::vector<T>& v)
48{
49 return QByteArray(reinterpret_cast<const char*>(v.data()), qsizetype(v.size() * sizeof(T)));
50}
51
52// Unit icosphere, subdivided twice (320 triangles)
53void unitSphere(QByteArray& vertices, QByteArray& indices, int& indexCount)
54{
55 const float t = (1.0f + std::sqrt(5.0f)) / 2.0f;
56 std::vector<QVector3D> v = {{-1, t, 0}, {1, t, 0}, {-1, -t, 0}, {1, -t, 0}, {0, -1, t}, {0, 1, t}, {0, -1, -t}, {0, 1, -t}, {t, 0, -1}, {t, 0, 1}, {-t, 0, -1}, {-t, 0, 1}};
57 for (auto& p : v)
58 p.normalize();
59 std::vector<uint32_t> idx = {0, 11, 5, 0, 5, 1, 0, 1, 7, 0, 7, 10, 0, 10, 11, 1, 5, 9, 5, 11, 4, 11, 10, 2, 10, 7, 6, 7, 1, 8,
60 3, 9, 4, 3, 4, 2, 3, 2, 6, 3, 6, 8, 3, 8, 9, 4, 9, 5, 2, 4, 11, 6, 2, 10, 8, 6, 7, 9, 8, 1};
61 for (int level = 0; level < 2; ++level) {
62 std::map<std::pair<uint32_t, uint32_t>, uint32_t> midpoints;
63 auto midpoint = [&](uint32_t a, uint32_t b) {
64 const auto key = std::minmax(a, b);
65 const auto it = midpoints.find(key);
66 if (it != midpoints.end())
67 return it->second;
68 v.push_back(((v[a] + v[b]) * 0.5f).normalized());
69 return midpoints[key] = uint32_t(v.size() - 1);
70 };
71 std::vector<uint32_t> next;
72 for (size_t i = 0; i < idx.size(); i += 3) {
73 const uint32_t a = idx[i], b = idx[i + 1], c = idx[i + 2];
74 const uint32_t ab = midpoint(a, b), bc = midpoint(b, c), ca = midpoint(c, a);
75 next.insert(next.end(), {a, ab, ca, b, bc, ab, c, ca, bc, ab, bc, ca});
76 }
77 idx = std::move(next);
78 }
79 std::vector<MeshVertex> mv;
80 for (const auto& p : v)
81 mv.push_back({p.x(), p.y(), p.z(), p.x(), p.y(), p.z()});
82 vertices = toBytes(mv);
83 indices = toBytes(idx);
84 indexCount = int(idx.size());
85}
86
87// Unit cylinder along +y from -0.5 to 0.5, with caps
88void unitCylinder(QByteArray& vertices, QByteArray& indices, int& indexCount)
89{
90 constexpr int kSides = 12;
91 std::vector<MeshVertex> mv;
92 std::vector<uint32_t> idx;
93 for (int i = 0; i < kSides; ++i) {
94 const float a = 2.0f * float(M_PI) * float(i) / float(kSides);
95 const float x = std::cos(a), z = std::sin(a);
96 mv.push_back({x, 0.5f, z, x, 0, z});
97 mv.push_back({x, -0.5f, z, x, 0, z});
98 }
99 for (int i = 0; i < kSides; ++i) {
100 const uint32_t top = uint32_t(2 * i), bottom = top + 1;
101 const uint32_t nextTop = uint32_t(2 * ((i + 1) % kSides)), nextBottom = nextTop + 1;
102 idx.insert(idx.end(), {top, nextTop, bottom, bottom, nextTop, nextBottom});
103 }
104 for (const float y : {0.5f, -0.5f}) {
105 const uint32_t centre = uint32_t(mv.size());
106 mv.push_back({0, y, 0, 0, y * 2.0f, 0});
107 for (int i = 0; i < kSides; ++i) {
108 const float a = 2.0f * float(M_PI) * float(i) / float(kSides);
109 mv.push_back({std::cos(a), y, std::sin(a), 0, y * 2.0f, 0});
110 }
111 for (int i = 0; i < kSides; ++i) {
112 const uint32_t cur = centre + 1 + uint32_t(i), next = centre + 1 + uint32_t((i + 1) % kSides);
113 if (y > 0)
114 idx.insert(idx.end(), {centre, next, cur});
115 else
116 idx.insert(idx.end(), {centre, cur, next});
117 }
118 }
119 vertices = toBytes(mv);
120 indices = toBytes(idx);
121 indexCount = int(idx.size());
122}
123
124} // namespace
125
126//=============================================================================================================
127// DEFINE MEMBER METHODS
128//=============================================================================================================
129
131: m_bbMin(std::numeric_limits<float>::max(),
132 std::numeric_limits<float>::max(),
133 std::numeric_limits<float>::max())
134, m_bbMax(std::numeric_limits<float>::lowest(),
135 std::numeric_limits<float>::lowest(),
136 std::numeric_limits<float>::lowest())
137{
138 unitSphere(m_sphereVertices, m_sphereIndices, m_sphereIndexCount);
139 unitCylinder(m_cylinderVertices, m_cylinderIndices, m_cylinderIndexCount);
140}
141
142//=============================================================================================================
143
145
146//=============================================================================================================
147
149{
150 m_implantation = implantation;
151 const int contactCount = int(m_implantation.contacts().size());
152 m_leadOfContact.fill(-1, contactCount);
153 const QVector<IEEGLIB::IeegLead>& leads = m_implantation.leads();
154 for (int l = 0; l < leads.size(); ++l) {
155 for (const int c : leads[l].contacts)
156 m_leadOfContact[c] = l;
157 }
158 m_contactColors.fill(QColor(), contactCount);
159 m_contactValues.clear();
160 m_selectedContact = -1;
161 m_hoveredContact = -1;
162 computeBoundingBox();
163 buildBodies();
164 buildContactInstances();
165}
166
167//=============================================================================================================
168
170{
171 return int(m_implantation.contacts().size());
172}
173
174//=============================================================================================================
175
176void ElectrodeObject::setContactValues(const QMap<QString, float>& values, const QString& colormap)
177{
178 if (values.isEmpty()) {
180 return;
181 }
182 m_contactValues = values;
183 m_colormap = colormap;
184 m_valueMin = 0.0f;
185 m_valueMax = std::numeric_limits<float>::lowest();
186 for (const float v : values) {
187 m_valueMin = std::min(m_valueMin, v);
188 m_valueMax = std::max(m_valueMax, v);
189 }
190
191 const QVector<IEEGLIB::IeegContact>& contacts = m_implantation.contacts();
192 m_contactColors.fill(QColor());
193 for (int c = 0; c < contacts.size(); ++c) {
194 const auto it = values.find(contacts[c].name);
195 if (it != values.end())
196 m_contactColors[c] = QColor::fromRgb(DISPLIB::ColorMap::valueToColor(normalized(it.value()), colormap));
197 }
198 buildContactInstances();
199}
200
201//=============================================================================================================
202
204{
205 m_contactValues.clear();
206 m_contactColors.fill(QColor());
207 buildContactInstances();
208}
209
210//=============================================================================================================
211
212float ElectrodeObject::normalized(float value) const
213{
214 return m_valueMax > m_valueMin ? qBound(0.0f, (value - m_valueMin) / (m_valueMax - m_valueMin), 1.0f) : 1.0f;
215}
216
217//=============================================================================================================
218
219QVector<uint32_t> ElectrodeObject::surfaceColors(const Eigen::MatrixX3f& vertices, double distance) const
220{
221 if (m_contactValues.isEmpty() || vertices.rows() == 0)
222 return {};
223
224 // Weights (vertices x contacts) of ECoG contacts moved onto the surface plus depth contacts in place
225 const QVector<IEEGLIB::IeegContact>& contacts = m_implantation.contacts();
226 const QVector<IEEGLIB::IeegLead>& leads = m_implantation.leads();
227 const Eigen::Index n = contacts.size();
228 Eigen::MatrixX3f subdural = Eigen::MatrixX3f::Constant(n, 3, std::numeric_limits<float>::quiet_NaN());
229 Eigen::MatrixX3f depth = subdural;
230 Eigen::VectorXd value = Eigen::VectorXd::Zero(n);
231 Eigen::VectorXd hasValue = Eigen::VectorXd::Zero(n);
232 for (int c = 0; c < n; ++c) {
233 const auto it = m_contactValues.find(contacts[c].name);
234 if (it == m_contactValues.end())
235 continue;
236 const int lead = m_leadOfContact.value(c, -1);
237 const bool isDepth = lead < 0 || leads[lead].type == IEEGLIB::IeegLeadType::Depth;
238 (isDepth ? depth : subdural).row(c) = contacts[c].position.transpose();
239 value(c) = it.value();
240 hasValue(c) = 1.0;
241 }
242 const Eigen::SparseMatrix<double> weights = IEEGLIB::IeegSurfaceMap::weights(subdural, vertices, distance, IEEGLIB::IeegSurfaceMapMode::Sum, true) +
244 const Eigen::VectorXd weighted = weights * value;
245 const Eigen::VectorXd reach = weights * hasValue;
246
247 QVector<uint32_t> colors(static_cast<int>(vertices.rows()), packABGR(0, 0, 0, 0));
248 for (int v = 0; v < colors.size(); ++v) {
249 if (reach(v) <= 0.0)
250 continue;
251 const QRgb rgb = DISPLIB::ColorMap::valueToColor(normalized(static_cast<float>(weighted(v) / reach(v))), m_colormap);
252 colors[v] = packABGR(qRed(rgb), qGreen(rgb), qBlue(rgb), static_cast<uint32_t>(255.0 * std::min(1.0, reach(v))));
253 }
254 return colors;
255}
256
257//=============================================================================================================
258
259void ElectrodeObject::selectContact(const QString& name)
260{
261 m_selectedContact = name.isEmpty() ? -1 : m_implantation.indexOf(name);
262 buildContactInstances();
263}
264
265//=============================================================================================================
266
268{
269 selectContact(QString());
270}
271
272//=============================================================================================================
273
275{
276 if (index == m_hoveredContact)
277 return;
278 m_hoveredContact = index;
279 buildContactInstances();
280}
281
282//=============================================================================================================
283
285{
286 return m_selectedContact >= 0 ? m_implantation.contacts()[m_selectedContact].name : QString();
287}
288
289//=============================================================================================================
290
292{
293 const double hue = std::fmod(double(index) * 0.618033988749895, 1.0);
294 return QColor::fromHslF(float(hue), 0.72f, 0.62f);
295}
296
297//=============================================================================================================
298
299float ElectrodeObject::contactRadius(int contact) const
300{
301 const int lead = m_leadOfContact.value(contact, -1);
302 const bool depth = lead < 0 || m_implantation.leads()[lead].type == IEEGLIB::IeegLeadType::Depth;
304}
305
306//=============================================================================================================
307
308void ElectrodeObject::buildContactInstances()
309{
310 const QVector<IEEGLIB::IeegContact>& contacts = m_implantation.contacts();
311 std::vector<InstanceData> instances;
312 instances.reserve(size_t(contacts.size()));
313 for (int c = 0; c < contacts.size(); ++c) {
314 const Eigen::Vector3f& p = contacts[c].position;
315 QMatrix4x4 model;
316 model.translate(p.x(), p.y(), p.z());
317 model.scale(contactRadius(c));
318 const QColor color = m_contactColors.value(c).isValid() ? m_contactColors[c] : leadColor(std::max(0, m_leadOfContact.value(c, 0)));
319 InstanceData d;
320 std::memcpy(d.model, model.constData(), sizeof(d.model));
321 d.color[0] = float(color.redF());
322 d.color[1] = float(color.greenF());
323 d.color[2] = float(color.blueF());
324 d.color[3] = float(color.alphaF());
325 d.isSelected = (c == m_selectedContact || c == m_hoveredContact) ? 1.0f : 0.0f;
326 instances.push_back(d);
327 }
328 m_contactInstances = toBytes(instances);
329 ++m_contactGeneration;
330}
331
332//=============================================================================================================
333
334void ElectrodeObject::buildBodies()
335{
336 std::vector<InstanceData> instances;
337 auto addCylinder = [&](const QVector3D& from, const QVector3D& to, float radius, const QColor& color) {
338 const QVector3D axis = to - from;
339 const float length = axis.length();
340 if (length < 1e-6f)
341 return;
342 QMatrix4x4 model;
343 model.translate((from + to) * 0.5f);
344 model.rotate(QQuaternion::rotationTo(QVector3D(0.0f, 1.0f, 0.0f), axis / length));
345 model.scale(radius, length, radius);
346 InstanceData d;
347 std::memcpy(d.model, model.constData(), sizeof(d.model));
348 d.color[0] = float(color.redF());
349 d.color[1] = float(color.greenF());
350 d.color[2] = float(color.blueF());
351 d.color[3] = 1.0f;
352 d.isSelected = 0.0f;
353 instances.push_back(d);
354 };
355 const QVector<IEEGLIB::IeegContact>& contacts = m_implantation.contacts();
356 auto pos = [&](int c) {
357 const Eigen::Vector3f& p = contacts[c].position;
358 return QVector3D(p.x(), p.y(), p.z());
359 };
360
361 const QVector<IEEGLIB::IeegLead>& leads = m_implantation.leads();
362 for (int l = 0; l < leads.size(); ++l) {
363 const IEEGLIB::IeegLead& lead = leads[l];
364 const QVector<int>& order = lead.contacts;
365 if (order.size() < 2)
366 continue;
367 // The body is darker than the contacts so these stand out along it
368 const QColor body = leadColor(l).darker(150);
370 const QVector3D tip = pos(order.first());
371 const QVector3D tail = pos(order.last());
372 const QVector3D dir = (tail - tip).normalized();
373 addCylinder(tip - dir * kShaftOvershoot, tail + dir * kShaftOvershoot, kShaftRadius, body);
374 continue;
375 }
376 // Strips and grids: ties between neighbours along rows, and down columns for a grid of known shape
377 const int cols = (lead.type == IEEGLIB::IeegLeadType::Grid && lead.cols > 0) ? lead.cols : int(order.size());
378 auto tie = [&](int i, int j) {
379 if ((pos(order[i]) - pos(order[j])).length() <= kMaxTieLength)
380 addCylinder(pos(order[i]), pos(order[j]), kTieRadius, body);
381 };
382 for (int i = 0; i < order.size(); ++i) {
383 if ((i + 1) % cols != 0 && i + 1 < order.size())
384 tie(i, i + 1);
385 if (i + cols < order.size())
386 tie(i, i + cols);
387 }
388 }
389 m_bodyInstances = toBytes(instances);
390 m_bodyCount = int(instances.size());
391 ++m_bodyGeneration;
392}
393
394//=============================================================================================================
395
396int ElectrodeObject::intersect(const QVector3D& rayOrigin, const QVector3D& rayDir, float& dist) const
397{
398 int best = -1;
399 float bestT = std::numeric_limits<float>::max();
400 const QVector<IEEGLIB::IeegContact>& contacts = m_implantation.contacts();
401 for (int c = 0; c < contacts.size(); ++c) {
402 const float r = contactRadius(c);
403 const Eigen::Vector3f& p = contacts[c].position;
404 const QVector3D oc = rayOrigin - QVector3D(p.x(), p.y(), p.z());
405 const float b = QVector3D::dotProduct(oc, rayDir);
406 const float disc = b * b - (oc.lengthSquared() - r * r);
407 if (disc >= 0.0f) {
408 const float t = -b - std::sqrt(disc);
409 if (t > 0.0f && t < bestT) {
410 bestT = t;
411 best = c;
412 }
413 }
414 }
415 if (best >= 0)
416 dist = bestT;
417 return best;
418}
419
420//=============================================================================================================
421
423{
424 return (index >= 0 && index < m_implantation.contacts().size()) ? &m_implantation.contacts()[index] : nullptr;
425}
426
427//=============================================================================================================
428
429QString ElectrodeObject::contactLabel(int index) const
430{
431 const IEEGLIB::IeegContact* contact = contactAt(index);
432 const int lead = m_leadOfContact.value(index, -1);
433 if (!contact || lead < 0)
434 return QString();
435 const IEEGLIB::IeegLead& l = m_implantation.leads()[lead];
436 const QString kind = l.type == IEEGLIB::IeegLeadType::Depth ? QStringLiteral("depth lead")
437 : l.type == IEEGLIB::IeegLeadType::Strip ? QStringLiteral("strip")
438 : QStringLiteral("grid");
439 return QStringLiteral("%1 (%2 %3)").arg(contact->name, kind, l.name);
440}
441
442//=============================================================================================================
443
445{
446 return {{m_sphereVertices.constData(), quint32(m_sphereVertices.size()), 0},
447 {m_sphereIndices.constData(), quint32(m_sphereIndices.size()), 0},
448 {m_contactInstances.constData(), quint32(m_contactInstances.size()), m_contactGeneration},
449 m_sphereIndexCount,
451}
452
453//=============================================================================================================
454
456{
457 return {{m_cylinderVertices.constData(), quint32(m_cylinderVertices.size()), 0},
458 {m_cylinderIndices.constData(), quint32(m_cylinderIndices.size()), 0},
459 {m_bodyInstances.constData(), quint32(m_bodyInstances.size()), m_bodyGeneration},
460 m_cylinderIndexCount,
461 m_bodyCount};
462}
463
464//=============================================================================================================
465
467{
468 return m_bbMin;
469}
470
471//=============================================================================================================
472
474{
475 return m_bbMax;
476}
477
478//=============================================================================================================
479
480void ElectrodeObject::computeBoundingBox()
481{
482 m_bbMin = QVector3D(std::numeric_limits<float>::max(), std::numeric_limits<float>::max(), std::numeric_limits<float>::max());
483 m_bbMax = QVector3D(std::numeric_limits<float>::lowest(), std::numeric_limits<float>::lowest(), std::numeric_limits<float>::lowest());
484 const QVector<IEEGLIB::IeegContact>& contacts = m_implantation.contacts();
485 for (int c = 0; c < contacts.size(); ++c) {
486 const float pad = contactRadius(c);
487 const QVector3D p(contacts[c].position.x(), contacts[c].position.y(), contacts[c].position.z());
488 m_bbMin = QVector3D(std::min(m_bbMin.x(), p.x() - pad), std::min(m_bbMin.y(), p.y() - pad), std::min(m_bbMin.z(), p.z() - pad));
489 m_bbMax = QVector3D(std::max(m_bbMax.x(), p.x() + pad), std::max(m_bbMax.y(), p.y() + pad), std::max(m_bbMax.z(), p.z() + pad));
490 }
491}
Static scalar-to-colour lookup helpers (Jet, Hot, Bone, Viridis, Cool, RedBlue, MNE) used by every pl...
Renderable view of an sEEG / ECoG implantation: contact spheres and lead bodies.
Map sEEG / ECoG contact values onto surface vertices, as MNE-Python's stc_near_sensors.
3-D brain visualisation using the Qt RHI rendering backend.
uint32_t packABGR(uint32_t r, uint32_t g, uint32_t b, uint32_t a=0xFF)
Definition rendertypes.h:54
static QRgb valueToColor(double v, const QString &sMap)
Definition colormap.h:683
CPU streams of an instanced mesh: one shape (vertices, indices) drawn once per instance.
const IEEGLIB::IeegContact * contactAt(int index) const
static constexpr float kDepthContactRadius
InstancedStreams bodyStreams() const
void setContactValues(const QMap< QString, float > &values, const QString &colormap=QStringLiteral("Hot"))
static constexpr float kTieRadius
static QColor leadColor(int index)
const IEEGLIB::IeegImplantation & implantation() const
QString contactLabel(int index) const
QVector< uint32_t > surfaceColors(const Eigen::MatrixX3f &vertices, double distance=0.01) const
static constexpr float kShaftOvershoot
void setImplantation(const IEEGLIB::IeegImplantation &implantation)
void selectContact(const QString &name)
static constexpr float kMaxTieLength
int intersect(const QVector3D &rayOrigin, const QVector3D &rayDir, float &dist) const
static constexpr float kShaftRadius
InstancedStreams contactStreams() const
static constexpr float kSubduralContactRadius
One recording contact.
One lead: a depth shaft, a strip or a grid.
QVector< int > contacts
Contacts of an sEEG / ECoG implantation and the leads they belong to.
const QVector< IeegLead > & leads() const
const QVector< IeegContact > & contacts() const
static Eigen::SparseMatrix< double > weights(const Eigen::MatrixX3f &contacts, const Eigen::MatrixX3f &vertices, double distance=0.01, IeegSurfaceMapMode mode=IeegSurfaceMapMode::Sum, bool project=true)