文章目录

  • 前言
  • 一、双目图像帧Frame的构造函数
  • 二、计算特征点匹配与成功匹配点对的深度ComputeStereoMatches()
  • 三、具体过程
    • 1. 准备阶段
    • 2. 右目图每行特征点统计
    • 3. 匹配的准备阶段
    • 4. 粗匹配
    • 5. 精确匹配
    • 6. 亚像素插值
    • 7. 删除离散点(outliers)
    • 8. 关于“胜者为王(Winner Take All)”学习策略
  • 总结

前言

与单目的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_leftimg_right对应的ORB特征点集

  1. 行特征点统计:
    统计img_right每行上的ORB特征点集,便于使用立体匹配(行搜索/极限搜索)进行同名点搜索,提升计算速度。
  2. 粗匹配:
    根据步骤1的结果,对img_lefti行的ORB特征点pi,在img_right的第i行上的ORB特征点集中搜索相似的ORB特征点,记对应的匹配点为qi
  3. 精确匹配:
    以点qi为中心,半径为r的范围内,进行块匹配(归一化SAD),进一步优化匹配结果。
  4. 亚像素精度优化:
    步骤3得到的视差为uchar/int类型精度,并不一定是真实视差,通过亚像素插值(抛物线插值)获得float精度的真实视差。
  5. 最优视差值/深度选择:
    通过胜者为王算法(WTA)获取最佳匹配点。
  6. 删除离缺点(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 imagevector<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匹配算法:基本思想:差的绝对值之和。
此算法常用于图像块匹配,将每个像素对应数值之差的绝对值求和,据此评估两个图像块的相似度。
特点:该算法快速、但并不精确,通常用于多级处理的初步筛选。

基本流程:

来源:CSDN@u012507022

        // 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+1cv::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的图像块patchcv::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,并调整自身权值。

来源:bilibili@带你了解人工智能

其实在整个过程中都包含着WTA的思想。


总结

与单目相比,双目的图像帧Frame构造函数包含了计算双目间特征点匹配的部分,即两帧图像稀疏立体匹配

【ORB-SLAM2源码梳理5】关于双目帧Frame的构造函数相关推荐

  1. ORB SLAM2源码解读(三):Frame类

    文章目录 前言 构造函数 双目相机 RGBD相机 单目相机 ExtractORB:提取特征点 ComputeBoW:计算词袋数据 SetPose:设置相机外参 isInFrustum:判断一个MapP ...

  2. ORB-SLAM2源码笔记(4)——帧Frame和关键帧KeyFrame

    Frame类中的相机参数为static类型,表示所有Frame对象共享一份相机参数 特征点提取ExtractORB 在Frame类构造函数中调用成员变量mpORBextractorLeft和mpORB ...

  3. ed2k 网络中搜索资源并选择资源下载的分析及eMule源码梳理

    上一篇博客中,客户端已连接到ed2k网络及客户端与服务器交互的eMule源码梳理,这里将开始搜索资源并下载及客户端与客户端交互的eMule源码梳理 emule 源码下载地址  http://downl ...

  4. Android 源码梳理

    Android 源码梳理 前言 作为霜枫司机一年学习的总结,附上帅照一张. 目录 1. Android系统启动过程分析 2. Linux内核文件系统 3. Android进程间通信源码梳理 4. An ...

  5. 【ORB-SLAM2源码梳理6】Track()函数的第一步:单目初始化MonocularInitialization()

    文章目录 前言 一.Track()函数 二.单目初始化MonocularInitialization() 1. 判断单目初始化器是否创建,若没有就创建. 2. 已创建初始化器,判断特征点数目 3. 在 ...

  6. vite预构建源码梳理

    对于"为什么要进行依赖预构建?"这个问题vite 文档已经解释的很清楚了,那么预构建大概的流程是什么样的呢? 启动预构建 从文档中我们知道在服务启动前会进行预构建,对应源码位置在s ...

  7. Linux动态库加载函数dlopen源码梳理(一)

    下载了libc的源码,现在就开始libc源码的学习,最近了解到了linux动态库的相关知识,那么就从linux动态库加载函数dlopen进行梳理学习吧. 如果还没下载libc源码,可通过 https: ...

  8. Android_HandlerThread 源码梳理

    前言 Android 多线程还有HandleThread,看名字就可以能感觉到得到,会是handler和Thread的综合使用.那到底是怎么样的呢,现在就跟随Android的源码来看看他的工作原理是什 ...

  9. 源码梳理——Jedis中的集合JedisByteHashMap

    一.JedisByteHashMap JedisByteHashMap是Jedis中实现存储键和值均为byte[]字节数组的Map集合类,它利用HashMap作为键-值对实际存储集合,对Map中的方法 ...

最新文章

  1. 好评如潮的C#实战图书
  2. android 类加载器 DexClassLoader的用法,以及引出的插件架构
  3. 专注力 化繁为简的惊人力量
  4. mvcc原理_MVCC原理探究及MySQL源码实现分析
  5. PCL编程笔记——Assertion `px != 0' failed.
  6. GPX格式地图轨迹生成python
  7. 看不见你的笑我怎么睡得着
  8. 我自己常用的几个JS深浅拷贝的方法
  9. 说一下dubbo项目简单的搭建过程_dubbo学习(1)--简单的入门搭建实例
  10. 网站服务器坏了要修多久,大学服务器电脑坏了,一分钟修好收500,朋友:有钱不挣是傻子!...
  11. 如何用excel做正交分析_利用Excel进行正交设计及分析.pdf
  12. 企业级监控系统概述、监控类型及发展趋势
  13. 鼠标cursor属性和如何使用cursor的url属性
  14. 洛谷 题单2分支结构(freshman锻炼牛犇勿喷)
  15. 记录一次华为特战队的机试题
  16. 蓝牙耳机无法打开计算机,蓝牙无线耳机怎么连接电脑 蓝牙耳机连接电脑方法【详解】...
  17. 空间变换网络(Spatial Transform Networks,STN)
  18. 软件工程学习(二)软件架构4+1视图
  19. C/C++ 求凸多边形对角线交点个数(洛谷原创题)
  20. 关于ret指令的理解

热门文章

  1. Marvell深耕智能家居行业 赞同语音控制将成必然发展趋势
  2. ring buffer 环形队列 C++实现
  3. seo从入门到精通_SEO可以干什么,一定要看(SEO职业攻略)
  4. 印象笔记,幕布, Effie 哪个适合公众号主?
  5. Java基础—— Java的语言基础
  6. 华为电子邮件显示未读邮件1_电子邮件简介已经过去
  7. 计算机辅助编程可分为,东大18春学期《计算机辅助数控编程》在线作业123【辅导资料100分】...
  8. 智源社区周刊:Yann LeCun撰文预测自主智能发展;NYU学者认为通用人工智能的讨论没有意义...
  9. ARIMA模型(一)定义与介绍
  10. 只能选一次,30万亿房贷明年将按LPR定价