本文转载请注明出处 —— polobymulberry-博客园

0x00 - 前言


【AR实验室】mulberryAR : ORBSLAM2+VVSION末尾提及了iPhone5s真机测试结果,其中ExtractORB函数,也就是提取图像的ORB特征这一块耗时很可观。所以这也是目前需要优化的重中之重。此处,我使用【AR实验室】mulberryAR :添加连续图像作为输入中添加的连续图像作为输入。这样的好处有两个,一个就是保证输入一致,那么单线程提取特征和并行提取特征两种方法优化对比就比较有可信度,另一个是可以使用iOS模拟器来跑程序了,因为不需要打开摄像头的,测试起来相当方便,更有多种机型任你选。

目前对特征提取这部分优化就只有两个想法:

  • 将特征提取的过程并行化。
  • 减少提取的特征点数量。

第二种方法很容易,只需要在配置文件中更改提取特征点的数目即可,此处不赘述。本文主要集中第一种方法,初步尝试将特征提取并行化。

0x01 - ORB-SLAM2特征提取过程耗时分析


ORB-SLAM2中特征提取函数叫做ExtractORB,是Frame类的一个成员函数。用来提取当前Frame的ORB特征点。

// flag是给双目相机用的,单目相机默认flag为0
// 提取im上的ORB特征点
void Frame::ExtractORB(int flag, const cv::Mat &im)
{
if(flag==)
// mpORBextractorLeft是ORBextractor对象,因为ORBextractor重载了()
// 所以才会有下面这种用法
(*mpORBextractorLeft)(im,cv::Mat(),mvKeys,mDescriptors);
else
(*mpORBextractorRight)(im,cv::Mat(),mvKeysRight,mDescriptorsRight);
}

从上面代码可以看出ORB-SLAM2特征提取主要调用的是ORBextractor重载的()函数。我们给该函数重要的几个部分打点,测试每个部分的耗时。

重要提示-测试代码执行时间:

测试某段代码执行的时间有很多种方法,比如:

clock_t begin = clock();
//...
clock_t end = clock();
cout << "execute time = " << (end - begin) / CLOCKS_PER_SEC << "s" << endl;

不过我之前在多线程求和【原】C++11并行计算 — 数组求和中使用上述方法计时,发现这个方法对于多线程计算存在bug。因为目前我是基于iOS平台,所以此处我使用了iOS中计算时间的方式。另外又因为在C++文件中不能直接使用Foundation组件,所以采用对应的CoreFoundation。

CFAbsoluteTime beginTime = CFAbsoluteTimeGetCurrent();
CFDateRef beginDate = CFDateCreate(kCFAllocatorDefault, beginTime);
// ...
CFAbsoluteTime endime = CFAbsoluteTimeGetCurrent();
CFDateRef endDate = CFDateCreate(kCFAllocatorDefault, endTime);
CFTimeInterval timeInterval = CFDateGetTimeIntervalSinceDate(endDate, beginDate);
cout << "execure time = " << (double)(timeInterval) * 1000.0 << "ms" << endl;

将上述计时代码插入到operator()函数中,目前函数整体看起来如下,主要是对三个部分进行计时,分别为ComputePyramid、ComputeKeyPointsOctTree和ComputeDescriptors:

