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) m_hasPenFid[i] =
false;
159 const int i =
static_cast<int>(id);
160 m_modelFid[i] = posInModel;
161 m_hasModelFid[i] =
true;
166 return m_hasModelFid[
static_cast<int>(id)];
171 return m_hasModelFid[1] && m_hasModelFid[2] && m_hasModelFid[3];
176 return m_modelFid[
static_cast<int>(id)];
181 return m_hasPenFid[1] && m_hasPenFid[2] && m_hasPenFid[3];
188 if (!m_havePenPos)
return false;
189 m_penVertex = m_penPosition;
190 m_hasPenVertex =
true;
191 qInfo() <<
"Captured pen vertex (CZ) at" << m_penVertex * 1000.0f <<
"mm";
194 emit m_pPoints->pointsChanged();
212 const float dNL = (pNas - pLpa).length() * 1000.0f;
213 const float dNR = (pNas - pRpa).length() * 1000.0f;
214 const float dLR = (pLpa - pRpa).length() * 1000.0f;
215 const float minSpread = std::min({dNL, dNR, dLR});
216 if (minSpread < 20.0f) {
217 qWarning() <<
"Registration failed: pen fiducials too close"
218 <<
"(min spread" << minSpread <<
"mm, need > 20 mm)";
219 m_registrationValid =
false;
241 const float mNL = (mNas - mLpa).length() * 1000.0f;
242 const float mNR = (mNas - mRpa).length() * 1000.0f;
243 const float mLR = (mLpa - mRpa).length() * 1000.0f;
245 const float maxRatio = std::max({dNL/mNL, dNR/mNR, dLR/mLR});
246 const float minRatio = std::min({dNL/mNL, dNR/mNR, dLR/mLR});
247 if (maxRatio / minRatio > 2.0f) {
248 qWarning() <<
"Registration: shape mismatch — pen fiducial triangle"
249 <<
"has very different proportions from model."
250 <<
"Check fiducial placement!";
256 const Eigen::Vector3d penC(
257 (pNas.x() + pLpa.x() + pRpa.x()) / 3.0,
258 (pNas.y() + pLpa.y() + pRpa.y()) / 3.0,
259 (pNas.z() + pLpa.z() + pRpa.z()) / 3.0);
260 const Eigen::Vector3d modC(
261 (mNas.x() + mLpa.x() + mRpa.x()) / 3.0,
262 (mNas.y() + mLpa.y() + mRpa.y()) / 3.0,
263 (mNas.z() + mLpa.z() + mRpa.z()) / 3.0);
266 Eigen::Matrix3d P, Q;
267 P.col(0) = Eigen::Vector3d(pNas.x(), pNas.y(), pNas.z()) - penC;
268 P.col(1) = Eigen::Vector3d(pLpa.x(), pLpa.y(), pLpa.z()) - penC;
269 P.col(2) = Eigen::Vector3d(pRpa.x(), pRpa.y(), pRpa.z()) - penC;
271 Q.col(0) = Eigen::Vector3d(mNas.x(), mNas.y(), mNas.z()) - modC;
272 Q.col(1) = Eigen::Vector3d(mLpa.x(), mLpa.y(), mLpa.z()) - modC;
273 Q.col(2) = Eigen::Vector3d(mRpa.x(), mRpa.y(), mRpa.z()) - modC;
276 const Eigen::Matrix3d H = P * Q.transpose();
279 Eigen::JacobiSVD<Eigen::Matrix3d>
svd(H, Eigen::ComputeFullU | Eigen::ComputeFullV);
280 const Eigen::Matrix3d U =
svd.matrixU();
281 const Eigen::Matrix3d V =
svd.matrixV();
284 const double d = (V * U.transpose()).determinant();
285 Eigen::Matrix3d D = Eigen::Matrix3d::Identity();
286 D(2, 2) = (d > 0.0) ? 1.0 : -1.0;
287 Eigen::Matrix3d
R = V * D * U.transpose();
290 Eigen::Vector3d t = modC -
R * penC;
302 if (m_hasPenVertex && m_hasModelVertex) {
303 const Eigen::Vector3d penCZ(m_penVertex.x(), m_penVertex.y(), m_penVertex.z());
304 const Eigen::Vector3d modCZ(m_modelVertex.x(), m_modelVertex.y(), m_modelVertex.z());
306 const Eigen::Vector3d mappedCZ =
R * penCZ + t;
307 const double errCurrent = (mappedCZ - modCZ).norm();
310 Eigen::Matrix3d D_alt = D;
311 D_alt(2, 2) = -D(2, 2);
312 const Eigen::Matrix3d R_alt = V * D_alt * U.transpose();
313 const Eigen::Vector3d t_alt = modC - R_alt * penC;
314 const Eigen::Vector3d mappedCZ_alt = R_alt * penCZ + t_alt;
315 const double errAlt = (mappedCZ_alt - modCZ).norm();
317 if (errAlt < errCurrent) {
325 m_worldToModel.setToIdentity();
326 for (
int r = 0; r < 3; ++r) {
327 for (
int c = 0; c < 3; ++c) {
328 m_worldToModel(r, c) =
static_cast<float>(
R(r, c));
330 m_worldToModel(r, 3) =
static_cast<float>(t(r));
333 m_headToWorld.setToIdentity();
334 m_headToDevice = m_deviceToWorld.inverted() * m_headToWorld;
335 m_registrationValid =
true;
336 qInfo() <<
"Registration succeeded (analytical paired).";
342 const QMatrix4x4 headFrame = buildHeadFrame();
343 const QVector3D ez(headFrame(0, 2), headFrame(1, 2), headFrame(2, 2));
344 if (ez.length() < 1e-6f) {
348 m_headToWorld = headFrame;
349 m_headToDevice = m_deviceToWorld.inverted() * m_headToWorld;
350 m_worldToModel.setToIdentity();
351 m_registrationValid =
true;
352 qInfo() <<
"Registration succeeded (head-frame fallback).";
359void PolhemusCoregistration::onPointReceived(
int station,
360 const QVector3D& position,
361 const QQuaternion& orientation)
364 const QVector3D pos(m_mirrorX ? -position.x() : position.x(),
365 m_mirrorY ? -position.y() : position.y(),
368 if (station == m_trackerStation) {
369 m_deviceToWorld = buildDevicePose(pos, orientation);
371 }
else if (station == m_penStation) {
375 const float sinEl = 2.0f * (orientation.scalar() * orientation.y()
376 - orientation.x() * orientation.z());
377 constexpr float kGimbalSinEl = 0.9848f;
378 const bool gimbalLock = (std::abs(sinEl) > kGimbalSinEl);
381 const QVector3D tipAdj = m_tipOffsetEnabled
382 ? orientation.rotatedVector(m_penTipOffset) : QVector3D();
383 m_penPosition = pos + tipAdj;
384 m_penOrientation = orientation;
393 constexpr float kMinAngleDeg = 3.0f;
394 constexpr float kMaxPosJumpM = 0.05f;
395 constexpr float kGimbalSinEl2 = 0.9848f;
398 const float sinEl2 = 2.0f * (orientation.scalar() * orientation.y()
399 - orientation.x() * orientation.z());
400 if (std::abs(sinEl2) > kGimbalSinEl2) {
403 bool accept = m_pivotOrientations.empty();
405 const QQuaternion& prev = m_pivotOrientations.back();
406 float dot = std::abs(QQuaternion::dotProduct(prev, orientation));
407 float angleDeg = 2.0f * std::acos(std::min(dot, 1.0f)) * (180.0f / 3.14159265f);
408 float posDelta = (pos - m_pivotPositions.back()).length();
409 accept = (angleDeg >= kMinAngleDeg) && (posDelta < kMaxPosJumpM);
412 m_pivotPositions.push_back(pos);
413 m_pivotOrientations.push_back(orientation);
416 float spanDeg = 0.0f;
417 if (m_pivotOrientations.size() > 1) {
419 const auto& first = m_pivotOrientations.front();
420 for (
size_t k = 1; k < m_pivotOrientations.size(); ++k) {
421 float d = std::abs(QQuaternion::dotProduct(first, m_pivotOrientations[k]));
422 if (d < minDot) minDot = d;
424 spanDeg = 2.0f * std::acos(std::min(minDot, 1.0f)) * (180.0f / 3.14159265f);
432 }
else if (station == m_probeStation) {
433 m_probePosition = pos;
434 m_probeOrientation = orientation;
435 m_haveProbePos =
true;
440void PolhemusCoregistration::onPenButtonPressedFromConn(
int station,
441 const QVector3D& position,
442 const QQuaternion& orientation)
444 if (station == m_penStation) {
446 const QVector3D pos(m_mirrorX ? -position.x() : position.x(),
447 m_mirrorY ? -position.y() : position.y(),
450 const QVector3D tipAdj = m_tipOffsetEnabled
451 ? orientation.rotatedVector(m_penTipOffset) : QVector3D();
452 m_penPosition = pos + tipAdj;
453 m_penOrientation = orientation;
459 m_pivotPositions.clear();
460 m_pivotOrientations.clear();
461 qInfo() <<
"Pivot calibration: collecting — pivot the pen around its tip";
466 solvePivotCalibration();
476QMatrix4x4 PolhemusCoregistration::buildDevicePose(
const QVector3D& trackerPos,
477 const QQuaternion& trackerOri)
const
479 QMatrix4x4 trackerToWorld;
480 trackerToWorld.setToIdentity();
481 trackerToWorld.translate(trackerPos);
482 trackerToWorld.rotate(trackerOri);
485 offset.setToIdentity();
486 offset.translate(m_offsetTranslation);
487 offset.rotate(m_offsetRotation);
489 return trackerToWorld * offset;
492QMatrix4x4 PolhemusCoregistration::buildHeadFrame()
const
498 const QVector3D origin = (lpa + rpa) * 0.5f;
499 const QVector3D ex = (nas - origin).normalized();
500 const QVector3D eyApprox = (lpa - origin).normalized();
502 const QVector3D ey = (eyApprox - QVector3D::dotProduct(eyApprox, ex) * ex).normalized();
503 const QVector3D ez = QVector3D::crossProduct(ex, ey).normalized();
506 frame.setToIdentity();
507 frame(0, 0) = ex.x(); frame(0, 1) = ey.x(); frame(0, 2) = ez.x(); frame(0, 3) = origin.x();
508 frame(1, 0) = ex.y(); frame(1, 1) = ey.y(); frame(1, 2) = ez.y(); frame(1, 3) = origin.y();
509 frame(2, 0) = ex.z(); frame(2, 1) = ey.z(); frame(2, 2) = ez.z(); frame(2, 3) = origin.z();
510 frame(3, 0) = 0.0f; frame(3, 1) = 0.0f; frame(3, 2) = 0.0f; frame(3, 3) = 1.0f;
520 m_pivotPositions.clear();
521 m_pivotOrientations.clear();
522 m_pivotResidualMm = 0.0f;
524 qInfo() <<
"Pivot calibration: press stylus button to begin collecting";
530 m_pivotPositions.clear();
531 m_pivotOrientations.clear();
536bool PolhemusCoregistration::solvePivotCalibration()
538 const int N =
static_cast<int>(m_pivotPositions.size());
540 qWarning() <<
"Pivot calibration: only" << N <<
"samples, need at least 10";
549 Eigen::MatrixXd A(3 * N, 6);
550 Eigen::VectorXd b(3 * N);
552 for (
int i = 0; i < N; ++i) {
553 const QVector3D& p = m_pivotPositions[
static_cast<size_t>(i)];
554 const QQuaternion& q = m_pivotOrientations[
static_cast<size_t>(i)];
557 const QMatrix3x3 rm = q.toRotationMatrix();
559 const int row = 3 * i;
560 for (
int r = 0; r < 3; ++r) {
561 for (
int c = 0; c < 3; ++c) {
562 A(row + r, c) =
static_cast<double>(rm(r, c));
563 A(row + r, 3 + c) = (r == c) ? -1.0 : 0.0;
566 b(row + 0) = -
static_cast<double>(p.x());
567 b(row + 1) = -
static_cast<double>(p.y());
568 b(row + 2) = -
static_cast<double>(p.z());
572 Eigen::JacobiSVD<Eigen::MatrixXd>
svd(A, Eigen::ComputeThinU | Eigen::ComputeThinV);
575 const auto& sv =
svd.singularValues();
576 double cond = sv(0) / sv(sv.size() - 1);
578 qWarning() <<
"Pivot calibration: ill-conditioned (cond =" << cond
579 <<
"). Pivot the pen through a wider range of angles.";
585 Eigen::VectorXd x =
svd.solve(b);
587 QVector3D offset(
static_cast<float>(x(0)),
588 static_cast<float>(x(1)),
589 static_cast<float>(x(2)));
590 QVector3D tipPos(
static_cast<float>(x(3)),
591 static_cast<float>(x(4)),
592 static_cast<float>(x(5)));
596 for (
int i = 0; i < N; ++i) {
597 const QVector3D& p = m_pivotPositions[
static_cast<size_t>(i)];
598 const QQuaternion& q = m_pivotOrientations[
static_cast<size_t>(i)];
599 QVector3D computed = p + q.rotatedVector(offset);
600 float err = (computed - tipPos).length();
601 sumSq +=
static_cast<double>(err) *
static_cast<double>(err);
603 float rms =
static_cast<float>(std::sqrt(sumSq /
static_cast<double>(N))) * 1000.0f;
604 m_pivotResidualMm = rms;
606 qInfo() <<
"Pivot calibration:" << N <<
"samples, cond =" << cond
607 <<
", offset =" << offset.x() * 1000.0f << offset.y() * 1000.0f
608 << offset.z() * 1000.0f <<
"mm, RMS residual =" << rms <<
"mm";
610 m_penTipOffset = offset;
624 qWarning() <<
"Optical calibration: no pen position available";
628 const QMatrix4x4& dev = m_deviceToWorld;
629 if (dev.isIdentity()) {
630 qWarning() <<
"Optical calibration: no tracker data available";
638 QMatrix4x4 offsetInv;
639 offsetInv.setToIdentity();
640 offsetInv.translate(m_offsetTranslation);
641 offsetInv.rotate(m_offsetRotation);
642 offsetInv = offsetInv.inverted();
644 const QMatrix4x4 trackerToWorld = dev * offsetInv;
645 const QVector3D trackerPos(trackerToWorld(0, 3), trackerToWorld(1, 3), trackerToWorld(2, 3));
646 const QQuaternion trackerOri = QQuaternion::fromRotationMatrix(trackerToWorld.toGenericMatrix<3, 3>());
652 m_opticalCalibSamples.push_back(sample);
654 const int n =
static_cast<int>(m_opticalCalibSamples.size());
655 const QVector3D localFocus = trackerOri.inverted().rotatedVector(m_penPosition - trackerPos);
656 const float distToTracker = localFocus.length() * 1000.0f;
659 <<
"Optical calibration sample " << n <<
":"
660 <<
"\n tracker pos: (" << trackerPos.x()*1000.f <<
", " << trackerPos.y()*1000.f <<
", " << trackerPos.z()*1000.f <<
") mm"
661 <<
"\n focus point: (" << m_penPosition.x()*1000.f <<
", " << m_penPosition.y()*1000.f <<
", " << m_penPosition.z()*1000.f <<
") mm"
662 <<
"\n focus (local): (" << localFocus.x()*1000.f <<
", " << localFocus.y()*1000.f <<
", " << localFocus.z()*1000.f <<
") mm"
663 <<
"\n distance tracker\u2194focus: " << distToTracker <<
" mm";
669 const auto& prev = m_opticalCalibSamples[
static_cast<size_t>(n - 2)];
670 const QVector3D prevLocal = prev.trackerOri.inverted().rotatedVector(prev.focusPoint - prev.trackerPos);
671 const QVector3D curDir = localFocus.normalized();
672 const QVector3D prevDir = prevLocal.normalized();
673 const float dot = std::clamp(QVector3D::dotProduct(curDir, prevDir), -1.0f, 1.0f);
674 const float angleDeg = std::acos(dot) * (180.0f / 3.14159265f);
675 const float prevDist = prevLocal.length() * 1000.0f;
677 <<
" convergence angle (sample " << n-1 <<
"\u2194" << n <<
"): " << angleDeg <<
"\u00b0"
678 <<
" (distances: " << prevDist <<
" / " << distToTracker <<
" mm)";
686 m_opticalCalibSamples.clear();
687 m_opticalCalibValid =
false;
688 m_opticalCalibResidualMm = 0.0f;
689 m_opticalCalibDepthSpreadMm = 0.0f;
698 qWarning() <<
"Objective center capture: no pen position available";
702 const QMatrix4x4& dev = m_deviceToWorld;
703 if (dev.isIdentity()) {
704 qWarning() <<
"Objective center capture: no tracker data available";
709 QMatrix4x4 offsetInv;
710 offsetInv.setToIdentity();
711 offsetInv.translate(m_offsetTranslation);
712 offsetInv.rotate(m_offsetRotation);
713 offsetInv = offsetInv.inverted();
715 const QMatrix4x4 trackerToWorld = dev * offsetInv;
716 const QVector3D trackerPos(trackerToWorld(0, 3), trackerToWorld(1, 3), trackerToWorld(2, 3));
717 const QQuaternion trackerOri = QQuaternion::fromRotationMatrix(trackerToWorld.toGenericMatrix<3, 3>());
720 m_objectiveCenterLocal = trackerOri.inverted().rotatedVector(m_penPosition - trackerPos);
721 m_hasObjectiveCenter =
true;
723 const float distMm = m_objectiveCenterLocal.length() * 1000.0f;
725 <<
"Objective center captured (tracker-local): ("
726 << m_objectiveCenterLocal.x() * 1000.f <<
", "
727 << m_objectiveCenterLocal.y() * 1000.f <<
", "
728 << m_objectiveCenterLocal.z() * 1000.f <<
") mm"
729 <<
", distance from tracker: " << distMm <<
" mm";
736 m_objectiveCenterLocal = QVector3D();
737 m_hasObjectiveCenter =
false;
742 const int N =
static_cast<int>(m_opticalCalibSamples.size());
744 qWarning() <<
"Optical calibration: need at least 2 samples, have" << N;
749 std::vector<Eigen::Vector3d> localPoints(
static_cast<size_t>(N));
750 for (
int i = 0; i < N; ++i) {
751 const auto& s = m_opticalCalibSamples[
static_cast<size_t>(i)];
752 const QVector3D local = s.trackerOri.inverted().rotatedVector(s.focusPoint - s.trackerPos);
753 localPoints[
static_cast<size_t>(i)] = Eigen::Vector3d(local.x(), local.y(), local.z());
757 Eigen::Vector3d centroid = Eigen::Vector3d::Zero();
758 for (
const auto& p : localPoints) centroid += p;
759 centroid /=
static_cast<double>(N);
761 Eigen::MatrixXd centered(3, N);
762 for (
int i = 0; i < N; ++i) {
763 centered.col(i) = localPoints[
static_cast<size_t>(i)] - centroid;
766 Eigen::JacobiSVD<Eigen::MatrixXd>
svd(centered, Eigen::ComputeThinU);
767 Eigen::Vector3d axisDir =
svd.matrixU().col(0);
768 if (centroid.dot(axisDir) < 0.0) axisDir = -axisDir;
770 const double t0 = -centroid.dot(axisDir);
771 Eigen::Vector3d opticalCenter = centroid + t0 * axisDir;
783 const double knownDist =
static_cast<double>(m_knownTrackerToObjectiveDist);
784 if (m_hasObjectiveCenter) {
786 opticalCenter = Eigen::Vector3d(m_objectiveCenterLocal.x(),
787 m_objectiveCenterLocal.y(),
788 m_objectiveCenterLocal.z());
790 Eigen::MatrixXd rays(3, N);
791 for (
int i = 0; i < N; ++i)
792 rays.col(i) = localPoints[
static_cast<size_t>(i)] - opticalCenter;
794 Eigen::JacobiSVD<Eigen::MatrixXd> raySvd(rays, Eigen::ComputeThinU);
795 axisDir = raySvd.matrixU().col(0);
797 Eigen::Vector3d meanRay = Eigen::Vector3d::Zero();
798 for (
int i = 0; i < N; ++i) meanRay += rays.col(i);
799 if (meanRay.dot(axisDir) < 0.0) axisDir = -axisDir;
802 <<
"Optical calibration: using directly captured objective center, |O|="
803 << opticalCenter.norm() * 1000.0 <<
" mm";
804 }
else if (knownDist > 0.0 && N >= 2) {
806 double R = knownDist;
807 Eigen::Vector3d O = opticalCenter;
808 double Onorm = O.norm();
812 O = centroid.normalized() *
R;
816 for (
int iter = 0; iter < 30; ++iter) {
818 Eigen::MatrixXd rays(3, N);
819 for (
int i = 0; i < N; ++i)
820 rays.col(i) = localPoints[
static_cast<size_t>(i)] - O;
822 Eigen::JacobiSVD<Eigen::MatrixXd> raySvd(rays, Eigen::ComputeThinU);
823 Eigen::Vector3d d = raySvd.matrixU().col(0);
824 if (O.dot(d) < 0.0) d = -d;
830 Eigen::Vector3d grad = Eigen::Vector3d::Zero();
832 for (
int i = 0; i < N; ++i) {
833 Eigen::Vector3d v = localPoints[
static_cast<size_t>(i)] - O;
834 Eigen::Vector3d perp = v - v.dot(d) * d;
835 cost += perp.squaredNorm();
840 Eigen::Vector3d normal = O.normalized();
841 Eigen::Vector3d tangentGrad = grad - grad.dot(normal) * normal;
843 double gradNorm = tangentGrad.norm();
844 if (gradNorm < 1e-12)
break;
847 double step = 0.01 *
R / gradNorm;
848 for (
int ls = 0; ls < 10; ++ls) {
849 Eigen::Vector3d candidate = O - step * tangentGrad;
851 candidate = candidate.normalized() *
R;
854 Eigen::MatrixXd cRays(3, N);
855 for (
int i = 0; i < N; ++i)
856 cRays.col(i) = localPoints[
static_cast<size_t>(i)] - candidate;
857 Eigen::JacobiSVD<Eigen::MatrixXd> cSvd(cRays, Eigen::ComputeThinU);
858 Eigen::Vector3d cd = cSvd.matrixU().col(0);
861 for (
int i = 0; i < N; ++i) {
862 Eigen::Vector3d v = localPoints[
static_cast<size_t>(i)] - candidate;
863 Eigen::Vector3d perp = v - v.dot(cd) * cd;
864 cCost += perp.squaredNorm();
878 Eigen::MatrixXd finalRays(3, N);
879 for (
int i = 0; i < N; ++i)
880 finalRays.col(i) = localPoints[
static_cast<size_t>(i)] - opticalCenter;
881 Eigen::JacobiSVD<Eigen::MatrixXd> finalSvd(finalRays, Eigen::ComputeThinU);
882 axisDir = finalSvd.matrixU().col(0);
883 if (opticalCenter.dot(axisDir) < 0.0) axisDir = -axisDir;
886 <<
"Optical calibration: constrained refinement (R="
887 << knownDist * 1000.0 <<
" mm) converged, |O|="
888 << opticalCenter.norm() * 1000.0 <<
" mm";
893 for (
const auto& p : localPoints) {
894 const Eigen::Vector3d diff = p - opticalCenter;
895 const double along = diff.dot(axisDir);
896 const double perpSq = (diff - along * axisDir).squaredNorm();
899 const double rms = std::sqrt(sumSq /
static_cast<double>(N)) * 1000.0;
902 double minDepth = 1e30, maxDepth = -1e30;
903 for (
const auto& p : localPoints) {
904 const double depth = (p - opticalCenter).dot(axisDir);
905 minDepth = std::min(minDepth, depth);
906 maxDepth = std::max(maxDepth, depth);
908 const double depthSpreadMm = (maxDepth - minDepth) * 1000.0;
910 m_opticalAxisLocal = QVector3D(
static_cast<float>(axisDir.x()),
911 static_cast<float>(axisDir.y()),
912 static_cast<float>(axisDir.z()));
913 m_opticalCenterLocal = QVector3D(
static_cast<float>(opticalCenter.x()),
914 static_cast<float>(opticalCenter.y()),
915 static_cast<float>(opticalCenter.z()));
916 m_opticalCalibResidualMm =
static_cast<float>(rms);
917 m_opticalCalibDepthSpreadMm =
static_cast<float>(depthSpreadMm);
918 m_opticalCalibValid =
true;
920 const float distMm = m_opticalCenterLocal.length() * 1000.0f;
923 <<
"Optical calibration solved (" << N <<
" samples"
924 << (knownDist > 0.0 ?
", constrained R=" + QString::number(knownDist * 1000.0,
'f', 1) +
" mm" : QString()) <<
"):"
925 <<
"\n axis (local): (" << m_opticalAxisLocal.x() <<
", " << m_opticalAxisLocal.y() <<
", " << m_opticalAxisLocal.z() <<
")"
926 <<
"\n center (local): (" << m_opticalCenterLocal.x()*1000.f <<
", " << m_opticalCenterLocal.y()*1000.f <<
", " << m_opticalCenterLocal.z()*1000.f <<
") mm"
927 <<
"\n tracker\u2194center: " << distMm <<
" mm"
928 <<
"\n RMS residual: " << rms <<
" mm"
929 <<
"\n depth spread: " << depthSpreadMm <<
" mm (range " << minDepth*1000.0 <<
" .. " << maxDepth*1000.0 <<
" mm)";
935 qInfo() <<
" Per-sample convergence angles (tracker\u2192focus vs optical axis):";
936 for (
int i = 0; i < N; ++i) {
937 const Eigen::Vector3d& p = localPoints[
static_cast<size_t>(i)];
938 const Eigen::Vector3d trkToFocus = p.normalized();
939 const double cosAngle = std::clamp(trkToFocus.dot(axisDir), -1.0, 1.0);
940 const double angleDeg = std::acos(cosAngle) * (180.0 / 3.14159265358979);
941 const double depth = (p - opticalCenter).dot(axisDir);
942 const Eigen::Vector3d diff = p - opticalCenter;
943 const double perpDist = (diff - diff.dot(axisDir) * axisDir).norm() * 1000.0;
945 <<
" sample " << (i + 1) <<
": depth=" << depth*1000.0
946 <<
" mm, convergence angle=" << angleDeg <<
"\u00b0"
947 <<
", perp err=" << perpDist <<
" mm";
950 if (depthSpreadMm < 50.0) {
951 qWarning() <<
"Optical calibration: depth spread is only" << depthSpreadMm
952 <<
"mm. For a reliable axis direction, move the OPMI to vary the"
953 <<
"focal distance by at least 50 mm between samples.";
957 qWarning() <<
"Optical calibration: RMS residual" << rms
958 <<
"mm is large. Check that the stylus accurately marks the microscope focus point.";
963 if (knownDist <= 0.0) {
964 constexpr float kMinPlausibleMm = 100.0f;
965 constexpr float kMaxPlausibleMm = 500.0f;
966 if (distMm < kMinPlausibleMm || distMm > kMaxPlausibleMm) {
968 <<
"Optical calibration: tracker\u2194axis distance " << distMm
969 <<
" mm is outside the plausible range [" << kMinPlausibleMm
970 <<
", " << kMaxPlausibleMm <<
"] mm. Calibration data may be unreliable.";
980 if (!m_opticalCalibValid)
return false;
982 const QMatrix4x4& dev = m_deviceToWorld;
983 if (dev.isIdentity())
return false;
986 QMatrix4x4 offsetMat;
987 offsetMat.setToIdentity();
988 offsetMat.translate(m_offsetTranslation);
989 offsetMat.rotate(m_offsetRotation);
991 const QMatrix4x4 trackerToWorld = dev * offsetMat.inverted();
992 const QVector3D trackerPos(trackerToWorld(0, 3), trackerToWorld(1, 3), trackerToWorld(2, 3));
993 const QQuaternion trackerOri = QQuaternion::fromRotationMatrix(trackerToWorld.toGenericMatrix<3, 3>());
995 origin = trackerPos + trackerOri.rotatedVector(m_opticalCenterLocal);
996 direction = trackerOri.rotatedVector(m_opticalAxisLocal).normalized();
1004 if (!m_opticalCalibValid)
return false;
1006 const QMatrix4x4& dev = m_deviceToWorld;
1007 if (dev.isIdentity())
return false;
1009 QMatrix4x4 offsetMat;
1010 offsetMat.setToIdentity();
1011 offsetMat.translate(m_offsetTranslation);
1012 offsetMat.rotate(m_offsetRotation);
1014 const QMatrix4x4 trackerToWorld = dev * offsetMat.inverted();
1015 const QQuaternion trackerOri = QQuaternion::fromRotationMatrix(trackerToWorld.toGenericMatrix<3, 3>());
1019 const QVector3D rawUp = trackerOri.rotatedVector(QVector3D(0.0f, 0.0f, 1.0f));
1020 const QVector3D axis = trackerOri.rotatedVector(m_opticalAxisLocal).normalized();
1021 up = (rawUp - QVector3D::dotProduct(rawUp, axis) * axis).normalized();
1022 return up.lengthSquared() > 0.5f;
1028 float& correctionDeg)
1030 correctionDeg = 0.0f;
1031 if (!m_opticalCalibValid)
return false;
1034 const QMatrix4x4& dev = m_deviceToWorld;
1035 if (dev.isIdentity())
return false;
1037 QMatrix4x4 offsetMat;
1038 offsetMat.setToIdentity();
1039 offsetMat.translate(m_offsetTranslation);
1040 offsetMat.rotate(m_offsetRotation);
1042 const QMatrix4x4 trackerToWorld = dev * offsetMat.inverted();
1043 const QVector3D trackerPos(trackerToWorld(0, 3), trackerToWorld(1, 3), trackerToWorld(2, 3));
1044 const QQuaternion trackerOri = QQuaternion::fromRotationMatrix(trackerToWorld.toGenericMatrix<3, 3>());
1047 const QVector3D optCenterWorld = trackerPos + trackerOri.rotatedVector(m_opticalCenterLocal);
1050 const QVector3D toTarget = (targetWorldPos - optCenterWorld);
1051 if (toTarget.length() < 0.001f)
return false;
1053 const QVector3D desiredDirWorld = toTarget.normalized();
1056 const QVector3D currentDirWorld = trackerOri.rotatedVector(m_opticalAxisLocal).normalized();
1059 const float cosAngle = std::clamp(QVector3D::dotProduct(currentDirWorld, desiredDirWorld), -1.0f, 1.0f);
1060 correctionDeg = std::acos(cosAngle) * (180.0f / 3.14159265358979f);
1063 if (correctionDeg > 10.0f) {
1064 qWarning().nospace() <<
"Optical fine adjust: correction " << correctionDeg
1065 <<
"° exceeds 10° limit — rejected.";
1070 if (!m_opticalFineAdjustApplied)
1071 m_opticalAxisPreFineAdjust = m_opticalAxisLocal;
1074 const QQuaternion worldCorrection = QQuaternion::rotationTo(currentDirWorld, desiredDirWorld);
1078 const QQuaternion trackerOriInv = trackerOri.inverted();
1079 const QQuaternion localCorrection = trackerOriInv * worldCorrection * trackerOri;
1082 m_opticalAxisLocal = localCorrection.rotatedVector(m_opticalAxisLocal).normalized();
1083 m_opticalFineAdjustDeg = correctionDeg;
1084 m_opticalFineAdjustApplied =
true;
1087 <<
"Optical fine adjust applied: " << correctionDeg <<
"°"
1088 <<
"\n axis (local): (" << m_opticalAxisLocal.x() <<
", "
1089 << m_opticalAxisLocal.y() <<
", " << m_opticalAxisLocal.z() <<
")";
1099 if (!m_opticalFineAdjustApplied)
return;
1101 m_opticalAxisLocal = m_opticalAxisPreFineAdjust;
1102 m_opticalFineAdjustApplied =
false;
1103 m_opticalFineAdjustDeg = 0.0f;
1105 qInfo() <<
"Optical fine adjust cleared — axis restored to original calibration.";
1115void saveVec3(QSettings &s,
const QString &key,
const QVector3D &v)
1117 s.setValue(key +
"/x",
static_cast<double>(v.x()));
1118 s.setValue(key +
"/y",
static_cast<double>(v.y()));
1119 s.setValue(key +
"/z",
static_cast<double>(v.z()));
1122QVector3D loadVec3(
const QSettings &s,
const QString &key)
1125 static_cast<float>(s.value(key +
"/x", 0.0).toDouble()),
1126 static_cast<float>(s.value(key +
"/y", 0.0).toDouble()),
1127 static_cast<float>(s.value(key +
"/z", 0.0).toDouble()));
1130void saveMat4(QSettings &s,
const QString &key,
const QMatrix4x4 &m)
1132 QByteArray data(
reinterpret_cast<const char*
>(m.constData()), 16 *
sizeof(
float));
1133 s.setValue(key, data);
1136QMatrix4x4 loadMat4(
const QSettings &s,
const QString &key)
1138 QByteArray data = s.value(key).toByteArray();
1140 if (data.size() == 16 *
static_cast<int>(
sizeof(
float)))
1141 memcpy(m.data(), data.constData(), 16 *
sizeof(
float));
1145void saveQuat(QSettings &s,
const QString &key,
const QQuaternion &q)
1147 s.setValue(key +
"/w",
static_cast<double>(q.scalar()));
1148 s.setValue(key +
"/x",
static_cast<double>(q.x()));
1149 s.setValue(key +
"/y",
static_cast<double>(q.y()));
1150 s.setValue(key +
"/z",
static_cast<double>(q.z()));
1153QQuaternion loadQuat(
const QSettings &s,
const QString &key)
1156 static_cast<float>(s.value(key +
"/w", 1.0).toDouble()),
1157 static_cast<float>(s.value(key +
"/x", 0.0).toDouble()),
1158 static_cast<float>(s.value(key +
"/y", 0.0).toDouble()),
1159 static_cast<float>(s.value(key +
"/z", 0.0).toDouble()));
1168 settings.beginGroup(prefix);
1171 settings.setValue(
"trackerStation", m_trackerStation);
1172 settings.setValue(
"penStation", m_penStation);
1173 settings.setValue(
"mirrorX", m_mirrorX);
1174 settings.setValue(
"mirrorY", m_mirrorY);
1177 const char* fidNames[] = {
"LPA",
"NAS",
"RPA",
"CZ"};
1178 for (
int i = 0; i < 4; ++i) {
1179 settings.setValue(QString(
"hasPenFid/%1").arg(fidNames[i]), m_hasPenFid[i]);
1181 saveVec3(settings, QString(
"penFid/%1").arg(fidNames[i]), m_penFid[i]);
1185 for (
int i = 0; i < 4; ++i) {
1186 settings.setValue(QString(
"hasModelFid/%1").arg(fidNames[i]), m_hasModelFid[i]);
1187 if (m_hasModelFid[i])
1188 saveVec3(settings, QString(
"modelFid/%1").arg(fidNames[i]), m_modelFid[i]);
1192 settings.setValue(
"hasPenVertex", m_hasPenVertex);
1193 if (m_hasPenVertex) saveVec3(settings,
"penVertex", m_penVertex);
1194 settings.setValue(
"hasModelVertex", m_hasModelVertex);
1195 if (m_hasModelVertex) saveVec3(settings,
"modelVertex", m_modelVertex);
1198 saveVec3(settings,
"offsetTranslation", m_offsetTranslation);
1199 saveQuat(settings,
"offsetRotation", m_offsetRotation);
1202 saveVec3(settings,
"penTipOffset", m_penTipOffset);
1203 settings.setValue(
"tipOffsetEnabled", m_tipOffsetEnabled);
1206 settings.setValue(
"opticalCalibValid", m_opticalCalibValid);
1207 settings.setValue(
"knownTrackerToObjectiveDist",
static_cast<double>(m_knownTrackerToObjectiveDist));
1208 settings.setValue(
"hasObjectiveCenter", m_hasObjectiveCenter);
1209 if (m_hasObjectiveCenter)
1210 saveVec3(settings,
"objectiveCenterLocal", m_objectiveCenterLocal);
1211 if (m_opticalCalibValid) {
1212 saveVec3(settings,
"opticalAxisLocal", m_opticalAxisLocal);
1213 saveVec3(settings,
"opticalCenterLocal", m_opticalCenterLocal);
1214 settings.setValue(
"opticalCalibResidualMm",
static_cast<double>(m_opticalCalibResidualMm));
1215 settings.setValue(
"opticalCalibDepthSpreadMm",
static_cast<double>(m_opticalCalibDepthSpreadMm));
1218 settings.setValue(
"opticalFineAdjustApplied", m_opticalFineAdjustApplied);
1219 if (m_opticalFineAdjustApplied) {
1220 saveVec3(settings,
"opticalAxisPreFineAdjust", m_opticalAxisPreFineAdjust);
1221 settings.setValue(
"opticalFineAdjustDeg",
static_cast<double>(m_opticalFineAdjustDeg));
1226 const int nOptSamples =
static_cast<int>(m_opticalCalibSamples.size());
1227 settings.setValue(
"opticalCalibSampleCount", nOptSamples);
1228 for (
int i = 0; i < nOptSamples; ++i) {
1229 const auto& s = m_opticalCalibSamples[
static_cast<size_t>(i)];
1230 const QString key = QString(
"opticalCalibSample/%1").arg(i);
1231 saveVec3(settings, key +
"/trackerPos", s.trackerPos);
1232 saveQuat(settings, key +
"/trackerOri", s.trackerOri);
1233 saveVec3(settings, key +
"/focusPoint", s.focusPoint);
1237 settings.setValue(
"registrationValid", m_registrationValid);
1238 if (m_registrationValid) {
1239 saveMat4(settings,
"headToDevice", m_headToDevice);
1240 saveMat4(settings,
"headToWorld", m_headToWorld);
1241 saveMat4(settings,
"worldToModel", m_worldToModel);
1244 settings.endGroup();
1251 settings.beginGroup(prefix);
1252 if (settings.allKeys().isEmpty()) {
1253 settings.endGroup();
1258 m_trackerStation = settings.value(
"trackerStation", 1).toInt();
1259 m_penStation = settings.value(
"penStation", 2).toInt();
1260 m_mirrorX = settings.value(
"mirrorX",
false).toBool();
1261 m_mirrorY = settings.value(
"mirrorY",
false).toBool();
1264 const char* fidNames[] = {
"LPA",
"NAS",
"RPA",
"CZ"};
1265 for (
int i = 0; i < 4; ++i) {
1266 m_hasPenFid[i] = settings.value(QString(
"hasPenFid/%1").arg(fidNames[i]),
false).toBool();
1268 m_penFid[i] = loadVec3(settings, QString(
"penFid/%1").arg(fidNames[i]));
1272 for (
int i = 0; i < 4; ++i) {
1273 m_hasModelFid[i] = settings.value(QString(
"hasModelFid/%1").arg(fidNames[i]),
false).toBool();
1274 if (m_hasModelFid[i])
1275 m_modelFid[i] = loadVec3(settings, QString(
"modelFid/%1").arg(fidNames[i]));
1279 m_hasPenVertex = settings.value(
"hasPenVertex",
false).toBool();
1280 if (m_hasPenVertex) m_penVertex = loadVec3(settings,
"penVertex");
1281 m_hasModelVertex = settings.value(
"hasModelVertex",
false).toBool();
1282 if (m_hasModelVertex) m_modelVertex = loadVec3(settings,
"modelVertex");
1285 m_offsetTranslation = loadVec3(settings,
"offsetTranslation");
1286 m_offsetRotation = loadQuat(settings,
"offsetRotation");
1289 m_penTipOffset = loadVec3(settings,
"penTipOffset");
1290 m_tipOffsetEnabled = settings.value(
"tipOffsetEnabled",
false).toBool();
1293 m_knownTrackerToObjectiveDist =
static_cast<float>(settings.value(
"knownTrackerToObjectiveDist", 0.200).toDouble());
1294 m_hasObjectiveCenter = settings.value(
"hasObjectiveCenter",
false).toBool();
1295 if (m_hasObjectiveCenter)
1296 m_objectiveCenterLocal = loadVec3(settings,
"objectiveCenterLocal");
1297 m_opticalCalibValid = settings.value(
"opticalCalibValid",
false).toBool();
1298 if (m_opticalCalibValid) {
1299 m_opticalAxisLocal = loadVec3(settings,
"opticalAxisLocal");
1300 m_opticalCenterLocal = loadVec3(settings,
"opticalCenterLocal");
1301 m_opticalCalibResidualMm =
static_cast<float>(settings.value(
"opticalCalibResidualMm", 0.0).toDouble());
1302 m_opticalCalibDepthSpreadMm =
static_cast<float>(settings.value(
"opticalCalibDepthSpreadMm", 0.0).toDouble());
1305 m_opticalFineAdjustApplied = settings.value(
"opticalFineAdjustApplied",
false).toBool();
1306 if (m_opticalFineAdjustApplied) {
1307 m_opticalAxisPreFineAdjust = loadVec3(settings,
"opticalAxisPreFineAdjust");
1308 m_opticalFineAdjustDeg =
static_cast<float>(settings.value(
"opticalFineAdjustDeg", 0.0).toDouble());
1313 const int nOptSamples = settings.value(
"opticalCalibSampleCount", 0).toInt();
1314 m_opticalCalibSamples.clear();
1315 m_opticalCalibSamples.reserve(
static_cast<size_t>(nOptSamples));
1316 for (
int i = 0; i < nOptSamples; ++i) {
1317 const QString key = QString(
"opticalCalibSample/%1").arg(i);
1319 s.
trackerPos = loadVec3(settings, key +
"/trackerPos");
1320 s.
trackerOri = loadQuat(settings, key +
"/trackerOri");
1321 s.
focusPoint = loadVec3(settings, key +
"/focusPoint");
1322 m_opticalCalibSamples.push_back(s);
1326 m_registrationValid = settings.value(
"registrationValid",
false).toBool();
1327 if (m_registrationValid) {
1328 m_headToDevice = loadMat4(settings,
"headToDevice");
1329 m_headToWorld = loadMat4(settings,
"headToWorld");
1330 m_worldToModel = loadMat4(settings,
"worldToModel");
1333 settings.endGroup();
1338 const char* fidLabels[] = {
"",
"LPA",
"NAS",
"RPA" };
1339 for (
int i = 1; i <= 3; ++i) {
1340 if (m_hasPenFid[i]) {
1343 dp.
label = QString::fromLatin1(fidLabels[i]);
1346 m_pPoints->append(dp);
1351 if (m_registrationValid)
1354 return m_registrationValid;
Head–device coregistration using the Polhemus Fastrak.
Shared digitised-point store for Polhemus digitizer sessions.
Eigen::JacobiSVD< Eigen::Matrix3f > svd(S, Eigen::ComputeFullU|Eigen::ComputeFullV)
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.