【视觉SLAM十四讲】视觉里程计 1
文章目录
- 特征点匹配
- ORB 特征
- FAST 关键点
- BRIEF 描述子
- ORB 的改进
- 尺度不变性
- 旋转不变性
- 特征匹配
- 2D-2D:对极几何
- 本质矩阵
- 多于 8 对点的情况
- 单应矩阵
- 三角测量
- 3D-2D:PnP
- 直接线性变换(DLT)
- P3P
- EPnP
- 最小重投影误差
- 雅可比矩阵推导
- 优化算法代码实现
- 高斯牛顿法:
- 列文伯格-马夸尔特优化法:
- 3D-3D:ICP
- SVD 法求解
- SVD 法代码实现
特征点匹配
视觉里程计的核心问题是根据图像估计相机运动,而这通常通过帧间的图像变化实现。为了简化运算,我们会从图像中选取特征点,这些点在相机视角发生较小变化时仍然存在,通过特征点在图像上的变化,估计相机位姿的改变。
特征点由关键点和描述子两部分组成。前者指的是特征点在图像中的位置,后者则指该关键点周围像素的信息,这里讲讲比较有代表性的 ORB 特征
ORB 特征
FAST 关键点
FAST 算法的思路为:如果一个像素与周围邻域的像素差别过大时,就认为其为一个角点。由于仅仅需要比较像素之间的灰度值大小,计算量要小得多
其实现步骤如下:
- 在图像中选取一像素 ppp ,假设其亮度为 IPI_PIP
- 设定一个合适的阈值 TTT
- 选取以 ppp 为中心,半径为 3 的圆上的 16 个像素点
- 如果选取的像素点存在连续的 N 个点的亮度大于 IP+TI_P+TIP+T 或者小于 IP−TI_P-TIP−T ,那么该点可视为一个角点( NNN 通常取12)
对于 FAST-12 算法,还有个预测试操作:直接检测邻域圆上的第 1,5,9,13 的像素点亮度,只有至少其中 3 个亮度大于 IP+TI_P+TIP+T 或者小于 IP−TI_P-TIP−T ,才有可能是一个角点
BRIEF 描述子
在提取到关键点之后,我们要对每个点计算其描述子。BRIEF 描述子是一种二进制描述子。在特征点周围选取 256 对随机像素 p,qp,qp,q ,如果 IP>IqI_P > I_qIP>Iq ,则即为 1,反之则即为 0
BRIEF 描述子之间的相似性通常用汉明距离来体现,即两个描述子按位比较,不相同的位数有多少,汉明距离就是多少
ORB 的改进
FAST 和 BRIEF 主要有两个缺点:
- 不具有尺度不变性:同一目标在图像中可能因拍摄距离不同而呈现不同大小,可能被误判为其他物体
- 不具有旋转不变性:同上,同一目标的不同旋转状态可能被误判为其他物体
尺度不变性
ORB 采用的是图像金字塔,即以原始图像为第 0 层,进行多次降采样(缩小)生成一系列不同分辨率的图像,这样就可以在进行特征匹配时达到尺度不变性的效果
举个例子,我们在当前帧(frame 1)找到了一个特征点,此时相机向前移动靠近该特征点,使其在图像(frame 2)上占比变大。我们从 frame 2 图像金字塔的顶层开始搜索找到特征点的大致位置,然后将预测结果放大相应的倍数传递到下一层,如此反复直至在某一次获得了精确的匹配位置
旋转不变性
ORB 采用的是灰度质心法,其核心思想是为特征点计算出一个“主方向”,后续生成描述子时,会先将图像块旋转到该主方向后再进行计算

