1. 项目概述:为什么我们需要Sophus?
在机器人、自动驾驶、增强现实这些领域,我们经常需要处理三维空间中的旋转和平移。比如,一个机械臂的关节如何转动,一个相机在空间中如何移动,一个虚拟物体如何叠加到现实世界。这些变换,在数学上有一个非常优雅且强大的工具来描述——李群(Lie Group)和李代数(Lie Algebra)。
你可能已经用过或听说过Eigen库,它是C++里处理矩阵和向量的“瑞士军刀”,线性代数运算又快又好。但当你需要频繁地在旋转矩阵、四元数、旋转向量之间转换,或者需要计算这些变换的微小扰动(导数)时,光有Eigen就显得有些力不从心了。你需要不断地写一堆模板化的、容易出错的代码来处理这些特殊的数学对象。
这就是Sophus库登场的时候。Sophus是一个基于Eigen构建的、专门用于处理李群和李代数的C++库。它不是一个独立的数学库,而是Eigen的一个“超级外挂”,专注于SO(3)、SE(3)、SO(2)、SE(2)等这些在SLAM、机器人学中至关重要的群。简单来说,它把那些关于旋转和平移的、又抽象又容易写错的数学操作,封装成了清晰、高效且类型安全的C++类。如果你正在用C++写SLAM后端、机器人运动学、或者任何涉及三维几何变换的算法,Sophus几乎是一个绕不开的工具。它能让你从繁琐的数学公式实现中解放出来,更专注于算法逻辑本身。
2. 核心概念扫盲:李群与李代数到底是什么?
在深入使用Sophus之前,我们有必要花点时间,用最直白的方式理解一下它背后的数学概念。别怕,我们不推导公式,只讲直觉。
2.1 李群:描述“光滑”的变换集合
想象一下所有可能的、光滑的、连续的三维旋转。这些旋转构成一个集合,这个集合就是一个“群”(Group),因为它满足封闭性、结合律、有单位元(不旋转)、有逆元(反方向转回来)。又因为描述旋转的参数(比如欧拉角、轴角)是连续的,所以它是一个“李群”(Lie Group)。SO(3)就是所有三维旋转矩阵构成的李群。
同理,SE(3)是“特殊欧几里得群”,它描述的是三维空间中的刚体运动,即旋转加平移。一个SE(3)变换,可以用一个4x4的矩阵来表示,左上角3x3是旋转部分,右上角3x1是平移向量。
为什么用群?因为刚体变换可以连续地复合(先旋转再平移,或者先A变换再B变换),这正好对应了群的“乘法”操作。使用群的概念,能让我们的数学表达和代码实现非常统一和简洁。
2.2 李代数:描述“瞬时”的变换速度与扰动
李群是描述“状态”的(比如相机当前的位姿)。但当我们想讨论“变化”时,比如相机的运动速度、或者给当前位姿加一个微小的扰动,直接在李群上操作会很麻烦(因为李群空间对于加法不封闭)。
这时就需要李代数。你可以把李代数想象成李群在单位元处的“切空间”。一个李代数元素,比如 so(3),是一个三维向量,它的方向代表旋转轴,模长代表旋转角度。它描述的是一个“瞬时”的旋转速度或一个无穷小的旋转。
李群与李代数的核心关系:指数映射和对数映射。
- 指数映射 (Exp): 把一个李代数向量(so(3)),“积分”成对应的李群元素(SO(3))。物理意义是:给定一个旋转轴和角速度,积分一段时间后得到最终的旋转。
- 对数映射 (Log): 是Exp的逆运算。把一个李群元素(SO(3)),“求导”得到对应的李代数向量(so(3))。物理意义是:给定一个旋转,求其对应的旋转轴和角度。
这个关系是Sophus库的基石,也是优化算法(如Bundle Adjustment)中计算雅可比矩阵的关键。
注意: 对于初学者,一个常见的误解是把李代数 so(3) 直接当作旋转向量。虽然它们都是三维向量,且通过指数映射对应,但在一些细微的归一化处理上(特别是当旋转角度接近π时),直接等同可能会出问题。Sophus帮我们妥善处理了这些边界情况。
3. Sophus库的整体设计与模块解析
Sophus的设计哲学非常清晰:为每个李群(SO2, SO3, SE2, SE3)和李代数(so2, so3, se2, se3)提供对应的C++类,并实现它们之间以及它们与Eigen、STL容器之间的无缝交互。
3.1 核心类层次结构
Sophus库主要包含以下核心类,它们都定义在Sophus命名空间下:
- SO3d / SO3f: 表示三维旋转群,模板参数表示标量类型(double/float)。内部通常用一个单位四元数(Eigen::Quaternion)来存储,因为它在数值上更稳定。
- SE3d / SE3f: 表示三维刚体变换群。内部包含一个
SO3d对象表示旋转,和一个Eigen::Vector3d对象表示平移。 - SO2d / SO2f, SE2d / SE2f: 对应的二维版本,在平面机器人或某些简化模型中常用。
- 对应的李代数类:
Sophus::Vector3d(作为 so3),Sophus::Vector6d(作为 se3) 等。注意,这些李代数向量就是普通的Eigen向量,Sophus通过函数来操作它们。
3.2 关键特性与设计考量
- 基于Eigen,类型安全:所有类都重度使用Eigen的类型作为成员或基类。这意味着你可以像使用Eigen向量一样使用Sophus对象进行加减乘除(当然是在数学意义允许的范围内)。类型系统能帮助你在编译期捕获很多错误,比如误将SO3对象与向量点乘。
- 模板化标量类型:提供
Sophus::SO3和Sophus::SO3的模板特化,方便你在精度和速度之间权衡。大多数SLAM系统使用double以保证数值稳定性。 - 提供多种构造函数:可以从旋转矩阵、四元数、轴角、欧拉角等多种形式构造SO3。SE3可以从旋转和平移向量,或者直接从一个4x4变换矩阵构造。这大大提升了易用性。
// 多种方式构造SO3 Eigen::Matrix3d R = Eigen::AngleAxisd(M_PI/2, Eigen::Vector3d::UnitZ()).toRotationMatrix(); Sophus::SO3d SO3_R(R); // 从旋转矩阵构造 Eigen::Quaterniond q(R); Sophus::SO3d SO3_q(q); // 从四元数构造 // 从旋转向量(轴角)构造,注意这里是李代数 so3 Eigen::Vector3d so3(0, 0, M_PI/2); // 绕Z轴旋转90度 Sophus::SO3d SO3_so3 = Sophus::SO3d::exp(so3); // 指数映射 - 重载常用运算符:
*运算符被重载为群的乘法(变换的复合)。对于SO3d,*可以作用于另一个SO3d(旋转复合)或一个Vector3d(旋转一个点)。对于SE3d,*可以作用于另一个SE3d或一个Vector3d(用变换作用到一个三维点上)。Sophus::SO3d R1, R2; Eigen::Vector3d p(1, 0, 0); Sophus::SO3d R_composite = R1 * R2; // 旋转的复合 Eigen::Vector3d p_rotated = R1 * p; // 旋转点p Sophus::SE3d T1, T2; Sophus::SE3d T_composite = T1 * T2; // 变换的复合 Eigen::Vector3d p_transformed = T1 * p; // 变换点p (旋转+平移) - 提供指数/对数映射:
Sophus::SO3d::exp()和Sophus::SO3d::log()是核心函数。exp()将李代数向量转为李群,log()反之。SE3同理。 - 扰动模型:这是Sophus在SLAM中最重要的功能之一。它提供了计算李群关于李代数的导数(雅可比矩阵)的工具。例如,
SO3::JacobianR()可以计算当用右乘扰动模型时,旋转矩阵关于扰动小量的雅可比。这在图优化中计算残差关于位姿节点的导数时至关重要。
4. 从安装到“Hello World”:环境配置与基础使用
4.1 安装Sophus
Sophus是一个只有头文件的库(Header-only),这使它的安装变得极其简单。但为了使用方便,通常我们还是推荐用CMake来管理。
方法一:直接拷贝头文件(最快)
- 从Sophus的GitHub仓库(strasdat/Sophus)下载源代码。
- 将其中的
sophus目录(包含所有.hpp头文件)拷贝到你的项目目录下,或者系统的头文件路径中(如/usr/local/include)。 - 在你的CMakeLists.txt中,确保包含了Eigen库,并将Sophus的头文件路径加入包含目录。
find_package(Eigen3 REQUIRED) include_directories(${EIGEN3_INCLUDE_DIR}) include_directories(“path/to/your/sophus/directory”)
方法二:使用CMake FetchContent(推荐,便于版本管理)
cmake_minimum_required(VERSION 3.14) project(MySophusProject) set(CMAKE_CXX_STANDARD 14) # 使用FetchContent下载并编译Sophus include(FetchContent) FetchContent_Declare( sophus GIT_REPOSITORY https://github.com/strasdat/Sophus.git GIT_TAG v1.22.10 # 指定一个稳定版本 ) FetchContent_MakeAvailable(sophus) # 查找Eigen,Sophus会依赖它 find_package(Eigen3 REQUIRED) add_executable(test_sophus test_sophus.cpp) target_link_libraries(test_sophus Eigen3::Eigen Sophus::Sophus)这种方式能自动处理依赖,并确保你使用的是特定版本的Sophus。
实操心得: 我强烈推荐使用FetchContent方式。它不仅干净,避免了全局安装可能带来的版本冲突,而且在CI/CD(持续集成)环境中也能完美工作。直接拷贝头文件虽然快,但在团队协作或复杂项目中,管理头文件路径反而会更麻烦。
4.2 第一个Sophus程序:旋转一个点
让我们写一个简单的程序,感受一下Sophus的便利。
#include <iostream> #include <Eigen/Core> #include <Eigen/Geometry> #include <sophus/so3.hpp> #include <sophus/se3.hpp> int main() { // 1. 创建一个绕Z轴旋转90度的旋转 Eigen::Matrix3d R = Eigen::AngleAxisd(M_PI / 2, Eigen::Vector3d::UnitZ()).toRotationMatrix(); Sophus::SO3d SO3_R(R); // 从旋转矩阵构造SO3 std::cout << “Rotation matrix R:\n” << R << std::endl; std::cout << “SO3 from matrix:\n” << SO3_R.matrix() << std::endl; // 验证 // 2. 使用四元数构造同样的旋转 Eigen::Quaterniond q(R); Sophus::SO3d SO3_q(q); std::cout << “SO3 from quaternion is equal? ” << SO3_R.isApprox(SO3_q) << std::endl; // 3. 使用指数映射:从旋转向量(李代数)构造 Eigen::Vector3d so3(0, 0, M_PI / 2); // so3 = [0,0,pi/2]^T Sophus::SO3d SO3_exp = Sophus::SO3d::exp(so3); std::cout << “SO3 from exp(so3):\n” << SO3_exp.matrix() << std::endl; // 4. 对数映射:从SO3得到对应的李代数 Sophus::Vector3d so3_log = SO3_R.log(); std::cout << “so3 from log(SO3): ” << so3_log.transpose() << std::endl; // 5. 扰动模型:计算旋转后的点关于旋转的导数(雅可比) Eigen::Vector3d p(1, 0, 0); // 原始点 Eigen::Vector3d p_rotated = SO3_R * p; // 旋转后的点 std::cout << “p after rotation: ” << p_rotated.transpose() << std::endl; // 使用右乘扰动模型,计算 d(R*p) / d(phi), phi是so3小扰动 Eigen::Matrix3d Jr = SO3_R.jacobianR(); // 这是一个3x3矩阵 std::cout << “Right Jacobian of SO3:\n” << Jr << std::endl; // 6. SE3示例:一个旋转+平移的变换 Eigen::Vector3d t(1, 2, 3); // 平移向量 Sophus::SE3d SE3_Rt(R, t); // 从R和t构造SE3 // 或者从变换矩阵构造 Eigen::Matrix4d T = Eigen::Matrix4d::Identity(); T.block<3, 3>(0, 0) = R; T.block<3, 1>(0, 3) = t; Sophus::SE3d SE3_T(T); std::cout << “SE3 transform matrix:\n” << SE3_T.matrix() << std::endl; // 用SE3变换一个点 Eigen::Vector3d p_transformed = SE3_T * p; // 等价于 R*p + t std::cout << “p transformed by SE3: ” << p_transformed.transpose() << std::endl; return 0; }编译并运行这个程序,你会直观地看到SO3/SE3的创建、群乘法、指数/对数映射等基本操作。注意jacobianR()的输出,它在优化中会频繁用到。
5. 深入核心:Sophus在SLAM后端优化中的实战应用
理论学习之后,我们来看Sophus如何解决实际问题。在视觉SLAM的BA(Bundle Adjustment)优化中,我们需要优化相机位姿(李群元素)和三维点坐标。优化过程(如使用g2o, Ceres, GTSAM库)需要我们知道误差函数关于优化变量的导数,即雅可比矩阵。
5.1 定义重投影误差与李代数扰动模型
假设我们有一个世界点P_w(Eigen::Vector3d),和相机位姿T_cw(Sophus::SE3d),表示从世界坐标系到相机坐标系的变换。相机内参为K。该点在图像上的投影为:u = π(K * (T_cw * P_w)),其中π是将三维点(除以Z)投影到二维的函数。
观测到的像素坐标为z。重投影误差为:e = u - z。
我们需要优化T_cw。但T_cw是李群SE(3),我们通常在其李代数se(3)空间进行扰动。设扰动量为ξ(一个6维向量,前3维旋转,后3维平移),扰动后的位姿为T_cw' = T_cw * exp(ξ^∧)(右乘扰动模型)。
那么,误差关于李代数扰动ξ的雅可比为:J = ∂e/∂ξ = ∂e/∂u * ∂u/∂P_c * ∂P_c/∂ξ其中P_c = T_cw * P_w是点在相机坐标系下的坐标。
5.2 使用Sophus计算雅可比矩阵
∂P_c/∂ξ这一项正是SE(3)的导数。根据李群理论,对于右乘扰动:∂(T * exp(ξ^∧) * P)/∂ξ |_{ξ=0} = [I, -P_c^∧](一个3x6的矩阵,其中^∧是将向量转换为反对称矩阵的算子)。
Sophus库为我们提供了计算这个导数的便捷方法。虽然Sophus没有直接提供一个函数输出这个矩阵,但它提供了计算扰动后变换的基础,我们可以结合Eigen手动计算,或者利用Sophus提供的generator()函数。但在实际使用中,我们更常直接使用优化库(如Ceres)的自动微分或解析导数功能,而Sophus确保了我们的变量类型能被这些库正确识别和操作。
下面是一个概念性的代码片段,展示如何在Ceres优化问题中使用Sophus的SE3:
#include <ceres/ceres.h> #include <sophus/se3.hpp> // 定义使用SE3的重投影误差CostFunction class ReprojectionErrorSE3 : public ceres::SizedCostFunction<2, 7> { // 2维残差,7维位姿参数(四元数+平移向量) public: ReprojectionErrorSE3(const Eigen::Vector2d& observed_p, const Eigen::Vector3d& P_w, const Eigen::Matrix3d& K) : observed_p_(observed_p), P_w_(P_w), K_(K) {} virtual bool Evaluate(double const* const* parameters, double* residuals, double** jacobians) const { // 1. 从参数数组读取位姿(四元数+平移向量) Eigen::Map<const Eigen::Quaterniond> q(parameters[0]); Eigen::Map<const Eigen::Vector3d> t(parameters[0] + 4); Sophus::SE3d T_cw(q, t); // 构造SE3 // 2. 将世界点变换到相机坐标系 Eigen::Vector3d P_c = T_cw * P_w_; // 3. 投影到归一化平面,再乘以内参 double inv_z = 1.0 / P_c.z(); double x = P_c.x() * inv_z; double y = P_c.y() * inv_z; Eigen::Vector3d p_pixel_homo = K_ * Eigen::Vector3d(x, y, 1); Eigen::Vector2d predicted_p(p_pixel_homo.x(), p_pixel_homo.y()); // 4. 计算残差 residuals[0] = predicted_p.x() - observed_p_.x(); residuals[1] = predicted_p.y() - observed_p_.y(); // 5. 计算雅可比(此处省略详细的解析雅可比计算,通常可使用Ceres的自动微分) if (jacobians != nullptr && jacobians[0] != nullptr) { // 计算误差关于点P_c的导数 Eigen::Matrix<double, 2, 3> d_e_d_pc; // ... 计算过程 ... // 计算P_c关于李代数扰动ξ的导数 (3x6矩阵) Eigen::Matrix<double, 3, 6> d_pc_d_xi = Eigen::Matrix<double, 3, 6>::Zero(); d_pc_d_xi.block<3, 3>(0, 0) = Eigen::Matrix3d::Identity(); // 关于平移部分 d_pc_d_xi.block<3, 3>(0, 3) = -Sophus::SO3d::hat(P_c); // 关于旋转部分,使用hat算子 // 链式法则得到最终雅可比 Eigen::Matrix<double, 2, 6> jacobian_xi = d_e_d_pc * d_pc_d_xi; // 还需要将李代数扰动ξ的雅可比转换为关于四元数+平移参数(7维)的雅可比 // 这涉及额外的左乘扰动雅可比矩阵,比较复杂... // 在实际中,我们经常直接优化李代数ξ(6维),或者使用Ceres的自动微分来避免手动推导。 } return true; } private: Eigen::Vector2d observed_p_; Eigen::Vector3d P_w_; Eigen::Matrix3d K_; };可以看到,即使使用Sophus,手动推导和实现SE3的完整雅可比矩阵仍然相当复杂。因此,在现代SLAM实践中,更常见的做法是:
- 使用李代数作为优化变量:直接优化一个6维向量
se3,在误差函数内部用Sophus::SE3d::exp(se3)将其转换为SE3d。这样雅可比矩阵就是关于6维李代数的,推导相对标准。 - 使用自动微分:像Ceres这样的库,如果你将位姿参数定义为
Eigen::Vector3d(平移)和Eigen::Quaterniond(旋转),并使用Ceres的EigenQuaternionParameterization,它可以利用自动微分来计算导数,无需手动推导复杂的李群雅可比。Sophus在这里的作用是提供了SE3d和SO3d类,使得在代码中构造和操作变换非常直观和安全。
注意事项: 在优化中,对四元数施加单位约束很重要。Ceres提供了
EigenQuaternionParameterization来处理。如果你直接优化李代数se3,则没有额外的约束。
6. 常见问题、坑点与调试技巧实录
即使有了Sophus这样优秀的库,在实际使用中还是会遇到各种问题。下面是我在项目中踩过的一些坑和总结的经验。
6.1 编译与链接问题
问题:
undefined reference to ‘Sophus::...’- 原因:Sophus虽然是头文件库,但它的某些实现(如模板的特化)在
.cpp文件中。如果你只包含了头文件,但没有将Sophus的源文件(如sophus/so3.cpp,sophus/se3.cpp)加入编译,或者没有链接对应的库,就会报错。 - 解决:
- 确保使用CMake的
find_package(Sophus REQUIRED)和target_link_libraries(your_target Sophus::Sophus)。现代CMake的target方式会自动处理包含目录和链接。 - 如果手动管理,确保编译并链接了
libsophus.a或libsophus.so。
- 确保使用CMake的
- 原因:Sophus虽然是头文件库,但它的某些实现(如模板的特化)在
问题:Eigen版本不兼容
- 原因:不同版本的Sophus可能依赖特定版本的Eigen。例如,较新的Sophus可能要求Eigen 3.3以上。
- 解决:查看Sophus源码仓库的README或CMakeLists.txt,确认其支持的Eigen版本。使用包管理器(如apt, vcpkg, conda)安装指定版本,或从源码编译对应版本的Eigen。
6.2 数值计算与精度问题
问题:四元数未归一化导致SO3构造函数抛出异常或结果错误
- 原因:
Sophus::SO3d的构造函数接受Eigen::Quaterniond,但它内部假设四元数是单位四元数。如果你传入了一个未归一化的四元数,虽然构造函数可能不会立即报错,但后续运算(如matrix()、log())会产生错误结果。 - 解决:在构造
SO3d之前,务必确保你的四元数是单位四元数。使用Eigen::Quaterniond::normalize()或Eigen::Quaterniond q(rotation_matrix);(Eigen的构造函数会自动归一化)。Eigen::Quaterniond q; // 假设q来自不可靠的来源 q.normalize(); // 关键步骤! Sophus::SO3d SO3_q(q);
- 原因:
问题:对数映射
log()在旋转角接近π时不稳定- 原因:从旋转矩阵或四元数计算旋转向量(轴角)时,当旋转角度接近π,存在奇异性。虽然Sophus的实现已经处理了这种情况,但数值精度可能下降。
- 解决:在SLAM中,通常我们保证相邻帧间的旋转是小量,不会接近π。如果算法中不可避免地会出现大旋转,考虑使用其他参数化方式(如直接使用四元数)进行优化,仅在需要时转换为李代数。
问题:
SE3d::inverse()的效率- 原因:求SE3的逆变换在滤波和优化中很常用。
T.inverse()会计算旋转矩阵的转置和平移向量的反向旋转,涉及一次矩阵转置运算。 - 优化:如果在一个紧循环中需要反复使用同一个变换的逆,应该将其缓存起来,而不是每次调用
inverse()。// 低效 for (const auto& point : points) { Eigen::Vector3d p_cam = T_wc.inverse() * point; // 每次循环都求逆! } // 高效 Sophus::SE3d T_cw = T_wc.inverse(); for (const auto& point : points) { Eigen::Vector3d p_cam = T_cw * point; }
- 原因:求SE3的逆变换在滤波和优化中很常用。
6.3 与第三方库的集成问题
问题:将Sophus类型传递给Ceres的
AddResidualBlock- 场景:你想直接优化
Sophus::SE3d对象。 - 难点:Ceres的优化变量必须是普通的双精度数组(
double*)。Sophus::SE3d内部存储方式(一个四元数加一个向量)对于Ceres是不透明的。 - 解决方案(两种):
- 优化参数块:不直接传递
SE3d,而是传递一个double[7]数组,前4个是四元数[qx, qy, qz, qw],后3个是平移[tx, ty, tz]。在CostFunction内部,再将其重新构造成Sophus::SE3d。同时,需要为这个参数块设置局部参数化EigenQuaternionParameterization。double pose[7]; // [qx, qy, qz, qw, tx, ty, tz] // ... 初始化 pose ... problem.AddParameterBlock(pose, 7, new ceres::EigenQuaternionParameterization()); - 优化李代数:传递一个
double[6]数组作为李代数se3。在CostFunction内部,使用Sophus::SE3d::exp(se3)来得到SE3d。这种方式更符合李群理论,且参数维度更少(6 vs 7),但雅可比推导需要处理指数映射的导数。对于Ceres自动微分,这种方式同样可行。
- 优化参数块:不直接传递
- 场景:你想直接优化
问题:Sophus与g2o的
VertexSE3Expmap- 场景:在g2o中优化位姿图。
- 解决:g2o有自己内置的SE3顶点类型
VertexSE3Expmap,它内部也是用李代数表示的。Sophus可以很方便地与它进行数据转换。通常,我们从g2o顶点读取或写入的是g2o::SE3Quat类型,可以轻松地与Sophus::SE3d相互转换(通过旋转矩阵和平移向量)。社区也有基于Sophus自定义g2o顶点和边的例子,用起来更统一。
6.4 调试与日志输出技巧
- 格式化输出:直接
cout << SO3_R可能输出的是内部四元数值。为了更直观,通常输出其旋转矩阵:cout << SO3_R.matrix()。对于SE3d,输出SE3_T.matrix()得到4x4变换矩阵。 - 验证群乘法:对于变换
T1和T2,验证(T1 * T2).inverse()是否等于T2.inverse() * T1.inverse()。这是一个快速检查变换复合和求逆是否正确的好方法。 - 验证指数/对数映射:对于一个小量
xi,验证SE3d::exp(xi).log()是否近似等于xi。这可以检查指数/对数映射的实现是否正确,以及数值精度是否可接受。Sophus::Vector6d xi = Sophus::Vector6d::Random() * 0.01; // 一个小随机扰动 Sophus::SE3d T = Sophus::SE3d::exp(xi); Sophus::Vector6d xi_recovered = T.log(); double error = (xi - xi_recovered).norm(); std::cout << “Exp-Log error: ” << error << std::endl; // 应该是一个非常小的数,如1e-12
7. 进阶话题:自定义Sophus类型与性能考量
7.1 使用模板特化与自定义标量类型
Sophus完全模板化,你可以使用Sophus::SO3和Sophus::SE3。但为了代码清晰,库提供了SO3d,SO3f,SE3d,SE3f的别名。在大多数对精度要求高的SLAM后端,使用double类型。
如果你的应用场景非常特殊,比如需要自动微分(例如用于Ceres的Jet类型),理论上可以特化Sophus的模板以支持自定义标量类型。但这需要对Sophus的内部实现有深入了解,通常不是必须的,因为Ceres等库的自动微分可以绕过对Sophus的直接求导。
7.2 性能优化小贴士
- 避免频繁构造/析构:在性能关键的循环中(如为地图中成千上万个点做位姿变换),尽量避免在循环内部构造临时的
SO3d或SE3d对象。应该先在循环外构造好变换对象。 - 利用Eigen的映射(Map):如果你有一段连续的内存(如一个
double数组)存储了多个位姿(四元数+平移),可以使用Eigen::Map来零拷贝地构造Quaterniond和Vector3d,进而构造Sophus::SE3d,避免内存拷贝。 - 矩阵乘法优化:
SE3d对点的变换T * p内部实现为R * p + t。Eigen会优化这个运算。但如果你要对大量点进行同一个变换,可以考虑手动提取旋转矩阵R和平移向量t,然后使用Eigen的广播功能进行批量运算,这可能比在循环中逐个调用operator*更快。Eigen::Matrix3d R = T.rotationMatrix(); Eigen::Vector3d t = T.translation(); // points_mat 是一个3xN的矩阵,每一列是一个点 Eigen::Matrix3Xd transformed_points = (R * points_mat).colwise() + t; - 注意调试模式下的性能:在Debug模式下,Eigen和Sophus会进行大量的边界检查,性能会慢很多。在Benchmark或发布产品时,务必在Release/RelWithDebInfo模式下编译和测试。
Sophus库将李群李代数这一强大的数学工具,以极其优雅和实用的方式带入了C++工程实践。它可能不会是你项目中代码量最多的部分,但绝对是保证几何计算正确性和代码简洁性的基石。理解其原理,掌握其常见用法和陷阱,能让你在开发机器人、SLAM、三维视觉等相关应用时更加得心应手。最开始接触那些exp、log、hat、vee运算符可能会觉得抽象,但多写几个例子,多调试几次,你会发现它们就像加减乘除一样,成为你处理三维空间变换的自然语言。