文章目录
- 06 单目初始化器 Initializer
- 6.1 成员变量/函数
- 6.2 初始化函数 Initialize()
- 6.3 计算基础矩阵 F \boldsymbol{F} F 和单应矩阵 H \boldsymbol{H} H
- 6.3.1 RANSAC 算法
- 6.3.2 八点法计算 F \boldsymbol{F} F 矩阵: ComputeF21()
- 6.3.3 计算基础矩阵 F \boldsymbol{F} F 及其得分:FindFundamental()
- 6.3.4 计算 H \boldsymbol{H} H 矩阵: ComputeH21()
- 6.3.5 计算单应矩阵 H \boldsymbol{H} H 及其得分 FindHomography()
- 6.3.6 卡方检验计算置信度得分: CheckFundamental()、CheckHomography()
- 6.3.7 归一化:Normalize()
- 6.3.8 检验 F \boldsymbol{F} F 和 H \boldsymbol{H} H 分解结果 :CheckRT()
- 6.4 三角化
06 单目初始化器 Initializer
单目 SLAM 初始化相关,双目和 RGBD 不会使用这个类。
单目相机运动得到两帧图像,调用该类,当满足一定条件时认为初始化成功,否则重新初始化。
6.1 成员变量/函数
用 reference frame 来初始化,这个 reference frame 就是 SLAM 正式开始的第一帧;用 current frame,也就是用 SLAM 逻辑上的第二帧来初始化整个 SLAM,得到最开始两帧之间的 R \boldsymbol{R} R、 t \boldsymbol{t} t,以及点云。
成员函数/变量 | 访问控制 | 意义 |
---|---|---|
Initializer(...) | public | 构造函数,用 reference frame 来初始化 |
bool Initialize(...) | public | 用 current frame,得到最开始两帧之间的 R \boldsymbol{R} R、 t \boldsymbol{t} t,以及点云 |
vector<cv::KeyPoint> mvKeys1 | private | 存储 reference frame 中的特征点 |
vector<cv::KeyPoint> mvKeys2 | private | 存储 current frame 中的特征点 |
vector<Match> mvMatches12 | private | 记录 Reference 到 Current 匹配上的特征点对 |
vector<bool> mvbMatched1 | private | 记录 Reference Frame 的每个特征点在 Current Frame 是否有匹配的特征点 |
cv::Mat mK | private | 相机内参 |
float mSigma, mSigma2 | private | 测量误差 |
int mMaxIterations | private | RANSAC 迭代次数 |
vector<vector<size_t> > mvSets | private | 二维容器
N
×
8
N\times8
N×8 每一层保存 RANSAC 计算 H 和 F 矩阵所需的八对点 |
6.2 初始化函数 Initialize()
主函数 Initialize()
根据两帧间的匹配关系恢复帧间运动并计算地图点位姿。
6.3 计算基础矩阵 F \boldsymbol{F} F 和单应矩阵 H \boldsymbol{H} H
6.3.1 RANSAC 算法
RANSAC 算法的核心是减少每次迭代所需的采样点数。从原理上来说,计算 F F F 矩阵最少只需要 7 对匹配点,计算 H H H 矩阵最少只需要 4 对匹配点。ORB-SLAM2 中为了编程方便,每次迭代使用 8 对匹配点计算 F F F 和 H H H。
6.3.2 八点法计算 F \boldsymbol{F} F 矩阵: ComputeF21()
对极约束
p 2 T F p 1 = 0 \boldsymbol{p}_2^T \boldsymbol{F} \boldsymbol{p}_1=0 p2TFp1=0
其中 p 1 、 p 2 \boldsymbol{p}_1、\boldsymbol{p}_2 p1、p2 为对应特征点。
( u 2 , v 2 , 1 ) ( f 11 f 12 f 13 f 21 f 22 f 23 f 31 f 32 f 33 ) ( u 1 v 1 1 ) = 0 \left(u_2, v_2, 1\right)\left(\begin{array}{lll} \mathrm{f}_{11} & \mathrm{f}_{12} & \mathrm{f}_{13} \\ \mathrm{f}_{21} & \mathrm{f}_{22} & \mathrm{f}_{23} \\ \mathrm{f}_{31} & \mathrm{f}_{32} & \mathrm{f}_{33} \end{array}\right)\left(\begin{array}{c} \mathrm{u}_1 \\ \mathrm{v}_1 \\ 1 \end{array}\right)=0 (u2,v2,1) f11f21f31f12f22f32f13f23f33 u1v11 =0
为方便计算先展开前两项,并记为 a 、 b 、 c a、b、c a、b、c
a = f 11 ∗ u 2 + f 21 ∗ v 2 + f 31 b = f 12 ∗ u 2 + f 22 ∗ v 2 + f 32 c = f 13 ∗ u 2 + f 23 ∗ v 2 + f 33 \begin{aligned} & a=f_{11} * u_2+f_{21} * v_2+f_{31} \\ & b=f_{12} * u_2+f_{22} * v_2+f_{32} \\ & c=f_{13} * u_2+f_{23} * v_2+f_{33} \end{aligned} a=f11∗u2+f21∗v2+f31b=f12∗u2+f22∗v2+f32c=f13∗u2+f23∗v2+f33
则上面的矩阵可记为
[
a
b
c
]
[
u
1
v
1
1
]
=
0
\left[\begin{array}{lll} a & b & c \end{array}\right]\left[\begin{array}{c} u_1 \\ v_1 \\ 1 \end{array}\right]=0
[abc]
u1v11
=0
展开
a ∗ u 1 + b ∗ v 1 + c = 0 a * u_1+b * v_1+c=0 a∗u1+b∗v1+c=0
代入 a 、 b 、 c a、b、c a、b、c
u 1 u 2 f 11 + u 1 v 2 f 21 + u 1 f 31 + v 1 u 2 f 12 + v 1 v 2 f 22 + v 1 f 32 + u 2 f 13 + v 2 f 23 + f 33 = 0 \mathrm{u}_1 \mathrm{u}_2 \mathrm{f}_{11}+\mathrm{u}_1 \mathrm{v}_2 \mathrm{f}_{21}+\mathrm{u}_1 \mathrm{f}_{31}+\mathrm{v}_1 \mathrm{u}_2 \mathrm{f}_{12}+\mathrm{v}_1 \mathrm{v}_2 \mathrm{f}_{22}+\mathrm{v}_1 \mathrm{f}_{32}+\mathrm{u}_2 \mathrm{f}_{13}+\mathrm{v}_2 \mathrm{f}_{23}+\mathrm{f}_{33}=0 u1u2f11+u1v2f21+u1f31+v1u2f12+v1v2f22+v1f32+u2f13+v2f23+f33=0
由于尺度不变性(可同时除 f 33 f_{33} f33),只有八个未知数,至少需要八对点
( u 1 1 u 2 1 u 1 1 v 2 1 u 1 1 v 1 1 u 2 1 v 1 1 v 2 1 v 1 1 u 2 1 v 2 1 1 u 1 2 u 2 2 u 1 2 v 2 2 u 1 2 v 1 2 u 2 2 v 1 2 v 2 2 v 1 2 u 2 2 v 2 2 1 ⋮ ⋮ ⋮ ⋮ ⋮ ⋮ ⋮ ⋮ ⋮ u 1 8 u 2 8 u 1 8 v 2 8 u 1 8 v 1 8 u 2 8 v 1 8 v 2 8 v 1 8 u 2 8 v 2 8 1 ) ( f 11 f 12 f 13 f 21 f 22 f 23 f 31 f 32 f 33 ) = 0 \left(\begin{array}{ccccccccc} \mathrm{u}_1^1 \mathrm{u}_2^1 & \mathrm{u}_1^1 \mathrm{v}_2^1 & \mathrm{u}_1^1 & \mathrm{v}_1^1 \mathrm{u}_2^1 & \mathrm{v}_1^1 \mathrm{v}_2^1 & \mathrm{v}_1^1 & \mathrm{u}_2^1 & \mathrm{v}_2^1 & 1 \\ \mathrm{u}_1^2 \mathrm{u}_2^2 & \mathrm{u}_1^2 \mathrm{v}_2^2 & \mathrm{u}_1^2 & \mathrm{v}_1^2 \mathrm{u}_2^2 & \mathrm{v}_1^2 \mathrm{v}_2^2 & \mathrm{v}_1^2 & \mathrm{u}_2^2 & \mathrm{v}_2^2 & 1 \\ \vdots & \vdots & \vdots & \vdots & \vdots & \vdots & \vdots & \vdots & \vdots \\ \mathrm{u}_1^8 \mathrm{u}_2^8 & \mathrm{u}_1^8 \mathrm{v}_2^8 & \mathrm{u}_1^8 & \mathrm{v}_1^8 \mathrm{u}_2^8 & \mathrm{v}_1^8 \mathrm{v}_2^8 & \mathrm{v}_1^8 & \mathrm{u}_2^8 & \mathrm{v}_2^8 & 1 \end{array}\right)\left(\begin{array}{c} \mathrm{f}_{11} \\ \mathrm{f}_{12} \\ \mathrm{f}_{13} \\ \mathrm{f}_{21} \\ \mathrm{f}_{22} \\ \mathrm{f}_{23} \\ \mathrm{f}_{31} \\ \mathrm{f}_{32} \\ \mathrm{f}_{33} \end{array}\right)=0 u11u21u12u22⋮u18u28u11v21u12v22⋮u18v28u11u12⋮u18v11u21v12u22⋮v18u28v11v21v12v22⋮v18v28v11v12⋮v18u21u22⋮u28v21v22⋮v2811⋮1 f11f12f13f21f22f23f31f32f33 =0
使用 SVD 分解求出最小二乘解。
cv::Mat u,w,vt;
cv::SVDecomp(A,w,u,vt,cv::SVD::MODIFY_A | cv::SVD::FULL_UV);
cv::Mat Fpre = vt.row(8).reshape(0, 3); // v的最后一列
cv::SVDecomp(Fpre,w,u,vt,cv::SVD::MODIFY_A | cv::SVD::FULL_UV);
w.at<float>(2)=0; // 秩2约束,将第3个奇异值设为0
return u*cv::Mat::diag(w)*vt;
6.3.3 计算基础矩阵 F \boldsymbol{F} F 及其得分:FindFundamental()
此函数主要流程为:
-
特征点归一化:
Normalize()
-
调用函数 ComputeF21() 计算基础矩阵,采用 RANSAC 算法,找到评分(卡方检验)最高的解:
ComputeH21()
、CheckHomography()
6.3.4 计算 H \boldsymbol{H} H 矩阵: ComputeH21()
若场景中的特征点都落在同一平面(墙、地面),则可以用单应矩阵进行运动估计。
p 2 = H p 1 \boldsymbol{p}_2=\boldsymbol{H} \boldsymbol{p}_1 p2=Hp1
p 1 、 p 2 \boldsymbol{p}_1、\boldsymbol{p}_2 p1、p2 为对应特征点。
一般来说,4 对点即可求解
( u 1 1 v 1 1 1 0 0 0 − u 1 1 u 2 1 − v 1 1 u 2 1 0 0 0 u 1 1 v 1 1 1 − u 1 1 v 2 1 − v 1 1 v 2 1 u 1 2 v 1 2 1 0 0 0 − u 1 2 u 2 2 − v 1 2 u 2 2 0 0 0 u 1 2 v 1 2 1 − u 1 2 v 2 2 − v 1 2 v 2 2 u 1 3 v 1 3 1 0 0 0 − u 1 3 u 2 3 − v 1 3 u 2 3 0 0 0 u 1 3 v 1 3 1 − u 1 3 v 2 3 − v 1 3 v 2 3 u 1 4 v 1 4 1 0 0 0 − u 1 4 u 2 4 − v 1 4 u 2 4 0 0 0 u 1 4 v 1 4 1 − u 1 4 v 2 4 − v 1 4 v 2 4 ) ( h 1 h 2 h 3 h 4 h 5 h 6 h 7 h 8 ) = ( u 2 1 v 2 1 u 2 2 v 2 2 u 2 3 v 2 3 u 2 4 v 2 4 ) . \left(\begin{array}{cccccccc} u_1^1 & v_1^1 & 1 & 0 & 0 & 0 & -u_1^1 u_2^1 & -v_1^1 u_2^1 \\ 0 & 0 & 0 & u_1^1 & v_1^1 & 1 & -u_1^1 v_2^1 & -v_1^1 v_2^1 \\ u_1^2 & v_1^2 & 1 & 0 & 0 & 0 & -u_1^2 u_2^2 & -v_1^2 u_2^2 \\ 0 & 0 & 0 & u_1^2 & v_1^2 & 1 & -u_1^2 v_2^2 & -v_1^2 v_2^2 \\ u_1^3 & v_1^3 & 1 & 0 & 0 & 0 & -u_1^3 u_2^3 & -v_1^3 u_2^3 \\ 0 & 0 & 0 & u_1^3 & v_1^3 & 1 & -u_1^3 v_2^3 & -v_1^3 v_2^3 \\ u_1^4 & v_1^4 & 1 & 0 & 0 & 0 & -u_1^4 u_2^4 & -v_1^4 u_2^4 \\ 0 & 0 & 0 & u_1^4 & v_1^4 & 1 & -u_1^4 v_2^4 & -v_1^4 v_2^4 \end{array}\right)\left(\begin{array}{c} h_1 \\ h_2 \\ h_3 \\ h_4 \\ h_5 \\ h_6 \\ h_7 \\ h_8 \end{array}\right)=\left(\begin{array}{c} u_2^1 \\ v_2^1 \\ u_2^2 \\ v_2^2 \\ u_2^3 \\ v_2^3 \\ u_2^4 \\ v_2^4 \end{array}\right) . u110u120u130u140v110v120v130v140101010100u110u120u130u140v110v120v130v1401010101−u11u21−u11v21−u12u22−u12v22−u13u23−u13v23−u14u24−u14v24−v11u21−v11v21−v12u22−v12v22−v13u23−v13v23−v14u24−v14v24 h1h2h3h4h5h6h7h8 = u21v21u22v22u23v23u24v24 .
这里为了和 ComputeH21()
一致,也采用八点法。通过 SVD 分解求出
H
\boldsymbol{H}
H 矩阵。
6.3.5 计算单应矩阵 H \boldsymbol{H} H 及其得分 FindHomography()
过程类似 FindFundamental()
。
6.3.6 卡方检验计算置信度得分: CheckFundamental()、CheckHomography()
卡方检验通过构造检验统计量 χ \chi χ 来比较期望结果和实际结果之间的差别,从而得出观察频数极值的发生概率。
χ 2 = Σ ( O − E ) 2 E \chi^2=\Sigma \frac{(\mathrm{O}-\mathrm{E})^2}{\mathrm{E}} χ2=ΣE(O−E)2
(1)CheckHomography()
计算重投影误差,也就是计算投影点与对应匹配点的像素距离的平方(计算值与真实值),作为卡方值。
有前面
p 2 = H p 1 \boldsymbol{p}_2=\boldsymbol{H} \boldsymbol{p}_1 p2=Hp1
将帧 1 特征点左乘单应矩阵,得到帧 2 中对应特征点的计算值。反之,则为 p 1 = H − 1 p 2 \boldsymbol{p}_1=\boldsymbol{H}^{-1} \boldsymbol{p}_2 p1=H−1p2
// Reprojection error in second image
// x1in2 = H21*x1
// 将图像1中的特征点单应到图像2中
// |u1| |h11 h12 h13||u2|
// |v1| = |h21 h22 h23||v2|
// |1 | |h31 h32 h33||1 |
const float w1in2inv = 1.0/(h31*u1+h32*v1+h33);
const float u1in2 = (h11*u1+h12*v1+h13)*w1in2inv;
const float v1in2 = (h21*u1+h22*v1+h23)*w1in2inv;
const float squareDist2 = (u2-u1in2)*(u2-u1in2)+(v2-v1in2)*(v2-v1in2); // 计算平方误差
const float chiSquare2 = squareDist2*invSigmaSquare; // 卡方
if(chiSquare2>th)
bIn = false;
else
score += th - chiSquare2; // 得分
if(bIn)
vbMatchesInliers[i]=true;
else
vbMatchesInliers[i]=false; // 大于阈值,则为外点,置为 false
(2)CheckFundamental()
对于函数 CheckFundamental()
:理想情况下,有
p
2
T
F
p
1
=
0
\boldsymbol{p}_2^T \boldsymbol{F} \boldsymbol{p}_1=0
p2TFp1=0,但由于误差存在,左式并不会恒等于零,于是,我们将它的值作为误差。
// Reprojection error in second image
// |f11 f12 f13|
// |a1 b1 c1| = |u2 v2 1| |f21 f22 f23|
// |f31 f32 f33|
const float a1 = f11*u2+f21*v2+f31; // 先展开前两项
const float b1 = f12*u2+f22*v2+f32;
const float c1 = f13*u2+f23*v2+f33;
// |v1|
//|a1 b1 c1||u1|=0? // 由于误差存在,并不为零,将其作为误差值
// |1 |
const float num1 = a1*u1+b1*v1+c1;
const float squareDist2 = num1*num1/(a1*a1+b1*b1); // 平方误差
const float chiSquare2 = squareDist2*invSigmaSquare;
if(chiSquare2>th)
bIn = false;
else
score += thScore - chiSquare2;
if(bIn)
vbMatchesInliers[i]=true;
else
vbMatchesInliers[i]=false;
6.3.7 归一化:Normalize()
用均值和一阶距绝对值将关键点归一化,归一化可以使特征点分布均匀,增强计算稳定性。
均值
u
ˉ
=
(
∑
i
=
1
N
u
i
)
/
N
\bar{u}=(\sum^N_{i=1}u_i)/N
uˉ=(i=1∑Nui)/N
一阶距绝对值
∣ u ˉ ∣ = ∑ i = 0 N ∣ u i − u ˉ ∣ / N |\bar{u}|=\sum_{i=0}^N\left|u_i-\bar{u}\right| / N ∣uˉ∣=i=0∑N∣ui−uˉ∣/N
归一化坐标
u i ′ = u i − u ˉ ∣ u ˉ ∣ u^{\prime}_i=\frac{u_i-\bar{u}}{|\bar{u}|} ui′=∣uˉ∣ui−uˉ
令 s X = 1 ∣ u ˉ x ∣ sX=\frac{1}{|\bar{u}_x|} sX=∣uˉx∣1, s Y = 1 ∣ u ˉ y ∣ sY=\frac{1}{|\bar{u}_y|} sY=∣uˉy∣1,那么变换到归一化坐标的变换矩阵为
[ x ′ y ′ 1 ] = [ s X 0 − mean X ∗ s X 0 s Y − mean Y ∗ s Y 0 0 1 ] [ x y 1 ] \left[\begin{array}{c} x^{\prime} \\ y^{\prime} \\ 1 \end{array}\right]=\left[\begin{array}{ccc} s X & 0 & - \text { mean } X * s X \\ 0 & s Y & - \text { mean } Y * s Y \\ 0 & 0 & 1 \end{array}\right]\left[\begin{array}{l} x \\ y \\ 1 \end{array}\right] x′y′1 = sX000sY0− mean X∗sX− mean Y∗sY1 xy1
代码
/*
* @param vKeys 特征点在图像上的坐标
* @param vNormalizedPoints 特征点归一化后的坐标
* @param T 将特征点归一化的矩阵
*/
void Initializer::Normalize(const vector<cv::KeyPoint> &vKeys, vector<cv::Point2f> &vNormalizedPoints, cv::Mat &T)
{
float meanX = 0;
float meanY = 0;
const int N = vKeys.size();
vNormalizedPoints.resize(N);
for(int i=0; i<N; i++)
{
meanX += vKeys[i].pt.x;
meanY += vKeys[i].pt.y;
}
meanX = meanX/N;
meanY = meanY/N; // 均值
float meanDevX = 0;
float meanDevY = 0;
// 将所有vKeys点减去中心坐标,使x坐标和y坐标均值分别为0
for(int i=0; i<N; i++)
{
vNormalizedPoints[i].x = vKeys[i].pt.x - meanX;
vNormalizedPoints[i].y = vKeys[i].pt.y - meanY;
meanDevX += fabs(vNormalizedPoints[i].x);
meanDevY += fabs(vNormalizedPoints[i].y);
}
meanDevX = meanDevX/N;
meanDevY = meanDevY/N; // 一阶距绝对值
float sX = 1.0/meanDevX;
float sY = 1.0/meanDevY;
// 将x坐标和y坐标分别进行尺度缩放,使得x坐标和y坐标的一阶绝对矩分别为1
for(int i=0; i<N; i++) // 得到归一化坐标
{
vNormalizedPoints[i].x = vNormalizedPoints[i].x * sX;
vNormalizedPoints[i].y = vNormalizedPoints[i].y * sY;
}
// 得到归一化坐标的变换矩阵
// |sX 0 -meanx*sX|
// |0 sY -meany*sY|
// |0 0 1 |
T = cv::Mat::eye(3,3,CV_32F);
T.at<float>(0,0) = sX;
T.at<float>(1,1) = sY;
T.at<float>(0,2) = -meanX*sX;
T.at<float>(1,2) = -meanY*sY;
}
6.3.8 检验 F \boldsymbol{F} F 和 H \boldsymbol{H} H 分解结果 :CheckRT()
使用基础矩阵
F
\boldsymbol{F}
F 分解
R
\boldsymbol{R}
R、
t
\boldsymbol{t}
t,数学上会得到四个可能的解,因此分解后调用函数 Initializer::CheckRT()
检验分解结果,取相机前方成功三角化数目最多的一组解。
显然只有在第一种解中,点 P \boldsymbol{P} P 在两个相机中均有正深度。
同样地,单应矩阵也需要检验,从而得出符合条件的 R , t \boldsymbol{R,t} R,t。
步骤:
① 遍历各匹配点对,调用三角化函数恢复三维点坐标
② 三维点坐标各值都必须为有限实数;
③ 深度值必须为正;
④ 三维点和两帧图像光心夹角需满足一定条件,夹角越大,视差越大,误差越小,三角化结果越准确;
⑤ 三维点的重投影误差需小于设定阈值。
将每种情况下满足要求的三维点的数目记录下来,进一步进行比较,得出最优解:
① 最优解成功三角化的数目明显优于次优解的点数目;
② 最优解的视差大于设定点阈值;
③ 最优解成功三角化的点数目大于设定阈值;
④ 最优解成功三角化点数目占所有特征点数目的 90% 以上。
6.4 三角化
三角测量恢复深度,进而求出三维点坐标。