假设我们有一大小为 N×NN\times NN×N 的图像块 AAA ,其几何中心为 CCC
-
计算零阶距(m00m_{00}m00)和一阶距(m10,m01m_{10},m_{01}m10,m01):灰度值加权的坐标和,即“质量”分布中心
mpq=∑x,y∈BxpyqI(x,y)p,q=0,1m_{pq}=\sum_{x,y\in B}x^py^qI(x,y)\quad p,q={0,1}mpq=∑x,y∈BxpyqI(x,y)p,q=0,1 -
计算质心:C=(m10m00,m01m00)C=\left( \frac{m_{10}}{m_{00}},\frac{m_{01}}{m_{00}} \right)C=(m00m10,m00m01)
-
连接几何中心 OOO 和质心 CCC ,得到一方向向量 OC→\overrightarrow {OC}OC ,于是特征点的方向可定义为
θ=arctan(m01m10)\theta = \arctan \left(\frac{m_{01}}{m_{10}}\right) θ=arctan(m10m01)
下面是一份灰度质心法的使用和描述子计算的大致代码:
void computeORB(const cv::Mat img, std::vector<cv::KeyPoint> &keyPoints, std::vector<DescType> &descriptors)
{const int half_patch_size = 8; // 用于计算方向的图像块大小const int half_boundary = 16; // 定义图像边界缓冲区for (auto &kp : keyPoints){// 除去边界上的点if (kp.pt.x < half_boundary || kp.pt.y < half_boundary || kp.pt.x >= img.cols - half_boundary || kp.pt.y >= img.rows - half_boundary){descriptors.push_back({});continue;}// 计算一阶矩float m10 = 0, m01 = 0;for (int dx = -half_patch_size; dx < half_patch_size; dx++){for (int dy = -half_patch_size; dy < half_patch_size; dy++){// 获取当前像素值uchar pixel = img.at<uchar>(kp.pt.y + dy, kp.pt.x + dx);m10 += dx * pixel;m01 += dy * pixel;}}// 计算特征点方向float m_sqrt = sqrt(m10 * m10 + m01 * m01);float sin_theta = m01 / m_sqrt;float cos_theta = m10 / m_sqrt;DescType desc(8, 0);for (int i = 0; i < 8; i++){uint32_t d = 0;for (int j = 0; j < 32; j++){int idx_pq = i * 8 + j;cv::Point2f p(ORB_pattern[idx_pq * 4], ORB_pattern[idx_pq * 4 + 1]);cv::Point2f q(ORB_pattern[idx_pq * 4 + 2], ORB_pattern[idx_pq * 4 + 3]);// 计算旋转后的坐标cv::Point2f p_rot = cv::Point2f(p.x * cos_theta - p.y * sin_theta, p.x * sin_theta + p.y * cos_theta) + kp.pt;cv::Point2f q_rot = cv::Point2f(q.x * cos_theta - q.y * sin_theta, q.x * sin_theta + q.y * cos_theta) + kp.pt;if (img.at<uchar>(p_rot.y, p_rot.x) < img.at<uchar>(q_rot.y, q_rot.x)){// 通过位运算 i << j,利用一个32位的整数存储32个二进制结果d |= 1 << j;}}desc[i] = d;}descriptors.push_back(desc);}
}
特征匹配
通过上面的步骤,我们现在得到两帧图像各自的特征点,那么我们该怎么找到他们之间的对应关系呢?
-
暴力匹配:对于两张图片的描述子集,一一计算汉明距离以找到对应的匹配点。实现简单,计算精确;但计算量大,仅适合特征点少的情况
-
快速近似最近邻(FLANN):在匹配前,FLANN 会先用其中一张图像的描述子集构建一个数据索引结构,这一步比较耗时但仅需进行一次,对于另一张图像的每一个描述子,在这个预先构建的索引结构上快速搜索找到近似的几个目标,最后利用最近邻距离和次近邻距离的比值筛选匹配结果。对于大规模的特征匹配速度几块,但结果相对并不精确,而且构建索引比较耗时,在较小特征点时使用可能得不偿失
2D-2D:对极几何
对于已匹配到特征点的相邻帧,可建立如下模型

