FA_融合和滤波(FF)-图优化事例一
·
FA:Formulas and Algorithm,FF:Fusion and Filtting
图优化工程实例
我们选用g2o做最经典的**2D 位姿图优化(Pose Graph)**案例:机器人在平面移动,顶点是 SE (2) 位姿((x,y,\theta)),边分为里程计二元边 + 回环二元边。
场景说明:小车沿矩形行走,里程计存在漂移,最后回到起点形成回环;用图优化修正整条轨迹漂移。
SE (2):平面位姿,(x,y)平移,(\theta)航向角;适合室内机器人、园区小车 2D 定位。
前置说明:
- g2o 结构:
Vertex顶点 +Edge边 +SparseOptimizer优化器- 目标:(\min\sum e_{ij}^T\Omega_{ij}e_{ij})
- 残差:(e = \boldsymbol{z}{-1}\cdot(\boldsymbol{T}_i{-1}\boldsymbol{T}_j)),(\boldsymbol{z})是观测到的相对位姿
一、工程流程分步拆解
Step1:构建图优化求解器(SparseOptimizer)
- 设置求解器类型:LM(Levenberg-Marquardt),鲁棒性优于高斯牛顿,工程首选
- 设置线性求解器:
BlockSolver+ 线性分解(CSparse/Cholesky,利用稀疏性) - 设置优化迭代次数,开启日志(调试用)
Step2:定义顶点(VertexSE2)
- 待优化变量:每个时刻机器人全局位姿([x,y,\theta])
- 编号 id 唯一;固定第一个顶点(id=0),固定原点消除自由度(缺少全局约束,优化有无穷多解)
- 顶点存储初始值:里程计递推得到的初值(有漂移)
Step3:定义边(EdgeSE2)
每条二元边连接两个顶点(i,j),存储相对位姿观测(z = [dx,dy,d\theta])(里程计 / ICP 匹配输出),同时配置信息矩阵(\Omega)(协方差逆)。
- 里程计边:连续帧之间(i 和 i+1),局部运动观测
- 回环边:检测回到历史位置,比如最后一帧和第 0 帧,用来消除漂移,是图优化核心价值
- 信息矩阵:对角阵,代表 x/y/ 航向角的置信度,噪声越小值越大
Step4:添加鲁棒核(可选,工程必加)
Huber 核:小残差用二范数,大残差线性衰减,抑制外点(匹配错误、里程计跳变),防止一条坏观测把整个轨迹带崩。
Step5:执行优化
- 初始化优化器
- 迭代优化(比如 50 轮),迭代过程逐步降低总残差
- 迭代完成后读取所有顶点优化后的位姿,对比优化前后轨迹
Step6:结果评估
- 优化前:里程计漂移,回环闭合误差很大
- 优化后:整条轨迹被回环约束拉回,全局一致性更好
二、完整 C++ 代码(g2o 2D 位姿图)
依赖:g2o 核心库,g2o/types/slam2d,g2o/solvers/csparse
CMakeLists 一并附上
// pose_graph_2d.cpp
#include <iostream>
#include <g2o/core/sparse_optimizer.h>
#include <g2o/core/block_solver.h>
#include <g2o/core/solver.h>
#include <g2o/core/optimization_algorithm_levenberg.h>
#include <g2o/solvers/csparse/csparse_wrapper.h>
#include <g2o/types/slam2d/vertex_se2.h>
#include <g2o/types/slam2d/edge_se2.h>
#include <g2o/core/robust_kernel_huber.h>
using namespace g2o;
using namespace std;
int main()
{
// ========= Step1:配置稀疏优化器 + LM求解器 =========
// 每个顶点维度3(x,y,theta),误差维度3
using BlockSolverType = BlockSolver<BlockSolverTraits<3,3>>;
using LinearSolverType = LinearSolverCSparse<BlockSolverType::PoseMatrixType>;
auto linearSolver = make_unique<LinearSolverType>();
auto blockSolver = make_unique<BlockSolverType>(move(linearSolver));
OptimizationAlgorithmLevenberg* solver = new OptimizationAlgorithmLevenberg(move(blockSolver));
SparseOptimizer optimizer;
optimizer.setAlgorithm(solver);
optimizer.setVerbose(true); // 打印迭代残差信息
// ========= Step2:添加SE2顶点,里程计初值(矩形轨迹,带漂移) =========
// 矩形:0→1→2→3→0,4个位姿节点
vector<VertexSE2*> vertices(4);
// 顶点0:原点,固定
vertices[0] = new VertexSE2();
vertices[0]->setId(0);
vertices[0]->setEstimate(SE2(0, 0, 0));
vertices[0]->setFixed(true);
optimizer.addVertex(vertices[0]);
// 顶点1:向右走 2m,初值
vertices[1] = new VertexSE2();
vertices[1]->setId(1);
vertices[1]->setEstimate(SE2(2.1, 0.05, 0.02)); // 加一点漂移噪声
optimizer.addVertex(vertices[1]);
// 顶点2:向上走2m
vertices[2] = new VertexSE2();
vertices[2]->setId(2);
vertices[2]->setEstimate(SE2(2.06, 2.08, 1.59));
optimizer.addVertex(vertices[2]);
// 顶点3:向左走2m
vertices[3] = new VertexSE2();
vertices[3]->setId(3);
vertices[3]->setEstimate(SE2(0.07, 2.03, 3.16));
optimizer.addVertex(vertices[3]);
// ========= Step3:添加里程计二元边(连续约束) =========
// 信息矩阵:x,y,theta的权重,对角矩阵
Eigen::Matrix3d info;
info.setIdentity();
info(0,0) = 100; // x方向置信度
info(1,1) = 100; // y方向置信度
info(2,2) = 400; // 角度置信度更高
// 边0-1,观测相对位姿:dx=2, dy=0, dtheta=0
EdgeSE2* e01 = new EdgeSE2();
e01->setVertex(0, vertices[0]);
e01->setVertex(1, vertices[1]);
e01->setMeasurement(SE2(2, 0, 0));
e01->setInformation(info);
optimizer.addEdge(e01);
// 边1-2:dx=0, dy=2, dtheta=pi/2
EdgeSE2* e12 = new EdgeSE2();
e12->setVertex(0, vertices[1]);
e12->setVertex(1, vertices[2]);
e12->setMeasurement(SE2(0, 2, M_PI/2));
e12->setInformation(info);
optimizer.addEdge(e12);
// 边2-3:dx=-2, dy=0, dtheta=pi/2
EdgeSE2* e23 = new EdgeSE2();
e23->setVertex(0, vertices[2]);
e23->setVertex(1, vertices[3]);
e23->setMeasurement(SE2(-2, 0, M_PI/2));
e23->setInformation(info);
optimizer.addEdge(e23);
// ========= Step4:添加【回环边】3→0!图优化核心 =========
EdgeSE2* e_loop = new EdgeSE2();
e_loop->setVertex(0, vertices[3]);
e_loop->setVertex(1, vertices[0]);
e_loop->setMeasurement(SE2(0, 0, 0)); // 观测:3号应该回到0号,相对位姿为0
e_loop->setInformation(info);
// 加Huber鲁棒核,抑制回环误匹配
RobustKernelHuber* huber = new RobustKernelHuber;
huber->setDelta(1.0);
e_loop->setRobustKernel(huber);
optimizer.addEdge(e_loop);
// ========= Step5:执行优化 =========
cout << "===== 优化前位姿 =====" << endl;
for(auto v : vertices){
cout << "id:" << v->id() << " x:" << v->estimate().translation()[0]
<< " y:" << v->estimate().translation()[1]
<< " theta:" << v->estimate().rotation().angle() << endl;
}
optimizer.initializeOptimization();
optimizer.optimize(50); // 最多迭代50次
cout << "\n===== 优化后位姿 =====" << endl;
for(auto v : vertices){
cout << "id:" << v->id() << " x:" << v->estimate().translation()[0]
<< " y:" << v->estimate().translation()[1]
<< " theta:" << v->estimate().rotation().angle() << endl;
}
// 释放内存略,工程用智能指针更好
return 0;
}
### CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
project(g2o_pose_graph)
set(CMAKE_CXX_STANDARD 14)
find_package(G2O REQUIRED)
include_directories(${G2O_INCLUDE_DIRS})
add_executable(pose_graph_2d pose_graph_2d.cpp)
target_link_libraries(pose_graph_2d
${G2O_LIBRARIES}
)
三、代码逐段重点解读
- 求解器选择
OptimizationAlgorithmLevenberg:LM 算法,迭代中自动调节阻尼,初值不好时不容易发散,机器人定位工程默认。LinearSolverCSparse:稀疏 Cholesky 分解,只处理非零元素,大规模轨迹才有优势。 - VertexSE2
setEstimate():设置优化初值(来自里程计 / IMU 前端,必须给初值,非线性优化依赖初值)setFixed(true):固定第一个顶点,固定全局坐标系原点,否则优化问题秩亏,有无穷多解。 - EdgeSE2
setMeasurement(SE2(dx, dy, dtheta)):传感器观测得到两个顶点之间的相对变换,不是全局坐标!这点是新手最容易踩坑。setInformation():信息矩阵(\Omega=\Sigma^{-1}),噪声越小,信息矩阵权重越大。 - 回环边 + 鲁棒核
回环边是图优化区别于 EKF 的关键:EKF 很难修正很久之前的历史漂移;回环边直接约束相隔很久的两个位姿,全局修正整条轨迹。RobustKernelHuber:如果回环匹配错了、观测是外点,不会让残差爆炸污染全部轨迹,自动驾驶 / 激光 SLAM 几乎必开。 - 输出对比
- 优化前:里程计累积漂移,顶点 3 离原点有明显偏移
- 优化后:在里程计约束 + 回环约束联合最小残差下,顶点 3 被拉回靠近原点,整条轨迹一致性提升
四、拓展到自动驾驶 / 3D 场景(衔接前面 Waymo/GTSAM)
上面是2D 位姿图 (g2o),而自动驾驶多源融合一般用GTSAM 因子图(iSAM2 增量优化),区别:
- g2o 位姿图:顶点只有位姿;适合离线建图、2D 激光 SLAM
- GTSAM 因子图:顶点可以是 IMU bias、速度、位姿;因子可以是 IMU 预积分因子、GPS 因子、激光地图匹配因子;支持增量优化 iSAM2,新增帧只更新受影响部分,适合车载实时定位。
GTSAM 的工程差异点(简单示例结构)
// GTSAM伪代码逻辑
NonlinearFactorGraph graph;
Values initial;
// 加先验因子(固定初始位姿)
graph.add(PriorFactor<Pose3>(0, Pose3(), noise));
// IMU预积分因子,连接pose i和i+1
graph.add(ImuFactor(...));
// GPS一元因子,约束当前pose
graph.add(GPSFactor(...));
// 回环因子
graph.add(BetweenFactor<Pose3>(...));
// iSAM2增量优化器,实时滑动窗口
ISAM2 isam;
isam.update(graph, initial);
Values result = isam.calculateEstimate();
五、工程常见问题
- 把全局坐标放进边的观测:边的 measurement 一定是相对位姿,不是全局 xy,会直接优化爆炸
- 信息矩阵随便填:噪声标定错误,多源融合权重失衡,GPS 和 IMU 互相打架
- 忘记固定顶点:无全局先验,优化解不唯一,结果随机漂移
- 不使用鲁棒核:单次错误匹配直接毁掉整条轨迹
- 初值太差:非线性优化收敛到局部最小值,回环修正失效
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐



所有评论(0)