void ORBextractor::operator()( InputArray _image, InputArray _mask, vector<KeyPoint>& _keypoints,
OutputArray _descriptors)
{
if(_image.empty())
return; Mat image = _image.getMat();
assert(image.type() == CV_8UC1 ); // 1.计算图像金字塔的时间
CFAbsoluteTime beginComputePyramidTime = CFAbsoluteTimeGetCurrent();
CFDateRef computePyramidBeginDate = CFDateCreate(kCFAllocatorDefault, beginComputePyramidTime);
// Pre-compute the scale pyramid
ComputePyramid(image);
CFAbsoluteTime endComputePyramidTime = CFAbsoluteTimeGetCurrent();
CFDateRef computePyramidEndDate = CFDateCreate(kCFAllocatorDefault, endComputePyramidTime);
CFTimeInterval computePyramidTimeInterval = CFDateGetTimeIntervalSinceDate(computePyramidEndDate, computePyramidBeginDate);
cout << "ComputePyramid time = " << (double)(computePyramidTimeInterval) * 1000.0 << endl; vector < vector<KeyPoint> > allKeypoints; // 2.计算关键点KeyPoint的时间
CFAbsoluteTime beginComputeKeyPointsTime = CFAbsoluteTimeGetCurrent();
CFDateRef computeKeyPointsBeginDate = CFDateCreate(kCFAllocatorDefault, beginComputeKeyPointsTime); ComputeKeyPointsOctTree(allKeypoints);
//ComputeKeyPointsOld(allKeypoints);
CFAbsoluteTime endComputeKeyPointsTime = CFAbsoluteTimeGetCurrent();
CFDateRef computeKeyPointsEndDate = CFDateCreate(kCFAllocatorDefault, endComputeKeyPointsTime);
CFTimeInterval computeKeyPointsTimeInterval = CFDateGetTimeIntervalSinceDate(computeKeyPointsEndDate, computeKeyPointsBeginDate);
cout << "ComputeKeyPointsOctTree time = " << (double)(computeKeyPointsTimeInterval) * 1000.0 << endl; Mat descriptors; int nkeypoints = ;
for (int level = ; level < nlevels; ++level)
nkeypoints += (int)allKeypoints[level].size();
if( nkeypoints == )
_descriptors.release();
else
{
_descriptors.create(nkeypoints, , CV_8U);
descriptors = _descriptors.getMat();
} _keypoints.clear();
_keypoints.reserve(nkeypoints); int offset = ; // 3.计算描述子的时间
CFAbsoluteTime beginComputeDescriptorsTime = CFAbsoluteTimeGetCurrent();
CFDateRef computeDescriptorsBeginDate = CFDateCreate(kCFAllocatorDefault, beginComputeDescriptorsTime);
for (int level = ; level < nlevels; ++level)
{
vector<KeyPoint>& keypoints = allKeypoints[level];
int nkeypointsLevel = (int)keypoints.size(); if(nkeypointsLevel==)
continue; // preprocess the resized image
Mat workingMat = mvImagePyramid[level].clone();
GaussianBlur(workingMat, workingMat, cv::Size(, ), , , BORDER_REFLECT_101); // Compute the descriptors
Mat desc = descriptors.rowRange(offset, offset + nkeypointsLevel);
computeDescriptors(workingMat, keypoints, desc, pattern); offset += nkeypointsLevel; // Scale keypoint coordinates
if (level != )
{
float scale = mvScaleFactor[level]; //getScale(level, firstLevel, scaleFactor);
for (vector<KeyPoint>::iterator keypoint = keypoints.begin(),
keypointEnd = keypoints.end(); keypoint != keypointEnd; ++keypoint)
keypoint->pt *= scale;
}
// And add the keypoints to the output
_keypoints.insert(_keypoints.end(), keypoints.begin(), keypoints.end());
}
CFAbsoluteTime endComputeDescriptorsTime = CFAbsoluteTimeGetCurrent();
CFDateRef computeDescriptorsEndDate = CFDateCreate(kCFAllocatorDefault, endComputeDescriptorsTime);
CFTimeInterval computeDescriptorsTimeInterval = CFDateGetTimeIntervalSinceDate(computeDescriptorsEndDate, computeDescriptorsBeginDate);
cout << "ComputeDescriptors time = " << (double)(computeDescriptorsTimeInterval) * 1000.0 << endl;
}

此时,使用iPhone7模拟器运行mulberryAR,并且运行我之前录制的一段连续图像帧,得到结果如下(此处我只截取前三帧的结果):

可以看出优化的重点在于ComputeKeyPointsOctTree、ComputeDescriptiors。

0x02 - ORB-SLAM2特征提取优化思路


ComputePyramid、ComputeKeyPointsOctTree和ComputeDescriptors函数中都会根据图像金字塔的不同层级做同样的操作,所以此处可以将图像金字塔不同层级的操作并行化。按照这个思路,对三个部分的代码进行了修改。

1.ComputePyramid函数并行化

