首页 > 代码库 > hog源码分析
hog源码分析
http://www.cnblogs.com/tornadomeet/archive/2012/08/15/2640754.html
在博客目标检测学习_1(用opencv自带hog实现行人检测) 中已经使用了opencv自带的函数detectMultiScale()实现了对行人的检测,当然了,该算法采用的是hog算法,那么hog算法是怎样实现的呢?这一节就来简单分析一下opencv中自带 hog源码。
网上也有不少网友对opencv中的hog源码进行了分析,很不错,看了很有收获。比如:
http://blog.csdn.net/raocong2010/article/details/6239431
该博客对该hog算法中用到的block,cell等概念有一定的图标解释;
http://blog.csdn.net/pp5576155/article/details/7029699
该博客是转载的,里面有opencv源码的一些注释,很有帮助。
http://gz-ricky.blogbus.com/logs/85326280.html
本博客对hog描述算子长度的计算做了一定介绍。
http://hi.baidu.com/susongzhi/item/3a3c758d7ff5cbdc5e0ec172
该博客对hog中快速算法的三线插值将得很详细。
http://blog.youtueye.com/work/opencv-hog-peopledetector-trainning.html
这篇博客对hog怎样训练和检测做了一定的讲解。
二、关于源码的一些简单说明
本文不是讲解hog理论的,所以需要对hog算法有一定了解,这些可以去参考hog提出者的博士论文,写得很详细。
按照正常流程,hog行人检测分为训练过程和检测过程,训练过程主要是训练得到svm的系数。在opencv源码中直接采用训练好了的svm系数,所以训练过程源码中没有涉及到多少。
首先还是对hog源码中一些固定参数来个简单说明:
检测窗口大小为128*64;
Block大小为16*16;
Cell大小为8*8;
Block在检测窗口中上下移动尺寸为8*8;
1个cell的梯度直方图化成9个bin;
滑动窗口在检测图片中滑动的尺寸为8*8;
代码中的一个hog描述子是针对一个检测窗口而言的,所以一个检测窗口共有105= ((128-16)/8+1)*((64-16)/8+1)个block;一个block中有4个cell,而一个cell的hog描述子向量的长度为 9;所以检测窗口的hog向量长度=3780=105*4*9维。
三、hog训练部分流程的简单理解
虽然hog源码中很少涉及到训练部分的代码,不过了解下训练过程的流程会对整个检测过程有个整体认识。
训练过程中正样本大小统一为128*64,即检测窗口的大小;该样本图片可以包含1个或多个行人。对该图片提前的hog特征长度刚好为3780维,每一个 特征对应一个正样本标签进行训练。在实际的训练过程中,我们并不是去google上收集或者拍摄刚好128*64大小且有行人的图片,而是收集包含行人的 任意图片(当然了,尺寸最好比128*64大),然后手工对这些正样本进行标注,即对有行人的地方画个矩形,其实也就是存了2个顶点的坐标而已,并把这个 矩形的信息存储起来;最好自己写一个程序,每读入一张图片,就把矩形区域的内容截取出来并缩放到统一尺寸128*64,这样,对处理过后的该图片进行 hog特征提取就可以当做正样本了。
负样本不需要统一尺寸,只需比128*64大,且图片中不能包含任何行人。实际过程中,由于是负样本,里面没有目标信息,所以不需要人工进行标注。程序中可以对该图片随机进行截取128*64大小的图片,并提取出其hog特征作为负样本。
四、hog行人检测过程
检测过程中采用的是滑动窗口法,对应本代码中,滑动窗口法的流程如下:
由上图可以看出,检测时,会对输入图片进行尺度缩放(一般是缩小),在每一层的图像上采用固定大小的滑动窗口(128*64)滑动,没个滑动窗口都提取出hog特征,送入到svm分类器中,看该窗口中是否有目标。有则存下目标区域来,无则继续滑动。
检测过程中用到的函数为detectMultiScale(),其参数分配图如下:
五、计算检测窗口中图像的梯度
计算梯度前如果需要gamma校正的话就先进行gamma校正,所谓的gamma校正就是把原来的每个通道像素值范围从0~255变换到 0~15.97(255开根号)。据作者说这样校正过后的图像计算的效果会更好,在计算梯度前不需要进行高斯滤波操作。
梯度的计算是分别计算水平梯度图和垂直梯度图,然后求幅值和相位。水平\垂直梯度卷积算子为:
在阅读该源码的时候,要特别注意梯度幅值和角度的存储方式。因为是对一个滑动窗口里的图像进行的,所以梯度幅值和角度按照道理来说应该都是128*64=8192维的向量。但实际过程中这2者都是用的128*64*2=16384维的向量。为什么呢?
因为这里的梯度和角度都是用到了二线插值的。每一个点的梯度角度可能是0~180度之间的任意值,而程序中将其离散化为9个bin,即每个bin占20 度。所以滑动窗口中每个像素点的梯度角度如果要离散化到这9个bin中,则一般它都会有2个相邻的bin(如果恰好位于某个bin的中心,则可认为对该 bin的权重为1即可)。从源码中可以看到梯度的幅值是用来计算梯度直方图时权重投票的,所以每个像素点的梯度幅值就分解到了其角度相邻的2个bin了, 越近的那个bin得到的权重越大。因此幅度图像用了2个通道,每个通道都是原像素点幅度的一个分量。同理,不难理解,像素点的梯度角度也用了2个通道,每 个通道中存储的是它相邻2个bin的bin序号。序号小的放在第一通道。
二线插值的示意图如下:
其中,假设那3条半径为离散化后bin的中心,红色虚线为像素点O(像素点在圆心处)的梯度方向,梯度幅值为A,该梯度方向与最近的相邻bin为 bin0,这两者之间的夹角为a.这该像素点O处存储的梯度幅值第1通道为A*(1-a),第2通道为A*a;该像素点O处存储的角度第1通道为 0(bin的序号为0),第2通道为1(bin的序号为1)。
另外在计算图像的梯度图和相位图时,如果该图像时3通道的,则3通道分别取梯度值,并且取梯度最大的那个通道的值为该点的梯度幅值。
六、HOG缓存结构体
HOG缓存思想是该程序作者加快hog算法速度采用的一种内存优化技术。由于我们对每幅输入图片要进行4层扫描,分别为图像金字塔层,每层中滑动窗口,每 个滑动窗口中滑动的block,每个block中的cell,其实还有每个cell中的像素点;有这么多层,每一层又是一个二维的,所以速度非常慢。作者 的采用的思想是HOG缓存,即把计算得到的每个滑动窗口的数据(其实最终是每个block的hog描述子向量)都存在内存查找表中,由于滑动窗口在滑动 时,很多个block都会重叠,因此重叠处计算过的block信息就可以直接从查找表中读取,这样就节省了很多时间。
在这个HOG存储结构体中,会计算滑动窗口内的hog描述子,而这又涉及到滑动窗口,block,cell直接的关系,其之间的关系可以参考下面示意图:
外面最大的为待检测的图片,对待检测的图片需要用滑动窗口进行滑动来判断窗口中是否有目标,每个滑动窗口中又有很多个重叠移动的block,每个 block中还有不重叠的cell。其实该程序的作者又将每个block中的像素点对cell的贡献不同,有将每个cell分成了4个区域,即图中蓝色虚 线最小的框。
那么block中不同的像素点对它的cell(默认参数为1个block有4个cell)的影响是怎样的呢?请看下面示意图。
如果所示,黑色框代表1个block,红实线隔开的为4个cell,每个cell用绿色虚线隔开的我们称之为4个区域,所以该block中共有16个区域,分别为A、B、C、…、O、P。
程序中将这16个区域分为4组:
第1组:A、D、M、P;该组内的像素点计算梯度方向直方图时只对其所在的cell有贡献。
第2组:B、C、N、O;该组内的像素点计算梯度直方图时对其所在的左右cell有贡献。
第3组:E、I、H、L;该组内的像素点计算梯度直方图时对其所在的上下cell有贡献。
第4组:F、G、J、K;该组内的像素点对其上下左右的cell计算梯度直方图时都有贡献。
那到底是怎么对cell贡献的呢?举个例子来说,E区域内的像素点对cell0和cell2有贡献。本来1个block对滑动窗口贡献的向量维数为36 维,即每个cell贡献9维,其顺序分别为cell0,cell1,cell2,cell3.而E区域内的像素由于同时对cell0和cell2有贡献, 所以在计算E区域内的像素梯度投票时,不仅要投向它本来的cell0,还要投向下面的cell2,即投向cell0和cell2有一个权重,该权重与该像 素点所在位置与cell0,cell2中心位置的距离有关。具体的关系可以去查看源码。
该结构体变量内存分配图如下,可以增强读代码的直观性:
在读该部分源码时,需要特别注意以下几个地方:
1) 结构体BlockData中有2个变量。1个BlockData结构体是对应的一个block数据。histOfs和imgOffset.其中 histOfs表示为该block对整个滑动窗口内hog描述算子的贡献那部分向量的起始位置;imgOffset为该block在滑动窗口图片中的坐标 (当然是指左上角坐标)。
2) 结构体PixData中有5个变量,1个PixData结构体是对应的block中1个像素点的数据。其中gradOfs表示该点的梯度幅度在滑动窗口图 片梯度幅度图中的位置坐标;qangleOfs表示该点的梯度角度在滑动窗口图片梯度角度图中的位置坐标;histOfs[]表示该像素点对1个或2个或 4个cell贡献的hog描述子向量的起始位置坐标(比较抽象,需要看源码才懂)。histWeight[]表示该像素点对1个或2个或4个cell贡献 的权重。gradWeight表示该点本身由于处在block中位置的不同因而对梯度直方图贡献也不同,其权值按照二维高斯分布(以block中心为二维 高斯的中心)来决定。
3) 程序中的count1,cout2,cout4分别表示该block中对1个cell、2个cell、4个cell有贡献的像素点的个数。
七、其他一些函数
该程序中还有一些其它的函数。
getblock()表示的是给定block在滑动窗口的位置以及图片的hog缓存指针,来获得本次block中计算hog特征所需要的信息。
normalizeBlockHistogram()指对block获取到的hog部分描述子进行归一化,其实该归一化有2层,具体看代码。
windowsInImage()实现的功能是给定测试图片和滑动窗口移动的大小,来获得该层中水平和垂直方向上需要滑动多少个滑动窗口。
getWindow()值获得一个滑动窗口矩形。
compute()是实际上计算hog描述子的函数,在测试和训练阶段都能用到。
detect()是检测目标是用到的函数,在detectMultiScale()函数内部被调用。
八、关于HOG的初始化
Hog初始化可以采用直接赋初值;也直接从文件节点中读取(有相应的格式,好像采用的是xml文件格式);当然我们可以读取初始值,也可以在程序中设置 hog算子的初始值并写入文件,这些工作可以采用源码中的read,write,load,save等函数来完成。
九、hog源码的注释
在读源码时,由于里面用到了intel的ipp库,优化了算法的速度,所以在程序中遇到#ifdef HAVE_IPP后面的代码时,可以直接跳过不读,直接读#else后面的代码,这并不影响对原hog算法的理解。
hog.cpp:
/*M/////////////////////////////////////////////////////////////////////////////////////////// IMPORTANT: READ BEFORE DOWNLOADING, COPYING, INSTALLING OR USING.//// By downloading, copying, installing or using the software you agree to this license.// If you do not agree to this license, do not download, install,// copy or use the software.////// License Agreement// For Open Source Computer Vision Library//// Copyright (C) 2000-2008, Intel Corporation, all rights reserved.// Copyright (C) 2009, Willow Garage Inc., all rights reserved.// Third party copyrights are property of their respective owners.//// Redistribution and use in source and binary forms, with or without modification,// are permitted provided that the following conditions are met://// * Redistribution‘s of source code must retain the above copyright notice,// this list of conditions and the following disclaimer.//// * Redistribution‘s in binary form must reproduce the above copyright notice,// this list of conditions and the following disclaimer in the documentation// and/or other materials provided with the distribution.//// * The name of the copyright holders may not be used to endorse or promote products// derived from this software without specific prior written permission.//// This software is provided by the copyright holders and contributors "as is" and// any express or implied warranties, including, but not limited to, the implied// warranties of merchantability and fitness for a particular purpose are disclaimed.// In no event shall the Intel Corporation or contributors be liable for any direct,// indirect, incidental, special, exemplary, or consequential damages// (including, but not limited to, procurement of substitute goods or services;// loss of use, data, or profits; or business interruption) however caused// and on any theory of liability, whether in contract, strict liability,// or tort (including negligence or otherwise) arising in any way out of// the use of this software, even if advised of the possibility of such damage.////M*/#include "precomp.hpp"#include <iterator>#ifdef HAVE_IPP#include "ipp.h"#endif/**************************************************************************************** The code below is implementation of HOG (Histogram-of-Oriented Gradients) descriptor and object detection, introduced by Navneet Dalal and Bill Triggs. The computed feature vectors are compatible with the INRIA Object Detection and Localization Toolkit (http://pascal.inrialpes.fr/soft/olt/)\****************************************************************************************/namespace cv{size_t HOGDescriptor::getDescriptorSize() const{ //下面2个语句是保证block中有整数个cell;保证block在窗口中能移动整数次 CV_Assert(blockSize.width % cellSize.width == 0 && blockSize.height % cellSize.height == 0); CV_Assert((winSize.width - blockSize.width) % blockStride.width == 0 && (winSize.height - blockSize.height) % blockStride.height == 0 ); //返回的nbins是每个窗口中检测到的hog向量的维数 return (size_t)nbins* (blockSize.width/cellSize.width)* (blockSize.height/cellSize.height)* ((winSize.width - blockSize.width)/blockStride.width + 1)* ((winSize.height - blockSize.height)/blockStride.height + 1);}//winSigma到底是什么作用呢?double HOGDescriptor::getWinSigma() const{ return winSigma >= 0 ? winSigma : (blockSize.width + blockSize.height)/8.;}//svmDetector是HOGDescriptor内的一个成员变量,数据类型为向量vector。//用来保存hog特征用于svm分类时的系数的.//该函数返回为真的实际含义是什么呢?保证与hog特征长度相同,或者相差1,但为什么//相差1也可以呢?bool HOGDescriptor::checkDetectorSize() const{ size_t detectorSize = svmDetector.size(), descriptorSize = getDescriptorSize(); return detectorSize == 0 || detectorSize == descriptorSize || detectorSize == descriptorSize + 1;}void HOGDescriptor::setSVMDetector(InputArray _svmDetector){ //这里的convertTo函数只是将图像Mat属性更改,比如说通道数,矩阵深度等。 //这里是将输入的svm系数矩阵全部转换成浮点型。 _svmDetector.getMat().convertTo(svmDetector, CV_32F); CV_Assert( checkDetectorSize() );}#define CV_TYPE_NAME_HOG_DESCRIPTOR "opencv-object-detector-hog"//FileNode是opencv的core中的一个文件存储节点类,这个节点用来存储读取到的每一个文件元素。//一般是读取XML和YAML格式的文件//又因为该函数是把文件节点中的内容读取到其类的成员变量中,所以函数后面不能有关键字constbool HOGDescriptor::read(FileNode& obj){ //isMap()是用来判断这个节点是不是一个映射类型,如果是映射类型,则每个节点都与 //一个名字对应起来。因此这里的if语句的作用就是需读取的文件node是一个映射类型 if( !obj.isMap() ) return false; //中括号中的"winSize"是指返回名为winSize的一个节点,因为已经知道这些节点是mapping类型 //也就是说都有一个对应的名字。 FileNodeIterator it = obj["winSize"].begin(); //操作符>>为从节点中读入数据,这里是将it指向的节点数据依次读入winSize.width,winSize.height //下面的几条语句功能类似 it >> winSize.width >> winSize.height; it = obj["blockSize"].begin(); it >> blockSize.width >> blockSize.height; it = obj["blockStride"].begin(); it >> blockStride.width >> blockStride.height; it = obj["cellSize"].begin(); it >> cellSize.width >> cellSize.height; obj["nbins"] >> nbins; obj["derivAperture"] >> derivAperture; obj["winSigma"] >> winSigma; obj["histogramNormType"] >> histogramNormType; obj["L2HysThreshold"] >> L2HysThreshold; obj["gammaCorrection"] >> gammaCorrection; obj["nlevels"] >> nlevels; //isSeq()是判断该节点内容是不是一个序列 FileNode vecNode = obj["SVMDetector"]; if( vecNode.isSeq() ) { vecNode >> svmDetector; CV_Assert(checkDetectorSize()); } //上面的都读取完了后就返回读取成功标志 return true;} void HOGDescriptor::write(FileStorage& fs, const String& objName) const{ //将objName名字输入到文件fs中 if( !objName.empty() ) fs << objName; fs << "{" CV_TYPE_NAME_HOG_DESCRIPTOR //下面几句依次将hog描述子内的变量输入到文件fs中,且每次输入前都输入 //一个名字与其对应,因此这些节点是mapping类型。 << "winSize" << winSize << "blockSize" << blockSize << "blockStride" << blockStride << "cellSize" << cellSize << "nbins" << nbins << "derivAperture" << derivAperture << "winSigma" << getWinSigma() << "histogramNormType" << histogramNormType << "L2HysThreshold" << L2HysThreshold << "gammaCorrection" << gammaCorrection << "nlevels" << nlevels; if( !svmDetector.empty() ) //svmDetector则是直接输入序列,也有对应的名字。 fs << "SVMDetector" << "[:" << svmDetector << "]"; fs << "}";}//从给定的文件中读取参数bool HOGDescriptor::load(const String& filename, const String& objname){ FileStorage fs(filename, FileStorage::READ); //一个文件节点有很多叶子,所以一个文件节点包含了很多内容,这里当然是包含的 //HOGDescriptor需要的各种参数了。 FileNode obj = !objname.empty() ? fs[objname] : fs.getFirstTopLevelNode(); return read(obj);}//将类中的参数以文件节点的形式写入文件中。void HOGDescriptor::save(const String& filename, const String& objName) const{ FileStorage fs(filename, FileStorage::WRITE); write(fs, !objName.empty() ? objName : FileStorage::getDefaultObjectName(filename));}//复制HOG描述子到c中void HOGDescriptor::copyTo(HOGDescriptor& c) const{ c.winSize = winSize; c.blockSize = blockSize; c.blockStride = blockStride; c.cellSize = cellSize; c.nbins = nbins; c.derivAperture = derivAperture; c.winSigma = winSigma; c.histogramNormType = histogramNormType; c.L2HysThreshold = L2HysThreshold; c.gammaCorrection = gammaCorrection; //vector类型也可以用等号赋值 c.svmDetector = svmDetector; c.nlevels = nlevels; } //计算图像img的梯度幅度图像grad和梯度方向图像qangle.//paddingTL为需要在原图像img左上角扩增的尺寸,同理paddingBR//为需要在img图像右下角扩增的尺寸。void HOGDescriptor::computeGradient(const Mat& img, Mat& grad, Mat& qangle, Size paddingTL, Size paddingBR) const{ //该函数只能计算8位整型深度的单通道或者3通道图像. CV_Assert( img.type() == CV_8U || img.type() == CV_8UC3 ); //将图像按照输入参数进行扩充,这里不是为了计算边缘梯度而做的扩充,因为 //为了边缘梯度而扩充是在后面的代码完成的,所以这里为什么扩充暂时还不明白。 Size gradsize(img.cols + paddingTL.width + paddingBR.width, img.rows + paddingTL.height + paddingBR.height); grad.create(gradsize, CV_32FC2); // <magnitude*(1-alpha), magnitude*alpha> qangle.create(gradsize, CV_8UC2); // [0..nbins-1] - quantized gradient orientation Size wholeSize; Point roiofs; //locateROI在此处是如果img图像是从其它父图像中某一部分得来的,那么其父图像 //的大小尺寸就为wholeSize了,img图像左上角相对于父图像的位置点就为roiofs了。 //对于正样本,其父图像就是img了,所以这里的wholeSize就和img.size()是一样的, //对应负样本,这2者不同;因为里面的关系比较不好懂,这里权且将wholesSize理解为 //img的size,所以roiofs就应当理解为Point(0, 0)了。 img.locateROI(wholeSize, roiofs); int i, x, y; int cn = img.channels(); //_lut为行向量,用来作为浮点像素值的存储查找表 Mat_<float> _lut(1, 256); const float* lut = &_lut(0,0); //gamma校正指的是将0~256的像素值全部开根号,即范围缩小了,且变换范围都不成线性了, if( gammaCorrection ) for( i = 0; i < 256; i++ ) _lut(0,i) = std::sqrt((float)i); else for( i = 0; i < 256; i++ ) _lut(0,i) = (float)i; //创建长度为gradsize.width+gradsize.height+4的整型buffer AutoBuffer<int> mapbuf(gradsize.width + gradsize.height + 4); int* xmap = (int*)mapbuf + 1; int* ymap = xmap + gradsize.width + 2; //言外之意思borderType就等于4了,因为opencv的源码中是如下定义的。 //#define IPL_BORDER_REFLECT_101 4 //enum{...,BORDER_REFLECT_101=IPL_BORDER_REFLECT_101,...} //borderType为边界扩充后所填充像素点的方式。 /* Various border types, image boundaries are denoted with ‘|‘ * BORDER_REPLICATE: aaaaaa|abcdefgh|hhhhhhh * BORDER_REFLECT: fedcba|abcdefgh|hgfedcb * BORDER_REFLECT_101: gfedcb|abcdefgh|gfedcba * BORDER_WRAP: cdefgh|abcdefgh|abcdefg * BORDER_CONSTANT: iiiiii|abcdefgh|iiiiiii with some specified ‘i‘ */ const int borderType = (int)BORDER_REFLECT_101; for( x = -1; x < gradsize.width + 1; x++ ) /*int borderInterpolate(int p, int len, int borderType) 其中参数p表示的是扩充后图像的一个坐标,相对于对应的坐标轴而言; len参数表示对应源图像的一个坐标轴的长度;borderType为扩充类型, 在上面已经有过介绍. 所以这个函数的作用是从扩充后的像素点坐标推断出源图像中对应该点 的坐标值。 */ //这里的xmap和ymap实际含义是什么呢?其实xmap向量里面存的就是 //扩充后图像第一行像素点对应与原图像img中的像素横坐标,可以看 //出,xmap向量中有些元素的值是相同的,因为扩充图像肯定会对应 //到原图像img中的某一位置,而img本身尺寸内的像素也会对应该位置。 //同理,ymap向量里面存的是扩充后图像第一列像素点对应于原图想img //中的像素纵坐标。 xmap[x] = borderInterpolate(x - paddingTL.width + roiofs.x, wholeSize.width, borderType) - roiofs.x; for( y = -1; y < gradsize.height + 1; y++ ) ymap[y] = borderInterpolate(y - paddingTL.height + roiofs.y, wholeSize.height, borderType) - roiofs.y; // x- & y- derivatives for the whole row int width = gradsize.width; AutoBuffer<float> _dbuf(width*4); float* dbuf = _dbuf; //DX为水平梯度图,DY为垂直梯度图,Mag为梯度幅度图,Angle为梯度角度图 //该构造方法的第4个参数表示矩阵Mat的数据在内存中存放的位置。由此可以 //看出,这4幅图像在内存中是连续存储的。 Mat Dx(1, width, CV_32F, dbuf); Mat Dy(1, width, CV_32F, dbuf + width); Mat Mag(1, width, CV_32F, dbuf + width*2); Mat Angle(1, width, CV_32F, dbuf + width*3); int _nbins = nbins; //angleScale==9/pi; float angleScale = (float)(_nbins/CV_PI);#ifdef HAVE_IPP Mat lutimg(img.rows,img.cols,CV_MAKETYPE(CV_32F,cn)); Mat hidxs(1, width, CV_32F); Ipp32f* pHidxs = (Ipp32f*)hidxs.data; Ipp32f* pAngles = (Ipp32f*)Angle.data; IppiSize roiSize; roiSize.width = img.cols; roiSize.height = img.rows; for( y = 0; y < roiSize.height; y++ ) { const uchar* imgPtr = img.data + y*img.step; float* imglutPtr = (float*)(lutimg.data + y*lutimg.step); for( x = 0; x < roiSize.width*cn; x++ ) { imglutPtr[x] = lut[imgPtr[x]]; } }#endif for( y = 0; y < gradsize.height; y++ ) {#ifdef HAVE_IPP const float* imgPtr = (float*)(lutimg.data + lutimg.step*ymap[y]); const float* prevPtr = (float*)(lutimg.data + lutimg.step*ymap[y-1]); const float* nextPtr = (float*)(lutimg.data + lutimg.step*ymap[y+1]);#else //imgPtr在这里指的是img图像的第y行首地址;prePtr指的是img第y-1行首地址; //nextPtr指的是img第y+1行首地址; const uchar* imgPtr = img.data + img.step*ymap[y]; const uchar* prevPtr = img.data + img.step*ymap[y-1]; const uchar* nextPtr = img.data + img.step*ymap[y+1];#endif float* gradPtr = (float*)grad.ptr(y); uchar* qanglePtr = (uchar*)qangle.ptr(y); //输入图像img为单通道图像时的计算 if( cn == 1 ) { for( x = 0; x < width; x++ ) { int x1 = xmap[x];#ifdef HAVE_IPP dbuf[x] = (float)(imgPtr[xmap[x+1]] - imgPtr[xmap[x-1]]); dbuf[width + x] = (float)(nextPtr[x1] - prevPtr[x1]);#else //下面2句把Dx,Dy就计算出来了,因为其对应的内存都在dbuf中 dbuf[x] = (float)(lut[imgPtr[xmap[x+1]]] - lut[imgPtr[xmap[x-1]]]); dbuf[width + x] = (float)(lut[nextPtr[x1]] - lut[prevPtr[x1]]);#endif } } //当cn==3时,也就是输入图像为3通道图像时的处理。 else { for( x = 0; x < width; x++ ) { //x1表示第y行第x1列的地址 int x1 = xmap[x]*3; float dx0, dy0, dx, dy, mag0, mag;#ifdef HAVE_IPP const float* p2 = imgPtr + xmap[x+1]*3; const float* p0 = imgPtr + xmap[x-1]*3; dx0 = p2[2] - p0[2]; dy0 = nextPtr[x1+2] - prevPtr[x1+2]; mag0 = dx0*dx0 + dy0*dy0; dx = p2[1] - p0[1]; dy = nextPtr[x1+1] - prevPtr[x1+1]; mag = dx*dx + dy*dy; if( mag0 < mag ) { dx0 = dx; dy0 = dy; mag0 = mag; } dx = p2[0] - p0[0]; dy = nextPtr[x1] - prevPtr[x1]; mag = dx*dx + dy*dy;#else //p2为第y行第x+1列的地址 //p0为第y行第x-1列的地址 const uchar* p2 = imgPtr + xmap[x+1]*3; const uchar* p0 = imgPtr + xmap[x-1]*3; //计算第2通道的幅值 dx0 = lut[p2[2]] - lut[p0[2]]; dy0 = lut[nextPtr[x1+2]] - lut[prevPtr[x1+2]]; mag0 = dx0*dx0 + dy0*dy0; //计算第1通道的幅值 dx = lut[p2[1]] - lut[p0[1]]; dy = lut[nextPtr[x1+1]] - lut[prevPtr[x1+1]]; mag = dx*dx + dy*dy; //取幅值最大的那个通道 if( mag0 < mag ) { dx0 = dx; dy0 = dy; mag0 = mag; } //计算第0通道的幅值 dx = lut[p2[0]] - lut[p0[0]]; dy = lut[nextPtr[x1]] - lut[prevPtr[x1]]; mag = dx*dx + dy*dy; #endif //取幅值最大的那个通道 if( mag0 < mag ) { dx0 = dx; dy0 = dy; mag0 = mag; } //最后求出水平和垂直方向上的梯度图像 dbuf[x] = dx0; dbuf[x+width] = dy0; } }#ifdef HAVE_IPP ippsCartToPolar_32f((const Ipp32f*)Dx.data, (const Ipp32f*)Dy.data, (Ipp32f*)Mag.data, pAngles, width); for( x = 0; x < width; x++ ) { if(pAngles[x] < 0.f) pAngles[x] += (Ipp32f)(CV_PI*2.); } ippsNormalize_32f(pAngles, pAngles, width, 0.5f/angleScale, 1.f/angleScale); ippsFloor_32f(pAngles,(Ipp32f*)hidxs.data,width); ippsSub_32f_I((Ipp32f*)hidxs.data,pAngles,width); ippsMul_32f_I((Ipp32f*)Mag.data,pAngles,width); ippsSub_32f_I(pAngles,(Ipp32f*)Mag.data,width); ippsRealToCplx_32f((Ipp32f*)Mag.data,pAngles,(Ipp32fc*)gradPtr,width);#else //cartToPolar()函数是计算2个矩阵对应元素的幅度和角度,最后一个参数为是否 //角度使用度数表示,这里为false表示不用度数表示,即用弧度表示。 //如果只需计算2个矩阵对应元素的幅度图像,可以采用magnitude()函数。 //-pi/2<Angle<pi/2; cartToPolar( Dx, Dy, Mag, Angle, false );#endif for( x = 0; x < width; x++ ) {#ifdef HAVE_IPP int hidx = (int)pHidxs[x];#else //-5<angle<4 float mag = dbuf[x+width*2], angle = dbuf[x+width*3]*angleScale - 0.5f; //cvFloor()返回不大于参数的最大整数 //hidx={-5,-4,-3,-2,-1,0,1,2,3,4}; int hidx = cvFloor(angle); //0<=angle<1;angle表示的意思是与其相邻的较小的那个bin的弧度距离(即弧度差) angle -= hidx; //gradPtr为grad图像的指针 //gradPtr[x*2]表示的是与x处梯度方向相邻较小的那个bin的幅度权重; //gradPtr[x*2+1]表示的是与x处梯度方向相邻较大的那个bin的幅度权重 gradPtr[x*2] = mag*(1.f - angle); gradPtr[x*2+1] = mag*angle;#endif if( hidx < 0 ) hidx += _nbins; else if( hidx >= _nbins ) hidx -= _nbins; assert( (unsigned)hidx < (unsigned)_nbins ); qanglePtr[x*2] = (uchar)hidx; hidx++; //-1在补码中的表示为11111111,与-1相与的话就是自己本身了; //0在补码中的表示为00000000,与0相与的结果就是0了. hidx &= hidx < _nbins ? -1 : 0; qanglePtr[x*2+1] = (uchar)hidx; } }}struct HOGCache{ struct BlockData { BlockData() : histOfs(0), imgOffset() {} int histOfs; Point imgOffset; }; struct PixData { size_t gradOfs, qangleOfs; int histOfs[4]; float histWeights[4]; float gradWeight; }; HOGCache(); HOGCache(const HOGDescriptor* descriptor, const Mat& img, Size paddingTL, Size paddingBR, bool useCache, Size cacheStride); virtual ~HOGCache() {}; virtual void init(const HOGDescriptor* descriptor, const Mat& img, Size paddingTL, Size paddingBR, bool useCache, Size cacheStride); Size windowsInImage(Size imageSize, Size winStride) const; Rect getWindow(Size imageSize, Size winStride, int idx) const; const float* getBlock(Point pt, float* buf); virtual void normalizeBlockHistogram(float* histogram) const; vector<PixData> pixData; vector<BlockData> blockData; bool useCache; vector<int> ymaxCached; Size winSize, cacheStride; Size nblocks, ncells; int blockHistogramSize; int count1, count2, count4; Point imgoffset; Mat_<float> blockCache; Mat_<uchar> blockCacheFlags; Mat grad, qangle; const HOGDescriptor* descriptor;};//默认的构造函数,不使用cache,块的直方图向量大小为0等HOGCache::HOGCache(){ useCache = false; blockHistogramSize = count1 = count2 = count4 = 0; descriptor = 0;}//带参的初始化函数,采用内部的init函数进行初始化HOGCache::HOGCache(const HOGDescriptor* _descriptor, const Mat& _img, Size _paddingTL, Size _paddingBR, bool _useCache, Size _cacheStride){ init(_descriptor, _img, _paddingTL, _paddingBR, _useCache, _cacheStride);}//HOGCache结构体的初始化函数void HOGCache::init(const HOGDescriptor* _descriptor, const Mat& _img, Size _paddingTL, Size _paddingBR, bool _useCache, Size _cacheStride){ descriptor = _descriptor; cacheStride = _cacheStride; useCache = _useCache; //首先调用computeGradient()函数计算输入图像的权值梯度幅度图和角度量化图 descriptor->computeGradient(_img, grad, qangle, _paddingTL, _paddingBR); //imgoffset是Point类型,而_paddingTL是Size类型,虽然类型不同,但是2者都是 //一个二维坐标,所以是在opencv中是允许直接赋值的。 imgoffset = _paddingTL; winSize = descriptor->winSize; Size blockSize = descriptor->blockSize; Size blockStride = descriptor->blockStride; Size cellSize = descriptor->cellSize; int i, j, nbins = descriptor->nbins; //rawBlockSize为block中包含像素点的个数 int rawBlockSize = blockSize.width*blockSize.height; //nblocks为Size类型,其长和宽分别表示一个窗口中水平方向和垂直方向上block的 //个数(需要考虑block在窗口中的移动) nblocks = Size((winSize.width - blockSize.width)/blockStride.width + 1, (winSize.height - blockSize.height)/blockStride.height + 1); //ncells也是Size类型,其长和宽分别表示一个block中水平方向和垂直方向容纳下 //的cell个数 ncells = Size(blockSize.width/cellSize.width, blockSize.height/cellSize.height); //blockHistogramSize表示一个block中贡献给hog描述子向量的长度 blockHistogramSize = ncells.width*ncells.height*nbins; if( useCache ) { //cacheStride= _cacheStride,即其大小是由参数传入的,表示的是窗口移动的大小 //cacheSize长和宽表示扩充后的图像cache中,block在水平方向和垂直方向出现的个数 Size cacheSize((grad.cols - blockSize.width)/cacheStride.width+1, (winSize.height/cacheStride.height)+1); //blockCache为一个float型的Mat,注意其列数的值 blockCache.create(cacheSize.height, cacheSize.width*blockHistogramSize); //blockCacheFlags为一个uchar型的Mat blockCacheFlags.create(cacheSize); size_t cacheRows = blockCache.rows; //ymaxCached为vector<int>类型 //Mat::resize()为矩阵的一个方法,只是改变矩阵的行数,与单独的resize()函数不相同。 ymaxCached.resize(cacheRows); //ymaxCached向量内部全部初始化为-1 for(size_t ii = 0; ii < cacheRows; ii++ ) ymaxCached[ii] = -1; } //weights为一个尺寸为blockSize的二维高斯表,下面的代码就是计算二维高斯的系数 Mat_<float> weights(blockSize); float sigma = (float)descriptor->getWinSigma(); float scale = 1.f/(sigma*sigma*2); for(i = 0; i < blockSize.height; i++) for(j = 0; j < blockSize.width; j++) { float di = i - blockSize.height*0.5f; float dj = j - blockSize.width*0.5f; weights(i,j) = std::exp(-(di*di + dj*dj)*scale); } //vector<BlockData> blockData;而BlockData为HOGCache的一个结构体成员 //nblocks.width*nblocks.height表示一个检测窗口中block的个数, //而cacheSize.width*cacheSize.heigh表示一个已经扩充的图片中的block的个数 blockData.resize(nblocks.width*nblocks.height); //vector<PixData> pixData;同理,Pixdata也为HOGCache中的一个结构体成员 //rawBlockSize表示每个block中像素点的个数 //resize表示将其转换成列向量 pixData.resize(rawBlockSize*3); // Initialize 2 lookup tables, pixData & blockData. // Here is why: // // The detection algorithm runs in 4 nested loops (at each pyramid layer): // loop over the windows within the input image // loop over the blocks within each window // loop over the cells within each block // loop over the pixels in each cell // // As each of the loops runs over a 2-dimensional array, // we could get 8(!) nested loops in total, which is very-very slow. // // To speed the things up, we do the following: // 1. loop over windows is unrolled in the HOGDescriptor::{compute|detect} methods; // inside we compute the current search window using getWindow() method. // Yes, it involves some overhead (function call + couple of divisions), // but it‘s tiny in fact. // 2. loop over the blocks is also unrolled. Inside we use pre-computed blockData[j] // to set up gradient and histogram pointers. // 3. loops over cells and pixels in each cell are merged // (since there is no overlap between cells, each pixel in the block is processed once) // and also unrolled. Inside we use PixData[k] to access the gradient values and // update the histogram //count1,count2,count4分别表示block中同时对1个cell,2个cell,4个cell有贡献的像素点的个数。 count1 = count2 = count4 = 0; for( j = 0; j < blockSize.width; j++ ) for( i = 0; i < blockSize.height; i++ ) { PixData* data = http://www.mamicode.com/0;>
objdetect.hpp中关于hog的部分:
//////////////// HOG (Histogram-of-Oriented-Gradients) Descriptor and Object Detector //////////////struct CV_EXPORTS_W HOGDescriptor{public: enum { L2Hys=0 }; enum { DEFAULT_NLEVELS=64 }; CV_WRAP HOGDescriptor() : winSize(64,128), blockSize(16,16), blockStride(8,8), cellSize(8,8), nbins(9), derivAperture(1), winSigma(-1), histogramNormType(HOGDescriptor::L2Hys), L2HysThreshold(0.2), gammaCorrection(true), nlevels(HOGDescriptor::DEFAULT_NLEVELS) {} //可以用构造函数的参数来作为冒号外的参数初始化传入,这样定义该类的时候,一旦变量分配了 //内存,则马上会被初始化,而不用等所有变量分配完内存后再初始化。 CV_WRAP HOGDescriptor(Size _winSize, Size _blockSize, Size _blockStride, Size _cellSize, int _nbins, int _derivAperture=1, double _winSigma=-1, int _histogramNormType=HOGDescriptor::L2Hys, double _L2HysThreshold=0.2, bool _gammaCorrection=false, int _nlevels=HOGDescriptor::DEFAULT_NLEVELS) : winSize(_winSize), blockSize(_blockSize), blockStride(_blockStride), cellSize(_cellSize), nbins(_nbins), derivAperture(_derivAperture), winSigma(_winSigma), histogramNormType(_histogramNormType), L2HysThreshold(_L2HysThreshold), gammaCorrection(_gammaCorrection), nlevels(_nlevels) {} //可以导入文本文件进行初始化 CV_WRAP HOGDescriptor(const String& filename) { load(filename); } HOGDescriptor(const HOGDescriptor& d) { d.copyTo(*this); } virtual ~HOGDescriptor() {} //size_t是一个long unsigned int型 CV_WRAP size_t getDescriptorSize() const; CV_WRAP bool checkDetectorSize() const; CV_WRAP double getWinSigma() const; //virtual为虚函数,在指针或引用时起函数多态作用 CV_WRAP virtual void setSVMDetector(InputArray _svmdetector); virtual bool read(FileNode& fn); virtual void write(FileStorage& fs, const String& objname) const; CV_WRAP virtual bool load(const String& filename, const String& objname=String()); CV_WRAP virtual void save(const String& filename, const String& objname=String()) const; virtual void copyTo(HOGDescriptor& c) const; CV_WRAP virtual void compute(const Mat& img, CV_OUT vector<float>& descriptors, Size winStride=Size(), Size padding=Size(), const vector<Point>& locations=vector<Point>()) const; //with found weights output CV_WRAP virtual void detect(const Mat& img, CV_OUT vector<Point>& foundLocations, CV_OUT vector<double>& weights, double hitThreshold=0, Size winStride=Size(), Size padding=Size(), const vector<Point>& searchLocations=vector<Point>()) const; //without found weights output virtual void detect(const Mat& img, CV_OUT vector<Point>& foundLocations, double hitThreshold=0, Size winStride=Size(), Size padding=Size(), const vector<Point>& searchLocations=vector<Point>()) const; //with result weights output CV_WRAP virtual void detectMultiScale(const Mat& img, CV_OUT vector<Rect>& foundLocations, CV_OUT vector<double>& foundWeights, double hitThreshold=0, Size winStride=Size(), Size padding=Size(), double scale=1.05, double finalThreshold=2.0,bool useMeanshiftGrouping = false) const; //without found weights output virtual void detectMultiScale(const Mat& img, CV_OUT vector<Rect>& foundLocations, double hitThreshold=0, Size winStride=Size(), Size padding=Size(), double scale=1.05, double finalThreshold=2.0, bool useMeanshiftGrouping = false) const; CV_WRAP virtual void computeGradient(const Mat& img, CV_OUT Mat& grad, CV_OUT Mat& angleOfs, Size paddingTL=Size(), Size paddingBR=Size()) const; CV_WRAP static vector<float> getDefaultPeopleDetector(); CV_WRAP static vector<float> getDaimlerPeopleDetector(); CV_PROP Size winSize; CV_PROP Size blockSize; CV_PROP Size blockStride; CV_PROP Size cellSize; CV_PROP int nbins; CV_PROP int derivAperture; CV_PROP double winSigma; CV_PROP int histogramNormType; CV_PROP double L2HysThreshold; CV_PROP bool gammaCorrection; CV_PROP vector<float> svmDetector; CV_PROP int nlevels;};
hog源码分析