OpenCV实战4: HOG+SVM实现行人检测

      目前基于机器学习方法的行人检测的主流特征描述子之一是HOG(Histogram of Oriented Gradient, 方向梯度直方图)HOG 特征是用于目标检测的特征描述子,它通过计算和统计图像局部区域的梯度方向直方图来构成特征,用这些特征描述原始图像。

       HOG的核心思想是所检测的局部物体外形能够被光强梯度或边缘方向的分布所描述。通过将整幅图像分割成小的连接区域(称为cells),每个cell生成一个方向梯度直方图或者cell中pixel的边缘方向,这些直方图的组合可表示出(所检测目标的目标)描述子。为改善准确率,局部直方图可以通过计算图像中一个较大区域(称为block)的光强作为measure被对比标准化,然后用这个值(measure)归一化这个block中的所有cells.这个归一化过程完成了更好的照射/阴影不变性。与其他描述子相比,HOG得到的描述子保持了几何和光学转化不变性(除非物体方向改变)。因此HOG描述子尤其适合人的检测。

      OpenCV实现了两种类型的基于HOG特征的行人检测,分别是SVM和Cascade,OpenCV自带的级联分类器的文件的位置在“XX\opencv\sources\data\hogcascades”(OpenCV4.x版本可用)。代码在文末。


行人检测: HOG + SVM 

       HOG: 方向梯度直方图(Histogram of Oriented Gradient, HOG)特征是一种在计算机视觉和图像处理中用来进行物体检测的特征描述子。HOG特征通过计算和统计图像局部区域的梯度方向直方图来构成特征. 

       HOG的缺点: 速度慢,实时性差;难以处理遮挡问题。
       SVM: (Support Vector Machine)指的是支持向量机,是常见的一种判别方法。在机器学习领域,是一个有监督的学习模型,通常用来进行模式识别、分类以及回归分析, 在行人检测中可以用作区分行人和非行人的分类器。

        在使用HOG + SVM进行行人检测时, 采集HOG特征的主要思想是通过对一幅图像进行分析, 局部目标的表象和形状可以被剃度或者边缘密度方向分布很好的好的描述. 我们对图像的各个像素点采集土堆或者边缘的方向直方图, 根据直方图的信息就可以描述图片的特征. 好在OpenCv 中已经提供了计算HOG特征的方法, 根据采集到的HOG特征向量, 供SVM分类使用. SVM简单来说就是一个分类器, 在行人检测中就可以转化为行人与非行人的两类分类问题, 在OpenCv中运用到的是基于网格法的SVM.使用采集到的正样本(行人)和负样本(非行人, 可以是汽车, 树木, 路灯等等)的HOG特征, 然后使用SVM分类器进行训练, 得到行人检测模型, 进行行人检测.

HOGDescriptor

HOGDescriptor的构造函数:

 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),
        free_coef(-1.f), nlevels(HOGDescriptor::DEFAULT_NLEVELS), signedGradient(false)
    {}

重要参数:

窗口大小 winSize(64,128), 块大小blockSize(16,16), 块滑动增量blockStride(8,8), 胞元大小cellSize(8,8), 梯度方向数nbins(9)。

上面这些都是HOGDescriptor的成员变量,括号里的数值是它们的默认值,它们反应了HOG描述子的参数。这里做了几个示意图来表示它们的含义。

nBins表示在一个胞元(cell)中统计梯度的方向数目,例如nBins=9时,在一个胞元内统计9个方向的梯度直方图,每个方向为180/9=20度。

HOG特征计算原理:

                                    

