如果你接触过机器人、SLAM、计算机视觉或者三维重建,Eigen 这个名字大概率已经听得耳朵起茧了。它几乎是 C++ 生态里做矩阵运算、几何变换绕不开的那套基础库,头文件搬过来就能用,性能和易用性都相当能打。这个系列前面几篇聊过基础矩阵类型、运算规则、线性方程组求解,这篇是第四篇,我想把重心从"算数"挪到"几何"和"分解"上——也就是旋转、位姿、四元数、特征值分解和 SVD 这些在真实项目里最高频、也最容易写错的部分。
1. 旋转的世界:为什么只靠旋转矩阵还不够
很多刚上手 Eigen 的同学,处理三维旋转时习惯只用Matrix3d,往里填九个数字,觉得够用了。但真到了工程里,尤其是涉及姿态估计、位姿插值、图优化、滤波更新的时候,旋转矩阵就成了一个很尴尬的存在——九个元素描述三个自由度,冗余不说,还有正交约束和行列式为 1 的约束,优化器稍微迭代几步,矩阵就变成一个不伦不类的"伪旋转矩阵"。所以刚开始做位姿相关模块时,我就吃过这个亏,最后不得不老老实实把旋转的几种表达方式都摸清楚。
1.1 旋转矩阵、旋转向量、欧拉角三兄弟
Eigen 里表达旋转的常见方式有四类:旋转矩阵、旋转向量(轴角)、欧拉角、四元数。前三个都在Eigen/Geometry模块里,用法最直白的应该是旋转向量对应的AngleAxis。
AngleAxis从名字就能看出来,它说的是"绕着某个轴转多少度"。比如绕 Z 轴转 45 度:
#include <Eigen/Core> #include <Eigen/Geometry> #include <iostream> int main() { // 绕 Z 轴旋转 45 度,单位向量表示转轴 Eigen::AngleAxisd rotation_vector(M_PI / 4, Eigen::Vector3d::UnitZ()); // 转成旋转矩阵 Eigen::Matrix3d rotation_matrix = rotation_vector.toRotationMatrix(); std::cout << "Rotation matrix:\n" << rotation_matrix << std::endl; // 从旋转矩阵再恢复出旋转向量 Eigen::AngleAxisd recovered(rotation_matrix); std::cout << "Axis: " << recovered.axis().transpose() << std::endl; std::cout << "Angle: " << recovered.angle() << std::endl; return 0; }这段代码没有什么难懂的地方,但我想强调两个点。
第一,AngleAxisd的底层数据不是四元数也不是矩阵,而是"轴 + 角"的存储方式,但你可以把它当成一个"旋转算子"来用:它和旋转矩阵相乘、和四元数相乘、直接作用于向量都可以。rotation_vector * point的效果就是把point旋转过去,这一点和旋转矩阵的用法完全一致。
第二,旋转向量也不是万能的。最明显的问题就是它不便于插值。虽然有人尝试直接在轴角空间做线性插值,但算出来的中间姿态往往不是最短路径,而且当角度接近 0 或者 2π 的时候,轴的估计会变得非常不稳定。所以做插值、平滑这些工作,还是四元数更靠谱,后面我会专门讲。
再说欧拉角。欧拉角直观,而且很多传感器(陀螺仪、云台)都直接用欧拉角输出姿态,你不接触它也不行。Eigen 里通过eulerAngles这个函数把旋转矩阵转成欧拉角。
// 按 Z、Y、X 的旋转顺序(即先绕 Z 轴,再绕 Y 轴,最后绕 X 轴)提取欧拉角 Eigen::Vector3d euler = rotation_matrix.eulerAngles(2, 1, 0); std::cout << "Euler angles (roll, pitch, yaw): " << euler.transpose() << std::endl;eulerAngles(2, 1, 0)里的参数用的是轴编号:0 代表 X 轴,1 代表 Y 轴,2 代表 Z 轴。也就是说(2, 1, 0)对应的是 ZYX 内旋顺序,这是航空、机器人领域非常常见的一种定义,通常对应 yaw、pitch、roll。
这里有一个特别容易踩的坑:欧拉角的定义不唯一。同一个姿态,你换一种旋转顺序,解出来的三个角度完全不同;就算旋转顺序一样,也可能因为正值负值范围的约定不同而产生差异。Eigen 返回的角度范围、象限处理都和自己写的约定未必一致。我的建议是:如果项目里只需要给人看、做简单的调试输出,用欧拉角没问题;但如果要拿这两个数字去和其他系统对接或者参与结算,一定要先确认双方的旋转顺序和轴向定义,否则差之毫厘谬以千里。
1.2 欧拉角最经典的问题:万向锁
既然说到欧拉角,就不得不提万向锁(Gimbal Lock)。用 ZYX 顺序的时候,如果 pitch 等于正负 90 度,roll 和 yaw 就变得不可分辨,相当于丢失了一个旋转自由度。这不是 Eigen 的问题,而是欧拉角这种表达方式本身在数学上就存在奇异点。所以很多做姿态估计的工程师,到后面都会慢慢转向四元数——它没有奇异点,而且插值特别方便。
如果你刚接触这些概念,我建议你先在本地把旋转矩阵、旋转向量、欧拉角三种表达方式互相转换的代码跑一遍,对着一组已知角度验证输出,把"表达方式"和"旋转结果"之间的关系彻底搞清楚,再往下走。
2. 四元数:被很多 C++ 开发者低估的姿态描述利器
四元数在不少初学者眼里是个玄学概念,"i 的平方等于负一"看起来简单,真到用的时候又不知道coeffs()的顺序、不知道两个四元数乘起来到底代表什么。但说实话,一旦你用惯四元数,就会觉得旋转矩阵和欧拉角都啰嗦。
2.1 先理解四元数的"双重覆盖"现象
简单来说,一个单位四元数可以用来表示一个三维旋转。它由一个实部和三个虚部组成,习惯上写成q = w + xi + yj + zk,其中w是实部,(x, y, z)是虚部对应的向量部分。单位四元数满足模长为 1。
四元数有两个很反直觉的性质。
第一个性质是:q和-q表示同一个旋转。也就是说,你把四个分量全部取相反数,旋转效果完全一样。这在做四元数插值或者求平均的时候特别容易出问题——两个物理上几乎一致的姿态,可能四元数表示却一个在正半区一个在负半区,如果直接硬插值,结果会绕一大圈。很多做骨骼动画的人都在这里吃过亏。
第二个性质是:四元数乘法不满足交换律,但我们可以用它表示旋转的复合。q1 * q2表示先应用q2的旋转,再应用q1的旋转,这和矩阵乘法R1 * R2的语义是一致的。你在写代码的时候要特别小心这个顺序,否则姿态会突然变得非常奇怪。
2.2 Eigen 里四元数的几个高频操作
Eigen 里的Quaterniond用法很直接。从旋转矩阵或者旋转向量构造四元数都只要一行:
Eigen::Quaterniond q1(rotation_matrix); // 从旋转矩阵构造 Eigen::Quaterniond q2(rotation_vector); // 从旋转向量(AngleAxis)构造反过来,把四元数转成旋转矩阵用toRotationMatrix():
Eigen::Matrix3d rot = q1.toRotationMatrix();旋转一个点也很自然:
Eigen::Vector3d point(1.0, 0.0, 0.0); Eigen::Vector3d rotated = q1 * point; // 相当于 rot * point但我用下来最想提醒大家的,是关于coeffs()的输出顺序。Eigen 中q.coeffs()返回的是一个四维向量,顺序是(x, y, z, w),注意,实部w在最后一位。这个顺序和很多资料里写的(w, x, y, z)不一样,也不符合我们平时手写四元数的习惯。
我记得到现在为止,团队里还有同事不小心写出q.coeffs()[3]当成 x 分量用的诡异代码。建议你第一次接触时就在代码里打印一下coeffs().transpose(),强制用经验把顺序印在脑子里。
2.3 四元数归一化与插值
和旋转矩阵需要保持正交性类似,四元数表示旋转的前提是单位化。Eigen 的Quaterniond在构造时并不会自动归一化,你需要确认自己传入的数据是单位模长。如果你做的是从传感器数据读四元数,那通常还好,一般上游已经归一化了;但如果是自己通过一些组合运算得到的四元数,就得主动调normalize()或者normalized()。
normalize()是就地归一化,normalized()返回一个归一化副本。如果你后续打算用这个四元数去乘向量或者转旋转矩阵,没有归一化会导致旋转之后向量模长变化,最终反映到点云配准或者位姿估计上,就是一个隐形的误差源。
提到插值,slerp是四元数最实用的操作之一。它做的是球面线性插值,两个姿态之间能走出自然的"最短弧"路径:
double t = 0.5; // 0 到 1 之间 Eigen::Quaterniond interpolated = q1.slerp(t, q2);slerp的结果也可以用normalized()保证一下模长,因为在某些特殊情况下数值误差会让模长略微偏离 1。做动画、机械臂轨迹规划、相机姿态平滑的时候,四元数 +slerp基本是标配。
3. 变换矩阵与 Isometry:把旋转和平移缝在一起
旋转矩阵只能描述姿态,但真实物体在三维空间里还有位置。一个刚体的完整位姿,旋转加平移,最简单的方式是用一个 4x4 的变换矩阵来表达。Eigen 里专门为这个场景提供了Isometry3d,表示等距变换,也就是刚体变换。
3.1 为什么需要用齐次坐标
我刚开始学图形学时也有疑问:为什么不能直接维护一个Matrix3d和一个Vector3d,非要搞 4x4 的齐次矩阵?原因主要有两个。
第一,数学上统一。一个 4x4 矩阵既能表示旋转,也能表示平移,还能表示投影、缩放、剪切这些操作。把旋转和平移打包成一个矩阵,组合变换的时候只需要做一次矩阵乘法,逻辑上非常干净。
第二,避免把旋转和平移混在一起算时搞错顺序。如果你分开存旋转矩阵和平移向量,那么在复合两个位姿的时候,你必须记得新平移 = 旋转1 * 平移2 + 平移1,这个公式写几次没问题,写多了就容易出低级错误。用齐次变换矩阵,一次 4x4 乘法就把复合关系算完了。
但 4x4 矩阵也有开销。它比 3x3 矩阵多了一行一列,如果只是大量做旋转向量乘法,显然没那么划算。所以 Eigen 里有Transform类,还针对常见情况提供了Isometry3d这种别名,让用户可以按需选择。
3.2 IsometryT 的 API 细节与组合顺序坑
Isometry3d底层是Transform<double, 3, Isometry>,Eigen 在内部仍然存储 3x3 旋转和 3x1 平移,但对外提供了统一的接口。
构造一个刚体变换很简单:
Eigen::Isometry3d T = Eigen::Isometry3d::Identity(); T.rotate(rotation_vector); // 设置旋转部分 T.pretranslate(Eigen::Vector3d(1, 2, 3)); // 设置平移部分 std::cout << "Transformation matrix:\n" << T.matrix() << std::endl;rotate和pretranslate这两个名字很形象,但我要提醒一句:Eigen 这里的rotate默认右乘更新,也就是说如果你连续调用两次rotate,它做的是T = T * R2 * R1这样的链式更新。对于从单位矩阵开始构造的场景,T.rotate(rotation_vector)会直接把这个旋转变换设置进去,这个是好用的。但如果你的T已经包含了一些信息,想"覆盖"旋转部分,最稳妥的手段还是直接访问底层矩阵:
T.linear() = rotation_matrix; // 直接覆盖旋转部分 T.translation() = Eigen::Vector3d(1, 2, 3);我个人更推荐后一种写法,尤其在位姿更新、拼接、重算的场景下。因为rotate的语义是"叠加旋转",不是"设置为这个旋转",不少人在这个接口上栽过跟头。
取变换矩阵的各个部分也有固定姿势:
Eigen::Matrix3d R = T.linear(); // 旋转部分 Eigen::Vector3d t = T.translation(); // 平移部分 Eigen::Matrix4d M = T.matrix(); // 完整 4x4 矩阵变换一个点,直接用T * point就行。这里的内部实现会先对point做旋转再加平移,等价于 4x4 矩阵乘以齐次坐标后的结果,但 Eigen 做了优化,没有真的把点升维成四维再算。
3.3 组合两个位姿时最容易犯的顺序错误
位姿组合在 SLAM 和机械臂运动学里非常常见。假设有两个刚体变换T1和T2,想把T1的坐标系套在T2的坐标系上,得到新的变换T = T1 * T2。关键问题是:这个乘法顺序到底是谁先应用?
Eigen 的Transform遵循普通矩阵乘法的语义:T = T1 * T2表示先应用T2(从右往左看是 T2 先作用),再应用T1。这和旋转矩阵的复合顺序完全一致——右侧的变换先作用在点上。
有些资料里会说"左乘表示在世界坐标系下变换,右乘表示在自身坐标系下变换",这句话是对的,但对新手来说容易在代码里看反。我的建议是永远用具体点来验证:取一个点,分别手动计算T1 * T2 * p和T2 * T1 * p,看看哪个结果符合你预期的坐标系关系。调试位姿相关的代码,这种"拿一个给定点手算验证"的习惯特别有价值。
4. 特征值与 SVD:Eigen 从"算工具"变成"思考工具"
很多人把 Eigen 当成一个"高级计算器",只会调A * B、A.inverse()。其实到了项目中期,特征值分解和奇异值分解才是真正体现"思考深度"的部分。它们能用于主成分分析、点云匹配、矩阵伪逆、陀螺仪噪声分析等一系列场景。
4.1 用 Eigen 求解特征值和特征向量
Eigen 里最常用的特征值求解器是SelfAdjointEigenSolver,它专门处理实对称矩阵的自伴随特征值分解。物理上很多矩阵天然是对称的,比如协方差矩阵、惯性矩阵、应力张量等,用这个求解器又稳又快。
#include <Eigen/Eigenvalues> Eigen::Matrix3d A; A << 4.0, 1.0, 0.0, 1.0, 3.0, 1.0, 0.0, 1.0, 2.0; Eigen::SelfAdjointEigenSolver<Eigen::Matrix3d> solver(A); std::cout << "Eigenvalues:\n" << solver.eigenvalues() << std::endl; std::cout << "Eigenvectors (each column):\n" << solver.eigenvectors() << std::endl;eigenvalues()返回一个向量,默认按升序排列:小的在前,大的在后。eigenvectors()返回一个矩阵,每一列是对应特征值的特征向量。注意,矩阵乘法验证A * v = lambda * v的时候,v必须是列向量,也就是eigenvectors().col(i)。
使用上有一个常见的坑:SelfAdjointEigenSolver的模板参数必须是固定维度或者动态维度都可以,但如果你的矩阵是动态尺寸,构造时最好先明确大小,避免反复分配内存。另外它只适用于对称矩阵;如果你对非对称矩阵调用,结果就是未定义的。面对一般矩阵,应该用EigenSolver,但它是复数域的,输出的是一个复数矩阵,判断是否理解到位的时候就容易看懵。
4.2 SVD:比特征值分解更通用、更稳的方法
奇异值分解(SVD)不要求矩阵是方阵,也不要求对称。一个任意矩阵A都能分解成U * S * V^T,其中U和V是正交矩阵,S是对角矩阵,对角线上的元素是奇异值,从大到小排列。
Eigen 里用JacobiSVD求解非常直接:
#include <Eigen/SVD> Eigen::MatrixXd A(3, 2); A << 1.0, 2.0, 2.0, 1.0, 3.0, 5.0; Eigen::JacobiSVD<Eigen::MatrixXd> svd( A, Eigen::ComputeFullU | Eigen::ComputeFullV); Eigen::MatrixXd U = svd.matrixU(); Eigen::VectorXd S = svd.singularValues(); Eigen::MatrixXd V = svd.matrixV();这里的ComputeFullU | ComputeFullV告诉求解器需要完整的U和V。如果矩阵尺寸特别大,可以用ComputeThinU和ComputeThinV来节省存储,跑起来也更快。后面做点云配准,我建议用 thin 版本就够了。
SVD 之所以重要,一是它数值稳定性好,当矩阵接近奇异或者说"近似退化"时,特征值分解往往表现不佳,而 SVD 依然会给出一个合理的分解结果;二是奇异值能直接反映矩阵的"秩"和"条件数",比如我们判断一个求解问题是否病态,就可以看最大奇异值和最小奇异值的比值。很多机器人领域的状态估计代码里,求矩阵伪逆都是用 SVD 而不是直接调inverse(),就是因为它对病态矩阵更友好。
4.3 求伪逆:SVD 带来的额外优势
求非方阵或奇异矩阵的"逆",很多时候我们希望得到的是最小二乘意义下的伪逆。标准做法就是先把矩阵 SVD 分解,取非零奇异值求倒数,再重建矩阵。Eigen 里甚至不需要自己重建,可以直接用svd.solve(b)来解最小二乘问题,它内部就做了伪逆相关的事情。这一招在做超定方程组求解、拟合、标定时都非常常用。
我在实际项目里比较少直接用手写 SVD 重建矩阵,因为JacobiSVD的solve接口已经很够了。但如果你需要自己构造伪逆矩阵,记得只对大于某个阈值的奇异值求倒数,其他置零。阈值可以根据矩阵尺寸和数据精度来定,我通常取最大奇异值乘以 1e-8 作为截断阈值。
5. 综合实战:用 SVD 完成一次点云配准
基础概念讲了这么多,如果只是"会用 API"还不够。这一节我们来做一个真正能用的东西:给定两组具有对应关系的三维点,用 SVD 求解它们之间的旋转矩阵和平移向量,也就是经典的点云配准(对应点已知的情况下)。
这个问题的数学背景是:假设有一组点p_i经过某个刚体变换R, t之后得到q_i,但我们只知道这些点对,想反解出R和t。公式表达就是最小化:
sum_i || R * p_i + t - q_i ||^2
5.1 简化问题:先去质心
这个优化问题有一个很经典的解,叫做 Kabsch 算法或者 Umeyama 算法。核心思想是先把两组点的质心对齐,这样平移量t可以先通过质心之差求出来,剩下的问题就只是一个纯旋转问题。
具体步骤是:
- 计算
p的质心p_centroid和q的质心q_centroid。 - 把两组点都减去各自的质心,得到去中心化后的点集
p_demean和q_demean。 - 构造 3x3 的协方差矩阵
H = p_demean^T * q_demean。 - 对
H做 SVD:H = U * S * V^T。 - 旋转矩阵
R = V * U^T。 - 如果
det(R) < 0,说明反射出现,需要把V的最后一列取反再重新计算R。 - 平移向量
t = q_centroid - R * p_centroid。
这个算法在三维点云配准、结构光标定、双目相机外参估计这些场景里都非常常见。我第一次实现时没有做第 6 步的反射判断,结果在高噪声数据上偶尔会出现一个"镜像旋转",当时排查了很久,所以这里一定要写清楚。
5.2 用 Eigen 写一个可运行的配准函数
下面是一个完整的函数示例,输入是两个Eigen::MatrixXd,每行是一个三维点,行数相同,顺序一一对应,输出是配准后的旋转矩阵和平移向量。
#include <Eigen/Core> #include <Eigen/SVD> #include <iostream> struct Pose { Eigen::Matrix3d R; Eigen::Vector3d t; }; Pose SolveRigidTransform(const Eigen::MatrixXd& p, const Eigen::MatrixXd& q) { // 这里假设 p 和 q 都是 N x 3 的矩阵 assert(p.rows() == q.rows()); assert(p.cols() == 3 && q.cols() == 3); // 1. 计算质心 Eigen::RowVector3d p_centroid = p.colwise().mean(); Eigen::RowVector3d q_centroid = q.colwise().mean(); // 2. 去质心 Eigen::MatrixXd p_demean = p.rowwise() - p_centroid; Eigen::MatrixXd q_demean = q.rowwise() - q_centroid; // 3. 协方差矩阵 H = sum_i p'_i * q'_i^T Eigen::Matrix3d H = p_demean.transpose() * q_demean; // 4. SVD 分解 Eigen::JacobiSVD<Eigen::Matrix3d> svd( H, Eigen::ComputeFullU | Eigen::ComputeFullV); Eigen::Matrix3d U = svd.matrixU(); Eigen::Matrix3d V = svd.matrixV(); // 5. 旋转矩阵 Eigen::Matrix3d R = V * U.transpose(); // 6. 检查反射情况 if (R.determinant() < 0) { V.col(2) *= -1.0; R = V * U.transpose(); } // 7. 平移 Eigen::Vector3d t = q_centroid.transpose() - R * p_centroid.transpose(); Pose result; result.R = R; result.t = t; return result; }这里有几处细节我认为值得单独拎出来讲。
colwise().mean()返回的是一个行向量,表示每一列的平均值。对点云来说正好就是所有点的平均坐标。去质心用p.rowwise() - p_centroid,这个表达式会把每一行都减去同一个行向量,是 Eigen 里少有的"广播"机制,写起来非常舒服。
H = p_demean.transpose() * q_demean这个操作的维度是3xN * Nx3,结果是 3x3,正好是协方差矩阵。很多教材里的写法是sum(p_i' * q_i'^T),等价于矩阵形式。用矩阵乘法一行代码搞定,也比显式循环快得多。
5.3 生成模拟数据验证结果
光写函数不够,还要证明它能跑。我用一个简单的模拟场景来验证:先随机生成一批三维点,构造一个已知的旋转和平移,施加到这些点上,再加一点噪声,最后用上面的函数去反解,对比误差。
#include <cmath> #include <cstdlib> #include <ctime> int main() { std::srand(static_cast<unsigned>(std::time(nullptr))); // 生成 50 个随机三维点 const int N = 50; Eigen::MatrixXd p(N, 3); for (int i = 0; i < N; ++i) { p(i, 0) = static_cast<double>(std::rand()) / RAND_MAX * 10.0; p(i, 1) = static_cast<double>(std::rand()) / RAND_MAX * 10.0; p(i, 2) = static_cast<double>(std::rand()) / RAND_MAX * 10.0; } // 已知的旋转(绕 Z 轴转 30 度)和平移 Eigen::AngleAxisd rotation(30.0 * M_PI / 180.0, Eigen::Vector3d::UnitZ()); Eigen::Matrix3d R_gt = rotation.toRotationMatrix(); Eigen::Vector3d t_gt(1.0, 2.0, 3.0); // 施加变换并加入小噪声 Eigen::MatrixXd q(N, 3); for (int i = 0; i < N; ++i) { Eigen::Vector3d pt = p.row(i).transpose(); Eigen::Vector3d qt = R_gt * pt + t_gt; qt += Eigen::Vector3d::Random() * 0.01; // 标准差约 0.01 的噪声 q.row(i) = qt.transpose(); } // 用函数求解 Pose result = SolveRigidTransform(p, q); std::cout << "Estimated R:\n" << result.R << std::endl; std::cout << "Ground truth R:\n" << R_gt << std::endl; std::cout << "Estimated t:\n" << result.t.transpose() << std::endl; std::cout << "Ground truth t:\n" << t_gt.transpose() << std::endl; // 计算旋转误差 Eigen::Matrix3d diff = result.R - R_gt; double rot_error = diff.norm(); double trans_error = (result.t - t_gt).norm(); std::cout << "Rotation error norm: " << rot_error << std::endl; std::cout << "Translation error norm: " << trans_error << std::endl; return 0; }噪声比较小的时候,旋转误差和平移误差应该都在 1e-2 量级甚至更低,这足以说明算法和代码没有大问题。如果你加大噪声到 0.1 以上,依然能保持大致正确,只是误差会变大。这个实验特别适合用来验证自己对 SVD 分解、矩阵乘法顺序、坐标维度转换的理解。
5.4 配准实战中的几个工程经验
第一,对应点匹配是另一个大问题。上面假设点对一一对应是已知的,真实点云里通常不知道哪个点对应哪个点,那就要走 ICP 或者特征匹配的路线。但不管前面的匹配怎么做,最后一步"用 SVD 求刚体变换"的流程都是一样的,所以这个函数可以直接复用。
第二,实际数据里经常混有离群点。一个离群点就可能把协方差矩阵拉偏,导致结果明显变差。所以在做 SVD 求解之前,不要天真地直接上全部点对,最好先用某种方式过滤离群点,或者做两步迭代:先粗配准,算出误差大的点对并剔除,再精配准。
第三,注意数值范围。如果点云坐标数值特别大,比如几十万量级,减去质心之后点坐标仍然很大,构造的H矩阵会出现数值差异太大的问题,SVD 的稳定性会受到影响。这时候可以先对点云做缩放归一化,或者使用更高精度的long double中间量。我用的做法是先把所有点绕质心做一个缩放到单位范围,解出变换后再把缩放补偿回去。
6. 几个我自己绕过的弯,说给你听
最后分享几个实操层面的小体会,都是我在项目里切切实实踩过的。
第一,Eigen 的调试输出别只盯着cout。虽然std::cout << matrix很方便,但矩阵一大就看不出哪里不对。我后来习惯在可疑的地方打印矩阵维度、对角线元素、范数这些统计量,再用matrix.isApprox(other, precision)做近似比较。这个函数比逐个元素比绝对差要科学得多,因为它考虑了数值尺度。
第二,别频繁在代码里用auto接收 Eigen 表达式的返回值。Eigen 有延迟计算机制,auto会把表达式本身存下来,而不是立刻算完。如果后面这个表达式引用的矩阵被修改或者生命周期结束,就可能得到神秘崩溃。我在很早以前就养成了"Eigen 返回值尽量显式声明类型"的习惯,尤其是像transpose()、matrixU()这些,要用具体类型接住。
第三,内存分配问题。Eigen 的固定尺寸对象(比如Matrix3d、Vector3d)默认不产生堆分配,性能很好;但一旦用MatrixXd,很多操作会触发动态内存分配。在实时性要求高的循环里,尽量把动态矩阵的尺寸提前固定下来,或者复用预分配好的对象,用noalias()防止乘法的临时变量。做点云配准这类运算时,这部分优化很值得做。
回到这篇的标题:Eigen 的几何模块和分解工具,说起来也就是一组头文件、几百行 API,但真正的价值在于你遇到实际问题时,知道该从口袋里掏出哪把钥匙。这篇文章把旋转表达、四元数、刚体变换、特征值分解和 SVD 从 API 到实战思路都过了一遍,也在配准案例里演示了它们是怎么协同工作的。希望你在项目里遇到类似需求时,能少走一点我当年走过的弯路。