1. 从零理解位姿图优化:为什么SLAM后端离不开它?
想象一下,你是一个在陌生城市里探险的机器人,手里只有一张白纸和一支笔。你每走一步,就根据自己步子的感觉(比如“我大概向前走了1米,右转了30度”)在纸上画一个点,并记录下这个位置。这就是里程计,它靠的是机器人自身的传感器(如轮子编码器、IMU)来推测运动。但问题是,就像人蒙着眼睛走路会越走越偏一样,里程计有累积误差。走个几十步后,你画出的轨迹可能已经和真实位置相差甚远了。
这时候,你突然看到了一个地标,比如一个醒目的红色邮筒。你发现:“哎?这个邮筒我十分钟前好像见过!” 这个“认出旧地点”的过程,在SLAM里就叫闭环检测。它提供了一个至关重要的信息:你当前估计的位置,和你历史上某个旧位置,应该是同一个物理点。这个信息就像一根橡皮筋,能把已经漂移的轨迹“拉”回正确的位置。
位姿图优化要解决的,就是这个“拉回”的问题。它把整个SLAM过程抽象成一张“图”:
- 节点:就是机器人每个时刻的位姿(位置和朝向),也就是你纸上画的那些点。
- 边:分为两种。一种是里程计边,连接相邻的节点,表示“我从上一个位置走到这个位置,传感器告诉我的相对运动是多少”。另一种是闭环边,连接非相邻的节点,表示“我通过识别地标,发现这两个位置其实是同一个地方,它们之间的相对运动应该是多少”。
优化器的任务,就是调整所有节点的位姿(那些点的位置),让它们尽可能满足所有边给出的约束。比如,一条里程计边说“节点A到节点B的位移是(1, 0, 0)”,但优化后A和B的位置算出来的位移是(1.2, 0.1, 0),这就有误差。优化器会同时考虑成百上千条这样的边,通过最小化所有边的误差平方和,得到一组全局最协调的位姿估计。
我刚开始做SLAM时,总觉得前端特征匹配、视觉里程计是核心,后端优化就是个“黑盒子”,调个库就完事了。后来在项目里吃了亏才发现,前端再精致,数据送到后端如果优化得不好,整个系统精度立刻掉一个数量级。尤其是当机器人进行长距离探索,闭环越来越多时,如何高效、稳定地求解这个大型非线性最小二乘问题,就成了系统成败的关键。这也是为什么Ceres、G2O、GTSAM这三个库在SLAM领域如此重要的原因,它们就是用来解这个“图优化”问题的核心引擎。
2. 三大神器初印象:Ceres、G2O、GTSAM各自是何方神圣?
在深入代码之前,我们得先摸清这三个库的“脾气”和“出身”,这能帮你快速判断在什么场景下该用谁。
Ceres Solver:通用灵活的“瑞士军刀” Ceres起源于谷歌,它的设计哲学是通用性。它不局限于SLAM或视觉问题,而是致力于解决广泛的非线性最小二乘优化问题。你可以把它想象成一个非常强大的计算器,你只需要告诉它“我的误差项是什么”,它就能帮你算出来怎么调整变量能让总误差最小。因为它足够通用,所以接口设计上更偏向于让用户自由地定义任何形式的误差函数。实测下来,Ceres的文档非常友好,新手跟着教程很容易就能跑通第一个优化程序。很多知名的视觉惯性SLAM系统,比如OKVIS、VINS-Mono,都选择了Ceres作为其后端。
G2O:为图优化而生的“专业工匠” 如果说Ceres是通用计算器,那G2O就是一台为“图结构优化”特制的机床。它的核心设计理念就是图。在G2O的世界里,万物皆顶点和边。你需要显式地定义顶点类(继承g2o::BaseVertex)和边类(继承g2o::BaseUnaryEdge或g2o::BaseBinaryEdge),然后把它们添加到优化器中。G2O内部会帮你自动构建海塞矩阵,并利用图结构的稀疏性进行高效求解。这种设计让它在SLAM、BA这类问题中非常自然和高效。ORB-SLAM系列就长期使用G2O作为其后端优化器。它的优点是针对图优化高度优化,但相对的,学习曲线比Ceres要陡峭一些,你需要理解它那套基于图的抽象。
GTSAM:基于概率图模型的“学院派大师” GTSAM来自佐治亚理工学院,它的理论基础最为深厚,根植于因子图和贝叶斯推断。在GTSAM看来,SLAM不是一个单纯的几何优化问题,而是一个概率推断问题:在给定所有传感器观测(因子)的条件下,最可能的机器人轨迹(变量)是什么?因此,GTSAM的API里充满了Factor(因子)和Values(值)的概念。它最大的特色是提供了增量平滑与建图(iSAM2) 算法,能够高效处理大规模在线SLAM问题,只更新受新观测影响的部分,而不是每次都重新优化整个图。这在机器人长期运行中至关重要。GTSAM的代码风格非常优雅,但概念上更抽象,需要你对概率图模型有一定理解才能用得得心应手。
简单打个比方:如果你要快速验证一个算法idea,追求上手快,选Ceres;如果你做传统的特征点SLAM,图结构固定,追求极致优化效率,选G2O;如果你的问题强依赖于概率模型、需要处理大规模增量式优化,或者你在研究最前沿的滤波平滑算法,那么GTSAM是你的菜。
3. 实战Ceres:手把手构建你的第一个位姿图
光说不练假把式,我们直接来看用Ceres怎么实现一个二维位姿图优化。我会把关键步骤拆开,并解释每一步为什么要这么做。
假设我们有两个位姿节点,通过一个相对观测连接。每个二维位姿由 (x, y, theta) 表示。
第一步:定义误差函数(核心中的核心) 在Ceres里,你需要定义一个仿函数(Functor)来计算残差。残差就是“观测值”和“根据当前位姿估计值计算出的预测值”之间的差异。
// 二维位姿图误差项
class PoseGraph2dErrorTerm {
public:
// 构造函数:传入实际的观测值(边的测量值)和它的信息矩阵(协方差逆的平方根)
PoseGraph2dErrorTerm(double measured_x, double measured_y, double measured_yaw,
const Eigen::Matrix3d& sqrt_information)
: measured_p_(measured_x, measured_y),
measured_yaw_(measured_yaw),
sqrt_information_(sqrt_information) {}
// 重载括号运算符:这是Ceres要求的标准接口
template <typename T>
bool operator()(const T* const pose_i, // 第一个位姿节点的参数块 [x, y, yaw]
const T* const pose_j, // 第二个位姿节点的参数块 [x, y, yaw]
T* residuals_ptr) const { // 输出:计算出的残差
// 1. 将传入的指针映射为Eigen向量,方便计算
Eigen::Map<const Eigen::Matrix<T, 2, 1>> p_i(pose_i); // pose_i的前两个是x,y
Eigen::Map<const Eigen::Matrix<T, 2, 1>> p_j(pose_j);
const T& yaw_i = pose_i[2];
const T& yaw_j = pose_j[2];
// 2. 根据当前位姿估计,计算预测的相对变换
// 旋转矩阵R_i^T,用于将向量从全局坐标系变换到位姿i的局部坐标系
Eigen::Rotation2D<T> R_i_inv(-yaw_i);
Eigen::Matrix<T, 2, 1> predicted_p = R_i_inv * (p_j - p_i); // 预测的平移
T predicted_yaw = ceres::NormalizeAngle(yaw_j - yaw_i); // 预测的旋转
// 3. 计算残差:预测值 - 观测值
Eigen::Map<Eigen::Matrix<T, 3, 1>> residuals(residuals_ptr);
residuals.template head<2>() = predicted_p - measured_p_.cast<T>();
residuals(2) = ceres::NormalizeAngle(predicted_yaw - T(mea


179

被折叠的 条评论
为什么被折叠?