该函数暂时无法进行并行化处理,因为里面在计算图像金字塔中第n层图像的时候,依赖第n-1层的图像,另外此函数在整个特征提取的部分占比不是很大,相对来说并行化意义不是很大。

2.ComputeKeyPointsOctTree函数并行化

该函数的并行化过程很容易,只需要将其中的for(int i = 0; i < nlevels; ++i)里面的函数做成单独函数,并添加到各自的thread中即可。不废话,直接上代码:

void ORBextractor::ComputeKeyPointsOctTree(vector<vector<KeyPoint> >& allKeypoints)
{
allKeypoints.resize(nlevels); vector<thread> computeKeyPointsThreads; for (int i = ; i < nlevels; ++i) {
computeKeyPointsThreads.push_back(thread(&ORBextractor::ComputeKeyPointsOctTreeEveryLevel, this, i, std::ref(allKeypoints)));
} for (int i = ; i < nlevels; ++i) {
computeKeyPointsThreads[i].join();
} // compute orientations
vector<thread> computeOriThreads;
for (int level = ; level < nlevels; ++level) {
computeOriThreads.push_back(thread(computeOrientation, mvImagePyramid[level], std::ref(allKeypoints[level]), umax));
} for (int level = ; level < nlevels; ++level) {
computeOriThreads[level].join();
}
}

其中ComputeKeyPointsOctTreeEveryLevel函数如下:

void ORBextractor::ComputeKeyPointsOctTreeEveryLevel(int level, vector<vector<KeyPoint> >& allKeypoints)
{
const float W = ; const int minBorderX = EDGE_THRESHOLD-;
const int minBorderY = minBorderX;
const int maxBorderX = mvImagePyramid[level].cols-EDGE_THRESHOLD+;
const int maxBorderY = mvImagePyramid[level].rows-EDGE_THRESHOLD+; vector<cv::KeyPoint> vToDistributeKeys;
vToDistributeKeys.reserve(nfeatures*); const float width = (maxBorderX-minBorderX);
const float height = (maxBorderY-minBorderY); const int nCols = width/W;
const int nRows = height/W;
const int wCell = ceil(width/nCols);
const int hCell = ceil(height/nRows); for(int i=; i<nRows; i++)
{
const float iniY =minBorderY+i*hCell;
float maxY = iniY+hCell+; if(iniY>=maxBorderY-)
continue;
if(maxY>maxBorderY)
maxY = maxBorderY; for(int j=; j<nCols; j++)
{
const float iniX =minBorderX+j*wCell;
float maxX = iniX+wCell+;
if(iniX>=maxBorderX-)
continue;
if(maxX>maxBorderX)
maxX = maxBorderX; vector<cv::KeyPoint> vKeysCell;
FAST(mvImagePyramid[level].rowRange(iniY,maxY).colRange(iniX,maxX),
vKeysCell,iniThFAST,true); if(vKeysCell.empty())
{
FAST(mvImagePyramid[level].rowRange(iniY,maxY).colRange(iniX,maxX),
vKeysCell,minThFAST,true);
} if(!vKeysCell.empty())
{
for(vector<cv::KeyPoint>::iterator vit=vKeysCell.begin(); vit!=vKeysCell.end();vit++)
{
(*vit).pt.x+=j*wCell;
(*vit).pt.y+=i*hCell;
vToDistributeKeys.push_back(*vit);
}
} }
} vector<KeyPoint> & keypoints = allKeypoints[level];
keypoints.reserve(nfeatures); keypoints = DistributeOctTree(vToDistributeKeys, minBorderX, maxBorderX,
minBorderY, maxBorderY,mnFeaturesPerLevel[level], level); const int scaledPatchSize = PATCH_SIZE*mvScaleFactor[level]; // Add border to coordinates and scale information
const int nkps = keypoints.size();
for(int i=; i<nkps ; i++)
{
keypoints[i].pt.x+=minBorderX;
keypoints[i].pt.y+=minBorderY;
keypoints[i].octave=level;
keypoints[i].size = scaledPatchSize;
}
}

在iPhone7模拟器上测试,得到如下结果(取前5帧图像测试):

可以看到通过并行处理,ComputeKeyPointsOctTree获得了2~3倍的提速。

