31#include <QCoreApplication>
52Eigen::Vector3f applyTransform(
const Eigen::Vector3f& point,
57 float r[3] = {point.x(), point.y(), point.z()};
59 return Eigen::Vector3f(r[0], r[1], r[2]);
71 m_loaded = (m_evoked.nave != -1 && m_evoked.data.rows() > 0);
79 if (m_loaded && m_evoked.baseline.first == m_evoked.baseline.second) {
81 float tmin = m_evoked.times.size() > 0 ? m_evoked.times(0) : 0.0f;
83 QPair<float, float> bl(tmin, 0.0f);
84 m_evoked.applyBaselineCorrection(bl);
94 if (!m_loaded || (!m_megMapping && !m_eegMapping))
98 if (m_evoked.info.chs.size() != newEvoked.
info.
chs.size())
102 for (
int i = 0; i < m_evoked.info.chs.size(); ++i) {
103 if (m_evoked.info.chs[i].ch_name != newEvoked.
info.
chs[i].ch_name)
108 if (m_evoked.info.bads != newEvoked.
info.
bads)
112 if (m_evoked.info.projs.size() != newEvoked.
info.
projs.size())
125 const QMap<QString, std::shared_ptr<BrainSurface>>& surfaces)
127 if (surfaces.contains(
"bem_head"))
128 return QStringLiteral(
"bem_head");
131 for (
auto it = surfaces.cbegin(); it != surfaces.cend(); ++it) {
132 if (!it.key().startsWith(
"bem_"))
136 if (fallback.isEmpty())
145 const QMap<QString, std::shared_ptr<BrainSurface>>& surfaces)
147 return surfaces.contains(
"sens_surface_meg")
148 ? QStringLiteral(
"sens_surface_meg")
156 if (targetTicks <= 0)
158 const double range =
static_cast<double>(maxVal - minVal);
162 const double raw = range /
static_cast<double>(targetTicks);
163 const double exponent = std::floor(std::log10(raw));
164 const double base = std::pow(10.0, exponent);
165 const double frac = raw / base;
167 double niceFrac = 1.0;
170 else if (frac <= 2.0)
172 else if (frac <= 5.0)
177 return static_cast<float>(niceFrac * base);
183 const QMap<QString, std::shared_ptr<BrainSurface>>& surfaces,
185 bool applySensorTrans)
187 if (!m_loaded || m_evoked.isEmpty())
193 m_megPositions.clear();
194 m_eegPositions.clear();
195 m_megMapping.reset();
196 m_eegMapping.reset();
199 m_megSurfaceKey = m_megOnHead
203 if (m_megOnHead && m_megSurfaceKey.isEmpty()) {
205 if (!m_megSurfaceKey.isEmpty())
206 qWarning() <<
"SensorFieldMapper: Head surface missing, falling back to helmet.";
210 if (m_megSurfaceKey.isEmpty() && m_eegSurfaceKey.isEmpty()) {
211 qWarning() <<
"SensorFieldMapper: No helmet/head surface for field mapping.";
216 bool hasDevHead =
false;
217 QMatrix4x4 devHeadQt;
218 if (!m_evoked.info.dev_head_t.isEmpty() &&
221 !m_evoked.info.dev_head_t.trans.isIdentity()) {
223 for (
int r = 0; r < 4; ++r)
224 for (
int c = 0; c < 4; ++c)
225 devHeadQt(r, c) = m_evoked.info.dev_head_t.trans(r, c);
228 QMatrix4x4 headToMri;
229 if (applySensorTrans && !headToMriTrans.
isEmpty()) {
230 for (
int r = 0; r < 4; ++r)
231 for (
int c = 0; c < 4; ++c)
232 headToMri(r, c) = headToMriTrans.
trans(r, c);
236 QList<FiffChInfo> megChs, eegChs;
237 QStringList megChNames, eegChNames;
239 auto isBad = [
this](
const QString& name) {
240 return m_evoked.info.bads.contains(name);
243 const int nChs = m_evoked.info.chs.size();
244 m_megPick.resize(nChs);
245 m_eegPick.resize(nChs);
246 int nMeg = 0, nEeg = 0;
248 for (
int k = 0; k < nChs; ++k) {
249 const auto& ch = m_evoked.info.chs[k];
250 if (isBad(ch.ch_name))
253 QVector3D pos(ch.chpos.r0(0), ch.chpos.r0(1), ch.chpos.r0(2));
257 pos = devHeadQt.map(pos);
258 if (applySensorTrans && !headToMriTrans.
isEmpty())
259 pos = headToMri.map(pos);
260 m_megPick(nMeg++) = k;
261 m_megPositions.push_back(Eigen::Vector3f(pos.x(), pos.y(), pos.z()));
263 megChNames.append(ch.ch_name);
265 if (applySensorTrans && !headToMriTrans.
isEmpty())
266 pos = headToMri.map(pos);
267 m_eegPick(nEeg++) = k;
268 m_eegPositions.push_back(Eigen::Vector3f(pos.x(), pos.y(), pos.z()));
270 eegChNames.append(ch.ch_name);
274 m_megPick.conservativeResize(nMeg);
275 m_eegPick.conservativeResize(nEeg);
278 constexpr float kIntrad = 0.06f;
279 constexpr float kMegMiss = 1e-4f;
280 constexpr float kEegMiss = 1e-3f;
292 ? m_evoked.info.dev_head_t
296 if (!m_megSurfaceKey.isEmpty() && surfaces.contains(m_megSurfaceKey) && !megChs.isEmpty()) {
302 if (norms.rows() != verts.rows()) {
304 const int nTris = idx.size() / 3;
306 Eigen::MatrixX3i tris(nTris, 3);
307 for (
int t = 0; t < nTris; ++t) {
308 tris(t, 0) =
static_cast<int>(idx[t * 3]);
309 tris(t, 1) =
static_cast<int>(idx[t * 3 + 1]);
310 tris(t, 2) =
static_cast<int>(idx[t * 3 + 2]);
316 if (verts.rows() > 0 && norms.rows() == verts.rows()) {
317 const QString coilPath = QCoreApplication::applicationDirPath() +
"/../resources/general/coilDefinitions/coil_def.dat";
323 if (m_megOnHead && !headMri.
isEmpty()) {
329 }
else if (!devHead.
isEmpty()) {
330 devToTarget = devHead;
333 Eigen::Vector3f origin = fittedOrigin;
334 if (m_megOnHead && !headMri.
isEmpty())
335 origin = applyTransform(origin, headMri);
337 auto coils = templates->create_meg_coils(
340 if (coils && coils->ncoil() > 0) {
342 *coils, verts, norms, origin,
343 m_evoked.info, megChNames,
347 qWarning() <<
"MEG coil definitions not found at" << coilPath;
353 if (!m_eegSurfaceKey.isEmpty() && surfaces.contains(m_eegSurfaceKey) && !eegChs.isEmpty()) {
357 if (verts.rows() > 0) {
358 Eigen::Vector3f origin = fittedOrigin;
360 origin = applyTransform(origin, headMri);
364 eegChs, eegChs.size(), headMri);
366 if (eegCoils && eegCoils->ncoil() > 0) {
368 *eegCoils, verts, origin,
369 m_evoked.info, eegChNames,
384 const Eigen::Vector3f fallback(0.0f, 0.0f, 0.04f);
391 auto gatherPoints = [&](
bool includeEeg) -> Eigen::MatrixXd {
392 QVector<Eigen::Vector3d> pts;
393 for (
const auto& dp : info.
dig) {
398 const double x = dp.r[0], y = dp.r[1], z = dp.r[2];
400 if (z < 0.0 && y > 0.0)
402 pts.append(Eigen::Vector3d(x, y, z));
406 Eigen::MatrixXd mat(pts.size(), 3);
407 for (
int i = 0; i < pts.size(); ++i)
408 mat.row(i) = pts[i].transpose();
412 Eigen::MatrixXd points = gatherPoints(
false);
413 if (points.rows() < 4)
414 points = gatherPoints(
true);
415 if (points.rows() < 4) {
416 qWarning() <<
"SensorFieldMapper::fitSphereOrigin: fewer than 4 dig "
417 "points – falling back to default origin (0, 0, 0.04).";
427 const int n =
static_cast<int>(points.rows());
428 Eigen::MatrixXd A(n, 4);
429 Eigen::VectorXd b(n);
430 for (
int i = 0; i < n; ++i) {
431 A(i, 0) = 2.0 * points(i, 0);
432 A(i, 1) = 2.0 * points(i, 1);
433 A(i, 2) = 2.0 * points(i, 2);
435 b(i) = points(i, 0) * points(i, 0) + points(i, 1) * points(i, 1) + points(i, 2) * points(i, 2);
441 Eigen::Matrix4d AtA = A.transpose() * A;
442 Eigen::Vector4d Atb = A.transpose() * b;
445 x = AtA.fullPivLu().solve(Atb);
447 const float cx =
static_cast<float>(x(0));
448 const float cy =
static_cast<float>(x(1));
449 const float cz =
static_cast<float>(x(2));
450 const float R =
static_cast<float>(
451 std::sqrt(x(0) * x(0) + x(1) * x(1) + x(2) * x(2) + x(3)));
456 return Eigen::Vector3f(cx, cy, cz);
466 if (!m_loaded || m_evoked.isEmpty())
469 const int nTimes =
static_cast<int>(m_evoked.data.cols());
474 auto peakGfpTime = [&](
const Eigen::VectorXi& pick) ->
int {
475 if (pick.size() == 0 || nTimes == 0)
478 double bestSS = -1.0;
479 for (
int t = 0; t < nTimes; ++t) {
481 for (
int i = 0; i < pick.size(); ++i) {
482 double v = m_evoked.data(pick(i), t);
497 if (m_megMapping && m_megMapping->rows() > 0 && m_megPick.size() > 0) {
498 const int tPeak = peakGfpTime(m_megPick);
499 Eigen::VectorXf meas(m_megPick.size());
500 for (
int i = 0; i < m_megPick.size(); ++i)
501 meas(i) =
static_cast<float>(m_evoked.data(m_megPick(i), tPeak));
503 Eigen::VectorXf mapped = (*m_megMapping) * meas;
504 m_megVmax = mapped.cwiseAbs().maxCoeff();
508 if (m_eegMapping && m_eegMapping->rows() > 0 && m_eegPick.size() > 0) {
509 const int tPeak = peakGfpTime(m_eegPick);
510 Eigen::VectorXf meas(m_eegPick.size());
511 for (
int i = 0; i < m_eegPick.size(); ++i)
512 meas(i) =
static_cast<float>(m_evoked.data(m_eegPick(i), tPeak));
514 Eigen::VectorXf mapped = (*m_eegMapping) * meas;
515 m_eegVmax = mapped.cwiseAbs().maxCoeff();
518 if (m_megVmax <= 0.0f)
520 if (m_eegVmax <= 0.0f)
527 QMap<QString, std::shared_ptr<BrainSurface>>& surfaces,
529 const QVector<SubView>& subViews)
531 if (!m_loaded || m_evoked.isEmpty())
535 auto applyMap = [&](
const QString& key,
536 const QString& contourPrefix,
537 const Eigen::VectorXi& pick,
538 const Eigen::MatrixXf* mat,
542 if (key.isEmpty() || !surfaces.contains(key))
545 auto surface = surfaces[key];
546 if (!visible || !mat || pick.size() == 0) {
548 updateContourSurfaces(surfaces, contourPrefix, *surface,
549 QVector<float>(), 0.0f,
false);
552 if (mat->cols() != pick.size()) {
554 updateContourSurfaces(surfaces, contourPrefix, *surface,
555 QVector<float>(), 0.0f,
false);
560 Eigen::VectorXf meas(pick.size());
561 for (
int i = 0; i < pick.size(); ++i)
562 meas(i) =
static_cast<float>(m_evoked.data(pick(i), m_timePoint));
564 Eigen::VectorXf mapped = (*mat) * meas;
568 const float maxAbs = globalMaxAbs;
571 QVector<uint32_t> colors(mapped.size());
572 for (
int i = 0; i < mapped.size(); ++i) {
573 double norm = (mapped(i) / maxAbs) * 0.5 + 0.5;
574 norm = qBound(0.0, norm, 1.0);
576 QRgb rgb = (m_colormap ==
"MNE")
580 uint32_t r = qRed(rgb);
581 uint32_t g = qGreen(rgb);
582 uint32_t b = qBlue(rgb);
585 surface->applySourceEstimateColors(colors);
589 QVector<float> values(mapped.size());
590 for (
int i = 0; i < mapped.size(); ++i)
591 values[i] = mapped(i);
593 constexpr int nContours = 21;
594 float step = (2.0f * maxAbs) /
static_cast<float>(nContours - 1);
595 updateContourSurfaces(surfaces, contourPrefix, *surface,
596 values, step, showContours);
604 for (
int i = 0; i < subViews.size(); ++i) {
605 anyMegField |= subViews[i].visibility.megFieldMap;
606 anyEegField |= subViews[i].visibility.eegFieldMap;
607 anyMegContours |= subViews[i].visibility.megFieldContours;
608 anyEegContours |= subViews[i].visibility.eegFieldContours;
611 applyMap(m_megSurfaceKey, m_megContourPrefix,
612 m_megPick, m_megMapping.get(),
614 anyMegField, anyMegContours);
616 applyMap(m_eegSurfaceKey, m_eegContourPrefix,
617 m_eegPick, m_eegMapping.get(),
619 anyEegField, anyEegContours);
624void SensorFieldMapper::updateContourSurfaces(
625 QMap<QString, std::shared_ptr<BrainSurface>>& surfaces,
626 const QString& prefix,
628 const QVector<float>& values,
633 auto hideContours = [&]() {
634 for (
const auto& suffix : {QStringLiteral(
"_neg"),
635 QStringLiteral(
"_zero"),
636 QStringLiteral(
"_pos")}) {
637 const QString key = prefix + suffix;
638 if (surfaces.contains(key))
639 surfaces[key]->setVisible(
false);
643 if (!visible || values.isEmpty() || step <= 0.0f) {
649 float minVal = values[0], maxVal = values[0];
650 for (
int i = 1; i < values.size(); ++i) {
651 minVal = std::min(minVal, values[i]);
652 maxVal = std::max(maxVal, values[i]);
656 QVector<float> negLevels, posLevels;
657 const bool hasZero = (minVal < 0.0f && maxVal > 0.0f);
658 for (
float lv = -step; lv >= minVal; lv -= step)
659 negLevels.append(lv);
660 for (
float lv = step; lv <= maxVal; lv += step)
661 posLevels.append(lv);
666 QVector<Eigen::Vector3f> verts;
667 QVector<Eigen::Vector3f> norms;
668 QVector<Eigen::Vector3i> tris;
671 auto addSegment = [](ContourBuf& buf,
672 const QVector3D& p0,
const QVector3D& p1,
673 const QVector3D& normal,
674 float halfW,
float shift) {
675 QVector3D dir = p1 - p0;
676 const float len = dir.length();
681 QVector3D binormal = QVector3D::crossProduct(normal, dir);
682 if (binormal.length() < 1e-6f)
683 binormal = QVector3D::crossProduct(QVector3D(0, 1, 0), dir);
684 if (binormal.length() < 1e-6f)
685 binormal = QVector3D::crossProduct(QVector3D(1, 0, 0), dir);
686 binormal.normalize();
688 const QVector3D off = normal * shift;
690 auto toEig = [](
const QVector3D& v) {
691 return Eigen::Vector3f(v.x(), v.y(), v.z());
696 const QVector3D w = binormal * halfW;
697 const int base = buf.verts.size();
698 Eigen::Vector3f n(normal.x(), normal.y(), normal.z());
700 buf.verts.append(toEig(p0 - w + off));
701 buf.verts.append(toEig(p0 + w + off));
702 buf.verts.append(toEig(p1 - w + off));
703 buf.verts.append(toEig(p1 + w + off));
708 buf.tris.append(Eigen::Vector3i(base, base + 1, base + 2));
709 buf.tris.append(Eigen::Vector3i(base + 1, base + 3, base + 2));
714 const QVector3D h = normal * halfW;
715 const int base = buf.verts.size();
716 Eigen::Vector3f n(binormal.x(), binormal.y(), binormal.z());
718 buf.verts.append(toEig(p0 - h + off));
719 buf.verts.append(toEig(p0 + h + off));
720 buf.verts.append(toEig(p1 - h + off));
721 buf.verts.append(toEig(p1 + h + off));
726 buf.tris.append(Eigen::Vector3i(base, base + 1, base + 2));
727 buf.tris.append(Eigen::Vector3i(base + 1, base + 3, base + 2));
732 auto buildContours = [&](
const QVector<float>& levels, ContourBuf& buf) {
736 if (rr.rows() == 0 || nn.rows() == 0 || idx.isEmpty())
739 constexpr float shift = 0.001f;
740 constexpr float halfW = 0.0005f;
742 for (
float level : levels) {
743 for (
int t = 0; t + 2 < idx.size(); t += 3) {
744 const int i0 = idx[t], i1 = idx[t + 1], i2 = idx[t + 2];
745 const float v0 = values[i0], v1 = values[i1], v2 = values[i2];
747 QVector3D p0(rr(i0, 0), rr(i0, 1), rr(i0, 2));
748 QVector3D p1(rr(i1, 0), rr(i1, 1), rr(i1, 2));
749 QVector3D p2(rr(i2, 0), rr(i2, 1), rr(i2, 2));
751 QVector3D n0(nn(i0, 0), nn(i0, 1), nn(i0, 2));
752 QVector3D n1(nn(i1, 0), nn(i1, 1), nn(i1, 2));
753 QVector3D n2(nn(i2, 0), nn(i2, 1), nn(i2, 2));
754 QVector3D triN = (n0 + n1 + n2).normalized();
755 if (triN.length() < 1e-6f)
756 triN = QVector3D::crossProduct(p1 - p0, p2 - p0).normalized();
758 QVector<QVector3D> hits;
759 auto checkEdge = [&](
const QVector3D& a,
const QVector3D& b,
760 float va,
float vb) {
763 float tval = (level - va) / (vb - va);
764 if (tval >= 0.0f && tval < 1.0f)
765 hits.append(a + (b - a) * tval);
767 checkEdge(p0, p1, v0, v1);
768 checkEdge(p1, p2, v1, v2);
769 checkEdge(p2, p0, v2, v0);
771 if (hits.size() == 2)
772 addSegment(buf, hits[0], hits[1], triN, halfW, shift);
777 ContourBuf negBuf, posBuf, zeroBuf;
778 buildContours(negLevels, negBuf);
779 buildContours(posLevels, posBuf);
781 QVector<float> zeroLevels = {0.0f};
782 buildContours(zeroLevels, zeroBuf);
786 auto updateSurf = [&](
const QString& suffix,
787 const ContourBuf& buf,
790 const QString key = prefix + suffix;
791 if (!show || buf.verts.isEmpty()) {
792 if (surfaces.contains(key))
793 surfaces[key]->setVisible(
false);
797 Eigen::MatrixX3f rr(buf.verts.size(), 3);
798 Eigen::MatrixX3f nn(buf.norms.size(), 3);
799 Eigen::MatrixX3i tris(buf.tris.size(), 3);
800 for (
int i = 0; i < buf.verts.size(); ++i) {
801 rr.row(i) = buf.verts[i];
802 nn.row(i) = buf.norms[i];
804 for (
int i = 0; i < buf.tris.size(); ++i)
805 tris.row(i) = buf.tris[i];
807 std::shared_ptr<BrainSurface> csurf;
808 if (surfaces.contains(key)) {
809 csurf = surfaces[key];
811 csurf = std::make_shared<BrainSurface>();
812 surfaces[key] = csurf;
814 csurf->createFromData(rr, nn, tris, color);
815 csurf->setVisible(
true);
818 updateSurf(
"_neg", negBuf, QColor(0, 0, 255, 200), visible && !negBuf.verts.isEmpty());
819 updateSurf(
"_zero", zeroBuf, QColor(0, 0, 0, 220), visible && !zeroBuf.verts.isEmpty());
820 updateSurf(
"_pos", posBuf, QColor(255, 0, 0, 200), visible && !posBuf.verts.isEmpty());
FIFF channel descriptor record (FIFF_CH_INFO): per-channel logical/scanner numbers,...
Symbolic FIFF tag, block, value, unit and channel-type constants shared across FIFFLIB.
#define FIFFV_POINT_EXTRA
#define FIFFV_COORD_DEVICE
4x4 affine FIFF coordinate transform (FIFF_COORD_TRANS) annotated with source/destination coordinate-...
return FiffCoordTrans(from_frame, to_frame, R, moveVec)
Static scalar-to-colour lookup helpers (Jet, Hot, Bone, Viridis, Cool, RedBlue, MNE) used by every pl...
Renderable cortical / BEM mesh with interleaved vertex attributes and Qt-RHI buffer management.
Lightweight render-related enums (ShaderMode, VisualizationMode) shared across disp3D.
Builds the dense sensor-to-surface mapping matrix and the iso-contour overlay for MEG / EEG evoked da...
Container of FwdCoil instances representing either a sensor-type template database or a concrete per-...
Sphere-model field interpolator that maps measured MEG/EEG values onto a dense scalp or cortical surf...
Reader and in-memory representation of a single FreeSurfer triangular surface (e.g....
FIFF file I/O, in-memory data structures and high-level readers/writers.
3-D brain visualisation using the Qt RHI rendering backend.
QRgb mneAnalyzeColor(double v)
uint32_t packABGR(uint32_t r, uint32_t g, uint32_t b, uint32_t a=0xFF)
constexpr int FWD_COIL_ACCURACY_NORMAL
static QRgb valueToColor(double v, const QString &sMap)
Viewport subdivision holding its own camera, projection, and scissor rectangle.
ViewVisibilityProfile visibility
Renderable cortical surface mesh with per-vertex color, curvature data, and GPU buffer management.
Eigen::MatrixX3f vertexPositions() const
Eigen::MatrixX3f vertexNormals() const
static constexpr VisualizationMode ModeSurface
QVector< uint32_t > triangleIndices() const
void apply(QMap< QString, std::shared_ptr< BrainSurface > > &surfaces, const SubView &singleView, const QVector< SubView > &subViews)
static QString findHelmetSurfaceKey(const QMap< QString, std::shared_ptr< BrainSurface > > &surfaces)
static float contourStep(float minVal, float maxVal, int targetTicks)
void setEvoked(const FIFFLIB::FiffEvoked &evoked)
bool buildMapping(const QMap< QString, std::shared_ptr< BrainSurface > > &surfaces, const FIFFLIB::FiffCoordTrans &headToMriTrans, bool applySensorTrans)
bool hasMappingFor(const FIFFLIB::FiffEvoked &newEvoked) const
static QString findHeadSurfaceKey(const QMap< QString, std::shared_ptr< BrainSurface > > &surfaces)
const FIFFLIB::FiffEvoked & evoked() const
static Eigen::Vector3f fitSphereOrigin(const FIFFLIB::FiffInfo &info, float *radius=nullptr)
Labelled 4x4 FIFF affine: source frame, destination frame, rotation, translation and cached inverse.
static FiffCoordTrans combine(int from, int to, const FiffCoordTrans &t1, const FiffCoordTrans &t2)
Eigen::Matrix< float, 4, 4, Eigen::DontAlign > trans
Single averaged evoked response: time axis, data, baseline, channel info and averaging metadata.
Full FIFF measurement info: per-channel descriptors, sampling and filter setup, projectors,...
QList< FiffDigPoint > dig
FiffCoordTrans dev_head_t
static Eigen::MatrixX3f compute_normals(const Eigen::MatrixX3f &rr, const Eigen::MatrixX3i &tris)
static FwdCoilSet::UPtr read_coil_defs(const QString &name)
static FwdCoilSet::UPtr create_eeg_els(const QList< FIFFLIB::FiffChInfo > &chs, int nch, const FIFFLIB::FiffCoordTrans &t=FIFFLIB::FiffCoordTrans())
static std::unique_ptr< Eigen::MatrixXf > computeEegMapping(const FwdCoilSet &coils, const Eigen::MatrixX3f &vertices, const Eigen::Vector3f &origin, float intrad=0.06f, float miss=1e-3f)
static std::unique_ptr< Eigen::MatrixXf > computeMegMapping(const FwdCoilSet &coils, const Eigen::MatrixX3f &vertices, const Eigen::MatrixX3f &normals, const Eigen::Vector3f &origin, float intrad=0.06f, float miss=1e-4f)
Eigen::MatrixX3f apply_trans(const Eigen::MatrixX3f &rr, bool do_move=true) const