HOG+SVM做行人检测,是非常经典的做法,但是真正使用过的人可以发现,就OpenCV提供的检测算算法而言,其实时性是非常差的。事实上,OpenCV中还做了一定的优化,比如利用CPU对多尺度行人检测进行一个并行计算,但是,在我笔记本上运行一次完整的检测过程需要1~2秒不等,这种检测速度,若是应用到无人驾驶技术上,检测到人估计那人已经撞飞了。。。为了提高检测速度,利用GPU并行计算是非常合适是解决办法之一。
研究了CUDA、HOG,SVM有一阵子了,甚至已经着手开始并行计算的任务,但是,却忽略了一个很重要、很关键的问题-------这些大量的检测时间都耗费在哪里了?
感谢老师的指点,让我重新思考这个问题,虽然可能在之前我有简单的考虑过这个问题,但是,工程上是不相信简单的思考的,需要的应该是实实在在的数据!
时间到底消耗在哪了?是HOG计算?是多尺度检测?是SVM计算?····还是?
只有真正搞清楚了这个问题,才能对症下药!
于是,我开始重新温故整个行人检测的过程。很容易,我们发现,去除模板训练的过程(因为这个可以是离线操作,线上不需要做),最主要的操作函数是detectMultiScale函数,这是一个多尺度行人检测的函数!
那什么是多尺度行人检测?这里感谢钱师兄给我的解释,简单明了
模板的大小往往是确定的,用得比较多的模板(好吧,我用得比较多)是(128,64)的模板,那么模板中人的大小实际上就有了一个限制。而我们处理的待检测图片的大小是不一定的,可能是640*480,可能是1280*760,这些完全取决于你使用的硬件分辨的支持程度,大小不一带来的问题就是人的大小和模板可能相去甚远,这些大小上的差别很可能就会让算法漏检!为了解决这个问题,其中一个解决办法,也正是detectMultiScale函数中使用的办法就是,将待检测图片缩小成很多个尺寸的图片,那么这些图片中人的大小总会有一个尺寸是适合(其实也不一定,我觉得受到了图片层数的限制)模板的大小的,这样就能够检测出来了。一般默认的是64层,这就是多尺度检测!
我们可以看看detectMultiScale函数的源码
void HOGDescriptor::detectMultiScale( const Mat& img, vector<Rect>& foundLocations, vector<double>& foundWeights, double hitThreshold, Size winStride, Size padding, double scale0, double finalThreshold, bool useMeanshiftGrouping) const { double scale = 1.; int levels = 0; //******************第一部分****************** vector<double> levelScale; for( levels = 0; levels < nlevels; levels++ ) { levelScale.push_back(scale); if( cvRound(img.cols/scale) < winSize.width || cvRound(img.rows/scale) < winSize.height || scale0 <= 1 ) break; scale *= scale0; } levels = std::max(levels, 1); levelScale.resize(levels); //********************************************* std::vector<Rect> allCandidates; std::vector<double> tempScales; std::vector<double> tempWeights; std::vector<double> foundScales; Mutex mtx; //****************第二部分************************ parallel_for_(Range(0, (int)levelScale.size()), HOGInvoker(this, img, hitThreshold, winStride, padding, &levelScale[0], &allCandidates, &mtx, &tempWeights, &tempScales)); //********************************************** std::copy(tempScales.begin(), tempScales.end(), back_inserter(foundScales)); foundLocations.clear(); std::copy(allCandidates.begin(), allCandidates.end(), back_inserter(foundLocations)); foundWeights.clear(); std::copy(tempWeights.begin(), tempWeights.end(), back_inserter(foundWeights)); //********************第三部分*********************** if ( useMeanshiftGrouping ) { groupRectangles_meanshift(foundLocations, foundWeights, foundScales, finalThreshold, winSize); } else { groupRectangles(foundLocations, foundWeights, (int)finalThreshold, 0.2); } }
经时间测算,第一部分和第三部分时间大概只耗费很短的时间,98%以上的时间都耗费在了第二部分。看到
parallel_for_函数,自然而然地想到了平行计算!是的CPU也是可以有线程的,但是显然相比于GPU,CPU不是很擅长这个活(就计算而言,CPU很多线程都用来执行各种进程了)。
parallel_for_这个函数要搞明白我认为尚需时日,但或许不需要先了解那么清楚这个函数的的细节,先搞清楚一下这个函数是对什么进行并行计算的! 实际上,这是开辟Range(0, (int)levelScale.size())个线程来完成HOGInvoker(···)函数的计算。我们找到
HOGInvoker这个类别,可能乍一看,这个函数不就是个赋值的函数么?
class HOGInvoker : public ParallelLoopBody { public: HOGInvoker( const HOGDescriptor* _hog, const Mat& _img, double _hitThreshold, Size _winStride, Size _padding, const double* _levelScale, std::vector<Rect> * _vec, Mutex* _mtx, std::vector<double>* _weights=0, std::vector<double>* _scales=0 ) { hog = _hog; img = _img; hitThreshold = _hitThreshold; winStride = _winStride; padding = _padding; levelScale = _levelScale; vec = _vec; weights = _weights; scales = _scales; mtx = _mtx; } void operator()( const Range& range ) const { int i, i1 = range.start, i2 = range.end; double minScale = i1 > 0 ? levelScale[i1] : i2 > 1 ? levelScale[i1+1] : std::max(img.cols, img.rows); Size maxSz(cvCeil(img.cols/minScale), cvCeil(img.rows/minScale)); Mat smallerImgBuf(maxSz, img.type()); vector<Point> locations; vector<double> hitsWeights; for( i = i1; i < i2; i++ ) { double scale = levelScale[i]; Size sz(cvRound(img.cols/scale), cvRound(img.rows/scale)); Mat smallerImg(sz, img.type(), smallerImgBuf.data); if( sz == img.size() ) smallerImg = Mat(sz, img.type(), img.data, img.step); else resize(img, smallerImg, sz); hog->detect(smallerImg, locations, hitsWeights, hitThreshold, winStride, padding); Size scaledWinSize = Size(cvRound(hog->winSize.width*scale), cvRound(hog->winSize.height*scale)); mtx->lock(); for( size_t j = 0; j < locations.size(); j++ ) { vec->push_back(Rect(cvRound(locations[j].x*scale), cvRound(locations[j].y*scale), scaledWinSize.width, scaledWinSize.height)); if (scales) { scales->push_back(scale); } } mtx->unlock(); if (weights && (!hitsWeights.empty())) { mtx->lock(); for (size_t j = 0; j < locations.size(); j++) { weights->push_back(hitsWeights[j]); } mtx->unlock(); } } } const HOGDescriptor* hog; Mat img; double hitThreshold; Size winStride; Size padding; const double* levelScale; std::vector<Rect>* vec; std::vector<double>* weights; std::vector<double>* scales; Mutex* mtx; };
其实学过C++的人就明白,operator函数就是一个运算符重载函数,它对“()”进行重载。而其中最主要的,也是最耗时的一步,我想大家一眼就看到了,经过时间测算,也的确是它--------detect!
我们再看到detect的源码
void HOGDescriptor::detect(const Mat& img, vector<Point>& hits, vector<double>& weights, double hitThreshold, Size winStride, Size padding, const vector<Point>& locations) const { hits.clear(); if( svmDetector.empty() ) return; if( winStride == Size() ) winStride = cellSize; Size cacheStride(gcd(winStride.width, blockStride.width), gcd(winStride.height, blockStride.height)); size_t nwindows = locations.size(); padding.width = (int)alignSize(std::max(padding.width, 0), cacheStride.width); padding.height = (int)alignSize(std::max(padding.height, 0), cacheStride.height); Size paddedImgSize(img.cols + padding.width*2, img.rows + padding.height*2); //***************************第一部分************************** HOGCache cache(this, img, padding, padding, nwindows == 0, cacheStride); //************************************************************ if( !nwindows ) nwindows = cache.windowsInImage(paddedImgSize, winStride).area(); const HOGCache::BlockData* blockData = &cache.blockData[0]; int nblocks = cache.nblocks.area(); int blockHistogramSize = cache.blockHistogramSize; size_t dsize = getDescriptorSize(); double rho = svmDetector.size() > dsize ? svmDetector[dsize] : 0; vector<float> blockHist(blockHistogramSize); //*******************第二部分*********************** for( size_t i = 0; i < nwindows; i++ ) { //********************一次for循环********************** Point pt0; if( !locations.empty() ) { pt0 = locations[i]; if( pt0.x < -padding.width || pt0.x > img.cols + padding.width - winSize.width || pt0.y < -padding.height || pt0.y > img.rows + padding.height - winSize.height ) continue; } else { pt0 = cache.getWindow(paddedImgSize, winStride, (int)i).tl() - Point(padding); CV_Assert(pt0.x % cacheStride.width == 0 && pt0.y % cacheStride.height == 0); } double s = rho; const float* svmVec = &svmDetector[0]; #ifdef HAVE_IPP int j; #else int j, k; #endif for( j = 0; j < nblocks; j++, svmVec += blockHistogramSize ) { const HOGCache::BlockData& bj = blockData[j]; Point pt = pt0 + bj.imgOffset; const float* vec = cache.getBlock(pt, &blockHist[0]); #ifdef HAVE_IPP Ipp32f partSum; ippsDotProd_32f(vec,svmVec,blockHistogramSize,&partSum); s += (double)partSum; #else for( k = 0; k <= blockHistogramSize - 4; k += 4 ) s += vec[k]*svmVec[k] + vec[k+1]*svmVec[k+1] + vec[k+2]*svmVec[k+2] + vec[k+3]*svmVec[k+3]; for( ; k < blockHistogramSize; k++ ) s += vec[k]*svmVec[k]; #endif } if( s >= hitThreshold ) { hits.push_back(pt0); weights.push_back(s); } //************************************************* } //************************************************ }
对上述几个关键的部分进行时间测试:
第一部分: 20%的时间
第二部分: 80%的时间
一次for循环: 0.1ms左右
所以,时间消耗的地方我们终于找到了!20%的时间我们耗费在了HOG算子计算的前部分,80%的时间是HOG算子的计算以及处理。
针对上述结果,我及时调整了并行计算的改写方案:
第一阶段: 改写detect函数中的第二部分!也就是nwindows的那个for循环,每一个for循环开一个线程,一共开nwindows个线程。理由很显而易见,for循环整个时间占用很多,但是每次循环的时间却很短,这说明运行次数非常之多!这非常适合并行处理!
第二阶段: 改写detect函数的第一部分,正如(七)中的computeGradient函数改写一样,使其也并行计算!
其他不变,仍利用CPU的并行计算对64个尺度的图片进行并行处理!
下图为第一方案简单示意图:
方案准备:
改写CUDA最主要的是数据交换,所以这里要搞清楚要输入什么数据,要输出什么数据
输入数据: 改写方案:
Mat& img ptrstep<uchar>, int img_h, int img_w
double hitThreshold
size winstride int winstride_w, int winstride_h
size padding int padding_w, int padding_h
BlockData* blockdata ptrstepsz<uchar3>
输出数据:
vector<point> point ptrstepsz<uchar3>
vector<double> weight ptrstep<uchar>
注:1.Mat img我计划输入的灰度图像,所以只能用ptrstep<uchar>类型承接,但是这个类型没有长度成员,所以只能将宽度和长度信息输入进去!
2.BlockData结构体包括了一个int和一个point类型,所以用三通道分别承接x,y和int;
相关推荐
《闵大荒之旅(三)---- 抄抄改改opencv之GPUvsCPU》这篇博文主要探讨了在计算机视觉领域中,OpenCV库的GPU加速与CPU执行的对比。OpenCV是一个广泛使用的开源计算机视觉和机器学习库,它支持多种平台,并且提供了...
CPU历史之旅--回望过去的脚步.mht CPU历史之旅--回望过去的脚步.mht CPU历史之旅--回望过去的脚步.mht CPU历史之旅--回望过去的脚步.mht CPU历史之旅--回望过去的脚步.mht
我的Android进阶之旅------>Android疯狂连连看游戏的实现 可以参考博客:http://blog.csdn.net/ouyang_peng/article/details/14115627
C#发现之旅 --- C#开发Windows Service程序
Flutter学习之旅----环境搭建与Hello World,对应博客地址为http://blog.csdn.net/zhangxiangliang2/article/details/75566412
FPGA之旅---例化FPGA开发 FPGA(Field-Programmable Gate Array),即现场可编程门阵列,是一种基于集成电路的可编程逻辑设备。FPGA之旅---例化FPGA开发是FPGA开发的入门教程,本资源将指导读者从基本概念到实际...
精益之旅--制造业效率研究.pdf
本篇《C#发现之旅 --- C#开发Windows Service程序》将带领读者深入了解如何使用C#来开发Windows Service程序。 ### Windows Service基础 首先,我们需要了解Windows Service的概念和特性。Windows Service是一种...
在“C#发现之旅 --- C#开发Windows Service程序”中,我们将深入探讨如何利用C#来创建和管理这样的服务。这个实例是根据提供的PDF教程编写的,确保了代码的有效性和实用性。现在,让我们详细了解一下Windows服务以及...
游戏之旅--我的编程感悟.part1.rar(共3部分)
游戏之旅--我的编程感悟.part2.rar(共3部分)
游戏之旅--我的编程感悟.part3.rar(共3部分)
登月之旅10-19.ppt
_fire__我的NVIDIA开发之旅--实例分割模型YOLACT的TensorRT_API模型搭建_yolact_tensorrt_api
资源名称:游戏之旅-我的编程感悟资源截图: 资源太大,传百度网盘了,链接在附件中,有需要的同学自取。
Cocos2d-x-3.x游戏开发之旅-钟迪龙著 全新pdf版和附书代码(代码为工程文件,可复制) 附带目录标签
5--[少儿编程奇幻之旅-整装待发].zip源码scratch2.0 3.0编程项目源文件源码案例素材源代码5--[少儿编程奇幻之旅-整装待发].zip源码scratch2.0 3.0编程项目源文件源码案例素材源代码5--[少儿编程奇幻之旅-整装待发]....