HOG特征的提取过程:

      1、Gamma归一化: 对图像颜色进行Gamma归一化处理,降低局部阴影及背景因素的影响.

      2、计算梯度:通过差分计算出图像在水平方向上及垂直方向上的梯度,然后得到各个像素点的梯度的幅值及方向:

                           

     3、划分cell 
     将整个窗口划分成大小相同互不重叠的细胞单元cell(如8×8像素),计算出每个cell的梯度大小及方向.然后将每像素的梯度方向在0−180(无向:0-180,有向:0-360)平均分为9个bins,统计每个cell的梯度直方图(不同梯度的个数),即可形成每个cell的descriptor. 

     采用9个bin的直方图来统计这8*8个像素的梯度信息。也就是将cell的梯度方向360度分成9个方向块,例如:如果这个像素的梯度方向是20-40度,直方图第2个bin的计数就加一,这样,对cell内每个像素用梯度方向在直方图中进行加权投影(映射到固定的角度范围),就可以得到这个cell的梯度方向直方图了,就是该cell对应的9维特征向量(因为有9个bin)。

       像素梯度方向用到了,那么梯度大小呢?梯度大小就是作为投影的权值的。例如说:这个像素的梯度方向是20-40度,然后它的梯度大小是2,那么直方图第2个bin的计数就不是加一了,而是加二(假设啊)。

      4、组合成block,统计block直方图 

     将2×2个相邻的cell组成大小为16×16的像素块即block.依次将block大小的滑动窗口从左到右从上到下滑动,求其梯度方向直方图向量,一个block内所有cell的特征descriptor串联起来便得到该block的HOG特征descriptor。

  由于局部光照的变化以及前景-背景对比度的变化,使得梯度强度的变化范围非常大。这就需要对梯度强度做归一化。归一化能够进一步地对光照、阴影和边缘进行压缩。

        作者采取的办法是:把各个细胞单元组合成大的、空间上连通的区间(blocks)。这样,一个block内所有cell的特征向量串联起来便得到该block的HOG特征。这些区间是互有重叠的,这就意味着:每一个单元格的特征会以不同的结果多次出现在最后的特征向量中。我们将归一化之后的块描述符(向量)就称之为HOG描述符。

  区间有两个主要的几何形状——矩形区间(R-HOG)和环形区间(C-HOG)。R-HOG区间大体上是一些方形的格子,它可以有三个参数来表征:每个区间中细胞单元的数目、每个细胞单元中像素点的数目、每个细胞的直方图通道数目。

       例如:行人检测的最佳参数设置是:3×3细胞/区间、6×6像素/细胞、9个直方图通道。则一块的特征数为:3*3*9;

不同大小的cell与不同大小的block作用下的效果对比:

5、梯度直方图归一化 
       作者对比了L2-norm、L1-norm、L1-sqrt等归一化方法,发现都比非标准数据有显着的改善.其中L2-norm和L1-sqrt效果最好,而L1-norm检测效果要比L2-norm和L1-sqrt低5%

6、收集HOG特征

      最后一步就是将检测窗口中所有重叠的块进行HOG特征的收集,并将它们结合成最终的特征向量供分类使用。那么一个图像的HOG特征维数是多少呢?

        总结:Dalal提出的Hog特征提取的过程:把样本图像分割为若干个像素的单元(cell),把梯度方向平均划分为9个区间(bin),在每个单元里面对所有像素的梯度方向在各个方向区间进行直方图统计,每个细胞单元得到一个9维的特征向量,每相邻的4个单元构成一个块(block),把一个块内的特征向量联起来得到36维的特征向量,用块对样本图像进行扫描,扫描步长为一个单元。最后将所有块的特征串联起来,就得到了人体的特征。

        例如,对于64*128的图像而言,每8*8的像素组成一个cell,每2*2个cell组成一个block(16×16像素),因为每个cell有9个特征,所以每个block内有4*9=36个特征,以8个像素为步长,那么,水平方向将有7个扫描窗口(block),垂直方向将有15个扫描窗口(block),这样检测窗口block的数量有((128-16)/8+1)×((64-16)/8+1)=15×7。也就是说,64*128的图片,总共有36*7*15=3780个特征。

可视化HOG

举个例子:在8*8的网格中计算梯度直方图

    把图像分割成8*8大小的网格(cell),每个网格都会计算一个梯度直方图,用直方图来表示也可以更加抗噪,一个gradient可能会有噪音,但是用直方图来表示后就不会对噪音那么敏感了。对于64*128的这幅图来说,8*8的网格已经足够大来表示有趣的特征比如脸,头等等。直方图是有9个bin的向量,代表的是角度0,20,40,60.....160。每个8*8的cell的梯度如图左所示:箭头是梯度的方向,长度是梯度的大小,可以发现箭头的指向方向是像素强度都变化方向,幅值是强度变化的大小。

把这8*8的cell里面所有的像素点都分别加到这9个bin里面去,就构建了一个9-bin的直方图,上面的网格对应的直方图如下:

                       

       我们创建了基于图片的梯度直方图,但是一个图片的梯度对于整张图片的光线会很敏感。我们就想让我们的直方图归一化从而不受光线变化影响。先考虑对向量用l2归一化的步骤是:
