news 2026/10/22 5:48:40

视觉SLAM中Sophus库SO(3)与SE(3)位姿表示及优化实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
视觉SLAM中Sophus库SO(3)与SE(3)位姿表示及优化实战指南

1. 视觉SLAM里的位姿表示,为什么绕不开Sophus

做视觉SLAM的人,迟早会撞上李群李代数这堵墙。相机位姿、地图点变换、优化过程中的增量更新,全都离不开旋转矩阵和平移向量的组合。刚开始你可能觉得用Eigen::Matrix3d和Eigen::Vector3d拼一拼就够了,但一旦进入后端优化,需要求导、需要迭代更新、需要处理约束,裸矩阵那套写法很快就会让你崩溃。

Sophus这个库就是来解决这个问题的。它把SO(3)和SE(3)这两个李群封装成了类似矩阵的类,让你能像操作普通矩阵一样做乘法、求逆、取对数、求伴随,同时还能通过指数映射在流形上做无约束优化。说白了,它把“旋转矩阵必须正交且行列式为1”这个约束,通过李代数参数化给消掉了,优化器只需要在三维或六维向量空间里随便跑,映射回去自然满足约束。

这篇文章适合谁看?如果你正在学视觉SLAM,已经写过简单的PnP或者ICP,但一到g2o、Ceres或者自己手写高斯牛顿的时候就被Sophus::SO3d的各种API搞晕,那这篇就是给你准备的。我会把SO(3)和SE(3)的构造、更新、求导、常见坑点全部拆开讲一遍,代码可以直接抄。

2. Sophus的安装与工程集成,别在第一步卡住

2.1 安装方式选择与依赖处理

Sophus本身是header-only的模板库,但它的编译和安装过程有几个容易踩的坑。最稳妥的方式是源码编译安装,因为不同版本对Eigen和fmt的依赖不一样。

git clone https://github.com/strasdat/Sophus.git cd Sophus mkdir build && cd build cmake .. -DCMAKE_BUILD_TYPE=Release -DBUILD_SOPHUS_TESTS=OFF make -j4 sudo make install

这里有几个关键点。第一,BUILD_SOPHUS_TESTS建议关掉,测试用例编译很慢而且容易因为fmt版本问题报错。第二,Sophus新版本依赖fmt库,如果系统里没有,先装libfmt-dev。第三,如果你用的是ROS环境,注意ROS自带的Sophus可能版本很老,API和新版有差异,建议统一用源码安装的版本,在CMakeLists里显式指定路径。

在CMakeLists.txt里的集成方式:

find_package(Sophus REQUIRED) find_package(Eigen3 REQUIRED) include_directories(${Sophus_INCLUDE_DIRS} ${EIGEN3_INCLUDE_DIR}) target_link_libraries(your_target ${Sophus_LIBRARIES})

注意:Sophus的CMake配置文件在不同版本里导出的变量名可能不同,有的是Sophus_INCLUDE_DIRS,有的是Sophus_INCLUDE_DIR。如果find_package成功但include报错,用message(STATUS ${Sophus_INCLUDE_DIRS})打印一下确认。

2.2 头文件包含与命名空间约定

Sophus的核心头文件就两个:#include <sophus/so3.hpp>和#include <sophus/se3.hpp>。所有类型都在Sophus命名空间下。常用的类型别名有:

  • Sophus::SO3d:双精度SO(3),内部用四元数存储
  • Sophus::SE3d:双精度SE(3),内部用四元数加平移向量存储
  • Sophus::SO3f、Sophus::SE3f:单精度版本,嵌入式场景用

我个人的习惯是在文件开头加using namespace Sophus;,但如果你在大型项目里,建议还是老老实实写Sophus::SO3d,避免和别的库冲突。

3. SO(3)的核心用法:旋转的构造、更新与求导

3.1 四种构造方式与适用场景

SO(3)表示三维旋转,Sophus提供了多种构造入口,每种对应不同的数据来源:

// 方式一:从旋转矩阵构造(最常用) Eigen::Matrix3d R = Eigen::AngleAxisd(M_PI/4, Eigen::Vector3d::UnitZ()).toRotationMatrix(); Sophus::SO3d SO3_R(R); // 方式二:从四元数构造 Eigen::Quaterniond q(R); Sophus::SO3d SO3_q(q); // 方式三:从旋转向量(轴角)构造,内部自动做指数映射 Eigen::Vector3d omega(0, 0, M_PI/4); Sophus::SO3d SO3_exp = Sophus::SO3d::exp(omega); // 方式四:直接构造单位旋转 Sophus::SO3d SO3_I; // 默认是单位旋转

