文章目录
前言
与单目的Frame构造函数相比,双目的Frame构造函数最大的不同点是:
在构造函数中会进行双目间的特征点的匹配,单目则没有。
其他关键步骤没有太大的区别,但还是有必要专门梳理一遍的。
go、
一、双目图像帧Frame的构造函数
Frame::Frame(const cv::Mat &imLeft, // 左目图
const cv::Mat &imRight, // 右目图
const double &timeStamp, // 时间戳
ORBextractor* extractorLeft, // 左目图像特征点提取器的句柄
ORBextractor* extractorRight, // 右目图像特征点提取器的句柄
ORBVocabulary* voc, // 字典词袋voc
cv::Mat &K, // 相机内参矩阵
cv::Mat &distCoef, // 相机去畸变参数
const float &bf, // 相机基线长度与焦距的乘积
const float &thDepth) // 区分远近点的深度阈值
:mpORBvocabulary(voc),mpORBextractorLeft(extractorLeft),mpORBextractorRight(extractorRight), mTimeStamp(timeStamp), mK(K.clone()),mDistCoef(distCoef.clone()), mbf(bf), mThDepth(thDepth),
mpReferenceKF(static_cast<KeyFrame*>(NULL)) // 参数初始化列表
step1:图像帧ID自增;
step2:计算图像金字塔参数(从左目的ORB特征点提取器中获取);
注:非要从以右目为主也行,看个人喜好。
step3:对双目图像分别提取ORB特征点,开启双线程进行计算;
step4:去畸变;
step5:计算双目间特征点匹配,并计算匹配成功的特征点其深度
step6:计算去畸变后图像边界;
step7:将特征点分配到网格。
二、计算特征点匹配与成功匹配点对的深度ComputeStereoMatches()
Frame.cc
void Frame::ComputeStereoMatches();
功能:在右目图中,为左目图的每个特征点分别找到匹配点,完成双目两帧图像稀疏立体匹配。
大致流程:
输入:两帧立体矫正匹配后的图像img_left
和img_right
对应的ORB特征点集
- 行特征点统计:
统计img_right
每行上的ORB特征点集,便于使用立体匹配(行搜索/极限搜索)进行同名点搜索,提升计算速度。 - 粗匹配:
根据步骤1的结果,对img_left
第i
行的ORB特征点pi
,在img_right
的第i
行上的ORB特征点集中搜索相似的ORB特征点,记对应的匹配点为qi
。 - 精确匹配:
以点qi
为中心,半径为r的范围内,进行块匹配(归一化SAD),进一步优化匹配结果。 - 亚像素精度优化:
步骤3得到的视差为uchar/int
类型精度,并不一定是真实视差,通过亚像素插值(抛物线插值)获得float
精度的真实视差。 - 最优视差值/深度选择:
通过胜者为王算法(WTA)获取最佳匹配点。 - 删除离缺点(outliers):
块匹配相似度阈值判断,归一化SAD最小并不代表就一定是正确匹配,比如光照变化、弱纹理等会造成误匹配。
输出:亚像素精度的稀疏特征点视差/深度图mvDepth
及匹配结果mvuRight
三、具体过程
1. 准备阶段
mvuRight = vector<float>(N,-1.0f); // 存储右目图匹配点索引
mvDepth = vector<float>(N,-1.0f); // 存储特征点的深度信息
const int thOrbDist = (ORBmatcher::TH_HIGH+ORBmatcher::TH_LOW)/2; // ORB特征点相似度阈值
const int nRows = mpORBextractorLeft->mvImagePyramid[0].rows; // 金字塔第0层(原图)图像高(height)nRows
// 创建一个nRows行的vector容器,每一行为一个size_t类型的二维向量容器,其第一维代表行坐标,第二维代表列坐标。
// 例如,vRowIndices[0] = [1,2,5,8, 11]
// 行为图像高度height,由于每一行特征点数目不确定,故列是不确定的。
vector<vector<size_t> > vRowIndices(nRows,vector<size_t>());
for(int i=0; i<nRows; i++)
vRowIndices[i].reserve(200); // 重设大小,200可能作者随缘设的
const int Nr = mvKeysRight.size(); // 右目图特征点数量,N表示数量,r表示右图,且不能被修改。
2. 右目图每行特征点统计
for(int iR=0; iR<Nr; iR++)
{
const cv::KeyPoint &kp = mvKeysRight[iR]; // 获取特征点iR的y坐标,即行号
const float &kpY = kp.pt.y;
// 计算特征点ir在行方向上可能的偏移范围,即可能的行号为[kpY + r, kpY -r]
// 2:假设在全尺度(scale=1)的情况下,有2个像素的偏移,随着尺度的变化,r也会变化。
const float r = 2.0f*mvScaleFactors[mvKeysRight[iR].octave]; // .(int)octave代表是金字塔的某层
const int maxr = ceil(kpY+r); // 向上取整
const int minr = floor(kpY-r); // 向下取整
for(int yi=minr;yi<=maxr;yi++) // 将右目特征点保存在可能的行号中
vRowIndices[yi].push_back(iR);
}
3. 匹配的准备阶段
对于立体矫正后的两张图,在列方向(x)存在最大视差maxD
和最小视差minD
,即在左目图中的任意特征点p,在右图上匹配点的范围应该为[p - maxD, p - minD]
,而不需要遍历整一行。
maxD = baseline * length_focal / minZ
minD = baseline * length_focal / maxZ
// Set limits for search
const float minZ = mb; // mb:基线长度,单位为米
const float minD = 0;
const float maxD = mbf/minZ;
// For each left keypoint search a match in the right image
vector<pair<int, int> > vDistIdx; // 保存SAD块匹配相似度和左图特征点的索引
vDistIdx.reserve(N);
// 遍历左图所有特征点,将左图特征点数据暂存,获取vRowIndices容器中对应行的右目图特征点数据
for(int iL=0; iL<N; iL++)
{
// 暂存左图特征点的数据
const cv::KeyPoint &kpL = mvKeys[iL];
const int &levelL = kpL.octave;
const float &vL = kpL.pt.y;
const float &uL = kpL.pt.x;
const vector<size_t> &vCandidates = vRowIndices[vL]; // 获取vRowIndices对应行中存在的右目特征点的列坐标
if(vCandidates.empty())
continue;
const float minU = uL-maxD; // 理论上的最佳搜索范围
const float maxU = uL-minD;
if(maxU<0)
continue;
// 初始化是相似度和最佳匹配距离变量
int bestDist = ORBmatcher::TH_HIGH; // 初始化最佳相似度,用最大相似度
size_t bestIdxR = 0; // 默认描述子距离越小,精度越高
const cv::Mat &dL = mDescriptors.row(iL); // 获得左目图描述子的索引行
4. 粗匹配
汉明距离(Hamming distance):两个二进制串之间的汉明距离,指的是其不同位(bit)数的个数。
二进制描述子用汉明距离表两个特征点之间的相似程度。
来源:《视觉SLAM14讲》
将左图特征点iL
与右图中的可能的匹配点进行逐个比较,得到最相似匹配点的相似度和索引
for(size_t iC=0; iC<vCandidates.size(); iC++)
{
const size_t iR = vCandidates[iC]; // 已经不是前面的iR,此处是右目特征点的列坐标
const cv::KeyPoint &kpR = mvKeysRight[iR]; // 获得未校正的右目特征点
// 左图特征点iL与待匹配点iC的空间尺度差超过2(是否在相似范围内),舍弃该点
if(kpR.octave<levelL-1 || kpR.octave>levelL+1)
continue;
const float &uR = kpR.pt.x; // 获得右目特征点的x坐标
if(uR>=minU && uR<=maxU) // 若在理论搜索范围内
{
const cv::Mat &dR = mDescriptorsRight.row(iR); // 取出右图特征点的描述子列坐标(x)
const int dist = ORBmatcher::DescriptorDistance(dL,dR); // 计算左右描述子的汉明距离,即相似度
if(dist<bestDist) // 更新最小相似度及其对应的列坐标(x)
{
bestDist = dist;
bestIdxR = iR;
}
}
}
5. 精确匹配
SAD匹配算法:基本思想:差的绝对值之和。
此算法常用于图像块匹配,将每个像素对应数值之差的绝对值求和,据此评估两个图像块的相似度。
特点:该算法快速、但并不精确,通常用于多级处理的初步筛选。
基本流程:
// Subpixel match by correlation
// 若刚才匹配过程中的最佳描述子距离小于给定的阈值,则进行精确匹配
if(bestDist<thOrbDist)
{
// 计算右图特征点x坐标和对应的金字塔尺度
const float uR0 = mvKeysRight[bestIdxR].pt.x;
const float scaleFactor = mvInvScaleFactors[kpL.octave];
// 尺度缩放后的左右图特征点坐标
const float scaleduL = round(kpL.pt.x*scaleFactor); // 左图特征点尺度
const float scaledvL = round(kpL.pt.y*scaleFactor);
const float scaleduR0 = round(uR0*scaleFactor); // 右图
// sliding window search。滑动窗口搜索
const int w = 5; // SAD相似度窗口半径
// 提取左图中,以特征点(scaleduL,scaledvL)为中心, 半径为w的图像块patch。
// 最终滑动窗口尺寸为2*w+1
cv::Mat IL = mpORBextractorLeft->mvImagePyramid[kpL.octave].rowRange(scaledvL-w,scaledvL+w+1).colRange(scaleduL-w,scaleduL+w+1);
IL.convertTo(IL,CV_32F);
// 图像块均值归一化,降低亮度变化对相似度计算的影响
IL = IL - IL.at<float>(w,w) *cv::Mat::ones(IL.rows,IL.cols,CV_32F);
int bestDist = INT_MAX; // 初始化最佳相似度
int bestincR = 0; // 初始化滑动窗口搜索优化得到的列坐标偏移量
const int L = 5; // 滑动窗口的滑动范围为(-L, L),x轴方向上
vector<float> vDists; // 初始化存储图像块相似度
vDists.resize(2*L+1);
// 列数方向起点 iniu = r0 - 最大窗口滑动范围 - 图像块尺寸
// 列数方向终点 eniu = r0 + 最大窗口滑动范围 + 图像块尺寸 + 1
const float iniu = scaleduR0+L-w;
const float endu = scaleduR0+L+w+1;
// 判断搜索是否越界
if(iniu<0 || endu >= mpORBextractorRight->mvImagePyramid[kpL.octave].cols)
continue;
// 在搜索范围内从左到右滑动,并计算图像块相似度
for(int incR=-L; incR<=+L; incR++)
{
// 提取右图中,以特征点(scaleduL,scaledvL)为中心, 半径为w的图像块patch
cv::Mat IR = mpORBextractorRight->mvImagePyramid[kpL.octave].rowRange(scaledvL-w,scaledvL+w+1).colRange(scaleduR0+incR-w,scaleduR0+incR+w+1);
IR.convertTo(IR,CV_32F);
// 图像块均值归一化,降低亮度变化对相似度计算的影响
IR = IR - IR.at<float>(w,w) *cv::Mat::ones(IR.rows,IR.cols,CV_32F);
float dist = cv::norm(IL,IR,cv::NORM_L1);
// cv::norm()用于计算一个或者两个数组之间的范数
if(dist<bestDist) // 更新最小的SAD值和偏移量
{
bestDist = dist;
bestincR = incR;
}
vDists[L+incR] = dist; // L+incR 为精细化后的匹配点列坐标(x)
}
if(bestincR==-L || bestincR==L) // 搜索窗口越界判断ß
continue;
6. 亚像素插值
使用最佳匹配点及其左右相邻点构成抛物线,使用3点拟合抛物线的方式,用极小值代替之前计算视差值dist
。
亚像素的理解:
在相机成像的过程中,获得的图像数据是将图像进行了离散化的处理,由于感光元件本身的能力限制,到成像面上每个像素只代表附近的颜色。例如两个感官原件上的像素之间有4.5um的间距,宏观上它们是连在一起的,微观上它们之间还有无数微小的东西存在,这些存在于两个实际物理像素之间的像素,就被称为“亚像素”。
亚像素实际上应该是存在的,只是缺少更小的传感器将其检测出来而已,因此只能在软件上将其近似计算出来。
如下图所示,每四个红色点围成的矩形区域为实际原件上的像素点,黑色点为亚像素点:
根据相邻两像素之间插值情况的不同,可以调整亚像素的精度,例如四分之一,就是将每个像素从横向和纵向上当做四个像素点。也就是上面图里的红色点之间有三个黑色点。这样通过亚像素插值的方法可以实现从小矩形到大矩形的映射,从而提高分辨率。
来源:CSDN@Murphy.AI
公式参考opencv sgbm源码中的亚像素插值公式,或论文<> 公式7。
// Sub-pixel match (Parabola fitting)
const float dist1 = vDists[L+bestincR-1];
const float dist2 = vDists[L+bestincR];
const float dist3 = vDists[L+bestincR+1];
const float deltaR = (dist1-dist3)/(2.0f*(dist1+dist3-2.0f*dist2)); // 公式
if(deltaR<-1 || deltaR>1) // 亚像素精度的修正量应该是在[-1,1]之间,否则就是误匹配
continue;
// Re-scaled coordinate 根据亚像素精度偏移量delta调整最佳匹配索引
float bestuR = mvScaleFactors[kpL.octave]*((float)scaleduR0+(float)bestincR+deltaR);
float disparity = (uL-bestuR);
if(disparity>=minD && disparity<maxD)
{
if(disparity<=0) // 如果存在负视差,则约束为0.01
{
disparity=0.01;
bestuR = uL-0.01;
}
// 根据视差值计算深度信息,保存最相似点的列坐标(x)信息,保存归一化sad最小相似度
// 最优视差值/深度选择
mvDepth[iL]=mbf/disparity;
mvuRight[iL] = bestuR;
vDistIdx.push_back(pair<int,int>(bestDist,iL));
}
7. 删除离散点(outliers)
块匹配相似度阈值判断,归一化SAD值最小,并不代表就一定是匹配的,比如光照变化、弱纹理、无纹理等同样会造成误匹配。
此处误匹配判断条件 norm_sad > 1.5 * 1.4 * median
sort(vDistIdx.begin(),vDistIdx.end()); // 对SAD值进行排序,sort默认升序排列
const float median = vDistIdx[vDistIdx.size()/2].first;
const float thDist = 1.5f*1.4f*median;
for(int i=vDistIdx.size()-1;i>=0;i--)
{
if(vDistIdx[i].first<thDist) // 阈值范围内,则跳出循环
break;
else // 剔除这个离散点,值置为-1
{
mvuRight[vDistIdx[i].second]=-1;
mvDepth[vDistIdx[i].second]=-1;
}
}
8. 关于“胜者为王(Winner Take All)”学习策略
定义:对于输入层接收到的某一个输入量X,竞争层的所有神经元均有输出响应,其中响应值最大的神经元称为“在竞争中获胜的神经元”,其他神经元的输出一律被抑制。
大致步骤:
① 参数(输入、各神经元对应的权向值)归一化;
② 竞争层所有的神经元对应的权向值与输入模式向量进行相似性比较(欧氏距离最小最相似);
③ 获胜神经元兴奋输出为1,并调整自身权值。
其实在整个过程中都包含着WTA的思想。
总结
与单目相比,双目的图像帧Frame构造函数包含了计算双目间特征点匹配的部分,即两帧图像稀疏立体匹配。