53#include <QCoreApplication>
71Eigen::Vector3f applyTransform(
const Eigen::Vector3f &point,
74 if (trans.
isEmpty())
return point;
75 float r[3] = {point.x(), point.y(), point.z()};
77 return Eigen::Vector3f(r[0], r[1], r[2]);
89 m_loaded = (m_evoked.nave != -1 && m_evoked.data.rows() > 0);
97 if (m_loaded && m_evoked.baseline.first == m_evoked.baseline.second) {
99 float tmin = m_evoked.times.size() > 0 ? m_evoked.times(0) : 0.0f;
101 QPair<float,float> bl(tmin, 0.0f);
102 m_evoked.applyBaselineCorrection(bl);
112 if (!m_loaded || (!m_megMapping && !m_eegMapping))
116 if (m_evoked.info.chs.size() != newEvoked.
info.
chs.size())
120 for (
int i = 0; i < m_evoked.info.chs.size(); ++i) {
121 if (m_evoked.info.chs[i].ch_name != newEvoked.
info.
chs[i].ch_name)
126 if (m_evoked.info.bads != newEvoked.
info.
bads)
130 if (m_evoked.info.projs.size() != newEvoked.
info.
projs.size())
143 const QMap<QString, std::shared_ptr<BrainSurface>> &surfaces)
145 if (surfaces.contains(
"bem_head"))
146 return QStringLiteral(
"bem_head");
149 for (
auto it = surfaces.cbegin(); it != surfaces.cend(); ++it) {
150 if (!it.key().startsWith(
"bem_"))
continue;
153 if (fallback.isEmpty())
162 const QMap<QString, std::shared_ptr<BrainSurface>> &surfaces)
164 return surfaces.contains(
"sens_surface_meg")
165 ? QStringLiteral(
"sens_surface_meg")
173 if (targetTicks <= 0)
return 0.0f;
174 const double range =
static_cast<double>(maxVal - minVal);
175 if (range <= 0.0)
return 0.0f;
177 const double raw = range /
static_cast<double>(targetTicks);
178 const double exponent = std::floor(std::log10(raw));
179 const double base = std::pow(10.0, exponent);
180 const double frac = raw / base;
182 double niceFrac = 1.0;
183 if (frac <= 1.0) niceFrac = 1.0;
184 else if (frac <= 2.0) niceFrac = 2.0;
185 else if (frac <= 5.0) niceFrac = 5.0;
186 else niceFrac = 10.0;
188 return static_cast<float>(niceFrac * base);
194 const QMap<QString, std::shared_ptr<BrainSurface>> &surfaces,
196 bool applySensorTrans)
198 if (!m_loaded || m_evoked.isEmpty())
return false;
203 m_megPositions.clear();
204 m_eegPositions.clear();
205 m_megMapping.reset();
206 m_eegMapping.reset();
209 m_megSurfaceKey = m_megOnHead
213 if (m_megOnHead && m_megSurfaceKey.isEmpty()) {
215 if (!m_megSurfaceKey.isEmpty())
216 qWarning() <<
"SensorFieldMapper: Head surface missing, falling back to helmet.";
220 if (m_megSurfaceKey.isEmpty() && m_eegSurfaceKey.isEmpty()) {
221 qWarning() <<
"SensorFieldMapper: No helmet/head surface for field mapping.";
226 bool hasDevHead =
false;
227 QMatrix4x4 devHeadQt;
228 if (!m_evoked.info.dev_head_t.isEmpty() &&
231 !m_evoked.info.dev_head_t.trans.isIdentity()) {
233 for (
int r = 0; r < 4; ++r)
234 for (
int c = 0; c < 4; ++c)
235 devHeadQt(r, c) = m_evoked.info.dev_head_t.trans(r, c);
238 QMatrix4x4 headToMri;
239 if (applySensorTrans && !headToMriTrans.
isEmpty()) {
240 for (
int r = 0; r < 4; ++r)
241 for (
int c = 0; c < 4; ++c)
242 headToMri(r, c) = headToMriTrans.
trans(r, c);
246 QList<FiffChInfo> megChs, eegChs;
247 QStringList megChNames, eegChNames;
249 auto isBad = [
this](
const QString &name) {
250 return m_evoked.info.bads.contains(name);
253 for (
int k = 0; k < m_evoked.info.chs.size(); ++k) {
254 const auto &ch = m_evoked.info.chs[k];
255 if (isBad(ch.ch_name))
continue;
257 QVector3D pos(ch.chpos.r0(0), ch.chpos.r0(1), ch.chpos.r0(2));
260 if (hasDevHead) pos = devHeadQt.map(pos);
261 if (applySensorTrans && !headToMriTrans.
isEmpty()) pos = headToMri.map(pos);
263 m_megPositions.append(Eigen::Vector3f(pos.x(), pos.y(), pos.z()));
265 megChNames.append(ch.ch_name);
267 if (applySensorTrans && !headToMriTrans.
isEmpty()) pos = headToMri.map(pos);
269 m_eegPositions.append(Eigen::Vector3f(pos.x(), pos.y(), pos.z()));
271 eegChNames.append(ch.ch_name);
276 constexpr float kIntrad = 0.06f;
277 constexpr float kMegMiss = 1e-4f;
278 constexpr float kEegMiss = 1e-3f;
292 if (!m_megSurfaceKey.isEmpty() && surfaces.contains(m_megSurfaceKey) && !megChs.isEmpty()) {
298 if (norms.rows() != verts.rows()) {
300 const int nTris = idx.size() / 3;
302 Eigen::MatrixX3i tris(nTris, 3);
303 for (
int t = 0; t < nTris; ++t) {
304 tris(t, 0) =
static_cast<int>(idx[t * 3]);
305 tris(t, 1) =
static_cast<int>(idx[t * 3 + 1]);
306 tris(t, 2) =
static_cast<int>(idx[t * 3 + 2]);
312 if (verts.rows() > 0 && norms.rows() == verts.rows()) {
313 const QString coilPath = QCoreApplication::applicationDirPath()
314 +
"/../resources/general/coilDefinitions/coil_def.dat";
315 std::unique_ptr<FWDLIB::FwdCoilSet> templates(
320 if (m_megOnHead && !headMri.
isEmpty()) {
326 }
else if (!devHead.
isEmpty()) {
327 devToTarget = devHead;
330 Eigen::Vector3f origin = fittedOrigin;
331 if (m_megOnHead && !headMri.
isEmpty())
332 origin = applyTransform(origin, headMri);
334 std::unique_ptr<FWDLIB::FwdCoilSet> coils(templates->create_meg_coils(
337 if (coils && coils->ncoil > 0) {
339 *coils, verts, norms, origin,
340 m_evoked.info, megChNames,
344 qWarning() <<
"MEG coil definitions not found at" << coilPath;
350 if (!m_eegSurfaceKey.isEmpty() && surfaces.contains(m_eegSurfaceKey) && !eegChs.isEmpty()) {
354 if (verts.rows() > 0) {
355 Eigen::Vector3f origin = fittedOrigin;
356 if (!headMri.
isEmpty()) origin = applyTransform(origin, headMri);
358 std::unique_ptr<FWDLIB::FwdCoilSet> eegCoils(
360 eegChs, eegChs.size(), headMri));
362 if (eegCoils && eegCoils->ncoil > 0) {
364 *eegCoils, verts, origin,
365 m_evoked.info, eegChNames,
380 const Eigen::Vector3f fallback(0.0f, 0.0f, 0.04f);
387 auto gatherPoints = [&](
bool includeEeg) -> Eigen::MatrixXd {
388 QVector<Eigen::Vector3d> pts;
389 for (
const auto &dp : info.
dig) {
394 const double x = dp.r[0], y = dp.r[1], z = dp.r[2];
396 if (z < 0.0 && y > 0.0)
398 pts.append(Eigen::Vector3d(x, y, z));
402 Eigen::MatrixXd mat(pts.size(), 3);
403 for (
int i = 0; i < pts.size(); ++i)
404 mat.row(i) = pts[i].transpose();
408 Eigen::MatrixXd points = gatherPoints(
false);
409 if (points.rows() < 4)
410 points = gatherPoints(
true);
411 if (points.rows() < 4) {
412 qWarning() <<
"SensorFieldMapper::fitSphereOrigin: fewer than 4 dig "
413 "points – falling back to default origin (0, 0, 0.04).";
414 if (radius) *radius = 0.0f;
422 const int n =
static_cast<int>(points.rows());
423 Eigen::MatrixXd A(n, 4);
424 Eigen::VectorXd b(n);
425 for (
int i = 0; i < n; ++i) {
426 A(i, 0) = 2.0 * points(i, 0);
427 A(i, 1) = 2.0 * points(i, 1);
428 A(i, 2) = 2.0 * points(i, 2);
430 b(i) = points(i, 0) * points(i, 0)
431 + points(i, 1) * points(i, 1)
432 + points(i, 2) * points(i, 2);
438 Eigen::Matrix4d AtA = A.transpose() * A;
439 Eigen::Vector4d Atb = A.transpose() * b;
442 x = AtA.fullPivLu().solve(Atb);
444 const float cx =
static_cast<float>(x(0));
445 const float cy =
static_cast<float>(x(1));
446 const float cz =
static_cast<float>(x(2));
447 const float R =
static_cast<float>(
448 std::sqrt(x(0) * x(0) + x(1) * x(1) + x(2) * x(2) + x(3)));
450 if (radius) *radius = R;
452 return Eigen::Vector3f(cx, cy, cz);
462 if (!m_loaded || m_evoked.isEmpty())
465 const int nTimes =
static_cast<int>(m_evoked.data.cols());
470 auto peakGfpTime = [&](
const QVector<int> &pick) ->
int {
471 if (pick.isEmpty() || nTimes == 0)
return 0;
473 double bestSS = -1.0;
474 for (
int t = 0; t < nTimes; ++t) {
476 for (
int i = 0; i < pick.size(); ++i) {
477 double v = m_evoked.data(pick[i], t);
480 if (ss > bestSS) { bestSS = ss; best = t; }
489 if (m_megMapping && m_megMapping->rows() > 0 && !m_megPick.isEmpty()) {
490 const int tPeak = peakGfpTime(m_megPick);
491 Eigen::VectorXf meas(m_megPick.size());
492 for (
int i = 0; i < m_megPick.size(); ++i)
493 meas(i) =
static_cast<float>(m_evoked.data(m_megPick[i], tPeak));
495 Eigen::VectorXf mapped = (*m_megMapping) * meas;
496 m_megVmax = mapped.cwiseAbs().maxCoeff();
500 if (m_eegMapping && m_eegMapping->rows() > 0 && !m_eegPick.isEmpty()) {
501 const int tPeak = peakGfpTime(m_eegPick);
502 Eigen::VectorXf meas(m_eegPick.size());
503 for (
int i = 0; i < m_eegPick.size(); ++i)
504 meas(i) =
static_cast<float>(m_evoked.data(m_eegPick[i], tPeak));
506 Eigen::VectorXf mapped = (*m_eegMapping) * meas;
507 m_eegVmax = mapped.cwiseAbs().maxCoeff();
510 if (m_megVmax <= 0.0f) m_megVmax = 1.0f;
511 if (m_eegVmax <= 0.0f) m_eegVmax = 1.0f;
517 QMap<QString, std::shared_ptr<BrainSurface>> &surfaces,
519 const QVector<SubView> &subViews)
521 if (!m_loaded || m_evoked.isEmpty())
return;
524 auto applyMap = [&](
const QString &key,
525 const QString &contourPrefix,
526 const QVector<int> &pick,
527 const QSharedPointer<Eigen::MatrixXf> &mat,
531 if (key.isEmpty() || !surfaces.contains(key))
return;
533 auto surface = surfaces[key];
534 if (!visible || !mat || pick.isEmpty()) {
536 updateContourSurfaces(surfaces, contourPrefix, *surface,
537 QVector<float>(), 0.0f,
false);
540 if (mat->cols() != pick.size()) {
542 updateContourSurfaces(surfaces, contourPrefix, *surface,
543 QVector<float>(), 0.0f,
false);
548 Eigen::VectorXf meas(pick.size());
549 for (
int i = 0; i < pick.size(); ++i)
550 meas(i) =
static_cast<float>(m_evoked.data(pick[i], m_timePoint));
552 Eigen::VectorXf mapped = (*mat) * meas;
556 const float maxAbs = globalMaxAbs;
559 QVector<uint32_t> colors(mapped.size());
560 for (
int i = 0; i < mapped.size(); ++i) {
561 double norm = (mapped(i) / maxAbs) * 0.5 + 0.5;
562 norm = qBound(0.0, norm, 1.0);
564 QRgb rgb = (m_colormap ==
"MNE")
568 uint32_t r = qRed(rgb);
569 uint32_t g = qGreen(rgb);
570 uint32_t b = qBlue(rgb);
573 surface->applySourceEstimateColors(colors);
577 QVector<float> values(mapped.size());
578 for (
int i = 0; i < mapped.size(); ++i)
579 values[i] = mapped(i);
581 constexpr int nContours = 21;
582 float step = (2.0f * maxAbs) /
static_cast<float>(nContours - 1);
583 updateContourSurfaces(surfaces, contourPrefix, *surface,
584 values, step, showContours);
592 for (
int i = 0; i < subViews.size(); ++i) {
593 anyMegField |= subViews[i].visibility.megFieldMap;
594 anyEegField |= subViews[i].visibility.eegFieldMap;
595 anyMegContours |= subViews[i].visibility.megFieldContours;
596 anyEegContours |= subViews[i].visibility.eegFieldContours;
599 applyMap(m_megSurfaceKey, m_megContourPrefix,
600 m_megPick, m_megMapping,
602 anyMegField, anyMegContours);
604 applyMap(m_eegSurfaceKey, m_eegContourPrefix,
605 m_eegPick, m_eegMapping,
607 anyEegField, anyEegContours);
612void SensorFieldMapper::updateContourSurfaces(
613 QMap<QString, std::shared_ptr<BrainSurface>> &surfaces,
614 const QString &prefix,
616 const QVector<float> &values,
621 auto hideContours = [&]() {
622 for (
const auto &suffix : {QStringLiteral(
"_neg"),
623 QStringLiteral(
"_zero"),
624 QStringLiteral(
"_pos")}) {
625 const QString key = prefix + suffix;
626 if (surfaces.contains(key)) surfaces[key]->setVisible(
false);
630 if (!visible || values.isEmpty() || step <= 0.0f) {
636 float minVal = values[0], maxVal = values[0];
637 for (
int i = 1; i < values.size(); ++i) {
638 minVal = std::min(minVal, values[i]);
639 maxVal = std::max(maxVal, values[i]);
643 QVector<float> negLevels, posLevels;
644 const bool hasZero = (minVal < 0.0f && maxVal > 0.0f);
645 for (
float lv = -step; lv >= minVal; lv -= step) negLevels.append(lv);
646 for (
float lv = step; lv <= maxVal; lv += step) posLevels.append(lv);
650 QVector<Eigen::Vector3f> verts;
651 QVector<Eigen::Vector3f> norms;
652 QVector<Eigen::Vector3i> tris;
655 auto addSegment = [](ContourBuf &buf,
656 const QVector3D &p0,
const QVector3D &p1,
657 const QVector3D &normal,
658 float halfW,
float shift) {
659 QVector3D dir = p1 - p0;
660 const float len = dir.length();
661 if (len < 1e-6f)
return;
664 QVector3D binormal = QVector3D::crossProduct(normal, dir);
665 if (binormal.length() < 1e-6f)
666 binormal = QVector3D::crossProduct(QVector3D(0, 1, 0), dir);
667 if (binormal.length() < 1e-6f)
668 binormal = QVector3D::crossProduct(QVector3D(1, 0, 0), dir);
669 binormal.normalize();
671 const QVector3D off = normal * shift;
673 auto toEig = [](
const QVector3D &v) {
674 return Eigen::Vector3f(v.x(), v.y(), v.z());
679 const QVector3D w = binormal * halfW;
680 const int base = buf.verts.size();
681 Eigen::Vector3f n(normal.x(), normal.y(), normal.z());
683 buf.verts.append(toEig(p0 - w + off));
684 buf.verts.append(toEig(p0 + w + off));
685 buf.verts.append(toEig(p1 - w + off));
686 buf.verts.append(toEig(p1 + w + off));
687 buf.norms.append(n); buf.norms.append(n);
688 buf.norms.append(n); buf.norms.append(n);
689 buf.tris.append(Eigen::Vector3i(base, base + 1, base + 2));
690 buf.tris.append(Eigen::Vector3i(base + 1, base + 3, base + 2));
695 const QVector3D h = normal * halfW;
696 const int base = buf.verts.size();
697 Eigen::Vector3f n(binormal.x(), binormal.y(), binormal.z());
699 buf.verts.append(toEig(p0 - h + off));
700 buf.verts.append(toEig(p0 + h + off));
701 buf.verts.append(toEig(p1 - h + off));
702 buf.verts.append(toEig(p1 + h + off));
703 buf.norms.append(n); buf.norms.append(n);
704 buf.norms.append(n); buf.norms.append(n);
705 buf.tris.append(Eigen::Vector3i(base, base + 1, base + 2));
706 buf.tris.append(Eigen::Vector3i(base + 1, base + 3, base + 2));
711 auto buildContours = [&](
const QVector<float> &levels, ContourBuf &buf) {
715 if (rr.rows() == 0 || nn.rows() == 0 || idx.isEmpty())
return;
717 constexpr float shift = 0.001f;
718 constexpr float halfW = 0.0005f;
720 for (
float level : levels) {
721 for (
int t = 0; t + 2 < idx.size(); t += 3) {
722 const int i0 = idx[t], i1 = idx[t + 1], i2 = idx[t + 2];
723 const float v0 = values[i0], v1 = values[i1], v2 = values[i2];
725 QVector3D p0(rr(i0, 0), rr(i0, 1), rr(i0, 2));
726 QVector3D p1(rr(i1, 0), rr(i1, 1), rr(i1, 2));
727 QVector3D p2(rr(i2, 0), rr(i2, 1), rr(i2, 2));
729 QVector3D n0(nn(i0, 0), nn(i0, 1), nn(i0, 2));
730 QVector3D n1(nn(i1, 0), nn(i1, 1), nn(i1, 2));
731 QVector3D n2(nn(i2, 0), nn(i2, 1), nn(i2, 2));
732 QVector3D triN = (n0 + n1 + n2).normalized();
733 if (triN.length() < 1e-6f)
734 triN = QVector3D::crossProduct(p1 - p0, p2 - p0).normalized();
736 QVector<QVector3D> hits;
737 auto checkEdge = [&](
const QVector3D &a,
const QVector3D &b,
738 float va,
float vb) {
739 if (va == vb)
return;
740 float tval = (level - va) / (vb - va);
741 if (tval >= 0.0f && tval < 1.0f)
742 hits.append(a + (b - a) * tval);
744 checkEdge(p0, p1, v0, v1);
745 checkEdge(p1, p2, v1, v2);
746 checkEdge(p2, p0, v2, v0);
748 if (hits.size() == 2)
749 addSegment(buf, hits[0], hits[1], triN, halfW, shift);
754 ContourBuf negBuf, posBuf, zeroBuf;
755 buildContours(negLevels, negBuf);
756 buildContours(posLevels, posBuf);
758 QVector<float> zeroLevels = {0.0f};
759 buildContours(zeroLevels, zeroBuf);
763 auto updateSurf = [&](
const QString &suffix,
764 const ContourBuf &buf,
767 const QString key = prefix + suffix;
768 if (!show || buf.verts.isEmpty()) {
769 if (surfaces.contains(key)) surfaces[key]->setVisible(
false);
773 Eigen::MatrixX3f rr(buf.verts.size(), 3);
774 Eigen::MatrixX3f nn(buf.norms.size(), 3);
775 Eigen::MatrixX3i tris(buf.tris.size(), 3);
776 for (
int i = 0; i < buf.verts.size(); ++i) {
777 rr.row(i) = buf.verts[i];
778 nn.row(i) = buf.norms[i];
780 for (
int i = 0; i < buf.tris.size(); ++i)
781 tris.row(i) = buf.tris[i];
783 std::shared_ptr<BrainSurface> csurf;
784 if (surfaces.contains(key)) {
785 csurf = surfaces[key];
787 csurf = std::make_shared<BrainSurface>();
788 surfaces[key] = csurf;
790 csurf->createFromData(rr, nn, tris, color);
791 csurf->setVisible(
true);
794 updateSurf(
"_neg", negBuf, QColor(0, 0, 255, 200), visible && !negBuf.verts.isEmpty());
795 updateSurf(
"_zero", zeroBuf, QColor(0, 0, 0, 220), visible && !zeroBuf.verts.isEmpty());
796 updateSurf(
"_pos", posBuf, QColor(255, 0, 0, 200), visible && !posBuf.verts.isEmpty());
ColorMap class declaration.
#define FIFFV_POINT_EXTRA
#define FIFFV_COORD_DEVICE
FiffChInfo class declaration.
FiffCoordTrans class declaration.
SensorFieldMapper class declaration.
BrainSurface class declaration.
QRgb mneAnalyzeColor(double v)
Lightweight render-related enums shared across the disp3D_rhi library.
uint32_t packABGR(uint32_t r, uint32_t g, uint32_t b, uint32_t a=0xFF)
Surface class declaration.
Sphere-model field mapping.
#define FWD_COIL_ACCURACY_NORMAL
FwdCoilSet class declaration.
FIFF file I/O and data structures (raw, epochs, evoked, covariance, forward).
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 vertexNormals() const
QVector< uint32_t > triangleIndices() const
Eigen::MatrixX3f vertexPositions() const
static constexpr VisualizationMode ModeSurface
static QString findHeadSurfaceKey(const QMap< QString, std::shared_ptr< BrainSurface > > &surfaces)
static float contourStep(float minVal, float maxVal, int targetTicks)
bool hasMappingFor(const FIFFLIB::FiffEvoked &newEvoked) const
const FIFFLIB::FiffEvoked & evoked() const
static Eigen::Vector3f fitSphereOrigin(const FIFFLIB::FiffInfo &info, float *radius=nullptr)
bool buildMapping(const QMap< QString, std::shared_ptr< BrainSurface > > &surfaces, const FIFFLIB::FiffCoordTrans &headToMriTrans, bool applySensorTrans)
static QString findHelmetSurfaceKey(const QMap< QString, std::shared_ptr< BrainSurface > > &surfaces)
void setEvoked(const FIFFLIB::FiffEvoked &evoked)
void apply(QMap< QString, std::shared_ptr< BrainSurface > > &surfaces, const SubView &singleView, const QVector< SubView > &subViews)
Coordinate transformation description.
Eigen::Matrix< float, 4, 4, Eigen::DontAlign > trans
FIFF measurement file information.
QList< FiffDigPoint > dig
FiffCoordTrans dev_head_t
static Eigen::MatrixX3f compute_normals(const Eigen::MatrixX3f &rr, const Eigen::MatrixX3i &tris)
static FwdCoilSet * create_eeg_els(const QList< FIFFLIB::FiffChInfo > &chs, int nch, const FIFFLIB::FiffCoordTrans &t=FIFFLIB::FiffCoordTrans())
static FwdCoilSet * read_coil_defs(const QString &name)
static QSharedPointer< Eigen::MatrixXf > computeEegMapping(const FwdCoilSet &coils, const Eigen::MatrixX3f &vertices, const Eigen::Vector3f &origin, float intrad=0.06f, float miss=1e-3f)
static QSharedPointer< 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)
static FiffCoordTrans combine(int from, int to, const FiffCoordTrans &t1, const FiffCoordTrans &t2)
Eigen::MatrixX3f apply_trans(const Eigen::MatrixX3f &rr, bool do_move=true) const