此时存在对极约束
x2Tt∧Rx1=p2TK−Tt∧RK−1p1=0(x是p在归一化平面上的像素坐标)x_2^Tt^\wedge Rx_1 =p_2^TK^{-T}t^\wedge RK^{-1}p_1=0\quad(x是p在归一化平面上的像素坐标) x2Tt∧Rx1=p2TK−Tt∧RK−1p1=0(x是p在归一化平面上的像素坐标)
其中,我们通常定义基础矩阵 E=t∧R,本质矩阵 F=K−TEK−1\text{基础矩阵}~E=t^\wedge R,\quad \text{本质矩阵}~F=K^{-T}EK^{-1}基础矩阵 E=t∧R,本质矩阵 F=K−TEK−1
可以进一步简化对极约束:x2TEx1=p2TFp1=0x_2^TEx_1=p_2^TFp_1=0x2TEx1=p2TFp1=0
本质矩阵
本质矩阵 EEE 是一个 3×33\times33×3 矩阵
- 对 EEE ,乘以任意一个非零常数,对极约束仍然满足,故可认为 EEE 在不同尺度下是等价的
- 本质矩阵的奇异值必定是 [σ,σ,0]T(σ>0)[\sigma,\sigma,0]^T(\sigma>0)[σ,σ,0]T(σ>0)
- 由于平移和旋转各有 3 个自由度,故 t∧Rt^\wedge Rt∧R 共有 6 个自由度,由于尺度等价性,故 EEE 实际有 5 个自由度
通常使用八点法来线性求解 EEE ,八点法利用 8 对匹配点构建线性方程组,通过 SVD 求解 EEE
对于一对匹配点,其归一化坐标为 x1=[u1,v1,1],x2[u2,v2,1]x_1=[u_1,v_1,1],~~x_2[u_2,v_2,1]x1=[u1,v1,1], x2[u2,v2,1]
(u2,v2,1)(e1e2e3e4e5e6e7e8e9)(u1u2u3)=0\left( u_2,v_2,1 \right)\begin{pmatrix} e_1 & e_2 & e_3 \\ e_4 & e_5 & e_6 \\ e_7 & e_8 & e_9 \end{pmatrix}\begin{pmatrix} u_1 \\ u_2 \\ u_3 \end{pmatrix} = 0 (u2,v2,1)e1e4e7e2e5e8e3e6e9u1u2u3=0
将 eee 转化为列向量,写成与 eee 有关的线性形式
[u2u1,u2v1,u2,v2u1,v2v1,v2,u1,v1,1]⋅e=0[u_2u_1,u_2v_1,u_2,v_2u_1,v_2v_1,v_2,u_1,v_1,1]\cdot e=0 [u2u1,u2v1,u2,v2u1,v2v1,v2,u1,v1,1]⋅e=0
以此类推我们就得到了八个线性方程,如果这八对点都不共面(系数矩阵满秩),则可得到唯一解
对于估得的本质矩阵 EEE ,现在的任务是从中恢复出 R,tR,tR,t 。 对 EEE 进行奇异值分解(SVD)
E=UΣVTE=U\Sigma V^T E=UΣVT
我们会得到 4 个可能的 (R,t)(R,t)(R,t) 解:
t1=U(:,3),t2=−U(:,3)R1=URz(π2)VT,R2=URZ(−π2)VTt_1=U(:,3),\quad t_2=-U(:,3) \\ R_1=UR_z(\frac{\pi}{2})V^T, \quad R_2=UR_Z(-\frac{\pi}{2})V^T t1=U(:,3),t2=−U(:,3)R1=URz(2π)VT,R2=URZ(−2π)VT
只有正确解能够在两个相机中都拥有正的深度,只要检测点在两个相机下的深度就可筛选出正确解
多于 8 对点的情况
当匹配对数超过 8 对时,八点法可能构成超定方程无法求解,需要采取其他方法
-
最小二乘法:
-
设系数矩阵为 AAA ,此时有 Ae=0Ae=0Ae=0
mine∣∣Ae∣∣22=mine∣∣UΣVTe∣∣2\underset{e}{\min}||Ae||_2^2=\underset{e}{\min}||U\Sigma V^Te||^2 emin∣∣Ae∣∣22=emin∣∣UΣVTe∣∣2 -
因为 UUU 是正交矩阵不影响范数且对变量无影响,故舍去。此时设 y=VTey=V^Tey=VTe ,由于 VVV 是正交矩阵,∣y∣=∣e∣=1|y|=|e|=1∣y∣=∣e∣=1
-
此时问题变为最小化 ∣Σy∣2=]σ12y12+σ22y22+…+σ92y92|\Sigma y|^2=]\sigma_1^2y_1^2+\sigma_2^2y_2^2+\ldots+\sigma_9^2y_9^2∣Σy∣2=]σ12y12+σ22y22+…+σ92y92
-
由于 σ1≥σ2≥…≥σ9\sigma_1 \ge \sigma_2 \ge \ldots \ge \sigma_9σ1≥σ2≥…≥σ9 ,权重必然被分配给 y9y_9y9 ,即令 y=[0,0,…,0,1]Ty=[0,0,\ldots,0,1]^Ty=[0,0,…,0,1]T 。因此, e=Vy=V.,9e=Vy=V_{.,9}e=Vy=V.,9
-
将求得的向量 eee 转换会 3×33\times33×3 的矩阵 E∗E^*E∗
-
强制内在约束:对 E∗E^*E∗ 进行 SVD 分解,将其奇异值矩阵设为 Σ′=diag((σ1+σ2)/2,(σ1+σ2)/2,0)\Sigma'= diag((\sigma_1+\sigma_2)/2,(\sigma_1+\sigma_2)/2,0)Σ′=diag((σ1+σ2)/2,(σ1+σ2)/2,0) ,再重构得到最终的本质矩阵 EEE
最小二乘法虽然在数学上是最优解,但对误匹配和噪声及其敏感
-
-
随机采样一致性:
- 从所有匹配点中随机抽取 8 对,并使用这 8 对点使用八点法求出一个本质矩阵 EiE_iEi
- 计算所有匹配点在模型 EiE_iEi 下的误差(即对极约束 x2TEix1\mathbf{x}_2^T E_i \mathbf{x}_1x2TEix1 的值),如果误差小于阈值,则视为该模型的内点
- 重复以上步骤 N 次,最有模型即所有迭代中内点数量最多的那个
随机采样一致性虽然计算时间不确定,但其出色的鲁棒性,使其在实际应用中几乎总是使用
单应矩阵
当场景中的特征点都落在同一平面上(或者相机纯旋转)时,可以通过单应性进行运动估计
设匹配好的特征点 p1,p2p_1,p_2p1,p2 ,特征点落在平面 PPP 上,该平面的法向量为 nnn ,原点(第一个相机的光心)到平面的负距离为 ddd
nTP+d=0⇒−nTPd=1n^TP+d=0\quad \Rightarrow \quad -\frac{n^TP}{d}=1 nTP+d=0⇒−dnTP=1
那么:
p2≃K(RP+t)≃K(RP+t⋅(−nTPd))≃K(R−tnTd)K−1p1\begin{align*} p_2 &\simeq K(RP+t) \\ &\simeq K\left(RP+t \cdot (-\frac{n^TP}{d})\right) \\ &\simeq K\left(R-\frac{tn^T}{d}\right)K^{-1}p_1 \end{align*} p2≃K(RP+t)≃K(RP+t⋅(−dnTP))≃K(R−dtnT)K−1p1
于是我们得到了 p1p_1p1 和 p2p_2p2 之间的关系:p2≃Hp1p_2 \simeq Hp_1p2≃Hp1
消去尺度因子得到
u2=h1u1+h2v1+h3h7u1+h8v1+h9v2=h4u1+h5v1+h6h7u1+h8v1+h9u_2=\frac{h_1u_1+h_2v_1+h_3}{h_7u_1+h_8v_1+h_9} \qquad v_2=\frac{h_4u_1+h_5v_1+h_6}{h_7u_1+h_8v_1+h_9} u2=h7u1+h8v1+h9h1u1+h2v1+h3v2=h7u1+h8v1+h9h4u1+h5v1+h6
固定 h9=1h_9=1h9=1 ,可整理得
h1u1+h2v1+h3−h7u1u2−h8v1u2=u2h4u1+h5v1+h6−h7u1v2−h8v1v2=v2h_1u_1+h_2v_1+h_3-h_7u_1u_2-h_8v_1u_2=u_2 \\ h_4u_1+h_5v_1+h_6-h_7u_1v_2-h_8v_1v_2=v_2 h1u1+h2v1+h3−h7u1u2−h8v1u2=u2h4u1+h5v1+h6−h7u1v2−h8v1v2=v2
提取其约束,令
A1=(u11v111000−u11u21−v11u21000u11v111−iu11v21−v11v21)A_1=\begin{pmatrix} u_1^1 & v_1^1 & 1 & 0 & 0 & 0 & -u_1^1u_2^1 & -v_1^1u_2^1 \\ 0 & 0 & 0 & u_1^1 & v_1^1 & 1 & -iu_1^1v_2^1 & -v_1^1v_2^1 \end{pmatrix} A1=(u110v110100u110v1101−u11u21−iu11v21−v11u21−v11v21)
将 4 对匹配点的方程合并,以恢复单应矩阵 HHH ,后面的求解就与本质矩阵 EEE 类似了
三角测量
对于单目 slam ,仅通过单张图像无法获得像素的深度信息,我们需要通过三角测量的方法估计深度
三角测量是已知两个相机之间的相对位姿 (R,t)(R,t)(R,t) 和一对匹配点 p1p_1p1 和 p2p_2p2 ,计算该匹配点所对应的三维空间点 PPP 的位置
如图,O1,O2O_1,O_2O1,O2 是相机光心,p1,p2p_1,p2p1,p2 是匹配点。由于噪声的存在,两条射线未必会相交,因此找到一 PPP 点使它到两条射线的距离之和最小就是三角测量的目标
s1x1=p1s2x2=p2=Rp1+ts_1x_1=p_1\\ s_2x_2=p_2=Rp_1+t s1x1=p1s2x2=p2=Rp1+t
其中 s1,s2s_1,s_2s1,s2 是 PPP 点在两个坐标系下的深度值
s2x2=s1Rx1+t⇒s2x2∧x2=0=s1x2∧Rx1+x2∧ts_2x_2=s_1Rx_1+t \quad \Rightarrow \quad s_2x_2^\wedge x_2=0=s_1x_2^\wedge Rx_1+x_2^\wedge t s2x2=s1Rx1+t⇒s2x2∧x2=0=s1x2∧Rx1+x2∧t
于是我们得到了一个关于未知数 s1s_1s1 的线性方程
3D-2D:PnP
当我们得知了场景中一些特征点的 3D 位置,并且又在当前帧观测到了,那么最少只需 3 个点对就可以估计相机运动
直接线性变换(DLT)
将旋转矩阵 RRR 和平移向量 ttt 视为未知的线性变量,直接构建线性方程求解
假设某个空间点 P=(X,Y,Z,1)TP=(X,Y,Z,1)^TP=(X,Y,Z,1)T ,在图像 I1I_1I1 中投影到点 x1=(u1,v1,1)Tx_1=(u_1,v_1,1)^Tx1=(u1,v1,1)T ,此时定义一个包含了旋转和平移信息的增广矩阵 [R∣t][R|t][R∣t](和变换矩阵不同,其并不需要满足任何旋转矩阵的约束),此时我们得到如下展开形式:
s(u1v11)=(t1t2t3t4t5t6t7t8t9t10t11t12)(XYZ1)s\begin{pmatrix}u_1\\v_1\\1\end{pmatrix}=\begin{pmatrix}t_1&t_2&t_3&t_4\\t_5&t_6&t_7&t_8\\t_9&t_{10}&t_{11}&t_{12}\end{pmatrix}\begin{pmatrix}X\\Y\\Z\\1\end{pmatrix} su1v11=t1t5t9t2t6t10t3t7t11t4t8t12XYZ1
这里将旋转矩阵和平移向量看成 12 个未知数,因此最少需要 6 对匹配点来求解。但是通过该方法求得的 RRR 可能并不满足旋转矩阵应有的约束,因此我们需要找到一个最合适的旋转矩阵来对它近似。
P3P
仅需 3 对不共线的 3D-2D 点和一对验证点就能求解位姿
P3P 利用三角形相似,建立如下方程
OA2+OB2−2⋅OA⋅OB⋅cos(∠AOB)=AB2OA^2+OB^2-2\cdot OA\cdot OB\cdot\cos(∠AOB)=AB^2 OA2+OB2−2⋅OA⋅OB⋅cos(∠AOB)=AB2
其余两边建立类似方程,我们就可以得到 A,B,CA,B,CA,B,C 在相机坐标系下的 3D3D3D 坐标,最后问题就转换为了一个 3D−3D3D-3D3D−3D 问题
P3P 只能利用 3 点的信息,而且受噪声影响严重,只要 3 点共线或者存在误匹配,则算法失效
EPnP
EPnP 适用于任意数量点(≥4\ge4≥4),其核心思想是将 3D 点表示为四个控制点的加权和,将问题转化为求解控制点在相机坐标系下的坐标
-
选择世界坐标系下点的质心以及三个主方向上的点
-
每个 3D 点都可以表示为四个控制点的加权和
Piω=∑j=14aijcjωP_i^{\omega}=\sum_{j=1}^4a_{ij}c_j^\omega Piω=j=1∑4aijcjω -
将控制点在相机坐标系下的坐标代入投影方程建立线性方程组
-
通过 SVD 求解控制点下的坐标代入相机坐标系下的坐标,并利用控制点在世界坐标和空间坐标系下的坐标,通过 3D−3D3D-3D3D−3D 方法来求解
EPnP 因为稳定高效,是最常用的 PnPPnPPnP 方法之一(OpenCV 的 slovePnP 就是 EPnP)
最小重投影误差
上面的线性方法通常先求相机位姿,再求空间点的位置,非线性优化方法同样可以用于求解相机位姿和 3D 点位置,不同的是,非线性优化会将它们都视为优化变量。
设三维空间点 Pi=[Xi,Yi,Zi]T\mathbf{P_i}=[X_i,Y_i,Z_i]^TPi=[Xi,Yi,Zi]T ,其投影的像素坐标为 ui=[ui,vi]T\mathbf{u_i}=[u_i,v_i]^Tui=[ui,vi]T ,要计算相机位姿 R,t(其李群表示为 T)R,t(\text{其李群表示为~}\mathbf{T})R,t(其李群表示为 T) ,投影方程如下
si[uivi1]=KT[XiYiZi1](siui)=KTPis_i\begin{bmatrix}u_i\\v_i\\1 \end{bmatrix}=KT\begin{bmatrix}X_i\\Y_i\\Z_i\\1 \end{bmatrix}\\(s_iu_i)=KTP_i siuivi1=KTXiYiZi1(siui)=KTPi
于是我们可以构建最小二乘问题
T∗=argminT12∑i=1n∣∣ui−1siKTPi∣∣22T^*=\arg\underset{T}{\min}\frac{1}{2}\sum_{i=1}^n||u_i-\frac{1}{s_i}KTP_i||_2^2 T∗=argTmin21i=1∑n∣∣ui−si1KTPi∣∣22
其思路可以大致理解为:已知以目标在世界坐标系下的位置,利用估计的相机位姿将其转换为相机坐标系下的位置,再通过相机内参将其投影到图像平面,不断调整相机位姿的假设使投影点与实际观测点靠近
由于位姿属于李群,存在约束,所以可以先用李代数来参数化位姿,将其转化为无约束优化问题
定义 T=exp(ξ∧)T=\exp(\xi^\wedge)T=exp(ξ∧) 其中 ξ=[ρ,ϕ]T∈R6\xi=[\rho,\phi]^T\in \mathbb{R}^6ξ=[ρ,ϕ]T∈R6
则问题可重写为
ξ∗=argminξ12∑i=1n∣∣ui−π(exp(ξ∧)Pi)∣∣2\xi^*=\arg\underset{\xi}{\min}\frac{1}{2}\sum_{i=1}^n||u_i-\pi(\exp(\xi^\wedge)P_i)||^2 ξ∗=argξmin21i=1∑n∣∣ui−π(exp(ξ∧)Pi)∣∣2
在进行高斯牛顿法或者列文伯格-马夸尔特法进行优化之前,我们需要求每个误差项关于优化变量的导数。设定误差量为 eee ,则有
e(x+Δx)≈e(x)+JTΔxe(x+\Delta x)\approx e(x)+J^T\Delta x e(x+Δx)≈e(x)+JTΔx
雅可比矩阵推导
重投影误差关于李代数的导数可表示为
∂e∂δξ=∂e∂P′∂P′∂δξ\frac{\partial e}{\partial \delta\xi}=\frac{\partial e}{\partial P'}\frac{\partial P'}{\partial \delta\xi} ∂δξ∂e=∂P′∂e∂δξ∂P′
已知投影方程
u=fxX′Z′+cxv=fyY′Z′+cyu=f_x\frac{X'}{Z'}+c_x \qquad v=f_y\frac{Y'}{Z'}+c_y u=fxZ′X′+cxv=fyZ′Y′+cy
求偏导
∂e∂P′=−[∂u∂X′∂u∂Y′∂u∂Z′∂v∂X′∂v∂Y′∂v∂Z′]=−[fxZ′0−fxX′Z′20fyZ′−fyY′Z′2]\frac{\partial e}{\partial P'}=-\begin{bmatrix}\frac{\partial u}{\partial X'}&\frac{\partial u}{\partial Y'}&\frac{\partial u}{\partial Z'}\\\frac{\partial v}{\partial X'}&\frac{\partial v}{\partial Y'}&\frac{\partial v}{\partial Z'}\end{bmatrix}=-\begin{bmatrix}\frac{f_x}{Z'}&0&-\frac{f_xX'}{Z'^2}\\0&\frac{f_y}{Z'}&-\frac{f_yY'}{Z'^2}\end{bmatrix} ∂P′∂e=−[∂X′∂u∂X′∂v∂Y′∂u∂Y′∂v∂Z′∂u∂Z′∂v]=−[Z′fx00Z′fy−Z′2fxX′−Z′2fyY′]
根据李代数的左扰动模型
∂P′∂δξ=[I,−P′∧]=[1000Z′−Y′010−Z′0X′001Y′−X′0]\frac{\partial P'}{\partial\delta\xi}=\begin{bmatrix}I,-P'^\wedge\end{bmatrix}=\begin{bmatrix}1&0&0&0&Z'&-Y'\\0&1&0&-Z'&0&X'\\0&0&1&Y'&-X'&0\end{bmatrix} ∂δξ∂P′=[I,−P′∧]=1000100010−Z′Y′Z′0−X′−Y′X′0
合并两部分即可得到雅可比矩阵
J=∂e∂P′⋅∂P′∂δξ=−[fxZ′0−fxX′Z′2−fxX′Y′Z′2fx+fxX′2Z′2−fxY′Z′0fyZ′−fyY′Z′2−fy−fyY′2Z′2fyX′Y′Z′2fyX′Z′]J=\frac{\partial e}{\partial P'}\cdot\frac{\partial P'}{\partial \delta\xi}=-\begin{bmatrix} \frac{f_x}{Z'}&0&-\frac{f_xX'}{Z'^2}&-\frac{fxX'Y'}{Z'^2}&f_x+\frac{f_xX'^2}{Z'^2}&-\frac{f_xY'}{Z'}\\0&\frac{f_y}{Z'}&-\frac{f_yY'}{Z'^2}&-f_y-\frac{f_yY'^2}{Z'^2}&\frac{f_yX'Y'}{Z'^2}&\frac{f_yX'}{Z'} \end{bmatrix} J=∂P′∂e⋅∂δξ∂P′=−[Z′fx00Z′fy−Z′2fxX′−Z′2fyY′−Z′2fxX′Y′−fy−Z′2fyY′2fx+Z′2fxX′2Z′2fyX′Y′−Z′fxY′Z′fyX′]
如果仍需优化特征点的空间位置
∂e∂P=∂e∂P′∂P′∂P\frac{\partial e}{\partial P}=\frac{\partial e}{\partial P'}\frac{\partial P'}{\partial P} ∂P∂e=∂P′∂e∂P∂P′
其中 ∂e∂P′\frac{\partial e}{\partial P'}∂P′∂e 在上方已求出,由于 P′=RP+tP'=RP+tP′=RP+t ,易得 ∂P′∂P=R\frac{\partial P'}{\partial P}=R∂P∂P′=R
因此:
∂e∂P=−[fxZ′0−fxX′Z′20fyZ′−fyY′Z′2]R\frac{\partial e}{\partial P}=-\begin{bmatrix}\frac{f_x}{Z'}&0&-\frac{f_xX'}{Z'^2}\\0&\frac{f_y}{Z'}&-\frac{f_yY'}{Z'^2}\end{bmatrix}R ∂P∂e=−[Z′fx00Z′fy−Z′2fxX′−Z′2fyY′]R
优化算法代码实现
这里提供了大致代码的实现
高斯牛顿法:
void optimizePoseGaussNewton(const std::vector<cv::Point3d>& points_3d, const std::vector<cv::Point2d>& points_2d, cv::Mat& K, Sophus::SE3d& pose)
{const int iterations = 10; // 最大迭代次数double cost = 0, last_cost = 0; // 当前误差和上次迭代误差double fx = K.at<double>(0, 0);double fy = K.at<double>(1, 1);double cx = K.at<double>(0, 2);double cy = K.at<double>(1, 2);// 构建相机内参矩阵Eigen::Matrix3d K_eigen;K_eigen << fx, 0, cx,0, fy, cy,0, 0, 1;for (int iter = 0; iter < iterations; iter++){Sophus::Matrix6d H = Sophus::Matrix6d::Zero(); // 初始化海森矩阵Sophus::Vector6d b = Sophus::Vector6d::Zero(); // 初始化梯度cost = 0;// 计算雅可比矩阵和残差for (size_t i = 0; i < points_3d.size(); i++){// 将3D点转换到相机坐标系下Eigen::Vector3d pw(points_3d[i].x, points_3d[i].y, points_3d[i].z);Eigen::Vector3d pc = pose * pw; // P_camera = T * P_worlddouble X = pc[0], Y = pc[1], Z = pc[2];// 检查深度if (Z <= 0){std::cerr << "Depth is zero or negative!" << std::endl;continue;}// 投影到图像平面并计算重投影误差Eigen::Vector3d pixel_homo = K_eigen * pc; // 齐次坐标形式Eigen::Vector2d pixel(pixel_homo[0] / pixel_homo[2], pixel_homo[1] / pixel_homo[2]); // 转换回像素坐标Eigen::Vector2d observerd(points_2d[i].x, points_2d[i].y);Eigen::Vector2d error = observerd - pixel;// 累加误差平方和cost += error.squaredNorm();// 计算雅可比矩阵Eigen::Matrix<double, 2, 6> J;J(0, 0) = fx / Z;J(0, 1) = 0;J(0, 2) = -fx * X / (Z * Z);J(0, 3) = -fx * X * Y / (Z * Z);J(0, 4) = fx + fx * X * X / (Z * Z);J(0, 5) = -fx * Y / Z;J(1, 0) = 0;J(1, 1) = fy / Z;J(1, 2) = -fy * Y / (Z * Z);J(1, 3) = -fy - fy * Y * Y / (Z * Z);J(1, 4) = fy * X * Y / (Z * Z);J(1, 5) = fy * X / Z;// 构建高斯-牛顿方程H += J.transpose() * J;b += -J.transpose() * error;}// 求解线性方程 H * dx = bSophus::Vector6d dx;try{dx = H.ldlt().solve(b); // H 明确是对称正定矩阵,使用 LDLT 分解求解}catch (const std::exception& e){std::cerr << "Linear system solve failed: " << e.what() << std::endl;break;}// 检查解if (!dx.allFinite()){std::cerr << "Solution is not finite!" << std::endl;break;}// 检查收敛性if (iter > 0 && cost >= last_cost){std::cout << "Iteration " << iter << ", cost increased: " << cost << " -> " << last_cost << std::endl;break;}// 更新位姿pose = Sophus::SE3d::exp(dx) * pose;// 更新误差last_cost = cost;// 输出迭代信息std::cout << "Iteration " << iter << ", cost = " << cost << std::endl;std::cout << "dx norm = " << dx.norm() << std::endl;// 判断收敛if (dx.norm() < 1e-6){std::cout << "Converged in " << iter << " iterations." << std::endl;break;}}
}
列文伯格-马夸尔特优化法:
void optimizePoseLM(const std::vector<cv::Point3d>& points_3d, const std::vector<cv::Point2d>& points_2d, cv::Mat& K, Sophus::SE3d& pose)
{// 前面直到计算雅可比矩阵和残差的部分与 Gauss-Newton 相同,不再重复// 自适应阻尼Sophus::Vector6d dx;bool updateAccepted = false;while (!updateAccepted){try{// (H + lambda*I) * dx = bSophus::Matrix6d H_lm = H + lambda * Sophus::Matrix6d::Identity();dx = H_lm.ldlt().solve(b);if (!dx.allFinite()){throw std::runtime_error("Solution is not finite!");}}catch (const std::exception& e){std::cerr << "Linear system solve failed: " << e.what() << std::endl;lambda *= 4; // 增大阻尼因子,重新尝试continue;}// 尝试更新位姿Sophus::SE3d newPose = Sophus::SE3d::exp(dx) * pose;// 计算新的误差double newCost = 0;for (size_t i = 0; i < points_3d.size(); i++){Eigen::Vector3d pw(points_3d[i].x, points_3d[i].y, points_3d[i].z);Eigen::Vector3d pc = newPose * pw;// 投影到图像平面并计算重投影误差Eigen::Vector3d pixel_homo = K_eigen * pc;Eigen::Vector2d pixel(pixel_homo[0] / pixel_homo[2], pixel_homo[1] / pixel_homo[2]);Eigen::Vector2d observerd(points_2d[i].x, points_2d[i].y);Eigen::Vector2d error = observerd - pixel;newCost += error.squaredNorm();}/** 计算近似质量因子 ρ* ρ = [实际误差减少量] / [理论误差减少量]* 理论误差减少量 = -dx^T * (J^T * f + 0.5 * H * dx)* 简化后 ≈ dx^T * (λ * dx - b) (因为 H * dx ≈ λ * dx )*/double modelImprovement = dx.dot(b - lambda * dx);double actualImprovement = cost - newCost;double rho = actualImprovement / modelImprovement;// 根据 ρ 调整阻尼因子if (rho > 0){// 模型预测准确,接受更新pose = newPose;cost = newCost;updateAccepted = true;// 调整 λ:当模型很好时减少 λ,使模型更接近高斯-牛顿法lambda *= std::max(1.0 / 3.0, 1 - std::pow(2 * rho - 1, 3));lambda = std::max(lambda, 1e-6); // 防止 λ 过小}else{// 模型预测不准确,拒绝更新lambda *= 2; // 增大阻尼因子,重新尝试lambda = std::min(lambda, 1e5); // 防止 λ 过大}// 防止无限循环if (lambda > 1e5){std::cerr << "Lambda exploded! Exiting." << std::endl;break;}}// 输出迭代信息std::cout << "Iteration " << iter << ", cost = " << cost << ", lambda = " << lambda << std::endl;std::cout << "dx norm = " << dx.norm() << std::endl;// 判断收敛if (iter > 0 && cost >= last_cost - 1e-6){std::cout << "Converged in " << iter << " iterations." << std::endl;break;}if (dx.norm() < 1e-6){std::cout << "Converged in " << iter << " iterations." << std::endl;break;}// 更新误差last_cost = cost;}
}
这里建议搭配上一章的非线性优化一起看
3D-3D:ICP
当我们观测到两组 3D 点时(通过RGB-D相机或者三角化得到),我们可以直接使用 3D-3D 匹配点来估计相机运动
已知两组匹配 3D 点:
P={p1,p2,…,pn},Q={q1,q2,…,qn}P=\{p_1,p_2,\dots,p_n\},\quad Q=\{q_1,q_2,\dots,q_n\} P={p1,p2,…,pn},Q={q1,q2,…,qn}
构建最小二乘问题
minR,t12∑i=1n∣∣qi−(Rpi+t)∣∣2\underset{R,t}{\min}\frac{1}{2}\sum_{i=1}^n||q_i-(Rp_i+t)||^2 R,tmin21i=1∑n∣∣qi−(Rpi+t)∣∣2
SVD 法求解
-
计算质心
μp=1n∑i=1npi,μq=1n∑i=1nqi\mu_p=\frac{1}{n}\sum_{i=1}^np_i,\quad \mu_q=\frac{1}{n}\sum_{i=1}^nq_i μp=n1i=1∑npi,μq=n1i=1∑nqi -
去中心化
pi′=pi−μp,qi′=qi−μqp_i'=p_i-\mu_p,\quad q_i'=q_i-\mu_q pi′=pi−μp,qi′=qi−μq -
计算旋转矩阵
构建矩阵:
W=∑i=1nqi′p′iTW=\sum_{i=1}^nq_i'{p'}_i^T W=i=1∑nqi′p′iT
进行 SVD 分解
W=UΣVTW=U\Sigma V^T W=UΣVT
则旋转矩阵可定义为
R=UVTR=UV^T R=UVT
(如果 det(R)=−1\det(R)=-1det(R)=−1 ,需要去负号修正:R=U⋅diag(1,1,−1)⋅VTR=U\cdot\text{diag}(1,1,-1)\cdot V^TR=U⋅diag(1,1,−1)⋅VT)注:奇异值分解可以视为矩阵先进行了一次旋转 UUU ,沿着坐标轴进行缩放 Σ\SigmaΣ ,最后再进行一次旋转 VTV^TVT ,故旋转矩阵可以视为 R=UVTR=UV^TR=UVT
-
计算平移向量
t=μq−Rμpt=\mu_q-R\mu_p t=μq−Rμp
SVD 法代码实现
Sophus::SE3d solveICPSVD(const std::vector<Eigen::Vector3d>& pt1, const std::vector<Eigen::Vector3d>& pt2)
{// 1. 计算质心Eigen::Vector3d centroid1(0, 0, 0);Eigen::Vector3d centroid2(0, 0, 0);for (const auto& p : pt1) centroid1 += p;for (const auto& p : pt2) centroid2 += p;centroid1 /= pt1.size();centroid2 /= pt2.size();// 2. 去中心化Eigen::Matrix3d W = Eigen::Matrix3d::Zero();for (size_t i = 0; i < pt1.size(); ++i){Eigen::Vector3d p1 = pt1[i] - centroid1;Eigen::Vector3d p2 = pt2[i] - centroid2;W += p1 * p2.transpose();}// 3. SVD分解Eigen::JacobiSVD<Eigen::Matrix3d> svd(W, Eigen::ComputeFullU | Eigen::ComputeFullV);Eigen::Matrix3d U = svd.matrixU();Eigen::Matrix3d V = svd.matrixV();// 4. 计算旋转矩阵Eigen::Matrix3d R = V * U.transpose();// 5. 检查旋转矩阵的行列式if (R.determinant() < 0){Eigen::Matrix3d V_fixed = V;V_fixed.col(2) *= -1; // 翻转第二列R = V_fixed * U.transpose();}// 6. 计算平移向量Eigen::Vector3d t = centroid2 - R * centroid1;return Sophus::SE3d(R, t);
}
当然也可以通过非线性优化来求解,和上面大差不差,这里就不赘述了
特性 | 对极几何(2D-2D) | PnP(3D-2D) | ICP(3D-3D) |
---|---|---|---|
输入 | 两帧图像的2D特征匹配点 | 3D地图点+当前帧的2D检测 | 两组3D点云 |
输出 | 相机相对运动位姿(R、t)特征点深度 | 相机位姿(R、t) | 点云间的刚体变换(R、t) |
适用场景 | 单目相机SLAM初始化、双目相机 | 已知地图定位 | RGB-D相机SLAM、激光SLAM、点云配准 |
退化情况 | 纯旋转运动,匹配点共面、无纹理区域 | 匹配点共面、观测点噪声过高 | 点云特征不明显、初始位姿偏差过大 |
常用算法 | 八点法、五点法、RANSAC+八点法 | P3P、EPnP、DLT、非线性优化 | SVD、非线性优化 |