优化——Sim(3) 位姿优化
闭环线程中的 Sim(3) 位姿优化
理解在检测到潜在的闭环时,系统如何优化当前关键帧 (KF1) 与闭环候选关键帧 (KF2) 之间的相对位姿。此相对位姿通过一个 Sim(3) 变换(包含旋转、平移和尺度信息)来描述。关键点在于,此优化过程仅针对 Sim(3) 位姿进行,地图点的三维坐标在此步骤中保持固定,不参与优化。
一、核心思想:图优化 (Graph Optimization)
该优化过程依赖于图优化方法,具体实现通常借助 g2o (General Graph Optimization) 框架。图优化的基本组成元素如下:
- 顶点 (Vertices): 图中的节点,代表了系统中待优化的变量。在我们的场景中,主要是 Sim(3) 位姿。
- 边 (Edges): 连接顶点的边,代表了这些变量之间存在的约束关系。这些约束通常以误差函数的形式表达,优化的目标是最小化所有误差的总和。
二、顶点的选择 (Vertices)
在闭环的 Sim(3) 位姿优化问题中,主要涉及以下两种类型的顶点:
- 待优化的 Sim(3) 位姿:
- 描述: 这是我们核心的优化目标,代表了从闭环候选关键帧 KF2 到当前关键帧 KF1 的 7 自由度相似变换 (),其中 为尺度因子, 为旋转矩阵, 为平移向量。
- g2o 类型: 在 g2o 中,这类顶点通常表示为
g2o::VertexSim3Expmap
。它使用李代数 进行参数化。 - 图示 (参考图 14-3): 例如
g2oS12
,在优化器中可能对应optimizer.vertex(0)
。
- 匹配的地图点 (MapPoints):
- 描述: 这些是在 KF1 和 KF2 中共同观测到的三维地图点。在 Sim(3) 位姿优化阶段,这些地图点的三维坐标被认为是准确的,因此它们是固定的 (fixed),不参与优化。它们作为稳定的参考点,为优化 Sim(3) 位姿提供约束。
- g2o 类型: 在 g2o 中,三维点通常表示为
g2o::VertexSBAPointXYZ
(SBA: Sparse Bundle Adjustment)。 - 图示 (参考图 14-3): 例如点
P3D2c
,在优化器中可能对应optimizer.vertex(id2)
,其中id2
是该地图点在优化器中的唯一标识。
三、边的选择 (Edges)
边定义了顶点之间的约束,即地图点在 Sim(3) 变换下的投影关系。这些是二元边,因为它连接了两种类型的顶点:一个地图点顶点和一个 Sim(3) 位姿顶点。主要存在两种类型的边,对应于投影的方向:
- 正向投影边 (Forward Projection Edge):
- 描述: 将闭环候选关键帧 KF2 中的一个地图点(在其自身的相机坐标系下),通过待优化的 Sim(3) 位姿 (
g2oS12
),变换到当前关键帧 KF1 的相机坐标系下,并进一步投影到 KF1 的图像平面上。其误差是该投影点与 KF1 中对应特征点的实际观测位置之间的差异。 - g2o 类型:
g2o::EdgeSim3ProjectXYZ
。 - 图示 (参考图 14-3): 边
e12
连接了 KF2 中的地图点P3D2c
(通过optimizer.vertex(id2)
) 和 Sim(3) 位姿g2oS12
(通过optimizer.vertex(0)
), 其测量值 (measurement) 是 KF1 中的观测obs1
。
- 描述: 将闭环候选关键帧 KF2 中的一个地图点(在其自身的相机坐标系下),通过待优化的 Sim(3) 位姿 (
- 反向投影边 (Inverse Projection Edge):
- 描述: 将当前关键帧 KF1 中的一个地图点(在其自身的相机坐标系下),通过待优化的 Sim(3) 位姿的逆变换 (
g2oS12
的逆,即g2oS21
),变换到闭环候选关键帧 KF2 的相机坐标系下,并进一步投影到 KF2 的图像平面上。其误差是该投影点与 KF2 中对应特征点的实际观测位置之间的差异。 - g2o 类型:
g2o::EdgeInverseSim3ProjectXYZ
。
- 描述: 将当前关键帧 KF1 中的一个地图点(在其自身的相机坐标系下),通过待优化的 Sim(3) 位姿的逆变换 (
四、误差的定义 (以正向投影边 g2o::EdgeSim3ProjectXYZ
为例)
边的核心是其误差函数,优化器会尝试最小化所有边误差的平方和。对于 Sim(3) 投影边,误差通常是重投影误差。其计算步骤如下:
- 获取三维点坐标:
从地图点顶点中获取其在源关键帧(例如 KF2)的相机坐标系下的三维坐标 。在代码中如v2->estimate()
。 - 通过 Sim(3) 位姿进行变换:
使用当前估计的 Sim(3) 位姿 (从 KF2 到 KF1 的变换,代码中如v1->estimate()
),将三维点 从 KF2 的相机坐标系变换到 KF1 的相机坐标系下,得到 。
这对应代码中的v1->estimate().map(v2->estimate())
。map
函数内部实现:return s * (r * xyz) + t;
- 投影到归一化图像平面:
将变换到 KF1 相机坐标系下的三维点 投影到其归一化图像平面(平面),得到二维坐标 。
这对应代码中的project(transformed_point)
函数。project
函数内部实现:res(0) = v(0)/v(2); res(1) = v(1)/v(2);
- 转换为像素坐标:
使用目标关键帧 KF1 的相机内参数(焦距 和主点 ),将归一化平面坐标 转换为像素坐标 。
这对应代码中的cam_map1(projected_point)
函数。cam_map1
函数内部实现:res[0] = v[0]*_focal_length1[0] + _principle_point1[0]; res[1] = v[1]*_focal_length1[1] + _principle_point1[1];
- 计算误差向量:
误差 是实际观测到的像素坐标 (即 KF1 中匹配的特征点位置)与步骤 4 中投影得到的像素坐标 之间的差值。
这对应代码中的_error = obs - projected_pixel_coordinates;
五、Sim(3) 位姿优化的流程 (Optimizer::OptimizeSim3 函数详解)
在 ORB-SLAM2 中,Optimizer::OptimizeSim3
函数负责执行此优化。其主要步骤如下:
- 步骤 1:初始化 g2o 优化器
- 创建一个
g2o::SparseOptimizer
对象。 - 配置求解器:通常选择 Levenberg-Marquardt (LM) 算法 (
g2o::OptimizationAlgorithmLevenberg
)。 - 配置线性求解器:例如
g2o::LinearSolverDense
或g2o::LinearSolverCholmod
,取决于问题的规模和稀疏性。
- 创建一个
- 步骤 2:设置待优化的 Sim(3) 位姿作为顶点
- 创建一个
g2o::VertexSim3Expmap
类型的顶点vSim3
。 - 设置其初始估计值:使用传入的
g2oS12
。 - 设置顶点 ID:通常设为 0 (
vSim3->setId(0)
)。 - 设置顶点不固定:
vSim3->setFixed(false)
,因为这是我们要优化的变量。 - 处理尺度固定性:
vSim3->_fix_scale = bFixScale;
- 如果传感器是单目相机 (Monocular),
bFixScale
通常为false
,尺度参数会参与优化。 - 如果传感器是双目相机 (Stereo) 或 RGB-D 相机,
bFixScale
通常为true
,尺度参数固定为 1(或由双目/RGB-D直接提供),不参与优化。
- 存储相关的相机内参(主点
_principle_point1
,_principle_point2
和焦距_focal_length1
,_focal_length2
)到vSim3
顶点中,供边的误差函数计算使用。这些内参是从 KF1 和 KF2 的相机参数中获取的。 - 将此 Sim(3) 顶点添加到优化器中 (
optimizer.addVertex(vSim3)
)。
- 创建一个
- 步骤 3:设置匹配的地图点作为顶点 (固定)
- 获取 KF1 和 KF2 之间匹配的地图点列表 (
vpMatches1
)。 - 遍历每一对匹配的地图点 (pMP1 来自 KF1, pMP2 来自 KF2)。
- 检查有效性: 确保 pMP1 和 pMP2 都不是坏点 (
!pMP1->isBad() && !pMP2->isBad()
),并且 pMP2 在 KF2 中有对应的二维特征点索引 (i2 >= 0
)。 - 如果有效:
- 为 pMP1 创建顶点 (KF1 相关):
- 创建一个
g2o::VertexSBAPointXYZ
顶点vPoint1
。 - 获取 pMP1 的世界坐标
P3D1w
,然后使用 KF1 的位姿 (R1w, t1w) 将其转换到 KF1 的相机坐标系下P3D1c = R1w * P3D1w + t1w
。 - 设置
vPoint1
的估计值为P3D1c
。 - 设置顶点 ID (例如
2*i + 1
,以避免与 Sim(3) 顶点和其他点顶点冲突)。 - 设置顶点固定:
vPoint1->setFixed(true)
。 - 将
vPoint1
添加到优化器。
- 创建一个
- 为 pMP2 创建顶点 (KF2 相关):
- 创建一个
g2o::VertexSBAPointXYZ
顶点vPoint2
。 - 获取 pMP2 的世界坐标
P3D2w
,然后使用 KF2 的位姿 (R2w, t2w) 将其转换到 KF2 的相机坐标系下P3D2c = R2w * P3D2w + t2w
。 - 设置
vPoint2
的估计值为P3D2c
。 - 设置顶点 ID (例如
2*(i+1)
). - 设置顶点固定:
vPoint2->setFixed(true)
。 - 将
vPoint2
添加到优化器。
- 创建一个
- 为 pMP1 创建顶点 (KF1 相关):
- 对有效匹配进行计数 (
nCorrespondences++
)。
- 获取 KF1 和 KF2 之间匹配的地图点列表 (
- 步骤 4:设置地图点投影关系作为边
对于每一组通过了步骤 3 检查的有效匹配 (pMP1, pMP2, 以及 KF1 中的观测 kpUn1 和 KF2 中的观测 kpUn2):- 4.1 添加正向投影边 (KF2 -> KF1):
- 创建一个
g2o::EdgeSim3ProjectXYZ
类型的边e12
。 - 设置连接的顶点:
- 第一个顶点 (index 0 in edge) 是 KF2 的地图点顶点
vPoint2
(代码中用optimizer.vertex(id2)
)。 - 第二个顶点 (index 1 in edge) 是 Sim(3) 位姿顶点
vSim3
(代码中用optimizer.vertex(0)
)。
- 第一个顶点 (index 0 in edge) 是 KF2 的地图点顶点
- 设置测量值 (Measurement):使用 KF1 中对应的未畸变特征点的像素坐标
obs1 = (kpUn1.pt.x, kpUn1.pt.y)
。 - 设置信息矩阵 (Information Matrix):通常是一个对角矩阵,其对角元素与特征点观测的确定性有关,一般取图像金字塔层级的逆方差 (
invSigmaSquare1 = pKF1->mvInvLevelSigma2[kpUn1.octave]
)。信息矩阵是协方差矩阵的逆,表示测量值的可信度。
e12->setInformation(Eigen::Matrix2d::Identity() * invSigmaSquare1);
- 设置鲁棒核函数 (Robust Kernel):例如 Huber 核 (
g2o::RobustKernelHuber
),以减小离群点 (outliers) 对优化结果的负面影响。设置核函数的宽度 (deltaHuber
)。 - 将边
e12
添加到优化器,并存入列表vpEdges12
。
- 创建一个
- 4.2 添加反向投影边 (KF1 -> KF2):
- 创建一个
g2o::EdgeInverseSim3ProjectXYZ
类型的边e21
。 - 设置连接的顶点:
- 第一个顶点是 KF1 的地图点顶点
vPoint1
。 - 第二个顶点是 Sim(3) 位姿顶点
vSim3
。
- 第一个顶点是 KF1 的地图点顶点
- 设置测量值:使用 KF2 中对应的未畸变特征点的像素坐标
obs2
。 - 设置信息矩阵(基于 KF2 中特征点的金字塔层级)。
- 设置鲁棒核函数。
- 将边
e21
添加到优化器,并存入列表vpEdges21
。
- 创建一个
- 记录边的索引,用于后续的 outlier 剔除。
- 4.1 添加正向投影边 (KF2 -> KF1):
- 步骤 5:第一次 g2o 优化
- 初始化优化 (
optimizer.initializeOptimization()
)。 - 执行固定次数的迭代优化,例如 5 次 (
optimizer.optimize(5)
)。
- 初始化优化 (
- 步骤 6:用卡方检验剔除误差大的边 (Outlier Rejection)
- 遍历步骤 4 中添加的所有边对 (
vpEdges12[i]
和vpEdges21[i]
)。 - 对于每条边,计算其卡方误差 (
e12->chi2()
和e21->chi2()
)。卡方值是误差项经过信息矩阵加权后的平方和,表示这条边对当前模型拟合的好坏程度。 - 如果正向投影边或反向投影边的卡方误差任何一个超过预设的阈值
th2
:- 认为这对匹配是外点 (outlier)。
- 从优化器中移除这两条边 (
optimizer.removeEdge(e12)
,optimizer.removeEdge(e21)
). - 将对应的原始匹配关系
vpMatches1[idx]
标记为无效 (例如设为NULL
)。 - 将
vpEdges12[i]
和vpEdges21[i]
在列表中的对应位置也标记为无效。 - 累加坏点(被剔除的匹配对)的数量
nBad++
。
- 遍历步骤 4 中添加的所有边对 (
- 步骤 7:第二次 g2o 优化 (根据情况调整迭代次数)
- 根据
nBad
的数量决定迭代次数:- 如果
nBad > 0
(即有边被剔除),说明初始的匹配中存在较多外点,闭环质量可能不是非常好,此时进行更多次迭代,例如 10 次 (nMoreIterations = 10
)。 - 如果
nBad == 0
(没有边被剔除),说明初始匹配质量较高,进行较少次数迭代,例如 5 次 (nMoreIterations = 5
)。
- 如果
- 检查剩余有效匹配数量: 如果经过外点剔除后,剩余的有效匹配数量 (
nCorrespondences - nBad
) 过少(例如小于 10),则认为闭环质量太差,放弃本次优化,直接返回 0 (表示没有足够的内点支持)。 - 在剔除了外点之后,再次执行 g2o 优化 (
optimizer.optimize(nMoreIterations)
)。
- 根据
- 步骤 8:用优化后的结果更新 Sim(3) 位姿
- 从优化器中获取优化后的 Sim(3) 顶点:
g2o::VertexSim3Expmap* vSim3_recov = static_cast<g2o::VertexSim3Expmap*>(optimizer.vertex(0));
- 用该顶点的最新估计值更新输入的
g2oS12
参数:
g2oS12 = vSim3_recov->estimate();
- 返回内点的数量 (
nCorrespondences - nBad
),这个数量可以作为 Sim(3) 优化成功与否以及闭环质量的一个指标。
- 从优化器中获取优化后的 Sim(3) 顶点:
六、总结图 14-3 (Sim(3) 位姿优化中的顶点和边)
- 当前关键帧 KF1: 我们试图将闭环候选帧对齐到的参考帧。
- 闭环候选关键帧 KF2: 检测到的可能与 KF1 形成闭环的帧。
g2oS12
(指向optimizer.vertex(0)
): 核心待优化的 Sim(3) 变换。它描述了如何将 KF2 的坐标系(及其关联的地图点)变换到 KF1 的坐标系。P3D2c
(指向optimizer.vertex(id2)
): 一个在 KF2 的相机坐标系下表示的三维地图点。这个点在优化过程中其坐标是固定不变的。obs1
: 地图点P3D2c
在 KF1 图像上的实际观测到的二维像素坐标。这是边的测量值。e12
(类型为g2o::EdgeSim3ProjectXYZ()
): 一条正向投影边。它约束了g2oS12
。该边的误差计算过程是:将P3D2c
通过当前的g2oS12
估计值从 KF2 坐标系变换到 KF1 坐标系,再投影到 KF1 的图像平面,然后与obs1
比较得出重投影误差。
七、简要总结
闭环线程中的 Sim(3) 位姿优化是一个至关重要的步骤,它旨在精确估计当前关键帧与过去某个关键帧之间的相对位姿(包括尺度)。通过构建一个包含 Sim(3) 位姿和固定地图点的图模型,并利用它们之间的重投影约束,使用如图优化(g2o)的迭代方法来最小化重投影误差。这个过程还包括鲁棒的外点剔除机制,以确保优化结果的准确性和稳定性,为后续的闭环融合和全局姿态图优化奠定基础。