这四种方式里,exp是最需要理解的。它把李代数so(3)里的三维向量映射到李群SO(3)上,物理意义是“绕轴omega旋转|omega|角度”。在优化过程中,我们求出来的增量就是这种三维向量,用exp映射回去更新位姿。

实操心得:从旋转矩阵构造时,Sophus内部会做正交化处理,但如果你给的矩阵离正交矩阵太远(比如数值误差累积很大),结果可能不是你预期的。我一般会在构造前用SVD重新正交化一下,尤其是从优化器里拿到的矩阵。

3.2 对数映射与旋转向量提取

反过来,从SO(3)取李代数用log():

Sophus::SO3d SO3_R(R); Eigen::Vector3d omega = SO3_R.log();

这里有个细节:log()返回的向量方向是旋转轴,模长是旋转角度,角度范围在[0, π]。当旋转角度接近π时,数值上会有点不稳定,因为轴的方向会翻转。实际用的时候如果发现log结果跳变,检查一下是不是接近奇异点。

另外,Sophus还提供了SO3_R.angle()直接返回旋转角度,SO3_R.axis()返回单位旋转轴,比手动从log结果里算方便。

3.3 乘法、逆与伴随矩阵

SO(3)的乘法就是旋转的复合:

Sophus::SO3d R1 = Sophus::SO3d::exp(Eigen::Vector3d(0.1, 0.2, 0.3)); Sophus::SO3d R2 = Sophus::SO3d::exp(Eigen::Vector3d(0.4, 0.5, 0.6)); Sophus::SO3d R12 = R1 * R2; // 先R2后R1的旋转 Sophus::SO3d R1_inv = R1.inverse();

伴随矩阵Adj()在推导雅可比和坐标系变换时经常用到:

Eigen::Matrix3d Adj = R1.Adj();

伴随矩阵满足R * exp(omega) * R^T = exp(Adj * omega),这个性质在推导扰动模型的时候是核心工具。很多教程讲到这里一笔带过,但你如果自己推一遍SO(3)上的右扰动雅可比,就会发现伴随矩阵无处不在。

3.4 在流形上做增量更新

这是Sophus在SLAM里最核心的用法。优化器每次迭代算出一个三维增量delta,然后更新旋转:

Sophus::SO3d R_updated = Sophus::SO3d::exp(delta) * R_current; // 左乘更新 // 或者 Sophus::SO3d R_updated = R_current * Sophus::SO3d::exp(delta); // 右乘更新

左乘和右乘的区别在于扰动定义在哪个坐标系。左乘对应“在世界坐标系下施加扰动”,右乘对应“在局部坐标系下施加扰动”。视觉SLAM里两种都有用,取决于你的雅可比是怎么推的。我个人的经验是,BA优化里用右乘更直观,因为增量直接对应相机局部坐标系下的微小旋转。

常见坑:千万不要写成R_current + delta这种裸矩阵加法,结果不再是旋转矩阵,后续所有计算全错。必须通过exp映射回SO(3)再乘。

4. SE(3)的实战用法:位姿变换与优化更新

4.1 SE(3)的构造与数据访问

SE(3)表示刚体变换,包含旋转和平移:

// 从旋转矩阵和平移向量构造 Eigen::Matrix3d R = Eigen::Matrix3d::Identity(); Eigen::Vector3d t(1, 2, 3); Sophus::SE3d SE3_Rt(R, t); // 从四元数和平移向量构造 Eigen::Quaterniond q(R); Sophus::SE3d SE3_qt(q, t); // 从李代数构造(六维向量,前三维平移,后三维旋转) Eigen::Matrix<double, 6, 1> xi; xi << 1, 2, 3, 0.1, 0.2, 0.3; Sophus::SE3d SE3_exp = Sophus::SE3d::exp(xi);

访问内部数据:

