视觉里程计(Visual Odometry,简称VO)通过分析连续图像帧之间的运动关系来估计相机的位姿变化,是SLAM系统中最基础的功能模块之一。然而纯VO系统有一个绕不开的先天缺陷:每一帧的位姿估计都存在微小误差,这些误差在连续积分过程中不断叠加,运行时间一长,轨迹就会明显偏离真实位置,这就是所谓的VO漂移问题。典型表现是机器人绕一圈回到原点,但估计的位置却偏出去好几米。要解决这个问题,工程上主要依靠两大利器:闭环检测负责发现“我来过这里”,全局优化负责把误差重新摊平到整条轨迹上。

VO漂移是怎么产生的:误差累积的底层机制
要理解漂移,首先要明白VO的位姿估计是增量式的。系统在每一对相邻帧之间做特征匹配,通过PnP或对极几何求解相对运动,然后把当前帧的世界位姿用上一帧位姿乘以相对运动得到。数学上可以写成:T_k = T_{k-1} * ΔT_k。这个链式乘法结构决定了任何一帧的误差都会原封不动地传递给后面所有帧。
误差的来源是多方面的。首先是特征匹配中的误匹配,即使经过RANSAC筛选,仍可能有少量外点漏网;其次是相机标定误差,焦距和畸变系数不准会直接扭曲运动估计;再者是纯旋转与平移的耦合问题,当相机原地旋转时,对极几何约束会退化,估计出的平移分量噪声极大;另外还有滚动快门畸变、运动模糊、光照剧烈变化等环境因素。单看每一帧,误差可能只有几毫米,但一千帧下来,累积误差就能达到米级。
有一个常见的误区需要澄清:提高帧率并不能根治漂移。帧率高意味着相邻帧运动更小、匹配更容易,单帧精度确实会提升,但帧数也相应增多,误差累积的次数同样增加。实验表明,漂移大致与轨迹长度成正比,与帧率的关系并不显著。所以想靠“调参”消灭漂移是不现实的,必须从算法架构上引入绝对约束,这就引出了闭环检测。
闭环检测:让系统认出“我来过这里”
闭环检测(Loop Closure Detection)的核心任务是:当相机回到先前经过的场景时,从历史关键帧中找出与当前帧观测到同一地点的那一帧,建立一条跨越长时间间隔的约束边。这条边是全局优化中最重要的绝对约束,因为它把相隔几百上千帧的两个位姿直接连了起来,打破了误差链式传递的假设。
目前最主流的闭环检测方案是基于词袋模型(Bag of Words,BoW)的。以DBoW2为例,离线阶段用大量图像训练出一棵词汇树,每个局部特征描述子被量化到树上的某个叶子节点,即“单词”。在线阶段,每来一张关键帧,先提取特征并转换成词袋向量,然后与数据库中的历史关键帧计算相似度得分。得分超过阈值的历史帧成为闭环候选。词袋方案的查询复杂度与地图规模关系不大,速度非常快,ORB-SLAM2使用的正是这套方案。
// 简化的DBoW2闭环检测流程
BowVector currentBow;
vocab->transform(currentKeyframeDescriptors, currentBow); // 特征转词袋向量
// 与数据库中所有关键帧比对相似度
DBoW2::QueryResults ret;
database->query(currentBow, ret, 5); // 返回最相似的5帧
for (const auto& r : ret) {
if (r.Score > loopThreshold) {
KeyFrame* candidate = keyframeDatabase->get(r.Id);
// 还需几何校验:RANSAC + PnP 确认候选帧确实观测同一地点
if (geometricVerification(currentKeyframe, candidate)) {
loopCandidates.push_back(candidate);
}
}
}需要注意的是,单纯依赖外观相似度会产生大量假阳性。比如走廊、办公室这类重复性场景,不同位置的图像长得几乎一样。所以闭环检测一定要加几何校验:对候选帧与当前帧做特征匹配,用RANSAC加PnP求解位姿,只有内点数足够多且位姿合理才确认闭环。此外,如果用单目相机,还存在尺度不确定问题,闭环时要用Sim3(相似变换,含尺度因子)而不是SE3来求解相对位姿,否则优化时约束本身就是错的。除了BoW,还有基于深度学习的方案如NetVLAD,通过卷积网络提取全局描述子,对视角和光照变化更鲁棒,但计算开销更大,且需要额外部署模型。工程上RTAB-Map则采用增量式的基于外观的定位思路,对内存控制做得比较出色,适合长期建图场景。
全局优化:把累积误差摊平到整条轨迹
检测到闭环之后,如果只是把当前帧拉回到闭环帧附近,轨迹会出现一个突兀的“断裂”,前面的轨迹还是歪的。正确做法是全局优化:把所有关键帧位姿作为优化变量,帧间运动约束和闭环约束统一放到一个位姿图(Pose Graph)中,用非线性最小二乘求解,让整条轨迹在最小化残差的条件下平滑地修正。
位姿图的构建逻辑是这样的:每个关键帧是一个节点,相邻关键帧之间的相对位姿估计构成图的边,闭环检测确认的回环约束也是边,只是这条边跨越了很大的时间跨度。由于闭环约束通常更可靠(经过了严格几何校验),优化时可以给它分配更高的权重。求解器方面,g2o和Ceres Solver是最常用的两个选择,ORB-SLAM系列用的就是g2o。优化的收敛速度可以通过稀疏线性求解器(如CHOLMOD)大幅提升,几百个节点的位姿图通常几十毫秒就能收敛。
// 使用g2o进行位姿图优化的骨架代码
// 1. 声明优化器和SE3位姿顶点
g2o::SparseOptimizer optimizer;
optimizer.setAlgorithm(new g2o::BlockSolverX(
std::make_unique<g2o::LinearSolverCholmod<g2o::BlockSolverX::PoseMatrixType>>()));
for (auto& kf : keyframes) {
auto v = new g2o::VertexSE3();
v->setId(kf.id);
v->setEstimate(kf.pose); // 用当前估计值初始化
optimizer.addVertex(v);
}
// 2. 添加边:里程计边和闭环边统一处理
for (auto& e : edges) { // edges 包含相邻帧边 + 闭环边
auto edge = new g2o::EdgeSE3();
edge->setVertex(0, optimizer.vertex(e.from));
edge->setVertex(1, optimizer.vertex(e.to));
edge->setMeasurement(e.relativePose); // 相对位姿测量值
edge->setInformation(e.information); // 信息矩阵,闭环边给更大权重
optimizer.addEdge(edge);
}
// 3. 迭代优化
optimizer.initializeOptimization();
optimizer.optimize(20); // 迭代20次优化完成后还有一个容易被忽视的环节:地图点的修正。位姿图只调整了关键帧位姿,地图中三维点的位置如果不变,点云和位姿之间会出现不一致。完整做法是把关键帧和地图点一起放进BA(Bundle Adjustment)做全局光束平差,或者在位姿优化后按各观测帧的新位姿对地图点做三角化更新。另一个实用技巧是尺度图优化:单目系统中不同段的轨迹尺度可能不一致,闭环边以Sim3形式加入优化,让各段轨迹的尺度也参与调整,这正是ORB-SLAM单目版本的做法。
工程实践建议与常见陷阱
在落地闭环加全局优化时,有几个经验值得参考。第一,闭环检测不要在每帧普通帧上做,只对关键帧操作,既能降低计算量,也减少了冗余闭环。第二,闭环确认后立即触发优化会让系统卡顿,工程上通常把优化放到单独线程,主线程继续跟踪,优化完成后同步结果。第三,要设置连续多次确认机制,比如连续三帧都检测到同一候选闭环才接受,能显著减少假阳性带来的地图错乱。
常见的失败场景包括:快速运动导致图像模糊,特征匹配失败,闭环边测量本身不准,优化后地图反而更差;动态场景下行人占据画面主体,闭环匹配被干扰,这时候需要结合运动一致性检验剔除动态物体上的匹配点;还有纯旋转走廊场景,视差极小,三角化的地图点深度不准,闭环求解容易失败。针对这些情况,可以在闭环边上做鲁棒核函数(如Cauchy核、Huber核),让个别坏边不会主导整个优化结果。
总结来看,VO漂移是增量式估计的必然产物,无法靠单帧精度消除,只能靠闭环检测提供跨时间的绝对约束,再由全局位姿图优化把误差重新分配。理解了“检测—校验—建边—优化—同步”这条完整链路,再看ORB-SLAM、VINS-Fusion等开源系统的回环模块,就能抓住其设计的主线了。实际项目中建议先跑通BoW加g2o的经典组合,再根据场景特点逐步引入深度学习描述子或更精细的鲁棒核策略。