v = [128, 64, 32]
[(128^2) + (64^2) + (32^2) ]^0.5=146.64,把v中每一个元素除以146.64得到[0.87,0.43,0.22]
考虑另一个向量2*v,归一化后可以得到向量依旧是[0.87, 0.43, 0.22]。你可以明白归一化是把scale给移除了。

    你也许想到直接在我们得到的9*1的直方图上面做归一化,这也可以,但是更好的方法是从一个16*16的块上做归一化,也就是4个9*1的直方图组合成一个36*1的向量,然后做归一化,接着,窗口再朝后面挪8个像素(看动图)。重复这个过程把整张图遍历一边。

     通常HOG特征描述子是画出8*8网格(cell)中9*1归一化的直方图,见下图。你可以发现直方图的主要方向捕捉了这个人的外形,特别是躯干和腿。

                                                      

图中每一个红色箭头有9个发散的叉,对应直方图的九个位置,叉的长度表示该方向梯度的强度。

from:https://www.jianshu.com/p/395f0582c5f7

行人检测HOG+SVM

总体思路:
1、提取正负样本hog特征
2、投入svm分类器训练,得到model?
3、由model生成检测子?

4、利用检测子检测负样本,得到hardexample
5、提取hardexample的hog特征并结合第一步中的特征一起投入训练,得到最终检测子。

      opencv自带的hog检测子是3781维的。opencv默认的参数我们可以看到,winSize(64,128),blockSize(16,16),blockStride(8,8),cellSize(8,8),窗口大小64x128,块大小16x16,块步长8x8,那么窗口中块的数目是((64-16)/8+1)*((128-16)/8+1) = 7*15 =105个块,块大小为16x16,胞元大小为8x8,那么一个块中的胞元cell数目是 (16/8)*(16/8) =4个胞元,每一个胞元投影到9个bin,对应的向量就是9维,n= 105x4x9 = 3780,这就是这个窗口对应的特征了。另外一维是一维偏移。

训练行人检测的流程:

(1)准备训练样本集合;包括正样本集和负样本集;收集到足够的训练样本之后,你需要手动裁剪样本。例如,你想用Hog+SVM来对商业步行街的监控画面中进行行人检测,那么,你就应该用收集到的训练样本集合,手动裁剪画面中的行人。

(2)裁剪得到训练样本之后,将所有正样本放在一个文件夹中;将所有负样本放在另一个文件夹中;并将所有训练样本缩放到同样的尺寸大小。OpenCV自带的例子在训练时,就是将样本缩放为64*128进行训练的;

(3)提取所有正样本的Hog特征;提取所有负样本的Hog特征;

(4)对所有正负样本赋予样本标签;例如,所有正样本标记为1,所有负样本标记为0;

(5)将正负样本的Hog特征,标签,都输入到SVM中进行训练,m个样本,带m个标签,可提取到的hog特征数目为(m*3781);

(8)SVM训练之后,得到行人检测模型, 进行行人检测,将结果保存为文本文件。

(9)线性SVM进行训练之后得到的文本文件里面,有一个数组,叫做support vector,还有一个数组,叫做alpha,有一个浮点数,叫做rho;将alpha矩阵同support vector相乘,注意,alpha*supportVector,将得到一个列向量。之后,再该列向量的最后添加一个元素rho。如此,变得到了一个分类器,利用该分类器,直接替换opencv中行人检测默认的那个分类器(cv::HOGDescriptor::setSVMDetector()),就可以利用你的训练样本训练出来的分类器进行行人检测了。

      利用hog+svm检测行人,最终的检测方法是最基本的线性判别函数,wx + b = 0,刚才所求的3780维向量其实就是w,而加了一维的b就形成了opencv默认的3781维检测算子,而检测分为train和test两部分,在train期间我们需要提取一些列训练样本的hog特征使用svm训练最终的目的是为了得到我们检测的w以及b,在test期间提取待检测目标的hog特征x,带入方程是不是就能进行判别了呢?

from:https://blog.csdn.net/qq_26898461/article/details/46786033

HOG的应用:主要用在object detection 领域,特别是行人检测,智能交通系统,当然也有文章提到把HOG用在手势识别,人脸识别等方面。

HOG与SIFT区别

