Sophus库Cartesian实现:从向量空间到李群的工程实践

📅 2026/7/25 7:48:37
Sophus库Cartesian实现:从向量空间到李群的工程实践
1. 项目概述从向量空间到李群在机器人学、计算机视觉和SLAM即时定位与地图构建领域我们经常需要处理三维空间中的旋转和平移。一个最基础的需求就是描述一个点或一个向量在空间中的位置和方向。通常我们会使用一个三维向量[x, y, z]^T来表示一个点或一个方向这构成了我们熟悉的欧几里得向量空间也就是笛卡尔坐标系。这个空间本身是一个线性空间向量之间可以做加法和数乘非常直观。然而当我们开始处理一系列连续的位姿变换比如机器人的运动轨迹时仅仅使用向量空间就显得捉襟见肘了。因为位姿变换旋转平移的集合在数学上构成了一个李群Lie Group例如特殊欧几里得群 SE(3)。李群的优势在于它同时具备光滑流形和群的结构使得我们能够在其上进行微分运算从而定义出李代数Lie Algebra这是进行优化如Bundle Adjustment和滤波如卡尔曼滤波时计算雅可比矩阵的关键。那么一个自然而然的问题是我们最熟悉的那个简单的三维向量空间R^3它本身是不是一个李群呢如果是它的群运算是什么它的李代数又是什么Sophus库的cartesian.hpp文件正是为了回答这个问题而存在的。它巧妙地将欧几里得向量空间实现为一个李群通常称为R^3群或平移群。这个实现看似简单却蕴含着深刻的思想它是理解更复杂李群如 SO(3), SE(3)的绝佳跳板。对于正在学习Sophus库或者希望深入理解李群李代数在C中如何落地的开发者来说剖析cartesian.hpp是必不可少的一步。它剥离了旋转的复杂性让我们专注于李群最核心的框架群运算、指数/对数映射、扰动模型为后续处理带有旋转的群打下坚实的基础。2. 核心概念解析为什么向量空间需要成为李群在深入代码之前我们必须厘清几个核心概念理解为什么要把简单的向量“包装”成一个李群。这不仅仅是数学上的游戏而是工程实践中的强烈需求。2.1 欧几里得向量空间 (R^3) 的局限性在标准的三维欧几里得空间R^3中元素是向量v (x, y, z)。它的运算是向量的加法和标量乘法。这在描述绝对位置或相对位移时没有问题。但是在状态估计问题中我们经常需要处理“状态更新”。例如我们有一个机器人的位置估计p通过传感器得到一个位移测量值delta最简单的更新是p_new p delta。然而当我们的状态变量不仅仅是位置而是包含旋转的位姿T ∈ SE(3)时更新就不再是简单的加法了而是李群上的乘法T_new T * Exp(delta)其中delta是对应李代数空间中的向量Exp是指数映射。如果我们希望用一套统一的框架来处理纯平移、纯旋转以及刚体变换那么最好的方式就是让它们都属于“李群”这个大家庭。这样无论是优化中的残差计算还是滤波中的状态传播我们都可以调用同一套接口群乘法()、.log()、.exp()、^伴随映射等。cartesian.hpp就是将R^3“提升”为这样一个李群使其能够无缝集成到Sophus的通用李群框架中。2.2 李群 (Lie Group) 与李代数 (Lie Algebra) 的工程意义李群是一个连续光滑的群。对我们而言最重要的两个性质是群运算乘法定义了如何组合两个变换。光滑结构允许我们求导从而定义出李代数。李代数对应李群在单位元处的切空间。它是一个向量空间其元素可以看作是一种“无穷小变换”或“速度”。李代数和李群通过指数映射exp和对数映射log相互转换。exp: 将李代数中的一个向量通常很小映射为李群中的一个元素。物理意义是沿着某个“方向”李代数运动“单位时间”到达的新位置李群。log: 是exp的逆将李群中的一个元素尤其是靠近单位元的元素映射回李代数。物理意义是从单位元到该元素所需的“运动向量”。在SLAM的图优化中我们通常将位姿李群元素作为优化变量但优化过程是在李代数空间向量空间中进行的。因为李代数是向量空间我们可以直接在其上做加法来更新变量ξ - ξ Δξ其中ξ是李代数坐标。然后通过指数映射将更新后的李代数映射回李群T_new exp(ξ Δξ)。这比直接在李群上定义更新要自然和高效得多。cartesian.hpp为R^3实现了这套机制。2.3 Cartesian 群的数学定义具体到Sophus::Cartesian我们简称R^3群它的定义如下群元素就是三维向量(x, y, z)。群乘法定义为向量的加法。是的你没看错。单位元是零向量(0, 0, 0)。任意元素的逆元是其相反数-v。 验证一下封闭性向量加向量还是向量、结合律向量加法满足、单位元零向量、逆元相反数。完全符合群的定义。李代数由于群运算是加法李代数就是它自身也是R^3。因为加法本身是可交换的ab ba所以这是一个阿贝尔李群其李代数的李括号运算为零。指数/对数映射因为李代数和群在集合上是同一个东西R^3且群运算是加法所以指数映射和对数映射就是恒等映射即exp(v) v,log(v) v。这是理解这个实现的关键。有了这些理论基础我们再去看cartesian.hpp的代码就会觉得它“理所应当”它就是用C模板将上述数学定义严谨地实现出来并接入Sophus的通用接口。3.cartesian.hpp源码深度剖析Sophus库的cartesian.hpp文件通常位于sophus/geometry/目录下。它的实现非常精炼是模板元编程和李群概念结合的典范。我们来逐部分拆解。3.1 类模板声明与基本类型定义namespace Sophus { template class Scalar_, int Dim, class Options int class Cartesian; }首先它定义了一个类模板Cartesian。Scalar_标量类型通常是double或float表示向量每个分量的数据类型。Dim空间的维度。虽然我们最常用的是3维但实现是泛化的可以是任意正整数。Options可选参数用于控制Eigen的内存对齐等特性通常使用默认值。接下来在类内部会定义一系列公共类型别名using这是Eigen和Sophus的惯例用于在泛型编程中提取类型。public: using Scalar Scalar_; using Transformation Eigen::MatrixScalar, Dim, 1; // 群元素本身就是一个Dim维列向量 using Point Eigen::MatrixScalar, Dim, 1; // 点也是一个Dim维列向量 using Tangent Eigen::MatrixScalar, Dim, 1; // 切空间李代数向量形状和群元素一样 using Adjoint Eigen::MatrixScalar, Dim, Dim; // 伴随矩阵对于阿贝尔群是单位阵这里最关键的是Transformation被定义为Eigen::MatrixScalar, Dim, 1。这意味着Cartesian群的“变换矩阵”就是一个简单的列向量。这直观地反映了“群元素即向量”的概念。3.2 核心群运算的实现李群的核心是群乘法。对于Cartesian群乘法就是向量加法。// 群乘法 this * other SOPHUS_FUNCTION Transformation operator*(Transformation const other) const { return transformation_ other; // 直接返回向量加法结果 }这里的transformation_是类的私有成员存储当前的向量群元素。operator*被重载为加法完美体现了该群的数学定义。求逆运算也很简单SOPHUS_FUNCTION Transformation inverse() const { return -transformation_; // 逆元就是取反 }3.3 指数映射与对数映射如前所述对于Cartesian群指数和对数映射是平凡的恒等映射。但Sophus仍然提供了标准的接口。// 指数映射将李代数向量 omega 映射到群上 template class Derived SOPHUS_FUNCTION static Transformation exp(Eigen::MatrixBaseDerived const omega) { SOPHUS_ENSURE(omega.rows() Dim omega.cols() 1, “omega must be Dimx1 vector.”); return omega; // 恒等映射 } // 对数映射将群元素 R 映射到李代数 template class Derived SOPHUS_FUNCTION static Tangent log(Eigen::MatrixBaseDerived const R) { SOPHUS_ENSURE(R.rows() Dim R.cols() 1, “R must be Dimx1 vector.”); return R; // 恒等映射 }这里使用了Eigen::MatrixBaseDerived作为参数这是Eigen库中编写通用矩阵函数的常用技巧可以接受任意Eigen表达式列向量、块操作等的结果。SOPHUS_ENSURE是一个宏用于在调试模式下进行维度检查。注意虽然这里exp和log的实现就是直接返回输入但保留这个接口至关重要。它保证了所有Sophus李群对象SO3,SE3,Sim3,Cartesian都拥有完全相同的API。在模板函数中我们可以统一地调用T::exp(omega)和T.log()而无需关心T具体是哪种群。3.4 作用于点的变换一个李群作为变换最重要的功能之一就是作用于一个点将其变换到另一个位置。对于纯平移群Cartesian这就是点的平移。// 将变换 this 作用于点 p template class PointDerived SOPHUS_FUNCTION Point operator*(Eigen::MatrixBasePointDerived const p) const { SOPHUS_ENSURE(p.rows() Dim p.cols() 1, “p must be Dimx1 vector.”); return transformation_ p; // 点加向量即平移 }实现就是向量的加法。这符合我们的直觉一个平移向量t作用于点p得到新点p p t。3.5 伴随矩阵与李代数求导在优化中我们经常需要处理李代数的扰动。如果对一个群元素T左乘一个微小扰动exp(δξ)即T exp(δξ) * T那么T对应的李代数ξ与T对应的李代数ξ以及扰动δξ是什么关系这由伴随矩阵 Adj联系起来ξ ≈ ξ Adj_T * δξ。对于Cartesian这个阿贝尔群由于其李代数的李括号为零其伴随矩阵是单位阵。SOPHUS_FUNCTION Adjoint Adj() const { return Adjoint::Identity(); // 伴随矩阵是单位阵 }这意味着在Cartesian群上左扰动和右扰动是等价的且扰动直接加到李代数坐标上。这简化了许多计算。3.6 构造函数与数据访问类提供了多种构造函数// 默认构造为零向量单位元 Cartesian() : transformation_(Transformation::Zero()) {} // 从Eigen向量构造 template class Derived explicit Cartesian(Eigen::MatrixBaseDerived const vec) : transformation_(vec) {} // 从标量列表构造例如 Cartesian(1.0, 2.0, 3.0) Cartesian(Scalar const x, Scalar const y, Scalar const z) { SOPHUS_ENSURE(Dim 3, “Constructor only available for Dim3.”); transformation_ x, y, z; }以及数据访问函数// 返回内部向量的只读引用 Transformation const transformation() const { return transformation_; } // 返回内部向量的可写引用谨慎使用 Transformation transformation() { return transformation_; } // 重载括号运算符访问分量 Scalar const operator[](int i) const { return transformation_(i); } Scalar operator[](int i) { return transformation_(i); }实操心得直接使用transformation()返回的引用修改内部数据时必须确保不破坏对象的任何不变性。对于Cartesian来说任何向量都是合法的所以相对安全。但对于SO3旋转矩阵直接修改矩阵元素可能导致它不再是正交矩阵从而破坏对象的不变性。因此对于非阿贝尔群更安全的做法是通过setQuaternion或fromRotationMatrix这样的成员函数来修改状态。4. 在SLAM与视觉项目中的实战应用理解了Cartesian的实现我们来看看它在实际项目中如何被使用。它的主要用途是作为构建更复杂系统的“积木”以及在某些特定场景下简化问题。4.1 作为SE(3)的平移部分Sophus::SE3表示刚体变换包含旋转R(SO3) 和平移t(R^3)。在SE3的内部实现中平移部分t很可能就是一个Sophus::CartesianScalar, 3类型的对象或者至少遵循相同的接口。当我们从SE3中提取平移向量时我们得到的就是一个Cartesian群元素。Sophus::SE3d pose ...; // 某个位姿 Eigen::Vector3d t pose.translation(); // 获取平移向量 // 实际上pose.translation() 返回的可能就是 Cartesiandouble, 3 类型或者可以隐式转换为 Vector3d。 // 我们可以利用 Cartesian 的群运算 Sophus::Cartesiandouble, 3 delta_t(0.1, 0.0, 0.0); // 一个X方向的平移扰动 // 对位姿进行纯平移更新假设旋转不变 // 注意这不是标准的SE3更新标准更新应在李代数空间进行。 // 这里只是演示Cartesian作为独立群的使用。 auto new_translation t * delta_t; // 调用了 Cartesian 的 operator*即向量加法4.2 构建纯平移运动模型在一些简化的问题中如果我们可以忽略旋转例如地面机器人只在平面内做非常缓慢的移动或处理某些只有平移自由度的机械臂那么我们可以直接用CartesianScalar, 2或CartesianScalar, 3来表示状态。using Pose2d Sophus::Cartesiandouble, 2; // 二维平面位置 Pose2d current_pose(0.0, 0.0); // 接收到一个里程计测量值也是平移 Pose2d odometry_delta(0.5, 0.1); // 前进0.5米左移0.1米 // 状态预测使用李群乘法向量加法 Pose2d predicted_pose current_pose * odometry_delta; // 等价于 current_pose.transformation() odometry_delta std::cout “Predicted position: (“ predicted_pose[0] “, “ predicted_pose[1] “)\n”;在这个模型里我们依然使用了李群的框架operator*保持了代码模式与处理SE2/SE3的一致性。当未来需要加入旋转时只需将类型别名Pose2d从Cartesian改为SE2高层代码的逻辑结构可以基本保持不变。4.3 在优化问题中作为参数块在使用Ceres Solver或g2o等优化库时我们需要定义参数块ParameterBlock和其对应的局部参数化LocalParameterization。对于三维平移我们可以将其定义为一个Cartesian群。虽然我们通常直接使用Eigen::Vector3d作为参数块并使用EigenQuaternionParameterization或自定义的SE3Parameterization。但Sophus提供了统一的LieGroup概念。理论上我们可以为Sophus::Cartesian实现一个通用的LieGroupParameterization它知道如何对这个群进行oplus操作即李群上的更新T T * Exp(Δ)和lift/retract即log/exp。这样优化库就可以用统一的接口处理任何Sophus李群。// 伪代码展示概念 class LieGroupParameterization : public ceres::LocalParameterization { public: explicit LieGroupParameterization(int group_dim) : dim_(group_dim) {} virtual bool Plus(double const* T_raw, double const* delta_raw, double* T_plus_delta_raw) const { // 将 raw 指针转换为 Sophus 李群对象 T Sophus::Cartesiandouble, 3 T ...; Eigen::Mapconst Eigen::Vector3d delta(delta_raw); // 执行李群更新: T_plus_delta T * Exp(delta) // 对于 Cartesian Exp(delta) delta, 乘法是加法 auto T_plus_delta T * Sophus::Cartesiandouble, 3(delta); // 将结果写回 T_plus_delta_raw ... return true; } virtual bool ComputeJacobian(double const* T_raw, double* jacobian_raw) const { // 计算 Plus 操作关于 delta 的雅可比矩阵。 // 对于 Cartesian由于 Plus 是线性加法雅可比是单位阵。 Eigen::MapEigen::Matrixdouble, Dim, Dim, Eigen::RowMajor jacobian(jacobian_raw); jacobian.setIdentity(); return true; } virtual int GlobalSize() const { return dim_; } virtual int LocalSize() const { return dim_; } // 对于Cartesian局部维度等于全局维度 private: int dim_; };虽然在实际项目中我们可能不会特意为Cartesian实现这个因为直接使用Eigen::Vector3d配合恒等雅可比更简单但Sophus的这种设计体现了其架构的通用性和优雅性。5. 常见陷阱与性能优化指南即使是一个简单的Cartesian实现在使用时也有需要注意的地方特别是在高性能计算场景下。5.1 维度混淆与编译时检查Sophus大量利用了C的模板特性在编译时进行维度检查。如果你错误地混合了不同维度的Cartesian对象编译器会在第一时间报错。Sophus::Cartesiandouble, 2 pose2d(1.0, 2.0); Sophus::Cartesiandouble, 3 point3d(1.0, 2.0, 3.0); auto result pose2d * point3d; // 编译错误无法将 Dim2 的群与 Dim3 的点相乘这比在运行时才发现维度不匹配要安全得多。务必确保你操作的群和点、李代数向量的维度是一致的。5.2 隐式类型转换的代价Cartesian的Transformation类型是Eigen::MatrixScalar, Dim, 1。它和普通的Eigen::Vector3d在内存布局上是完全一致的因此它们之间的转换成本极低通常是零成本的。但是要注意避免不必要的临时对象创建。// 好的做法直接使用Eigen表达式避免中间变量 Eigen::Vector3d a(1,2,3), b(4,5,6); Sophus::Cartesiandouble, 3 ca a; // 构造可能涉及一次拷贝如果编译器没有优化 Sophus::Cartesiandouble, 3 cb b; auto result ca * cb; // 调用 operator*返回一个 Cartesian 对象 Eigen::Vector3d v_result result.transformation(); // 转换回 Vector3d // 更好的做法如果只是做一次加法直接用Eigen更高效。 Eigen::Vector3d v_result2 a b;在性能关键的循环中如果操作可以完全用Eigen向量运算表达那么直接使用Eigen::Vector3d可能比通过Cartesian对象间接操作更高效因为后者多了一层函数调用和对象构造/析构的开销。Cartesian的价值在于其抽象的接口和类型安全而非绝对的运行时性能。5.3 与 Eigen 代数运算的混合使用Cartesian重载了operator*用于群乘法。但Eigen::Vector3d也有operator*表示标量乘法或矩阵乘法如果尺寸匹配。在混合使用时要注意区分上下文。Sophus::Cartesiandouble, 3 t(1, 0, 0); Eigen::Vector3d v(0, 1, 0); auto r1 t * t; // 这是 Cartesian 的群乘法结果是 Cartesian(2, 0, 0) auto r2 t.transformation().array() * v.array(); // 这是逐元素乘法结果是 Vector3d(0, 0, 0) auto r3 t.transformation().dot(v); // 点积结果是 0 auto r4 t.transformation().cross(v); // 叉积结果是 Vector3d(0, 0, 1)清晰地区分“李群乘法”和“线性代数运算”是正确使用Sophus的关键。当你想进行向量空间内的线性代数运算时最好通过.transformation()方法取出内部的Eigen向量再进行操作。5.4 自定义标量类型与自动微分Sophus支持自定义标量类型这意味着你可以将double替换为ceres::Jet用于Ceres自动求导或stan::math::var用于Stan。这对于需要计算雅可比矩阵的优化问题非常有用。#include ceres/jet.h using Jet ceres::Jetdouble, 6; // 假设有6个优化变量 Sophus::CartesianJet, 3 t_with_jet(Jet(1.0, 0), Jet(2.0, 1), Jet(3.0, 2)); // 现在 t_with_jet 的每个分量都携带了导数信息 // 当你用这个对象参与运算时Ceres可以自动追踪梯度要启用这个功能你需要确保你的标量类型支持所有必要的数学运算,-,*,/,sqrt,cos,sin等。Sophus内部使用了Eigen而Eigen本身也支持自定义标量类型所以这套流程通常是可行的。这为复杂优化问题的导数计算提供了极大的便利。6. 扩展思考从 Cartesian 到更复杂的李群Cartesian是李群中最简单的例子。通过彻底理解它我们可以建立起一套分析更复杂李群的思维框架。6.1 对比 SO(3) 和 SE(3)让我们将Cartesian与SO(3)旋转群和SE(3)刚体运动群进行对比特性Cartesian (R^3)SO(3)SE(3)群元素向量v ∈ R^3旋转矩阵R ∈ R^{3x3},R^T R I, det(R)1齐次变换矩阵T [R, t; 0, 1]群乘法向量加法v1 * v2 v1 v2矩阵乘法R1 * R2矩阵乘法T1 * T2单位元零向量(0,0,0)单位矩阵I单位齐次矩阵[I, 0; 0, 1]逆元-vR^T[R^T, -R^T t; 0, 1]李代数R^3so(3)(3维向量对应旋转轴*角度)se(3)(6维向量前3维旋转后3维平移)指数映射exp(ω) ω(恒等)exp(φ^) I sinθ/θ φ^ (1-cosθ)/θ^2 (φ^)^2(罗德里格斯公式)复杂可分拆为旋转和平移部分对数映射log(v) v(恒等)log(R)求解旋转轴和角度复杂需从变换矩阵中提取作用于点p p v(平移)p R * p(旋转)p R * p t(旋转平移)伴随矩阵Adj(v) I(单位阵)Adj(R) RAdj(T) [R, t^ R; 0, R]是否阿贝尔是(交换群)否否从这个对比可以看出Cartesian的简单性体现在群乘法可交换、指数/对数映射平凡、伴随矩阵为单位阵。SO(3)和SE(3)则因为旋转的不可交换性而变得复杂。学习Cartesian就像在学习李群的“Hello World”它让我们熟悉了接口和概念而不被复杂的数学公式吓倒。6.2 实现自定义的李群理解了Cartesian的实现模式你甚至可以尝试为一些简单的李群实现自己的类。例如二维平面上的旋转群SO(2)其群元素可以用一个角度θ表示群乘法是角度相加模2π李代数就是实数轴R指数映射exp(ω) ω将角速度积分得到角度对数映射log(θ) θ将角度映射为角速度。它的实现会比Cartesian稍微复杂一点因为需要处理模2π的归一化但整体结构是相似的。这种练习能极大地加深你对李群理论及其数值实现的理解。Sophus的代码风格清晰注释良好是学习如何将抽象数学理论转化为高效、安全C代码的绝佳范本。7. 调试技巧与单元测试当你编写或使用依赖于Sophus::Cartesian或其他李群的代码时掌握一些调试技巧和建立单元测试的习惯至关重要。7.1 验证群性质对于任何你实现的或使用的李群都应该验证其基本群性质。对于Cartesian这几乎是显然的但对于更复杂的群这是一个重要的测试。#include sophus/cartesian.hpp #include iostream #include cassert void test_group_properties() { using Group Sophus::Cartesiandouble, 3; Group a(1.0, 2.0, 3.0); Group b(4.0, 5.0, 6.0); Group c(7.0, 8.0, 9.0); Group identity; // 默认构造为零向量 // 1. 封闭性: a*b 仍然是一个 Group Group ab a * b; static_assert(std::is_samedecltype(ab), Group::value, “Closure property violated.”); // 2. 结合律: (a*b)*c a*(b*c) Group left (a * b) * c; Group right a * (b * c); assert((left.transformation() - right.transformation()).norm() 1e-10); // 3. 单位元: a * identity a identity * a a assert(((a * identity).transformation() - a.transformation()).norm() 1e-10); assert(((identity * a).transformation() - a.transformation()).norm() 1e-10); // 4. 逆元: a * a.inverse() identity assert(((a * a.inverse()).transformation() - identity.transformation()).norm() 1e-10); std::cout “All group properties hold for Cartesian.\n”; }对于Cartesian这些断言都应该通过。如果未来你修改了代码运行这样的测试可以快速发现基本错误。7.2 验证指数/对数映射的一致性指数和对数映射应该是互逆的至少在单位元附近。这是一个关键的测试。void test_exp_log() { using Group Sophus::Cartesiandouble, 3; using Tangent Group::Tangent; // 测试1: log(exp(omega)) omega Tangent omega; omega.setRandom(); // 随机生成一个李代数向量 Group g Group::exp(omega); Tangent omega_recovered g.log(); assert((omega - omega_recovered).norm() 1e-10); // 测试2: exp(log(g)) g (对于任意群元素g) Group g2; g2.transformation().setRandom(); Tangent omega2 g2.log(); Group g2_recovered Group::exp(omega2); assert((g2.transformation() - g2_recovered.transformation()).norm() 1e-10); std::cout “Exp and log are consistent for Cartesian.\n”; }对于Cartesian这个测试是平凡的因为exp和log都是恒等映射。但对于SO(3)这个测试就非常重要可以验证罗德里格斯公式实现的正确性。7.3 使用 Eigen 的 IO 格式进行调试Sophus::Cartesian内部存储是Eigen::Vector因此可以直接利用Eigen的流输出进行调试非常方便。Sophus::Cartesiandouble, 3 t(1.1, 2.2, 3.3); std::cout “Cartesian t \n” t.transformation().transpose() std::endl; // 输出: Cartesian t 1.1 2.2 3.3 // 如果你想打印整个对象可能需要重载 operator或者直接打印其成员。 // 一个简单的办法是定义一个辅助函数 template class Scalar, int Dim std::ostream operator(std::ostream os, Sophus::CartesianScalar, Dim const c) { os c.transformation().transpose(); return os; } // 之后就可以直接 cout t;在GDB或LLDB调试器中你也可以直接打印t.transformation()来查看其值。7.4 性能剖析与编译器优化在Release模式下编译器会对简单的内联函数进行积极的优化。Cartesian的许多操作如operator*,exp,log都是非常简单的内联函数最终生成的机器码应该和直接操作Eigen::Vector3d没有区别。你可以通过查看反汇编或使用性能分析工具如perf来验证。一个常见的性能陷阱是在Debug模式下由于SOPHUS_ENSURE宏通常是断言检查的存在以及编译器优化被关闭Sophus的操作会比原始Eigen操作慢很多。这是正常的因为安全检查和调试信息带来了开销。在最终部署的性能关键代码中请确保使用Release编译模式-O2或-O3并可能定义NDEBUG宏来禁用断言。我个人在项目中的体会是Sophus::Cartesian更像一个“概念验证”和“接口统一者”。在绝大多数只涉及平移的场合直接使用Eigen::Vector3d是完全没有问题的而且更直接、更不容易让队友困惑。但是当你正在构建一个大型的、基于李群的框架或者你在学习Sophus库以期将来处理SO3/SE3时从Cartesian入手并理解其设计哲学无疑是性价比极高的投资。它就像一把钥匙帮你打开了李群李代数这个强大数学工具库的大门让你后续理解四元数、旋转矩阵、扰动模型、雅可比推导时能够站在一个更统一、更清晰的视角上。