3.ComputeDescriptors部分并行化

之所以这一部分叫做“部分”,而非“函数”是因为这部分涉及的函数相对于ComputeKeyPointsOctTree比较复杂,涉及的变量比较多。只有理清之间的关系才能安全地并行化。

此处也不赘述,直接贴出修改后的并行化代码:

vector<thread> computeDescThreads;
vector<vector<KeyPoint> > keypointsEveryLevel;
keypointsEveryLevel.resize(nlevels);
// 图像金字塔每层的offset与前面每层的offset有关,所以不能直接放在ComputeDescriptorsEveryLevel计算
for (int level = ; level < nlevels; ++level) {
computeDescThreads.push_back(thread(&ORBextractor::ComputeDescriptorsEveryLevel, this, level, std::ref(allKeypoints), descriptors, offset, std::ref(keypointsEveryLevel[level])));
int keypointsNum = (int)allKeypoints[level].size();
offset += keypointsNum;
} for (int level = ; level < nlevels; ++level) {
computeDescThreads[level].join();
}
// _keypoints要按照顺序进行插入,所以不能直接放在ComputeDescriptorsEveryLevel计算
for (int level = ; level < nlevels; ++level) {
_keypoints.insert(_keypoints.end(), keypointsEveryLevel[level].begin(), keypointsEveryLevel[level].end());
} // 其中ComputeDescriptorsEveryLevel函数如下
void ORBextractor::ComputeDescriptorsEveryLevel(int level, std::vector<std::vector<KeyPoint> > &allKeypoints, const Mat& descriptors, int offset, vector<KeyPoint>& _keypoints)
{
vector<KeyPoint>& keypoints = allKeypoints[level];
int nkeypointsLevel = (int)keypoints.size(); if(nkeypointsLevel==)
return; // preprocess the resized image
Mat workingMat = mvImagePyramid[level].clone();
GaussianBlur(workingMat, workingMat, cv::Size(, ), , , BORDER_REFLECT_101); // Compute the descriptors
Mat desc = descriptors.rowRange(offset, offset + nkeypointsLevel);
computeDescriptors(workingMat, keypoints, desc, pattern); // offset += nkeypointsLevel; // Scale keypoint coordinates
if (level != )
{
float scale = mvScaleFactor[level]; //getScale(level, firstLevel, scaleFactor);
for (vector<KeyPoint>::iterator keypoint = keypoints.begin(),
keypointEnd = keypoints.end(); keypoint != keypointEnd; ++keypoint)
keypoint->pt *= scale;
}
// And add the keypoints to the output
// _keypoints.insert(_keypoints.end(), keypoints.begin(), keypoints.end());
_keypoints = keypoints;
}

在iPhone7模拟器上测试,得到如下结果(取前5帧图像测试):

可以看到通过并行处理,ComputeDescriptors获得了2~3倍的提速。

0x03 - 并行化结果分析


0x02小节已经对比了每步优化的结果。此处从整体的角度对结果进行简单的分析。使用iPhone7模拟器跑了前5帧的对比结果:

从结果中可以看出,ORB特征提取速度有了2~3倍的提升,在TrackMonocular部分占比也下降了不少,暂时ORB特征提取不用作为性能优化的重点。后面将会从其他方面对ORB-SLAM2进行优化。