HOG和SIFT都属于描述子,以及由于在具体操作上有很多相似的步骤,所以致使很多人误认为HOG是SIFT的一种,其实两者在使用目的和具体处理细节上是有很大的区别的。HOG与SIFT的主要区别如下:

① SIFT是基于关键点特征向量的描述。

② HOG是将图像均匀的分成相邻的小块,然后在所有的小块内统计梯度直方图。

③ SIFT需要对图像尺度空间下对像素求极值点,而HOG中不需要。

④ SIFT一般有两大步骤,第一个步骤是对图像提取特征点,而HOG不会对图像提取特征点。

HOG的优点:

HOG表示的是边缘(梯度)的结构特征,因此可以描述局部的形状信息;

位置和方向空间的量化一定程度上可以抑制平移和旋转带来的影响;

采取在局部区域归一化直方图,可以部分抵消光照变化带来的影响。

由于一定程度忽略了光照颜色对图像造成的影响,使得图像所需要的表征数据的维度降低了。

而且由于它这种分块分单元的处理方法,也使得图像局部像素点之间的关系可以很好得到的表征。

HOG的缺点:

描述子生成过程冗长,导致速度慢,实时性差;

很难处理遮挡问题。

由于梯度的性质,该描述子对噪点相当敏感


这里给出OpenCV中sample代码示例:

做适当修改:创建Detector对象时,

Detector detector(0)表示使用getDefaultPeopleDetector;

Detector detector(1)表示使用getDaimlerPeopleDetector;

#include <opencv2/objdetect.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/imgproc.hpp>
#include <opencv2/videoio.hpp>
#include <iostream>
#include <iomanip>

	using namespace cv;
	using namespace std;

	
	class Detector
	{
		//enum Mode { Default, Daimler } m;
		enum { Default, Daimler };//定义枚举类型
		int m;
		HOGDescriptor hog, hog_d;
	public:
		//Detector() : m(Daimler), hog(), hog_d(Size(48, 96), Size(16, 16), Size(8, 8), Size(8, 8), 9)//构造函数,初始化对象时自动调用,m,hog,hog_d是数据成员,后跟一个放在圆括号中的初始化形式
		//{
		//	hog.setSVMDetector(HOGDescriptor::getDefaultPeopleDetector());
		//	hog_d.setSVMDetector(HOGDescriptor::getDaimlerPeopleDetector());
		//}
		Detector(int a) : m(a), hog(), hog_d(Size(48, 96), Size(16, 16), Size(8, 8), Size(8, 8), 9)//构造函数,初始化对象时自动调用,m,hog,hog_d是数据成员,后跟一个放在圆括号中的初始化形式
		{
			hog.setSVMDetector(HOGDescriptor::getDefaultPeopleDetector());
			hog_d.setSVMDetector(HOGDescriptor::getDaimlerPeopleDetector());
		}
		void toggleMode() { m = (m == Default ? Daimler : Default); }
		string modeName() const { return (m == Default ? "Default" : "Daimler"); }
		vector<Rect> detect(InputArray img)
		{
			// Run the detector with default parameters. to get a higher hit-rate
			// (and more false alarms, respectively), decrease the hitThreshold and
			// groupThreshold (set groupThreshold to 0 to turn off the grouping completely).
			vector<Rect> found;
			if (m == Default)
				hog.detectMultiScale(img, found, 0, Size(8, 8), Size(32, 32), 1.05, 2, false);
			else if (m == Daimler)
				hog_d.detectMultiScale(img, found, 0.5, Size(8, 8), Size(32, 32), 1.05, 2, true);
			return found;
		}
		void adjustRect(Rect & r) const
		{
			// The HOG detector returns slightly larger rectangles than the real objects,
			// so we slightly shrink the rectangles to get a nicer output.
			r.x += cvRound(r.width*0.1);
			r.width = cvRound(r.width*0.8);
			r.y += cvRound(r.height*0.07);
			r.height = cvRound(r.height*0.8);
		}
	};

	static const string keys = "{ help h   |   | print help message }"
		"{ camera c | 0 | capture video from camera (device index starting from 0) }"
		"{ video v  | D:/Program Files/OpenCV/opencv/sources/samples/data/vtest.avi| use video as input }";

	int main(int argc, char** argv)
	{
		CommandLineParser parser(argc, argv, keys);
		parser.about("This sample demonstrates the use ot the HoG descriptor.");
		if (parser.has("help"))
		{
			parser.printMessage();
			return 0;
		}
		int camera = parser.get<int>("camera");
		string file = parser.get<string>("video");
		if (!parser.check())
		{
			parser.printErrors();
			return 1;
		}

		VideoCapture cap;
		if (file.empty())
			cap.open(camera);
		else
			cap.open(file.c_str());
		if (!cap.isOpened())
		{
			cout << "Can not open video stream: '" << (file.empty() ? "<camera>" : file) << "'" << endl;
			return 2;
		}

		cout << "Press 'q' or <ESC> to quit." << endl;
		cout << "Press <space> to toggle between Default and Daimler detector" << endl;
		
		Detector detector(1);

		Mat frame;
		for (;;)
		{
			cap >> frame;
			if (frame.empty())
			{
				cout << "Finished reading: empty frame" << endl;
				break;
			}
			int64 t = getTickCount();
			vector<Rect> found = detector.detect(frame);
			t = getTickCount() - t;

			// show the window
			{
				ostringstream buf;
				buf << "Mode: " << detector.modeName() << " ||| "
					<< "FPS: " << fixed << setprecision(1) << (getTickFrequency() / (double)t);
				putText(frame, buf.str(), Point(10, 30), FONT_HERSHEY_PLAIN, 2.0, Scalar(0, 0, 255), 2, LINE_AA);
			}
			for (vector<Rect>::iterator i = found.begin(); i != found.end(); ++i)
			{
				Rect &r = *i;
				detector.adjustRect(r);
				rectangle(frame, r.tl(), r.br(), cv::Scalar(0, 255, 0), 2);
			}
			imshow("People detector", frame);

			// interact with user
			const char key = (char)waitKey(30);
			if (key == 27 || key == 'q') // ESC
			{
				cout << "Exit requested" << endl;
				break;
			}
			else if (key == ' ')
			{
				detector.toggleMode();
			}
		}
		return 0;
	}

