37 m_deviceToWorld.setToIdentity();
38 m_headToWorld.setToIdentity();
39 m_headToDevice.setToIdentity();
46 m_trackerStation = station;
51 m_penStation = station;
56 m_probeStation = station;
62 const QQuaternion& rotation)
64 m_offsetTranslation = translation;
65 m_offsetRotation = rotation;
73 disconnect(m_pConn,
nullptr,
this,
nullptr);
78 this, &PolhemusCoregistration::onPointReceived);
80 this, &PolhemusCoregistration::onPenButtonPressedFromConn);
92 static const char* labels[] = {
nullptr,
"LPA",
"NAS",
"RPA"};
93 const int ident =
static_cast<int>(id);
96 m_penFid[ident] = m_penPosition;
97 m_hasPenFid[ident] =
true;
99 qInfo().nospace() <<
"Fiducial captured: " << labels[ident]
100 <<
" (" << m_penPosition.x() * 1000.f <<
", "
101 << m_penPosition.y() * 1000.f <<
", "
102 << m_penPosition.z() * 1000.f <<
") mm";
105 for (
int j = 1; j <= 3; ++j) {
106 if (j != ident && m_hasPenFid[j]) {
107 float dist = (m_penFid[ident] - m_penFid[j]).length() * 1000.0f;
108 qInfo().nospace() <<
" " << labels[ident] <<
" ↔ " << labels[j]
109 <<
": " << dist <<
" mm"
110 << (dist > 200.0f ?
" *** WARNING: > 200 mm!" :
"");
114 m_pPoints->removeFiducial(
id);
118 dp.
label = QString::fromLatin1(labels[ident]);
121 m_pPoints->append(dp);
135 dp.
label = QStringLiteral(
"HSP-%1").arg(n);
138 m_pPoints->append(dp);
146 m_registrationValid =
false;
147 m_headToWorld.setToIdentity();
148 m_headToDevice.setToIdentity();
149 m_worldToModel.setToIdentity();
150 m_hasPenVertex =
false;
151 for (
int i = 0; i < 4; ++i)
152 m_hasPenFid[i] =
false;
160 const int i =
static_cast<int>(id);
161 m_modelFid[i] = posInModel;
162 m_hasModelFid[i] =
true;
167 return m_hasModelFid[
static_cast<int>(id)];
172 return m_hasModelFid[1] && m_hasModelFid[2] && m_hasModelFid[3];
177 return m_modelFid[
static_cast<int>(id)];
182 return m_hasPenFid[1] && m_hasPenFid[2] && m_hasPenFid[3];
191 m_penVertex = m_penPosition;
192 m_hasPenVertex =
true;
193 qInfo() <<
"Captured pen vertex (CZ) at" << m_penVertex * 1000.0f <<
"mm";
196 emit m_pPoints->pointsChanged();
214 const float dNL = (pNas - pLpa).length() * 1000.0f;
215 const float dNR = (pNas - pRpa).length() * 1000.0f;
216 const float dLR = (pLpa - pRpa).length() * 1000.0f;
217 const float minSpread = std::min({dNL, dNR, dLR});
218 if (minSpread < 20.0f) {
219 qWarning() <<
"Registration failed: pen fiducials too close"
220 <<
"(min spread" << minSpread <<
"mm, need > 20 mm)";
221 m_registrationValid =
false;
243 const float mNL = (mNas - mLpa).length() * 1000.0f;
244 const float mNR = (mNas - mRpa).length() * 1000.0f;
245 const float mLR = (mLpa - mRpa).length() * 1000.0f;
247 const float maxRatio = std::max({dNL / mNL, dNR / mNR, dLR / mLR});
248 const float minRatio = std::min({dNL / mNL, dNR / mNR, dLR / mLR});
249 if (maxRatio / minRatio > 2.0f) {
250 qWarning() <<
"Registration: shape mismatch — pen fiducial triangle"
251 <<
"has very different proportions from model."
252 <<
"Check fiducial placement!";
258 const Eigen::Vector3d penC(
259 (pNas.x() + pLpa.x() + pRpa.x()) / 3.0,
260 (pNas.y() + pLpa.y() + pRpa.y()) / 3.0,
261 (pNas.z() + pLpa.z() + pRpa.z()) / 3.0);
262 const Eigen::Vector3d modC(
263 (mNas.x() + mLpa.x() + mRpa.x()) / 3.0,
264 (mNas.y() + mLpa.y() + mRpa.y()) / 3.0,
265 (mNas.z() + mLpa.z() + mRpa.z()) / 3.0);
268 Eigen::Matrix3d P, Q;
269 P.col(0) = Eigen::Vector3d(pNas.x(), pNas.y(), pNas.z()) - penC;
270 P.col(1) = Eigen::Vector3d(pLpa.x(), pLpa.y(), pLpa.z()) - penC;
271 P.col(2) = Eigen::Vector3d(pRpa.x(), pRpa.y(), pRpa.z()) - penC;
273 Q.col(0) = Eigen::Vector3d(mNas.x(), mNas.y(), mNas.z()) - modC;
274 Q.col(1) = Eigen::Vector3d(mLpa.x(), mLpa.y(), mLpa.z()) - modC;
275 Q.col(2) = Eigen::Vector3d(mRpa.x(), mRpa.y(), mRpa.z()) - modC;
278 const Eigen::Matrix3d H = P * Q.transpose();
281 Eigen::JacobiSVD<Eigen::Matrix3d>
svd(H, Eigen::ComputeFullU | Eigen::ComputeFullV);
282 const Eigen::Matrix3d U =
svd.matrixU();
283 const Eigen::Matrix3d V =
svd.matrixV();
286 const double d = (V * U.transpose()).determinant();
287 Eigen::Matrix3d D = Eigen::Matrix3d::Identity();
288 D(2, 2) = (d > 0.0) ? 1.0 : -1.0;
289 Eigen::Matrix3d
R = V * D * U.transpose();
292 Eigen::Vector3d t = modC -
R * penC;
304 if (m_hasPenVertex && m_hasModelVertex) {
305 const Eigen::Vector3d penCZ(m_penVertex.x(), m_penVertex.y(), m_penVertex.z());
306 const Eigen::Vector3d modCZ(m_modelVertex.x(), m_modelVertex.y(), m_modelVertex.z());
308 const Eigen::Vector3d mappedCZ =
R * penCZ + t;
309 const double errCurrent = (mappedCZ - modCZ).norm();
312 Eigen::Matrix3d D_alt = D;
313 D_alt(2, 2) = -D(2, 2);
314 const Eigen::Matrix3d R_alt = V * D_alt * U.transpose();
315 const Eigen::Vector3d t_alt = modC - R_alt * penC;
316 const Eigen::Vector3d mappedCZ_alt = R_alt * penCZ + t_alt;
317 const double errAlt = (mappedCZ_alt - modCZ).norm();
319 if (errAlt < errCurrent) {
327 m_worldToModel.setToIdentity();
328 for (
int r = 0; r < 3; ++r) {
329 for (
int c = 0; c < 3; ++c) {
330 m_worldToModel(r, c) =
static_cast<float>(
R(r, c));
332 m_worldToModel(r, 3) =
static_cast<float>(t(r));
335 m_headToWorld.setToIdentity();
336 m_headToDevice = m_deviceToWorld.inverted() * m_headToWorld;
337 m_registrationValid =
true;
338 qInfo() <<
"Registration succeeded (analytical paired).";
344 const QMatrix4x4 headFrame = buildHeadFrame();
345 const QVector3D ez(headFrame(0, 2), headFrame(1, 2), headFrame(2, 2));
346 if (ez.length() < 1e-6f) {
350 m_headToWorld = headFrame;
351 m_headToDevice = m_deviceToWorld.inverted() * m_headToWorld;
352 m_worldToModel.setToIdentity();
353 m_registrationValid =
true;
354 qInfo() <<
"Registration succeeded (head-frame fallback).";
361void PolhemusCoregistration::onPointReceived(
int station,
362 const QVector3D& position,
363 const QQuaternion& orientation)
366 const QVector3D pos(m_mirrorX ? -position.x() : position.x(),
367 m_mirrorY ? -position.y() : position.y(),
370 if (station == m_trackerStation) {
371 m_deviceToWorld = buildDevicePose(pos, orientation);
373 }
else if (station == m_penStation) {
377 const float sinEl = 2.0f * (orientation.scalar() * orientation.y() - orientation.x() * orientation.z());
378 constexpr float kGimbalSinEl = 0.9848f;
379 const bool gimbalLock = (std::abs(sinEl) > kGimbalSinEl);
382 const QVector3D tipAdj = m_tipOffsetEnabled
383 ? orientation.rotatedVector(m_penTipOffset)
385 m_penPosition = pos + tipAdj;
386 m_penOrientation = orientation;
395 constexpr float kMinAngleDeg = 3.0f;
396 constexpr float kMaxPosJumpM = 0.05f;
397 constexpr float kGimbalSinEl2 = 0.9848f;
400 const float sinEl2 = 2.0f * (orientation.scalar() * orientation.y() - orientation.x() * orientation.z());
401 if (std::abs(sinEl2) > kGimbalSinEl2) {
404 bool accept = m_pivotOrientations.empty();
406 const QQuaternion& prev = m_pivotOrientations.back();
407 float dot = std::abs(QQuaternion::dotProduct(prev, orientation));
408 float angleDeg = 2.0f * std::acos(std::min(dot, 1.0f)) * (180.0f / 3.14159265f);
409 float posDelta = (pos - m_pivotPositions.back()).length();
410 accept = (angleDeg >= kMinAngleDeg) && (posDelta < kMaxPosJumpM);
413 m_pivotPositions.push_back(pos);
414 m_pivotOrientations.push_back(orientation);
417 float spanDeg = 0.0f;
418 if (m_pivotOrientations.size() > 1) {
420 const auto& first = m_pivotOrientations.front();
421 for (
size_t k = 1; k < m_pivotOrientations.size(); ++k) {
422 float d = std::abs(QQuaternion::dotProduct(first, m_pivotOrientations[k]));
426 spanDeg = 2.0f * std::acos(std::min(minDot, 1.0f)) * (180.0f / 3.14159265f);
434 }
else if (station == m_probeStation) {
435 m_probePosition = pos;
436 m_probeOrientation = orientation;
437 m_haveProbePos =
true;
442void PolhemusCoregistration::onPenButtonPressedFromConn(
int station,
443 const QVector3D& position,
444 const QQuaternion& orientation)
446 if (station == m_penStation) {
448 const QVector3D pos(m_mirrorX ? -position.x() : position.x(),
449 m_mirrorY ? -position.y() : position.y(),
452 const QVector3D tipAdj = m_tipOffsetEnabled
453 ? orientation.rotatedVector(m_penTipOffset)
455 m_penPosition = pos + tipAdj;
456 m_penOrientation = orientation;
462 m_pivotPositions.clear();
463 m_pivotOrientations.clear();
464 qInfo() <<
"Pivot calibration: collecting — pivot the pen around its tip";
469 solvePivotCalibration();
479QMatrix4x4 PolhemusCoregistration::buildDevicePose(
const QVector3D& trackerPos,
480 const QQuaternion& trackerOri)
const
482 QMatrix4x4 trackerToWorld;
483 trackerToWorld.setToIdentity();
484 trackerToWorld.translate(trackerPos);
485 trackerToWorld.rotate(trackerOri);
488 offset.setToIdentity();
489 offset.translate(m_offsetTranslation);
490 offset.rotate(m_offsetRotation);
492 return trackerToWorld * offset;
495QMatrix4x4 PolhemusCoregistration::buildHeadFrame()
const
501 const QVector3D origin = (lpa + rpa) * 0.5f;
502 const QVector3D ex = (nas - origin).normalized();
503 const QVector3D eyApprox = (lpa - origin).normalized();
505 const QVector3D ey = (eyApprox - QVector3D::dotProduct(eyApprox, ex) * ex).normalized();
506 const QVector3D ez = QVector3D::crossProduct(ex, ey).normalized();
509 frame.setToIdentity();
510 frame(0, 0) = ex.x();
511 frame(0, 1) = ey.x();
512 frame(0, 2) = ez.x();
513 frame(0, 3) = origin.x();
514 frame(1, 0) = ex.y();
515 frame(1, 1) = ey.y();
516 frame(1, 2) = ez.y();
517 frame(1, 3) = origin.y();
518 frame(2, 0) = ex.z();
519 frame(2, 1) = ey.z();
520 frame(2, 2) = ez.z();
521 frame(2, 3) = origin.z();
535 m_pivotPositions.clear();
536 m_pivotOrientations.clear();
537 m_pivotResidualMm = 0.0f;
539 qInfo() <<
"Pivot calibration: press stylus button to begin collecting";
545 m_pivotPositions.clear();
546 m_pivotOrientations.clear();
551bool PolhemusCoregistration::solvePivotCalibration()
553 const int N =
static_cast<int>(m_pivotPositions.size());
555 qWarning() <<
"Pivot calibration: only" << N <<
"samples, need at least 10";
564 Eigen::MatrixXd A(3 * N, 6);
565 Eigen::VectorXd b(3 * N);
567 for (
int i = 0; i < N; ++i) {
568 const QVector3D& p = m_pivotPositions[
static_cast<size_t>(i)];
569 const QQuaternion& q = m_pivotOrientations[
static_cast<size_t>(i)];
572 const QMatrix3x3 rm = q.toRotationMatrix();
574 const int row = 3 * i;
575 for (
int r = 0; r < 3; ++r) {
576 for (
int c = 0; c < 3; ++c) {
577 A(row + r, c) =
static_cast<double>(rm(r, c));
578 A(row + r, 3 + c) = (r == c) ? -1.0 : 0.0;
581 b(row + 0) = -
static_cast<double>(p.x());
582 b(row + 1) = -
static_cast<double>(p.y());
583 b(row + 2) = -
static_cast<double>(p.z());
587 Eigen::JacobiSVD<Eigen::MatrixXd>
svd(A, Eigen::ComputeThinU | Eigen::ComputeThinV);
590 const auto& sv =
svd.singularValues();
591 double cond = sv(0) / sv(sv.size() - 1);
593 qWarning() <<
"Pivot calibration: ill-conditioned (cond =" << cond
594 <<
"). Pivot the pen through a wider range of angles.";
600 Eigen::VectorXd x =
svd.solve(b);
602 QVector3D offset(
static_cast<float>(x(0)),
603 static_cast<float>(x(1)),
604 static_cast<float>(x(2)));
605 QVector3D tipPos(
static_cast<float>(x(3)),
606 static_cast<float>(x(4)),
607 static_cast<float>(x(5)));
611 for (
int i = 0; i < N; ++i) {
612 const QVector3D& p = m_pivotPositions[
static_cast<size_t>(i)];
613 const QQuaternion& q = m_pivotOrientations[
static_cast<size_t>(i)];
614 QVector3D computed = p + q.rotatedVector(offset);
615 float err = (computed - tipPos).length();
616 sumSq +=
static_cast<double>(err) *
static_cast<double>(err);
618 float rms =
static_cast<float>(std::sqrt(sumSq /
static_cast<double>(N))) * 1000.0f;
619 m_pivotResidualMm = rms;
621 qInfo() <<
"Pivot calibration:" << N <<
"samples, cond =" << cond
622 <<
", offset =" << offset.x() * 1000.0f << offset.y() * 1000.0f
623 << offset.z() * 1000.0f <<
"mm, RMS residual =" << rms <<
"mm";
625 m_penTipOffset = offset;
639 qWarning() <<
"Optical calibration: no pen position available";
643 const QMatrix4x4& dev = m_deviceToWorld;
644 if (dev.isIdentity()) {
645 qWarning() <<
"Optical calibration: no tracker data available";
653 QMatrix4x4 offsetInv;
654 offsetInv.setToIdentity();
655 offsetInv.translate(m_offsetTranslation);
656 offsetInv.rotate(m_offsetRotation);
657 offsetInv = offsetInv.inverted();
659 const QMatrix4x4 trackerToWorld = dev * offsetInv;
660 const QVector3D trackerPos(trackerToWorld(0, 3), trackerToWorld(1, 3), trackerToWorld(2, 3));
661 const QQuaternion trackerOri = QQuaternion::fromRotationMatrix(trackerToWorld.toGenericMatrix<3, 3>());
667 m_opticalCalibSamples.push_back(sample);
669 const int n =
static_cast<int>(m_opticalCalibSamples.size());
670 const QVector3D localFocus = trackerOri.inverted().rotatedVector(m_penPosition - trackerPos);
671 const float distToTracker = localFocus.length() * 1000.0f;
674 <<
"Optical calibration sample " << n <<
":"
675 <<
"\n tracker pos: (" << trackerPos.x() * 1000.f <<
", " << trackerPos.y() * 1000.f <<
", " << trackerPos.z() * 1000.f <<
") mm"
676 <<
"\n focus point: (" << m_penPosition.x() * 1000.f <<
", " << m_penPosition.y() * 1000.f <<
", " << m_penPosition.z() * 1000.f <<
") mm"
677 <<
"\n focus (local): (" << localFocus.x() * 1000.f <<
", " << localFocus.y() * 1000.f <<
", " << localFocus.z() * 1000.f <<
") mm"
678 <<
"\n distance tracker\u2194focus: " << distToTracker <<
" mm";
684 const auto& prev = m_opticalCalibSamples[
static_cast<size_t>(n - 2)];
685 const QVector3D prevLocal = prev.trackerOri.inverted().rotatedVector(prev.focusPoint - prev.trackerPos);
686 const QVector3D curDir = localFocus.normalized();
687 const QVector3D prevDir = prevLocal.normalized();
688 const float dot = std::clamp(QVector3D::dotProduct(curDir, prevDir), -1.0f, 1.0f);
689 const float angleDeg = std::acos(dot) * (180.0f / 3.14159265f);
690 const float prevDist = prevLocal.length() * 1000.0f;
692 <<
" convergence angle (sample " << n - 1 <<
"\u2194" << n <<
"): " << angleDeg <<
"\u00b0"
693 <<
" (distances: " << prevDist <<
" / " << distToTracker <<
" mm)";
701 m_opticalCalibSamples.clear();
702 m_opticalCalibValid =
false;
703 m_opticalCalibResidualMm = 0.0f;
704 m_opticalCalibDepthSpreadMm = 0.0f;
713 qWarning() <<
"Objective center capture: no pen position available";
717 const QMatrix4x4& dev = m_deviceToWorld;
718 if (dev.isIdentity()) {
719 qWarning() <<
"Objective center capture: no tracker data available";
724 QMatrix4x4 offsetInv;
725 offsetInv.setToIdentity();
726 offsetInv.translate(m_offsetTranslation);
727 offsetInv.rotate(m_offsetRotation);
728 offsetInv = offsetInv.inverted();
730 const QMatrix4x4 trackerToWorld = dev * offsetInv;
731 const QVector3D trackerPos(trackerToWorld(0, 3), trackerToWorld(1, 3), trackerToWorld(2, 3));
732 const QQuaternion trackerOri = QQuaternion::fromRotationMatrix(trackerToWorld.toGenericMatrix<3, 3>());
735 m_objectiveCenterLocal = trackerOri.inverted().rotatedVector(m_penPosition - trackerPos);
736 m_hasObjectiveCenter =
true;
738 const float distMm = m_objectiveCenterLocal.length() * 1000.0f;
740 <<
"Objective center captured (tracker-local): ("
741 << m_objectiveCenterLocal.x() * 1000.f <<
", "
742 << m_objectiveCenterLocal.y() * 1000.f <<
", "
743 << m_objectiveCenterLocal.z() * 1000.f <<
") mm"
744 <<
", distance from tracker: " << distMm <<
" mm";
751 m_objectiveCenterLocal = QVector3D();
752 m_hasObjectiveCenter =
false;
757 const int N =
static_cast<int>(m_opticalCalibSamples.size());
759 qWarning() <<
"Optical calibration: need at least 2 samples, have" << N;
764 std::vector<Eigen::Vector3d> localPoints(
static_cast<size_t>(N));
765 for (
int i = 0; i < N; ++i) {
766 const auto& s = m_opticalCalibSamples[
static_cast<size_t>(i)];
767 const QVector3D local = s.trackerOri.inverted().rotatedVector(s.focusPoint - s.trackerPos);
768 localPoints[
static_cast<size_t>(i)] = Eigen::Vector3d(local.x(), local.y(), local.z());
772 Eigen::Vector3d centroid = Eigen::Vector3d::Zero();
773 for (
const auto& p : localPoints)
775 centroid /=
static_cast<double>(N);
777 Eigen::MatrixXd centered(3, N);
778 for (
int i = 0; i < N; ++i) {
779 centered.col(i) = localPoints[
static_cast<size_t>(i)] - centroid;
782 Eigen::JacobiSVD<Eigen::MatrixXd>
svd(centered, Eigen::ComputeThinU);
783 Eigen::Vector3d axisDir =
svd.matrixU().col(0);
784 if (centroid.dot(axisDir) < 0.0)
787 const double t0 = -centroid.dot(axisDir);
788 Eigen::Vector3d opticalCenter = centroid + t0 * axisDir;
800 const double knownDist =
static_cast<double>(m_knownTrackerToObjectiveDist);
801 if (m_hasObjectiveCenter) {
803 opticalCenter = Eigen::Vector3d(m_objectiveCenterLocal.x(),
804 m_objectiveCenterLocal.y(),
805 m_objectiveCenterLocal.z());
807 Eigen::MatrixXd rays(3, N);
808 for (
int i = 0; i < N; ++i)
809 rays.col(i) = localPoints[
static_cast<size_t>(i)] - opticalCenter;
811 Eigen::JacobiSVD<Eigen::MatrixXd> raySvd(rays, Eigen::ComputeThinU);
812 axisDir = raySvd.matrixU().col(0);
814 Eigen::Vector3d meanRay = Eigen::Vector3d::Zero();
815 for (
int i = 0; i < N; ++i)
816 meanRay += rays.col(i);
817 if (meanRay.dot(axisDir) < 0.0)
821 <<
"Optical calibration: using directly captured objective center, |O|="
822 << opticalCenter.norm() * 1000.0 <<
" mm";
823 }
else if (knownDist > 0.0 && N >= 2) {
826 double R = knownDist;
827 Eigen::Vector3d O = opticalCenter;
828 const double along = centroid.dot(axisDir);
829 const double disc = along * along - centroid.squaredNorm() +
R *
R;
831 O = centroid + (-along + std::sqrt(disc)) * axisDir;
832 }
else if (O.norm() > 1e-9) {
833 O = O * (
R / O.norm());
835 O = centroid.normalized() *
R;
839 for (
int iter = 0; iter < 30; ++iter) {
841 Eigen::MatrixXd rays(3, N);
842 for (
int i = 0; i < N; ++i)
843 rays.col(i) = localPoints[
static_cast<size_t>(i)] - O;
845 Eigen::JacobiSVD<Eigen::MatrixXd> raySvd(rays, Eigen::ComputeThinU);
846 Eigen::Vector3d d = raySvd.matrixU().col(0);
854 Eigen::Vector3d grad = Eigen::Vector3d::Zero();
856 for (
int i = 0; i < N; ++i) {
857 Eigen::Vector3d v = localPoints[
static_cast<size_t>(i)] - O;
858 Eigen::Vector3d perp = v - v.dot(d) * d;
859 cost += perp.squaredNorm();
864 Eigen::Vector3d normal = O.normalized();
865 Eigen::Vector3d tangentGrad = grad - grad.dot(normal) * normal;
867 double gradNorm = tangentGrad.norm();
868 if (gradNorm < 1e-12)
872 double step = 0.01 *
R / gradNorm;
873 for (
int ls = 0; ls < 10; ++ls) {
874 Eigen::Vector3d candidate = O - step * tangentGrad;
876 candidate = candidate.normalized() *
R;
879 Eigen::MatrixXd cRays(3, N);
880 for (
int i = 0; i < N; ++i)
881 cRays.col(i) = localPoints[
static_cast<size_t>(i)] - candidate;
882 Eigen::JacobiSVD<Eigen::MatrixXd> cSvd(cRays, Eigen::ComputeThinU);
883 Eigen::Vector3d cd = cSvd.matrixU().col(0);
886 for (
int i = 0; i < N; ++i) {
887 Eigen::Vector3d v = localPoints[
static_cast<size_t>(i)] - candidate;
888 Eigen::Vector3d perp = v - v.dot(cd) * cd;
889 cCost += perp.squaredNorm();
903 Eigen::MatrixXd finalRays(3, N);
904 for (
int i = 0; i < N; ++i)
905 finalRays.col(i) = localPoints[
static_cast<size_t>(i)] - opticalCenter;
906 Eigen::JacobiSVD<Eigen::MatrixXd> finalSvd(finalRays, Eigen::ComputeThinU);
907 axisDir = finalSvd.matrixU().col(0);
908 if (opticalCenter.dot(axisDir) < 0.0)
912 <<
"Optical calibration: constrained refinement (R="
913 << knownDist * 1000.0 <<
" mm) converged, |O|="
914 << opticalCenter.norm() * 1000.0 <<
" mm";
919 for (
const auto& p : localPoints) {
920 const Eigen::Vector3d diff = p - opticalCenter;
921 const double along = diff.dot(axisDir);
922 const double perpSq = (diff - along * axisDir).squaredNorm();
925 const double rms = std::sqrt(sumSq /
static_cast<double>(N)) * 1000.0;
928 double minDepth = 1e30, maxDepth = -1e30;
929 for (
const auto& p : localPoints) {
930 const double depth = (p - opticalCenter).dot(axisDir);
931 minDepth = std::min(minDepth, depth);
932 maxDepth = std::max(maxDepth, depth);
934 const double depthSpreadMm = (maxDepth - minDepth) * 1000.0;
936 m_opticalAxisLocal = QVector3D(
static_cast<float>(axisDir.x()),
937 static_cast<float>(axisDir.y()),
938 static_cast<float>(axisDir.z()));
939 m_opticalCenterLocal = QVector3D(
static_cast<float>(opticalCenter.x()),
940 static_cast<float>(opticalCenter.y()),
941 static_cast<float>(opticalCenter.z()));
942 m_opticalCalibResidualMm =
static_cast<float>(rms);
943 m_opticalCalibDepthSpreadMm =
static_cast<float>(depthSpreadMm);
944 m_opticalCalibValid =
true;
946 const float distMm = m_opticalCenterLocal.length() * 1000.0f;
949 <<
"Optical calibration solved (" << N <<
" samples"
950 << (knownDist > 0.0 ?
", constrained R=" + QString::number(knownDist * 1000.0,
'f', 1) +
" mm" : QString()) <<
"):"
951 <<
"\n axis (local): (" << m_opticalAxisLocal.x() <<
", " << m_opticalAxisLocal.y() <<
", " << m_opticalAxisLocal.z() <<
")"
952 <<
"\n center (local): (" << m_opticalCenterLocal.x() * 1000.f <<
", " << m_opticalCenterLocal.y() * 1000.f <<
", " << m_opticalCenterLocal.z() * 1000.f <<
") mm"
953 <<
"\n tracker\u2194center: " << distMm <<
" mm"
954 <<
"\n RMS residual: " << rms <<
" mm"
955 <<
"\n depth spread: " << depthSpreadMm <<
" mm (range " << minDepth * 1000.0 <<
" .. " << maxDepth * 1000.0 <<
" mm)";
961 qInfo() <<
" Per-sample convergence angles (tracker\u2192focus vs optical axis):";
962 for (
int i = 0; i < N; ++i) {
963 const Eigen::Vector3d& p = localPoints[
static_cast<size_t>(i)];
964 const Eigen::Vector3d trkToFocus = p.normalized();
965 const double cosAngle = std::clamp(trkToFocus.dot(axisDir), -1.0, 1.0);
966 const double angleDeg = std::acos(cosAngle) * (180.0 / 3.14159265358979);
967 const double depth = (p - opticalCenter).dot(axisDir);
968 const Eigen::Vector3d diff = p - opticalCenter;
969 const double perpDist = (diff - diff.dot(axisDir) * axisDir).norm() * 1000.0;
971 <<
" sample " << (i + 1) <<
": depth=" << depth * 1000.0
972 <<
" mm, convergence angle=" << angleDeg <<
"\u00b0"
973 <<
", perp err=" << perpDist <<
" mm";
976 if (depthSpreadMm < 50.0) {
977 qWarning() <<
"Optical calibration: depth spread is only" << depthSpreadMm
978 <<
"mm. For a reliable axis direction, move the OPMI to vary the"
979 <<
"focal distance by at least 50 mm between samples.";
983 qWarning() <<
"Optical calibration: RMS residual" << rms
984 <<
"mm is large. Check that the stylus accurately marks the microscope focus point.";
989 if (knownDist <= 0.0) {
990 constexpr float kMinPlausibleMm = 100.0f;
991 constexpr float kMaxPlausibleMm = 500.0f;
992 if (distMm < kMinPlausibleMm || distMm > kMaxPlausibleMm) {
994 <<
"Optical calibration: tracker\u2194axis distance " << distMm
995 <<
" mm is outside the plausible range [" << kMinPlausibleMm
996 <<
", " << kMaxPlausibleMm <<
"] mm. Calibration data may be unreliable.";
1006 if (!m_opticalCalibValid)
1009 const QMatrix4x4& dev = m_deviceToWorld;
1010 if (dev.isIdentity())
1014 QMatrix4x4 offsetMat;
1015 offsetMat.setToIdentity();
1016 offsetMat.translate(m_offsetTranslation);
1017 offsetMat.rotate(m_offsetRotation);
1019 const QMatrix4x4 trackerToWorld = dev * offsetMat.inverted();
1020 const QVector3D trackerPos(trackerToWorld(0, 3), trackerToWorld(1, 3), trackerToWorld(2, 3));
1021 const QQuaternion trackerOri = QQuaternion::fromRotationMatrix(trackerToWorld.toGenericMatrix<3, 3>());
1023 origin = trackerPos + trackerOri.rotatedVector(m_opticalCenterLocal);
1024 direction = trackerOri.rotatedVector(m_opticalAxisLocal).normalized();
1032 if (!m_opticalCalibValid)
1035 const QMatrix4x4& dev = m_deviceToWorld;
1036 if (dev.isIdentity())
1039 QMatrix4x4 offsetMat;
1040 offsetMat.setToIdentity();
1041 offsetMat.translate(m_offsetTranslation);
1042 offsetMat.rotate(m_offsetRotation);
1044 const QMatrix4x4 trackerToWorld = dev * offsetMat.inverted();
1045 const QQuaternion trackerOri = QQuaternion::fromRotationMatrix(trackerToWorld.toGenericMatrix<3, 3>());
1049 const QVector3D rawUp = trackerOri.rotatedVector(QVector3D(0.0f, 0.0f, 1.0f));
1050 const QVector3D axis = trackerOri.rotatedVector(m_opticalAxisLocal).normalized();
1051 up = (rawUp - QVector3D::dotProduct(rawUp, axis) * axis).normalized();
1052 return up.lengthSquared() > 0.5f;
1058 float& correctionDeg)
1060 correctionDeg = 0.0f;
1061 if (!m_opticalCalibValid)
1065 const QMatrix4x4& dev = m_deviceToWorld;
1066 if (dev.isIdentity())
1069 QMatrix4x4 offsetMat;
1070 offsetMat.setToIdentity();
1071 offsetMat.translate(m_offsetTranslation);
1072 offsetMat.rotate(m_offsetRotation);
1074 const QMatrix4x4 trackerToWorld = dev * offsetMat.inverted();
1075 const QVector3D trackerPos(trackerToWorld(0, 3), trackerToWorld(1, 3), trackerToWorld(2, 3));
1076 const QQuaternion trackerOri = QQuaternion::fromRotationMatrix(trackerToWorld.toGenericMatrix<3, 3>());
1079 const QVector3D optCenterWorld = trackerPos + trackerOri.rotatedVector(m_opticalCenterLocal);
1082 const QVector3D toTarget = (targetWorldPos - optCenterWorld);
1083 if (toTarget.length() < 0.001f)
1086 const QVector3D desiredDirWorld = toTarget.normalized();
1089 const QVector3D currentDirWorld = trackerOri.rotatedVector(m_opticalAxisLocal).normalized();
1092 const float cosAngle = std::clamp(QVector3D::dotProduct(currentDirWorld, desiredDirWorld), -1.0f, 1.0f);
1093 correctionDeg = std::acos(cosAngle) * (180.0f / 3.14159265358979f);
1096 if (correctionDeg > 10.0f) {
1097 qWarning().nospace() <<
"Optical fine adjust: correction " << correctionDeg
1098 <<
"° exceeds 10° limit — rejected.";
1103 if (!m_opticalFineAdjustApplied)
1104 m_opticalAxisPreFineAdjust = m_opticalAxisLocal;
1107 const QQuaternion worldCorrection = QQuaternion::rotationTo(currentDirWorld, desiredDirWorld);
1111 const QQuaternion trackerOriInv = trackerOri.inverted();
1112 const QQuaternion localCorrection = trackerOriInv * worldCorrection * trackerOri;
1115 m_opticalAxisLocal = localCorrection.rotatedVector(m_opticalAxisLocal).normalized();
1116 m_opticalFineAdjustDeg = correctionDeg;
1117 m_opticalFineAdjustApplied =
true;
1120 <<
"Optical fine adjust applied: " << correctionDeg <<
"°"
1121 <<
"\n axis (local): (" << m_opticalAxisLocal.x() <<
", "
1122 << m_opticalAxisLocal.y() <<
", " << m_opticalAxisLocal.z() <<
")";
1132 if (!m_opticalFineAdjustApplied)
1135 m_opticalAxisLocal = m_opticalAxisPreFineAdjust;
1136 m_opticalFineAdjustApplied =
false;
1137 m_opticalFineAdjustDeg = 0.0f;
1139 qInfo() <<
"Optical fine adjust cleared — axis restored to original calibration.";
1150void saveVec3(QSettings& s,
const QString& key,
const QVector3D& v)
1152 s.setValue(key +
"/x",
static_cast<double>(v.x()));
1153 s.setValue(key +
"/y",
static_cast<double>(v.y()));
1154 s.setValue(key +
"/z",
static_cast<double>(v.z()));
1157QVector3D loadVec3(
const QSettings& s,
const QString& key)
1160 static_cast<float>(s.value(key +
"/x", 0.0).toDouble()),
1161 static_cast<float>(s.value(key +
"/y", 0.0).toDouble()),
1162 static_cast<float>(s.value(key +
"/z", 0.0).toDouble()));
1165void saveMat4(QSettings& s,
const QString& key,
const QMatrix4x4& m)
1167 QByteArray data(
reinterpret_cast<const char*
>(m.constData()), 16 *
sizeof(
float));
1168 s.setValue(key, data);
1171QMatrix4x4 loadMat4(
const QSettings& s,
const QString& key)
1173 QByteArray data = s.value(key).toByteArray();
1175 if (data.size() == 16 *
static_cast<int>(
sizeof(
float)))
1176 memcpy(m.data(), data.constData(), 16 *
sizeof(
float));
1180void saveQuat(QSettings& s,
const QString& key,
const QQuaternion& q)
1182 s.setValue(key +
"/w",
static_cast<double>(q.scalar()));
1183 s.setValue(key +
"/x",
static_cast<double>(q.x()));
1184 s.setValue(key +
"/y",
static_cast<double>(q.y()));
1185 s.setValue(key +
"/z",
static_cast<double>(q.z()));
1188QQuaternion loadQuat(
const QSettings& s,
const QString& key)
1191 static_cast<float>(s.value(key +
"/w", 1.0).toDouble()),
1192 static_cast<float>(s.value(key +
"/x", 0.0).toDouble()),
1193 static_cast<float>(s.value(key +
"/y", 0.0).toDouble()),
1194 static_cast<float>(s.value(key +
"/z", 0.0).toDouble()));
1203 settings.beginGroup(prefix);
1206 settings.setValue(
"trackerStation", m_trackerStation);
1207 settings.setValue(
"penStation", m_penStation);
1208 settings.setValue(
"mirrorX", m_mirrorX);
1209 settings.setValue(
"mirrorY", m_mirrorY);
1212 const char* fidNames[] = {
"LPA",
"NAS",
"RPA",
"CZ"};
1213 for (
int i = 0; i < 4; ++i) {
1214 settings.setValue(QString(
"hasPenFid/%1").arg(fidNames[i]), m_hasPenFid[i]);
1216 saveVec3(settings, QString(
"penFid/%1").arg(fidNames[i]), m_penFid[i]);
1220 for (
int i = 0; i < 4; ++i) {
1221 settings.setValue(QString(
"hasModelFid/%1").arg(fidNames[i]), m_hasModelFid[i]);
1222 if (m_hasModelFid[i])
1223 saveVec3(settings, QString(
"modelFid/%1").arg(fidNames[i]), m_modelFid[i]);
1227 settings.setValue(
"hasPenVertex", m_hasPenVertex);
1229 saveVec3(settings,
"penVertex", m_penVertex);
1230 settings.setValue(
"hasModelVertex", m_hasModelVertex);
1231 if (m_hasModelVertex)
1232 saveVec3(settings,
"modelVertex", m_modelVertex);
1235 saveVec3(settings,
"offsetTranslation", m_offsetTranslation);
1236 saveQuat(settings,
"offsetRotation", m_offsetRotation);
1239 saveVec3(settings,
"penTipOffset", m_penTipOffset);
1240 settings.setValue(
"tipOffsetEnabled", m_tipOffsetEnabled);
1243 settings.setValue(
"opticalCalibValid", m_opticalCalibValid);
1244 settings.setValue(
"knownTrackerToObjectiveDist",
static_cast<double>(m_knownTrackerToObjectiveDist));
1245 settings.setValue(
"hasObjectiveCenter", m_hasObjectiveCenter);
1246 if (m_hasObjectiveCenter)
1247 saveVec3(settings,
"objectiveCenterLocal", m_objectiveCenterLocal);
1248 if (m_opticalCalibValid) {
1249 saveVec3(settings,
"opticalAxisLocal", m_opticalAxisLocal);
1250 saveVec3(settings,
"opticalCenterLocal", m_opticalCenterLocal);
1251 settings.setValue(
"opticalCalibResidualMm",
static_cast<double>(m_opticalCalibResidualMm));
1252 settings.setValue(
"opticalCalibDepthSpreadMm",
static_cast<double>(m_opticalCalibDepthSpreadMm));
1255 settings.setValue(
"opticalFineAdjustApplied", m_opticalFineAdjustApplied);
1256 if (m_opticalFineAdjustApplied) {
1257 saveVec3(settings,
"opticalAxisPreFineAdjust", m_opticalAxisPreFineAdjust);
1258 settings.setValue(
"opticalFineAdjustDeg",
static_cast<double>(m_opticalFineAdjustDeg));
1263 const int nOptSamples =
static_cast<int>(m_opticalCalibSamples.size());
1264 settings.setValue(
"opticalCalibSampleCount", nOptSamples);
1265 for (
int i = 0; i < nOptSamples; ++i) {
1266 const auto& s = m_opticalCalibSamples[
static_cast<size_t>(i)];
1267 const QString key = QString(
"opticalCalibSample/%1").arg(i);
1268 saveVec3(settings, key +
"/trackerPos", s.trackerPos);
1269 saveQuat(settings, key +
"/trackerOri", s.trackerOri);
1270 saveVec3(settings, key +
"/focusPoint", s.focusPoint);
1274 settings.setValue(
"registrationValid", m_registrationValid);
1275 if (m_registrationValid) {
1276 saveMat4(settings,
"headToDevice", m_headToDevice);
1277 saveMat4(settings,
"headToWorld", m_headToWorld);
1278 saveMat4(settings,
"worldToModel", m_worldToModel);
1281 settings.endGroup();
1288 settings.beginGroup(prefix);
1289 if (settings.allKeys().isEmpty()) {
1290 settings.endGroup();
1295 m_trackerStation = settings.value(
"trackerStation", 1).toInt();
1296 m_penStation = settings.value(
"penStation", 2).toInt();
1297 m_mirrorX = settings.value(
"mirrorX",
false).toBool();
1298 m_mirrorY = settings.value(
"mirrorY",
false).toBool();
1301 const char* fidNames[] = {
"LPA",
"NAS",
"RPA",
"CZ"};
1302 for (
int i = 0; i < 4; ++i) {
1303 m_hasPenFid[i] = settings.value(QString(
"hasPenFid/%1").arg(fidNames[i]),
false).toBool();
1305 m_penFid[i] = loadVec3(settings, QString(
"penFid/%1").arg(fidNames[i]));
1309 for (
int i = 0; i < 4; ++i) {
1310 m_hasModelFid[i] = settings.value(QString(
"hasModelFid/%1").arg(fidNames[i]),
false).toBool();
1311 if (m_hasModelFid[i])
1312 m_modelFid[i] = loadVec3(settings, QString(
"modelFid/%1").arg(fidNames[i]));
1316 m_hasPenVertex = settings.value(
"hasPenVertex",
false).toBool();
1318 m_penVertex = loadVec3(settings,
"penVertex");
1319 m_hasModelVertex = settings.value(
"hasModelVertex",
false).toBool();
1320 if (m_hasModelVertex)
1321 m_modelVertex = loadVec3(settings,
"modelVertex");
1324 m_offsetTranslation = loadVec3(settings,
"offsetTranslation");
1325 m_offsetRotation = loadQuat(settings,
"offsetRotation");
1328 m_penTipOffset = loadVec3(settings,
"penTipOffset");
1329 m_tipOffsetEnabled = settings.value(
"tipOffsetEnabled",
false).toBool();
1332 m_knownTrackerToObjectiveDist =
static_cast<float>(settings.value(
"knownTrackerToObjectiveDist", 0.200).toDouble());
1333 m_hasObjectiveCenter = settings.value(
"hasObjectiveCenter",
false).toBool();
1334 if (m_hasObjectiveCenter)
1335 m_objectiveCenterLocal = loadVec3(settings,
"objectiveCenterLocal");
1336 m_opticalCalibValid = settings.value(
"opticalCalibValid",
false).toBool();
1337 if (m_opticalCalibValid) {
1338 m_opticalAxisLocal = loadVec3(settings,
"opticalAxisLocal");
1339 m_opticalCenterLocal = loadVec3(settings,
"opticalCenterLocal");
1340 m_opticalCalibResidualMm =
static_cast<float>(settings.value(
"opticalCalibResidualMm", 0.0).toDouble());
1341 m_opticalCalibDepthSpreadMm =
static_cast<float>(settings.value(
"opticalCalibDepthSpreadMm", 0.0).toDouble());
1344 m_opticalFineAdjustApplied = settings.value(
"opticalFineAdjustApplied",
false).toBool();
1345 if (m_opticalFineAdjustApplied) {
1346 m_opticalAxisPreFineAdjust = loadVec3(settings,
"opticalAxisPreFineAdjust");
1347 m_opticalFineAdjustDeg =
static_cast<float>(settings.value(
"opticalFineAdjustDeg", 0.0).toDouble());
1352 const int nOptSamples = settings.value(
"opticalCalibSampleCount", 0).toInt();
1353 m_opticalCalibSamples.clear();
1354 m_opticalCalibSamples.reserve(
static_cast<size_t>(nOptSamples));
1355 for (
int i = 0; i < nOptSamples; ++i) {
1356 const QString key = QString(
"opticalCalibSample/%1").arg(i);
1358 s.
trackerPos = loadVec3(settings, key +
"/trackerPos");
1359 s.
trackerOri = loadQuat(settings, key +
"/trackerOri");
1360 s.
focusPoint = loadVec3(settings, key +
"/focusPoint");
1361 m_opticalCalibSamples.push_back(s);
1365 m_registrationValid = settings.value(
"registrationValid",
false).toBool();
1366 if (m_registrationValid) {
1367 m_headToDevice = loadMat4(settings,
"headToDevice");
1368 m_headToWorld = loadMat4(settings,
"headToWorld");
1369 m_worldToModel = loadMat4(settings,
"worldToModel");
1372 settings.endGroup();
1377 const char* fidLabels[] = {
"",
"LPA",
"NAS",
"RPA"};
1378 for (
int i = 1; i <= 3; ++i) {
1379 m_pPoints->removeFiducial(
static_cast<FiducialId>(i));
1380 if (m_hasPenFid[i]) {
1383 dp.
label = QString::fromLatin1(fidLabels[i]);
1386 m_pPoints->append(dp);
1391 if (m_registrationValid)
1394 return m_registrationValid;
Eigen::JacobiSVD< Eigen::Matrix3f > svd(S, Eigen::ComputeFullU|Eigen::ComputeFullV)
Head–device coregistration using the Polhemus Fastrak.
Shared digitised-point store for Polhemus digitizer sessions.
Shared utilities (I/O helpers, spectral analysis, layout management, warp algorithms).
FiducialId
Identifier for the three cardinal fiducials.
@ HeadShape
Free head-shape point (continuous mode).
@ Fiducial
Anatomical landmark (NAS / LPA / RPA).
A single digitised point captured during the alignment session.
QString label
Human label ("NAS", "Cz", "HSP-42", …).
QVector3D position
Position in metres, sensor frame.
int identNumber
1-based id used by FIFF on export.
In-memory store of all points captured during a session.
Polhemus digitizer connection (mock + serial-port backends).
void penButtonPressed(int station, const QVector3D &position, const QQuaternion &orientation)
void pointReceived(int station, const QVector3D &position, const QQuaternion &orientation)
bool captureCurrentPenPositionAsVertex()
bool applyOpticalAxisFineAdjust(const QVector3D &targetWorldPos, float &correctionDeg)
Fine-adjust the optical axis so it passes through a known world-frame point (e.g. the probe tip touch...
void penPoseChanged(const QVector3D &position, const QQuaternion &orientation)
bool opticalUpInWorld(QVector3D &up) const
bool solveOpticalCalibration()
Fit a 3D line through the focus points in tracker-local frame.
void setConnection(PolhemusConnection *conn)
bool restoreSessionState(QSettings &settings, const QString &prefix=QStringLiteral("polhemus"))
QVector3D modelFiducial(FiducialId id) const
void setTrackerStation(int station)
bool captureCurrentPenPositionAsFiducial(FiducialId id)
void opticalCalibrationChanged()
bool hasAllModelFiducials() const
bool opticalRayInWorld(QVector3D &origin, QVector3D &direction) const
Compute the current optical axis ray in Polhemus world frame.
void setPenStation(int station)
void clearOpticalCalibSamples()
void penButtonPressed(const QVector3D &position, const QQuaternion &orientation)
void setModelFiducial(FiducialId id, const QVector3D &posInModel)
void devicePoseChanged(const QMatrix4x4 &deviceToWorld)
void startPivotCalibration()
void setProbeStation(int station)
bool captureObjectiveCenter()
Capture the current pen position as the objective lens center.
void pivotCalibrationDone(const QVector3D &offset, float residualMm)
void pivotStateChanged(PolhemusCoregistration::PivotState state)
bool captureOpticalCalibSample()
Record one calibration sample (current tracker pose + current pen position).
void cancelPivotCalibration()
void clearOpticalFineAdjust()
PolhemusCoregistration(QObject *parent=nullptr)
bool hasModelFiducial(FiducialId id) const
void setTrackerToDeviceOffset(const QVector3D &translation, const QQuaternion &rotation)
Set the rigid offset from the tracker sensor body frame to the device frame.
bool captureCurrentPenPositionAsHeadShape()
void resetRegistration()
Reset the registration state (headToWorld, headToDevice) to identity. Call this when the user clears ...
void clearObjectiveCenter()
void saveSessionState(QSettings &settings, const QString &prefix=QStringLiteral("polhemus")) const
void probePoseChanged(const QVector3D &position, const QQuaternion &orientation)
void pivotSampleCollected(int sampleCount, float angularSpanDeg)
bool hasAllPenFiducials() const
void registrationChanged()
bool computeRegistration()
Compute the head→device rigid transform from the three captured fiducials (NAS, LPA,...
QVector3D focusPoint
Stylus focus point in world frame (metres).
QVector3D trackerPos
Tracker position in world frame (metres).
QQuaternion trackerOri
Tracker orientation in world frame.