【AR实验室】mulberryAR:并行提取ORB特征的更多相关文章

  1. 【AR实验室】mulberryAR : ORBSLAM2+VVSION

    本文转载请注明出处 —— polobymulberry-博客园 0x00 - 前言 mulberryAR是我业余时间弄的一个AR引擎,目前主要支持单目视觉SLAM+3D渲染,并且支持iOS端,但是该引 ...

  2. 【AR实验室】ARToolKit之制作自己的Marker/NFT

    0x00 - 前言 看过example后,就会想自己动动手,这里改改那里修修.我们先试着添加自己喜欢的marker/nft进行识别. 比如我做了一个法拉利的marker: 还有网上找了一个法拉利log ...

  3. OpenCV特征点检测------ORB特征

    OpenCV特征点检测------ORB特征 ORB是是ORiented Brief的简称.ORB的描述在下面文章中: Ethan Rublee and Vincent Rabaud and Kurt ...

  4. ORB特征点检测

    Oriented FAST and Rotated BRIEF www.cnblogs.com/ronny   这篇文章我们将介绍一种新的具有局部不变性的特征 -- ORB特征,从它的名字中可以看出它 ...

  5. python实现gabor滤波器提取纹理特征 提取指静脉纹理特征 指静脉切割代码

    参考博客:https://blog.csdn.net/xue_wenyuan/article/details/51533953 https://blog.csdn.net/jinshengtao/ar ...

  6. SLAM: Orb_SLAM中的ORB特征

    原文链接:什么是ORB 关于Orb特征的获取:参考 最新版的OpenCV中新增加的ORB特征的使用 ORB是是ORiented Brief 的简称,对Brief的特定性质进行了改进. ORB的描述在下 ...

  7. OpenCV特征点检测——ORB特征

            ORB算法 目录(?)[+] 什么是ORB 如何解决旋转不变性 如何解决对噪声敏感的问题 关于尺度不变性 关于计算速度 关于性能 Related posts 什么是ORB 七 4 Ye ...

  8. OpenCV开发笔记(六十五):红胖子8分钟带你深入了解ORB特征点(图文并茂+浅显易懂+程序源码)

    若该文为原创文章,未经允许不得转载原博主博客地址:https://blog.csdn.net/qq21497936原博主博客导航:https://blog.csdn.net/qq21497936/ar ...

  9. (三)ORB特征匹配

    ORBSLAM2匹配方法流程 在基于特征点的视觉SLAM系统中,特征匹配是数据关联最重要的方法.特征匹配为后端优化提供初值信息,也为前端提供较好的里程计信息,可见,若特征匹配出现问题,则整个视觉SLA ...

随机推荐

  1. Markdown_04_折叠语法

    目录 一.折叠语法 参考资料 一.折叠语法 主要使用的是 html5的 details标签 (1)示例如下: <details> <summary>折叠文本</summa ...

  2. APUE学习笔记——10.18 system函数 与waitpid

    system函数 system函数用方便在一个进程中执行命令行(一行shell命令). 用法如下: #include <stdio.h> #include <stdlib.h> ...

  3. MetaPost使用

    简介 MetaPost是一种制图语言,由John D. Hobby开发. 如果你要学习它,可以去下面的网址看看. 官网:http://tug.org/metapost 权威手册:http://tug. ...

  4. c# 加密工具类

    using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Sec ...

  5. 【跟着stackoverflow学Pandas】How to iterate over rows in a DataFrame in Pandas-DataFrame按行迭代

    最近做一个系列博客,跟着stackoverflow学Pandas. 以 pandas作为关键词,在stackoverflow中进行搜索,随后安照 votes 数目进行排序: https://stack ...

  6. 我也说说Emacs吧(6) - Lisp速成

    前面我们学习了基本操作,也走马观花地看了不少emacs lisp的代码.这一章我们做一个lisp的速成讲座. Lisp的含义是表处理语言.它的代码组成结构都是用括号组成的表来表示的.Lisp中的功能, ...

  7. linux另一种安装方式

    linux中其实没有“安装”的概念:安装就是设下路径,拷贝文件,复制文件,运行下脚本这些(windows也应该如此) 法一.把bin运行路径设成环境变量 法二.ln一下,例如: 解压下载的文件: ta ...

  8. 【剑指offer】数组中的逆序对。C++实现

    原创文章,转载请注明出处! 博客文章索引地址 博客文章中代码的github地址 # 题目 # 思路 基于归并排序的思想统计逆序对:先把数组分割成子数组,再子数组合并的过程中统计逆序对的数目.统计逆序对 ...

  9. ubuntu下erlang man的安装

    下载 http://www.erlang.org/download/otp_doc_man_17.1.tar.gz 找到erlang 安装目录 解压 otp_doc_man_17.1.tar.gz s ...

  10. c语言 变量的存储类别以及对应的内存分配?

    <h4><strong>1.变量的存储类别</strong></h4>从变量值存在的角度来分,可以分为静态存储方式和动态存储方式.所谓静态存储方式指在程 ...