一、通俗易懂理解单目初始化快速特征匹配方法
1.参考资料:
[1] ORBSLAM2 source code
2.主要函数:
void Tracking::Track()
void Tracking::MonocularInitialization()
初始化特征匹配
int ORBmatcher::SearchForInitialization(Frame &F1, Frame &F2, vector<cv::Point2f> &vbPrevMatched, vector<int> &vnMatches12, int windowSize)
找候选匹配点函数,为什么要用网格呢,如果一个网格内没有特征点,直接跳过去,加速搜索。
vector<size_t> Frame::GetFeaturesInArea(const float &x, const float &y, const float &r, const int minLevel, const int maxLevel) const;
=========================================================================
题目:单目初始化的两帧是怎样的?
参考:
思路:
1.思路:
连续2帧特征点的数量都大于100才进行初始化。
2.图解(请用纸):
3.公式推导(请用纸):
要点程序:
Tracking::MonocularInitialization()
其他:
二、通俗易懂理解单目初始化中特征点匹配筛查原理和BUG解析
1.参考资料:
[1] ORBSLAM2 source code
2.主要函数:
找最优和次优的逻辑:
if(vMatchedDistance[i2]<=dist)
continue;
// 如果当前匹配距离更小,更新最佳次佳距离
if(dist<bestDist)
{
bestDist2=bestDist;
bestDist=dist;
bestIdx2=i2;
}
else if(dist<bestDist2)
{
bestDist2=dist;
}
这个BUG导致删除误差的能力下降了
筛选出在旋转角度差落在在直方图区间内数量最多的前三个bin的索引
void ORBmatcher::ComputeThreeMaxima(vector<int>* histo, const int L, int &ind1, int &ind2, int &ind3)
3.问题
1)旋转直方图中的那个BUG到底影响什么?为什么会影响呢?
阈值变大了,单个bin本来是12度,现在变为30度,比如说大部分都是12度以内,但是有的是20度,应该把他们删除,但是如果单个bin是30度的话,那就无法删除,这样弱化了对角度差过大的匹配的删除。
2)如果之前匹配了,那么把之前的删除,就可以信当前的那个匹配吗?
暴力匹配,那么重复怎么办?把之前的匹配不要了,当前的匹配放进去
3)描述子和关键点是根据什么进行关联的呢?
特征点的在vector中的索引值就是描述子的行号
三、通俗易懂理解单目初始化单应矩阵归一化及DLT计算原理
1.参考资料:
[1] ORBSLAM2 source code
[2] 复习SVD分解
[3]解Ax=b
2.主要函数:
bool Initializer::Initialize(const Frame &CurrentFrame, const vector<int> &vMatches12, cv::Mat &R21, cv::Mat &t21,
vector<cv::Point3f> &vP3D, vector<bool> &vbTriangulated)
void Initializer::FindHomography(vector<bool> &vbMatchesInliers, float &score, cv::Mat &H21)
为什么要对数据进行归一化?
对于不是很好的条件问题,数据归一化很重要,比如基础矩阵F直接线性变换DLT的计算方法。(如下说明)
归一化操作、归一化函数
一阶距、一阶绝对距
void Initializer::Normalize(const vector<cv::KeyPoint> &vKeys, vector<cv::Point2f> &vNormalizedPoints, cv::Mat &T)
这个式子自己展开下,就是和上面计算的过程一模一样。
cv::Mat Initializer::ComputeH21(
const vector<cv::Point2f> &vP1, //归一化后的点, in reference frame
const vector<cv::Point2f> &vP2) //归一化后的点, in current frame
cv::SVDecomp(A, //输入,待进行奇异值分解的矩阵
w, //输出,奇异值矩阵
u, //输出,矩阵U
vt, //输出,矩阵V^T
cv::SVD::MODIFY_A | //输入,MODIFY_A是指允许计算函数可以修改待分解的矩阵,官方文档上说这样可以加快计算速度、节省内存
cv::SVD::FULL_UV); //FULL_UV=把U和VT补充成单位正交方阵
// 返回最小奇异值所对应的右奇异向量
// 注意前面说的是右奇异值矩阵的最后一列,但是在这里因为是vt,转置后了,所以是行;由于A有9列数据,故最后一列的下标为8
return vt.row(8).reshape(0, //转换后的通道数,这里设置为0表示是与前面相同
3); //转换后的行数,对应V的最后一列
就是V^T的最后一行(也就是第9个奇异向量)变成3X3求解的H矩阵。
U是左奇异向量,是一个8X8的矩阵
V^T是右奇异向量,是一个9X9的矩阵
w是奇异值矩阵,奇异值在对角线上,是一个8X9对角矩阵
奇异值是按照从大到小的顺序排的
=========================================================================
题目:为什么要对图像坐标进行归一化,如何进行归一化,代码和公式各是什么。
参考:
思路:
1.思路:
为什么要对图像坐标进行归一化:能够提高运算结果的精度。
2.图解(请用纸):
3.公式推导(请用纸):
:均值
偏离程度的均值
要点程序:
// 归一化
// Step 4 计算归一化矩阵:其实就是前面做的操作用矩阵变换来表示而已
// |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;
// 去归一化
cv::Mat T1, T2;
Normalize(mvKeys1, vPn1, T1);
Normalize(mvKeys2, vPn2, T2);
// !
// 注意这里取的是归一化矩阵T2的转置,因为基础矩阵的定义和单应矩阵不同,两者去归一化的计算也不相同
cv::Mat T2t = T2.t();
F21i = T2t * Fn * T1;
其他:
// RANSAC 随机采样
// Step 2
// 在所有匹配特征点对中随机选择8对匹配特征点为一组,用于估计H矩阵和F矩阵
// 共选择 mMaxIterations (默认200) 组
// mvSets用于保存每次迭代时所使用的向量
mvSets = vector<vector<size_t> >(
mMaxIterations, //最大的RANSAC迭代次数
vector<size_t>(
8,
0)); //这个则是第二维元素的初始值,也就是第一维。这里其实也是一个第一维的构造函数,第一维vector有8项,每项的初始值为0.
//用于进行随机数据样本采样,设置随机数种子
DUtils::Random::SeedRandOnce(0);
//开始每一次的迭代
for (int it = 0; it < mMaxIterations; it++) {
//迭代开始的时候,所有的点都是可用的
vAvailableIndices = vAllIndices;
// Select a minimum set
//选择最小的数据样本集,使用八点法求,所以这里就循环了八次
for (size_t j = 0; j < 8; j++) {
// 随机产生一对点的id,范围从0到N-1
int randi = DUtils::Random::RandomInt(0, vAvailableIndices.size() - 1);
// idx表示哪一个索引对应的特征点对被选中
int idx = vAvailableIndices[randi];
//将本次迭代这个选中的第j个特征点对的索引添加到mvSets中
mvSets[it][j] = idx;
// 由于这对点在本次迭代中已经被使用了,所以我们为了避免再次抽到这个点,就在"点的可选列表"中,
// 将这个点原来所在的位置用vector最后一个元素的信息覆盖,并且删除尾部的元素
// 这样就相当于将这个点的信息从"点的可用列表"中直接删除了
vAvailableIndices[randi] = vAvailableIndices.back();
vAvailableIndices.pop_back();
} //依次提取出8个特征点对
} //迭代mMaxIterations次,选取各自迭代时需要用到的最小数据集
=========================================================================
题目:SVD分解中为什么第9个奇异向量就是最优解
参考:
思路:
1.思路:
2.图解(请用纸):
3.公式推导(请用纸):
要点程序:
其他TODO:
我们知道他的奇异值的维度是9(因为是要求9个未知量),但是为什么是第9个呢,咋们怎么知道他的奇异值个数呢。正交矩阵是方阵,所以是第九个。
为什么ATA的特征向量就是VT的特征向量?这个怎么看的?有什么数学推理吗?还有想问下这个回答好像没有回答为什么V的第9个奇异向量是最优解这个问答?
四、通俗易懂理解单目初始化根据得分找到最佳单应矩阵
1.参考资料:
[1] ORBSLAM2 source code
2.主要函数:
// 单应矩阵原理:X2=H21*X1,其中X1,X2 为归一化后的特征点
// 特征点归一化:vPn1 = T1 * mvKeys1, vPn2 = T2 * mvKeys2 得到:T2 * mvKeys2 = Hn * T1 * mvKeys1
// 进一步得到:mvKeys2 = T2.inv * Hn * T1 * mvKeys1
H21i = T2inv*Hn*T1;
//然后计算逆
H12i = H21i.inv();
float Initializer::CheckHomography(
const cv::Mat &H21, //从参考帧到当前帧的单应矩阵
const cv::Mat &H12, //从当前帧到参考帧的单应矩阵
vector<bool> &vbMatchesInliers, //匹配好的特征点对的Inliers标记
float sigma) //估计误差
void Initializer::FindFundamental(vector<bool> &vbMatchesInliers, float &score, cv::Mat &F21)
cv::Mat Initializer::ComputeF21(
const vector<cv::Point2f> &vP1, //归一化后的点, in reference frame
const vector<cv::Point2f> &vP2) //归一化后的点, in current frame
// 基础矩阵约束:p2^t*F21*p1 = 0,其中p1,p2 为齐次化特征点坐标
// 特征点归一化:vPn1 = T1 * mvKeys1, vPn2 = T2 * mvKeys2
// 根据基础矩阵约束得到:(T2 * mvKeys2)^t* Hn * T1 * mvKeys1 = 0
// 进一步得到:mvKeys2^t * T2^t * Hn * T1 * mvKeys1 = 0
F21i = T2t*Fn*T1;
float Initializer::CheckFundamental(
const cv::Mat &F21, //当前帧和参考帧之间的基础矩阵
vector<bool> &vbMatchesInliers, //匹配的特征点对属于inliers的标记
float sigma) //方差
极线
=========================================================================
题目:如何找到最佳单应矩阵。如何评判是最佳单应矩阵呢?具体怎么做呢?
参考:
Initializer::CheckFundamental
思路:
1.思路:
对每一对点正向反向进行计算点到线的距离,误差在允许范围内就累加得分。误差越大,得分越低。然后选择一个最高得分的单应矩阵。
2.图解(请用纸):
3.公式推导(请用纸):
要点程序:
其他:
五、通俗易懂理解卡方检验介绍及在源码中的应用
1.参考资料:
[1] ORBSLAM2 source code
[2] [ORB-SLAM2]卡方分布(Chi-squared)外点(outlier)剔除(说的太好了)
2.主要函数:
float Initializer::CheckHomography(
const cv::Mat &H21, //从参考帧到当前帧的单应矩阵
const cv::Mat &H12, //从当前帧到参考帧的单应矩阵
vector<bool> &vbMatchesInliers, //匹配好的特征点对的Inliers标记
float sigma) //估计误差
// 基于卡方检验计算出的阈值(假设测量有一个像素的偏差)
// 自由度为2的卡方分布,显著性水平为0.05,对应的临界阈值
const float th = 5.991;
...
if(chiSquare1>th)
bIn = false;
else
// 误差越大,得分越低
score += th - chiSquare1;
float Initializer::CheckFundamental(
const cv::Mat &F21, //当前帧和参考帧之间的基础矩阵
vector<bool> &vbMatchesInliers, //匹配的特征点对属于inliers的标记
float sigma)
// 基于卡方检验计算出的阈值
// 自由度为1的卡方分布,显著性水平为0.05,对应的临界阈值
const float th = 3.841;
// 自由度为2的卡方分布,显著性水平为0.05,对应的临界阈值
const float thScore = 5.991;
...
// Step 2.4 误差大于阈值就说明这个点是Outlier
// ? 为什么判断阈值用的 th(1自由度),计算得分用的thScore(2自由度)
// ? 可能是为了和CheckHomography 得分统一?
if(chiSquare1>th)
bIn = false;
else
// 误差越大,得分越低
score += thScore - chiSquare1;
LocalMapping.cpp
if(!bStereo2)
{
float u2 = fx2*x2*invz2+cx2;
float v2 = fy2*y2*invz2+cy2;
float errX2 = u2 - kp2.pt.x;
float errY2 = v2 - kp2.pt.y;
if((errX2*errX2+errY2*errY2)>5.991*sigmaSquare2)
continue;
}
3.问题
1)利用协方差对误差归一化是什么意思?
不同的金字塔层级,重投影误差都用同一个相同的阈值不合理,因为每层金字塔,他的不确定是不一样的,用协方差对误差进行归一化,也就是去量纲了。接下来就是阈值的设定。
2)什么是卡方分布,为什么要使用卡方分布,如何使用卡方分布,大于卡方分布值是什么意思呢?
在概率论和数理统计中,k个自由度的卡方分布为k个互相独立服从标准正态的随机变量的平方和。卡方分布的自由度即为向量的维度。
3)计算F矩阵的时候,为什么卡方分布的自由度为1
因为是计算的是点到线的距离
4)AX=0的求解是怎么进行的,如何解释SVD分解的结果中包含求解的结果呢?
5)AX=b呢?问题同上。
6)为什么F矩阵的秩为2,那么应该注意什么?
这是基础矩阵的重要性质。因为F求不出尺度,尺度等价性。
7)什么对角阵、正定阵、正交阵是什么意思呢?
正交阵<==>A的转置等于A的逆。
正定阵:x^Tx > 0,x是一个非0向量
对角矩阵:(diagonal matrix)是一个主对角线之外的元素皆为0的矩阵,常写为diag(a1,a2,...,an) 。
=========================================================================
题目:卡方检验在源码中是怎么应用的?卡方分布大概是什么意思。计算F矩阵的时候,为什么卡方分布的自由度为1。计算H矩阵的时候是多少呢?
参考:
思路:
1.思路:
自由度为2,服从高斯分布,出现的概率超过95%时对应的卡方分布的值,如果小于这个值的话,那就是内点。
H的话是重投影,自由度是2。
F的话是计算点到极线的距离,自由度是1,维度降低了。
2.图解(请用纸):
3.公式推导(请用纸):
要点程序:
其他:
=========================================================================
题目:利用协方差对误差归一化是什么意思?
参考:
思路:
1.思路:
不同的金字塔层级,重投影误差都用同一个相同的阈值不合理,因为每层金字塔,他的不确定是不一样的,用协方差对误差进行归一化,也就是去量纲了。接下来就是阈值的设定。使用卡方分布进行设定。
2.图解(请用纸):
3.公式推导(请用纸):
要点程序:
其他:
=========================================================================
题目:AX=0求解。AX=b求解在代码中有什么应用呢?
参考:
思路:
1.思路:
[!问题已提出][w1]11.通俗易懂理解单目初始化单应矩阵归一化及DLT计算原理
2.图解(请用纸):
3.公式推导(请用纸):
要点程序:
其他:
六、通俗易懂理解从单应矩阵恢复位姿及三角化
1.参考资料:
[1] ORBSLAM2 source code
2.主要函数:
bool Initializer::ReconstructH(
vector<bool> &vbMatchesInliers, //匹配点对的内点标记
cv::Mat &H21, //从参考帧到当前帧的单应矩阵
cv::Mat &K, //相机的内参数矩阵
cv::Mat &R21, //计算出来的相机旋转
cv::Mat &t21, //计算出来的相机平移
vector<cv::Point3f>
&vP3D, //世界坐标系下,三角化测量特征点对之后得到的特征点的空间坐标
vector<bool> &vbTriangulated, //特征点对被三角化测量的标记
float minParallax, //在进行三角化测量时,观测正常所允许的最小视差角
int minTriangulated) //最少被三角化的点对数(其实也是点个数)
通过H恢复R,t需要看吗?作者说不用看。不是重点。
CheckRT很关键的一个函数(很值得学习):
int Initializer::CheckRT(const cv::Mat &R, const cv::Mat &t,
const vector<cv::KeyPoint> &vKeys1,
const vector<cv::KeyPoint> &vKeys2,
const vector<Match> &vMatches12,
vector<bool> &vbMatchesInliers, const cv::Mat &K,
vector<cv::Point3f> &vP3D, float th2,
vector<bool> &vbGood, float ¶llax)
// 第二个相机的光心在世界坐标系下的坐标
cv::Mat O2 = -R.t() * t;
//给定投影矩阵P1,P2和图像上的点kp1,kp2,从而恢复3D坐标 (三角化)
void Initializer::Triangulate(
const cv::KeyPoint &kp1, //特征点, in reference frame
const cv::KeyPoint &kp2, //特征点, in current frame
const cv::Mat &P1, //投影矩阵P1
const cv::Mat &P2, //投影矩阵P2
cv::Mat &x3D) //三维点
三角化的分析看 第6讲 视觉前端(切题 Done)这个理解思路会更好。
//这个就是上面注释中的矩阵A
cv::Mat A(4, 4, CV_32F);
//构造参数矩阵A
A.row(0) = kp1.pt.x * P1.row(2) - P1.row(0);
A.row(1) = kp1.pt.y * P1.row(2) - P1.row(1);
A.row(2) = kp2.pt.x * P2.row(2) - P2.row(0);
A.row(3) = kp2.pt.y * P2.row(2) - P2.row(1);
//奇异值分解的结果
cv::Mat u, w, vt;
//对系数矩阵A进行奇异值分解
cv::SVD::compute(A, w, u, vt, cv::SVD::MODIFY_A | cv::SVD::FULL_UV);
//根据前面的结论,奇异值分解右矩阵的最后一行其实就是解,原理类似于前面的求最小二乘解,四个未知数四个方程正好正定
//别忘了我们更习惯用列向量来表示一个点的空间坐标
x3D = vt.row(3).t();
//为了符合其次坐标的形式,使最后一维为1
x3D = x3D.rowRange(0, 3) / x3D.at<float>(3);
取的是前4行的值,用第4个元素进行归一化 或者说用取的是前3行的值,用第3个元素进行归一化,有点奇怪?
VINS也是这样的,没问题。就是把第四维弄成1。
=========================================================================
题目:如何从单应矩阵中选择最佳的位姿。
参考:
思路:
1.思路:
(1)三角化的特征点的x,y,z坐标是否无穷大
(2)夹角小于0.36度认为不好
(3)2帧中重投影误差是否太大。
(4)看下good点的数量。找good点最多的,并且good点要足够突出(second good < 0.75* good)等
2.图解(请用纸):
3.公式推导(请用纸):
要点程序:
int Initializer::CheckRT(const cv::Mat &R, const cv::Mat &t,
const vector<cv::KeyPoint> &vKeys1,
const vector<cv::KeyPoint> &vKeys2,
const vector<Match> &vMatches12,
vector<bool> &vbMatchesInliers, const cv::Mat &K,
vector<cv::Point3f> &vP3D, float th2,
vector<bool> &vbGood, float ¶llax)
其他:
=========================================================================
题目:三角化理论请写出来。
参考:三角化的分析看 第6讲 视觉前端(切题 Done)这个理解思路会更好。
思路:
1.思路:
2.图解(请用纸):
3.公式推导(请用纸):
,为深度值,为相机归一化坐标,是投影矩阵,是点在世界坐标系下的坐标.
取第三行, , 所以
要点程序:
其他:
七.通俗易懂理解ORBSLAM2从基础矩阵得到最佳位姿和三维点
1.参考资料:
[1] ORBSLAM2 source code
2.主要函数:
bool Initializer::ReconstructF(
vector<bool> &vbMatchesInliers, //匹配好的特征点对的Inliers标记
cv::Mat &F21, //从参考帧到当前帧的基础矩阵
cv::Mat &K, //相机的内参数矩阵
cv::Mat &R21, //计算好的相机从参考帧到当前帧的旋转
cv::Mat &t21, //计算好的相机从参考帧到当前帧的平移
vector<cv::Point3f> &vP3D, //三角化测量之后的特征点的空间坐标
vector<bool> &vbTriangulated, //某个特征点是否被三角化了的标记
float minParallax, //认为三角化测量有效的最小视差角
int minTriangulated) //认为使用三角化测量进行数据判断的最小测量点数量
if (mpInitializer->Initialize(
mCurrentFrame, //当前帧
mvIniMatches, //当前帧和参考帧的特征点的匹配关系
Rcw, tcw, //初始化得到的相机的位姿
mvIniP3D, //进行三角化得到的空间点集合
vbTriangulated)) //以及对应于mvIniMatches来讲,其中哪些点被三角化了
=========================================================================
题目:F矩阵分解得到4组解,如何选择最好的呢?H矩阵分解是几组解呢?
参考:
思路:
1.思路:
F矩阵分解得到4组解,如何选择最好的呢?看每一种有效3D点的个数。看哪一种大于阈值0.7 * maxGood并且只有他一个。然后看下视差是否大于最小视差来判断最佳位姿。
H矩阵分解是几组解呢?8组解。看下good点的数量。找good点最多的,并且good点要足够突出(second good < 0.75* good)等
2.图解(请用纸):
3.公式推导(请用纸):
要点程序:
其他:
八.通俗易懂理解初始化3维点来构造初始化地图
1.参考资料:
[1] ORBSLAM2 source code
2.主要函数:
void Tracking::CreateInitialMapMonocular()
KeyFrame::KeyFrame(Frame &F, Map *pMap, KeyFrameDatabase *pKFDB)
void KeyFrame::ComputeBoW()
void MapPoint::ComputeDistinctiveDescriptors()
void MapPoint::UpdateNormalAndDepth()
九.通俗易懂理解初始化关键帧更新共视关系,尺度归一化
1.参考资料:
[1] ORBSLAM2 source code
[2] 通俗易懂理解KeyFrame类问题 要点(切题 Done)
2.主要函数:
void Tracking::CreateInitialMapMonocular()
pKFini->UpdateConnections();
pKFcur->UpdateConnections();
void KeyFrame::UpdateConnections()
void KeyFrame::AddConnection(KeyFrame *pKF, const int &weight)
void KeyFrame::UpdateBestCovisibles()
float medianDepth = pKFini->ComputeSceneMedianDepth(2);
float KeyFrame::ComputeSceneMedianDepth(const int q)
=========================================================================
题目:初始化场景的中值深度(地图点在某帧相机坐标系下深度的中间值)后,如何进行尺度归一化。
参考:
思路:
1.思路:
对当前帧和所有地图点进行尺度归一化。
2.图解(请用纸):
3.公式推导(请用纸):
要点程序:
float medianDepth = pKFini->ComputeSceneMedianDepth(2);
float invMedianDepth = 1.0f / medianDepth;
//两个条件,一个是平均深度要大于0,另外一个是在当前帧中被观测到的地图点的数目应该大于100
if (medianDepth < 0 || pKFcur->TrackedMapPoints(1) < 100) {
cout << "Wrong initialization, reseting..." << endl;
Reset();
return;
}
//将两帧之间的变换归一化到平均深度1的尺度下
// Scale initial baseline
cv::Mat Tc2w = pKFcur->GetPose();
// x/z y/z 将z归一化到1
Tc2w.col(3).rowRange(0, 3) = Tc2w.col(3).rowRange(0, 3) * invMedianDepth;
pKFcur->SetPose(Tc2w);
// Scale points
// 把3D点的尺度也归一化到1
//? 为什么是pKFini? 是不是就算是使用 pKFcur 得到的结果也是相同的?
vector<MapPoint*> vpAllMapPoints = pKFini->GetMapPointMatches();
for (size_t iMP = 0; iMP < vpAllMapPoints.size(); iMP++) {
if (vpAllMapPoints[iMP]) {
MapPoint* pMP = vpAllMapPoints[iMP];
pMP->SetWorldPos(pMP->GetWorldPos() * invMedianDepth);
}
}
网友评论