1、enum:

C++ 中会使用const或者#define定义整型常量,当整型常量有多个且之间的值的全部或部分有递加的时候,定义起来稍显繁琐,此时用枚举显得很简洁,枚举类型相较于#define的优势在于,定义常量简洁且易于管理,可以自动赋值且值不相等,类型安全检测。

//使用枚举//定义一个枚举变量,此变量可以具有多个可能的值

typedef  enum  weekDay{
MON=1,        //枚举类型中数据都是整型且从0开始,此处将第1个值设为1,则TUE ,        // 以下均从1开始递加
TUE,
WED,     //C++中逗号不是一条语句,不是一条语句就可以用回车分行
THU,      //这样有助于写注释
FRI,
SAT=7,   //可以再重新赋值,此时SAT=7,而不是6
SUN       //SUN=8,而不是7
}week_day;
week_day week=SUN;

如果不为其赋值,默认枚举类型从0依次增大,如上面代码中,定义enum后,代码中只要出现Default,它就是“int 0”;Default=0;Daimler=1。

enum { Default, Daimler };

其实enum的作用就是以英文单词代表数字常量。防止代码中数字常量过多,不知道他们具体代表什么意思。

2、构造函数与类名相同:

Detector(int a) : m(a), hog(), hog_d(Size(48, 96), Size(16, 16), Size(8, 8), Size(8, 8), 9)//构造函数,初始化对象时自动调用,m,hog,hog_d是数据成员,后跟一个放在圆括号中的初始化形式
		{
			hog.setSVMDetector(HOGDescriptor::getDefaultPeopleDetector());
			hog_d.setSVMDetector(HOGDescriptor::getDaimlerPeopleDetector());
		}

     构造函数初始化列表在构造函数名后添加一个冒号,冒号后是以逗号分隔的数据成员列表,每个数据成员后跟一个放在圆括号中的初始化形式。圆括号中的初始化形式可以是任意复杂的表达式。

代码: https://github.com/liuzheCSDN/OpenCV/blob/master/facedelete/pedestrianHOG.cpp

from :https://www.cnblogs.com/voyagflyer/p/5329146.html

from :https://blog.csdn.net/akadiao/article/details/79685323

from:https://blog.csdn.net/dcrmg/article/details/53047009

from:https://www.cnblogs.com/wjgaas/p/3597248.html

  • 19
    点赞
  • 173
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值