Eigen::Matrix3d rotation_matrix = SE3_Rt.rotationMatrix(); Eigen::Quaterniond quaternion = SE3_Rt.unit_quaternion(); Eigen::Vector3d translation = SE3_Rt.translation(); Eigen::Matrix4d matrix4x4 = SE3_Rt.matrix(); // 4x4齐次矩阵

这里要注意matrix()返回的是4x4矩阵,而rotationMatrix()只返回3x3旋转部分。在给OpenGL或者别的库传数据的时候别搞混了。

4.2 点变换与坐标系变换

SE(3)作用在三维点上:

Eigen::Vector3d p(1, 0, 0); Eigen::Vector3d p_transformed = SE3_Rt * p;

这个操作等价于R * p + t,但Sophus内部做了优化,比手动展开快。如果你要批量变换大量点,建议用SE3_Rt.matrix()取出4x4矩阵,然后做齐次坐标乘法,比逐个调用operator*效率高。

坐标系变换用乘法:

Sophus::SE3d T_wc; // 世界到相机 Sophus::SE3d T_cw = T_wc.inverse(); // 相机到世界 Sophus::SE3d T_wa = T_wb * T_ba; // 链式变换

注意:SE(3)的乘法顺序不能随便换。T1 * T2表示先做T2的变换,再做T1的变换。在SLAM里写坐标变换的时候,建议在变量名里把坐标系标清楚,比如T_wc表示world到camera,这样不容易搞反。

4.3 SE(3)上的扰动模型与雅可比

SE(3)的优化更新和SO(3)类似,但增量是六维的:

Eigen::Matrix<double, 6, 1> delta; delta << 0.01, 0.02, 0.03, 0.001, 0.002, 0.003; Sophus::SE3d T_updated = Sophus::SE3d::exp(delta) * T_current;

在推导雅可比的时候,SE(3)的伴随矩阵是6x6的:

Eigen::Matrix<double, 6, 6> Adj = T_current.Adj();

这个伴随矩阵在把扰动从一个坐标系转到另一个坐标系时非常关键。比如你在世界坐标系下定义了扰动,但雅可比是在相机坐标系下算的,就需要用伴随矩阵做转换。

4.4 实际优化中的更新策略对比

在视觉SLAM的BA优化里,SE(3)的更新有两种常见策略:

策略更新方式优点缺点
左乘更新T_new = exp(delta) * T_old扰动在世界坐标系,推导直观需要额外处理伴随矩阵
右乘更新T_new = T_old * exp(delta)扰动在局部坐标系,雅可比形式简洁需要明确局部坐标系定义

我实测下来,在Ceres里用右乘更新更顺手,因为Ceres的自动求导对局部参数化支持很好,Sophus::SE3d可以直接配合LocalParameterization使用。在g2o里两种都有现成的顶点类型,看你的边是怎么定义的。

5. 常见报错与排查技巧实录

5.1 编译期问题速查

报错信息原因解决方法
fatal error: sophus/so3.hpp: No such file头文件路径没配检查CMakeLists里的include路径
undefined reference to Sophus::SO3d::exp链接库没加确认target_link_libraries包含Sophus
static assertion failed: YOU_MIXED_MATRICES_OF_DIFFERENT_SIZESEigen矩阵维度不匹配检查旋转矩阵是不是3x3,平移是不是3x1
fmt/core.h: No such file缺少fmt依赖安装libfmt-dev或源码编译fmt

5.2 运行期数值问题排查

问题一:优化不收敛,位姿乱飞。

最常见的原因是增量更新写错了。检查是不是直接用了T + delta而不是T * exp(delta)。另一个可能是雅可比的符号搞反了,导致优化器往错误方向走。我一般会先固定住其他变量,只优化一个位姿,看能不能收敛到正确值,逐步排查。

问题二:log()结果跳变。

当旋转角度接近π时,log的轴方向会突然翻转。这不是bug,是李代数在π处的固有奇异性。解决方法是在接近π时用四元数插值或者双覆盖处理,或者干脆避免让旋转角度累积到π附近。

问题三:SE(3)的exp和log不对应。

检查你用的李代数向量顺序。Sophus的SE(3)李代数前三维是平移,后三维是旋转,但有些教材写的是反过来的。如果你从别的地方抄的代码,一定要确认顺序。

独家避坑:在写单元测试的时候,一定要加一条T * T.inverse() == Identity的检查,以及exp(log(T)) == T的往返测试。这两个测试能帮你抓住90%的API误用问题。

