43 float x, y, z, nx, ny, nz;
47QByteArray toBytes(
const std::vector<T>& v)
49 return QByteArray(
reinterpret_cast<const char*
>(v.data()), qsizetype(v.size() *
sizeof(T)));
53void unitSphere(QByteArray& vertices, QByteArray& indices,
int& indexCount)
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}};
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())
68 v.push_back(((v[a] + v[b]) * 0.5f).normalized());
69 return midpoints[key] = uint32_t(v.size() - 1);
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});
77 idx = std::move(next);
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());
88void unitCylinder(QByteArray& vertices, QByteArray& indices,
int& indexCount)
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});
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});
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});
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);
114 idx.insert(idx.end(), {centre, next, cur});
116 idx.insert(idx.end(), {centre, cur, next});
119 vertices = toBytes(mv);
120 indices = toBytes(idx);
121 indexCount = int(idx.size());
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())
138 unitSphere(m_sphereVertices, m_sphereIndices, m_sphereIndexCount);
139 unitCylinder(m_cylinderVertices, m_cylinderIndices, m_cylinderIndexCount);
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;
158 m_contactColors.fill(QColor(), contactCount);
159 m_contactValues.clear();
160 m_selectedContact = -1;
161 m_hoveredContact = -1;
162 computeBoundingBox();
164 buildContactInstances();
171 return int(m_implantation.contacts().size());
178 if (values.isEmpty()) {
182 m_contactValues = values;
183 m_colormap = colormap;
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);
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())
198 buildContactInstances();
205 m_contactValues.clear();
206 m_contactColors.fill(QColor());
207 buildContactInstances();
212float ElectrodeObject::normalized(
float value)
const
214 return m_valueMax > m_valueMin ? qBound(0.0f, (value - m_valueMin) / (m_valueMax - m_valueMin), 1.0f) : 1.0f;
221 if (m_contactValues.isEmpty() || vertices.rows() == 0)
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())
236 const int lead = m_leadOfContact.value(c, -1);
238 (isDepth ? depth : subdural).row(c) = contacts[c].position.transpose();
239 value(c) = it.value();
244 const Eigen::VectorXd weighted = weights * value;
245 const Eigen::VectorXd reach = weights * hasValue;
247 QVector<uint32_t> colors(
static_cast<int>(vertices.rows()),
packABGR(0, 0, 0, 0));
248 for (
int v = 0; v < colors.size(); ++v) {
252 colors[v] =
packABGR(qRed(rgb), qGreen(rgb), qBlue(rgb),
static_cast<uint32_t
>(255.0 * std::min(1.0, reach(v))));
261 m_selectedContact = name.isEmpty() ? -1 : m_implantation.indexOf(name);
262 buildContactInstances();
276 if (index == m_hoveredContact)
278 m_hoveredContact = index;
279 buildContactInstances();
286 return m_selectedContact >= 0 ? m_implantation.contacts()[m_selectedContact].name : QString();
293 const double hue = std::fmod(
double(index) * 0.618033988749895, 1.0);
294 return QColor::fromHslF(
float(hue), 0.72f, 0.62f);
299float ElectrodeObject::contactRadius(
int contact)
const
301 const int lead = m_leadOfContact.value(contact, -1);
308void ElectrodeObject::buildContactInstances()
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;
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)));
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);
328 m_contactInstances = toBytes(instances);
329 ++m_contactGeneration;
334void ElectrodeObject::buildBodies()
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();
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);
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());
353 instances.push_back(d);
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());
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)
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();
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);
382 for (
int i = 0; i < order.size(); ++i) {
383 if ((i + 1) % cols != 0 && i + 1 < order.size())
385 if (i + cols < order.size())
389 m_bodyInstances = toBytes(instances);
390 m_bodyCount = int(instances.size());
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);
408 const float t = -b - std::sqrt(disc);
409 if (t > 0.0f && t < bestT) {
424 return (index >= 0 && index < m_implantation.contacts().size()) ? &m_implantation.contacts()[index] :
nullptr;
432 const int lead = m_leadOfContact.value(index, -1);
433 if (!contact || lead < 0)
438 : QStringLiteral(
"grid");
439 return QStringLiteral(
"%1 (%2 %3)").arg(contact->
name, kind, l.
name);
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},
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,
480void ElectrodeObject::computeBoundingBox()
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));
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)
static QRgb valueToColor(double v, const QString &sMap)
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)
QVector3D boundingBoxMax() const
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)
void setHoveredContact(int index)
static constexpr float kMaxTieLength
int intersect(const QVector3D &rayOrigin, const QVector3D &rayDir, float &dist) const
QString selectedContact() const
void clearContactValues()
int totalContactCount() const
static constexpr float kShaftRadius
InstancedStreams contactStreams() const
static constexpr float kSubduralContactRadius
QVector3D boundingBoxMin() const
One lead: a depth shaft, a strip or a grid.
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)