5.3 性能相关的注意事项

Sophus的exp和log涉及三角函数和矩阵运算,在优化内循环里调用频率很高。如果你发现优化速度慢,可以考虑以下优化:

  • 用单精度SO3f和SE3f替代双精度,速度能快一倍左右,精度损失在视觉SLAM里通常可以接受
  • 避免在循环里反复构造SO(3)对象,尽量复用
  • 如果只需要旋转矩阵,直接用rotationMatrix(),不要先转四元数再转矩阵

6. 从SO(3)到SE(3)的进阶理解

6.1 两者的代数结构关系

SO(3)是SE(3)的子群,SE(3)可以看成SO(3)和R^3的半直积。这个数学结构决定了SE(3)的李代数不是简单的平移加旋转,而是有一个耦合项。具体来说,SE(3)的exp映射里,平移部分会被旋转部分影响:

// SE(3)的exp不是简单地把平移和旋转分开exp再拼起来 // 内部有V矩阵耦合

这个耦合在推导雅可比的时候必须考虑,否则优化会出问题。很多初学者在这里翻车,以为SE(3)就是SO(3)加个平移向量,结果雅可比推错了。

6.2 在视觉SLAM中的典型应用场景

场景用的群典型操作
相机位姿表示SE(3)构造、求逆、乘法
特征点三角化SE(3)点变换
BA优化SE(3)exp更新、伴随矩阵
IMU预积分SO(3)+SE(3)旋转更新、雅可比传递
回环检测位姿图SE(3)相对变换、优化

6.3 和其他库的互操作

Sophus和Eigen是无缝衔接的,但和ROS的tf、geometry_msgs转换时需要手动处理:

// Sophus转ROS geometry_msgs::Pose pose; pose.position.x = SE3.translation().x(); // ... 四元数赋值

反过来从ROS消息构造Sophus对象也很直接。关键是注意ROS用的四元数顺序是(x, y, z, w),而Eigen的构造函数也是这个顺序,但有些库用的是(w, x, y, z),别搞混。

7. 我踩过的那些坑和最后的小技巧

第一个坑是版本兼容性。Sophus在2020年前后的API有过一次大改,老版本的SO3d::exp返回类型和新版本不一样,如果你从网上抄的代码编译不过,先检查版本。我现在的习惯是在CMakeLists里锁定Sophus的版本号,避免不同机器上行为不一致。

第二个坑是SE3d::exp的输入向量顺序。我至少见过三种不同的约定:平移在前旋转在后、旋转在前平移在后、还有把平移除以旋转角度再放的。Sophus用的是平移在前旋转在后,但如果你从论文里抄公式,一定要确认论文用的哪种约定。

第三个坑是优化过程中的数值稳定性。当旋转增量很小时,exp的泰勒展开和直接计算会有微小差异,长时间迭代可能累积误差。我的做法是每隔几百次迭代,把SE(3)重新正交化一次,用SVD把旋转矩阵投影回SO(3)。

最后分享一个小技巧:如果你只需要旋转不要平移,用SO(3)比SE(3)快很多,因为SE(3)的exp和log涉及更复杂的矩阵运算。在IMU预积分里,旋转部分单独用SO(3)处理,平移部分单独算,比整体用SE(3)效率高不少。这个优化在我自己的VIO系统里实测能省15%左右的计算时间。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/22 5:48:17

C/C++课程设计:从能跑通到可交付的工程能力实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/18 9:28:37

Unity3D FPS完整版工程下载避坑指南:从选型到跑通

简介&#xff1a;这是一份基于Unity3D的FPS游戏完整源代码包&#xff0c;面向希望掌握第一人称射击游戏开发的中高级开发者&#xff0c;可用于毕业设计、课程项目或独立游戏起步参考。资源围绕Unity3D在FPS领域的关键技术展开&#xff0c;从场景构建、角色控制器、射击机制、网…

作者头像 李华
网站建设 2026/10/21 17:21:43

基于MP2642GR-Z与STM32的2S电池主动均衡方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/19 0:35:06

LCS转LIS:排列序列下最长公共子序列的O(n log n)优化

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/21 5:18:57

收银硬件的核心链路:从红外扫码枪选型到热敏小票打印机联调

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华