和gamma svm_(四十六)OpenCV HOG+SVM的物体检测

cd6355d2b52ee8c889f9f6b87a8d5840.png

时间为友,记录点滴。

初学武术的时候肯定不知道扎马步、打木桩这些基本功到底能有什么作用,就像我们讲过这么多特征点检测的方法Harris、SIFT、ORB、LBP,这些到底有什么用呢?算法最终给出来的一堆特征值如何用呢?所谓的尺度不变、旋转不变在实际应用中如何体现呢?总不能跟Demo中一样,就是为了划线吧。

cb49ab2e8f6ae27c349d9f3b9084d434.png

再了解一个特征值算法HOG,尝试用它跟刚刚聊完的SVM结合,做一些有实际意义用途的事情。


写在前面

HOG(方向直方图梯度)不是什么新的算法,比较成熟了。本篇内容主要借鉴了如下两篇文章内容:

1、2005年CVPR论文,使用HOG+SVM做行人检测:

https://hal.inria.fr/file/index/docid/548512/filename/hog_cvpr2005.pdf​hal.inria.fr

2、自带OpenCV官方属性的Satya 文章:

Histogram of Oriented Gradients​www.learnopencv.com
c7ada22fffb376f4f69f37155586a78c.png

HOG(Histogram of Oriented Gradients)

HOG直译过来就是方向梯度直方图法,是一种特征值检测的方式。它主要是利用了图片中特征点的梯度信息作为特征值,可以用来做行人、一些物品的检测。

作为一名严谨的理工男,当然是要先扣题目字眼。

1. Histogram 是什么?

这个难不到我们,我们在OpenCV的专栏和数字图像处理专栏中都提到过。但是,我们貌似都只提到过如何统计、绘制一幅图的灰度或者RGB的直方图。

511d8254cc64e007b1dbb77acc9bf2e5.png

实际中,直方图仅仅是个工具,横轴坐标当然可以随意定,纵轴用来统计横轴坐标的度量范围结果即可。所以在横轴定义的时候:

  • 首先是个闭区间,要最大限度囊括集合;
  • 其次对这个区间划分,以“段”为等级梯度;

2. Oriented Gradients

这个的核心就是梯度了。至于梯度,貌似也没有脱离我们的知识范畴。我们在《用梯度(一阶微分)实现图像锐化》和《锐化空间滤波器之梯度》也都讲过。

我们再回顾一下。

2.1 先从一维连续函数的导数说起

一维函数的求导过程:

它对应的几何意义:

50107f92d69486a815ff32d9433ed71f.png

所以在一维函数中,梯度可以先认为是一阶导数。

需要注意的是,这里仅仅是认为,因为梯度是一个标量,存在大小+方向两个维度,只存在多元函数中。

2.2 多元连续函数的偏导

问题来了,要是我们都只生活在

的世界里就好了,世界多清净啊。可自变量才不顾忌我们俗人的感受,比如增加到了两个:
,就需要用:
  • 来表示这个函数在y方向不变,函数值沿着x轴方向的导数;
  • 来表示这个函数在x方向不变,函数值沿着y轴方向的导数;

它的几何意义:

10c6b176a4eeb5a7655400e43f6eaf02.png
就是沿X/Y轴切面,然后再求导数

2.3 多元连续函数的方向导数

如果我们看出了偏导的门道,就是对切面求导数。那么是不是切面只能是X/Y轴方向呢?当然不是,理论上来说,可以沿着任何方向做垂直于X/Y平面的切面,这就是任意方向的变化率,也称方向导数(Directional Derivative).

如果函数

在点
可微分,那么函数在该点沿任一方向
的方向导数存在,且有:

其中

是方向
的方向余弦。

它的几何意义:

55f29d435fec289b296a22763e636768.png
阿尔法和贝塔就是方向l映射在x/y轴平面上与x/y轴的方向夹角

2.4 多元连续函数的梯度

了解了方向导数,梯度的概念就呼之欲出了。方向导数取最大值的那个就是梯度(Gradients ), 也就是函数变化率最大的方向。

问题来了,如何求方向导数的最大值呢?总不能再求导数吧。。。

我们先把方向导数的表达式拿过来:

其中
就是切面投影跟X轴的夹角,
就是切面投影跟Y轴的夹角;

我们令:

表示偏微分向量;

表示与方向
同方向的单位向量;

所以:

其中:
是两个向量的夹角

很明显,当

为0的时候,方向微分最大。所以:

就是最大方向微分,它就被称为
方向梯度;

它的模

就是梯度大小;

2.5 数字图像中的梯度

数字图像处理中的一阶微分是用梯度的幅值来实现的。对于f(x, y), f在坐标(x, y)处的梯度定义为二维列向量:

同模拟函数

的定义一样,该向量有重要的几何意义,它指出了在(x, y)位置处的最大变化率的方向。,它的幅值(长度)可以表示为
为:

.

完美,这样我们知道理解了HOG标题中隐藏的内容,这样不至于在实现的时候懵逼。


实现过程

提纲挈领,先把总流程贴一下:

85d66d53e92795fa48659d8a7d4c111d.png
摘自HOG for Human Detection论文

翻译过来就是:

  1. 图片Gamma和颜色的归一化;
  2. 计算梯度;
  3. 构建直方图;
  4. Block混叠空间块的归一化;
  5. 构建HOG特征描述子;
  6. SVM训练;

拆开看看如何实现:

1. 图片Gamma和颜色的归一化

就是用来对图片做Gamma均衡的,但是作者也坦言,他们尝试了对灰度、RGB和LAB的幂律均衡,但是收到的效果不是很明显。

所以,Satya 在实验过程中,直接跳过了一步。

结合实际例子看一看:

ee0ffafb14e3db3b5a6988010825d7f0.png
抠图找到建立HOG描述子的图片

Satya 的思路是从一张图片中扣出来行人作为建立HOG描述子的目标图像,resize成一张64x128像素的图像,需要指出的是抠图对宽高是有1:2的要求的;相对来说Naveet和Bill的论文中就是用了MIT pedestrian database和INRIA(貌似是他们自己搞出来的图像集合)。

2. 计算梯度

这个可难不倒我们,刚讲过了什么是梯度,以及如何计算梯度。论文作者也提到了用3x3的sobel算子来做梯度运算。然后通过公式求出梯度幅值和方向:

这对OpenCV简直小菜一碟。

需要指出的是,如果是灰度图,只计算目标像素的梯度,如果是彩色图,那么就要计算各个RGB分量上的梯度;

结合实际例子看一看:

cb4690632cfd7495570b6e742a135ebf.png
x偏导;y偏导;梯度;

还记的Sobel算子(kernel)吗?

c93cbd2f5719d5aaa976885cd99698b3.png

3. 构建直方图

这一步会稍微复杂一些,简单的说就是把上面每个像素的梯度的两个维度映射成直方图,其中以梯度的角度为横轴,梯度的模为纵轴。

还是直接结合例子吧:

3.1 选择Cell分割图片

62a9a45fc64090c7299ab7d1998b720f.png

Satya 选择了8x8个像素作为一个cell,这样把图像分割成了8x16个cell。

一个8*8的Cell有8*8*3=192个像素值,每个像素有两个值(幅值magnitude和方向direction,三个channel取最大magnitude那个),加起来就是8*8*2=128个数。

3.2 每个Cell的探究

fbaa3cf96bf99a9aaa797a631e4ce4de.png

还是要深入到其中一个Cell中看看它长什么样子。

对于这么一张64x128大小的小图,其中一个8x8的Cell竟然还可以分清出光头的边缘。最右边是我们计算出来的这个Cell的梯度对应数据,其中上面为幅值,下面为方向角度。

有意思的是,角度的表示不是0~360,而是0~180,被称为"无符号"梯度("unsigned" gradients)。反正梯度方向确认后,即便旋转180度,也依然是变化率最快的方向(就是要分变大还是变小了)。那为什么不用0-360度的表示呢?在实践中发现unsigned gradients比signed gradients在行人检测任务中效果更好。一些HOG的实现中可以让你指定signed gradients。

3.3 映射直方图

怎么映射呢?我们提过是以方向为横轴,幅值为纵轴。

横轴:[0, 180]度以20度为一个bin,平均分成9份;

0b1747cbdb37ad3e12dfff7a598e59b9.png

纵轴:按照像素梯度的方向找对应的bin,然后把该像素梯度对应的幅值按照比例放入到相应的bin中。

太抽象?看例子:

007a2a1832cc8190e9a429d413cfb7af.png
左上表为方向;右上表为幅值;

先来看蓝色圈圈出来的像素点,它的角度是80,幅值是2,所以它在第五个bin里面加了2,再来看红色的圈圈出来的像素点,它的角度是10,幅值是4,因为角度10介于0-20度的中间(正好一半),所以把幅值一分为二地放到0和20两个bin里面去。

3.4 构建最终直方图

860f2d153630b7b6ec7f68a001b38277.png

大功告成!别忘了每个Bin的横轴数字和纵轴数字代表什么含义奥。

4. Block混叠空间块的归一化

为什么要做混叠空间的归一化?论文作者说这样可以很好应对光线和前景-后景对比度的变化,而且评估了一系列的混叠空间大小(Block Size)和方格像素大小(Cell Size)组合下的错误率。

cfad6d882ea8d46dd98aadbbc4233088.png
论文认为3x3的Block配合6x6的Cell最合适

哈哈,Satya 在一开始选择Cell大小的时候就没想过论文作者的感受吗?

不管他们,看例子:

c6edc4007231977e452276edd7946437.gif

选用了2x2的Block,即4个9*1的直方图组合成一个36*1的向量,然后做归一化,然后间隔一个Cell依次向后循环,直到扫描完整张图像。

至于什么叫归一化,也难不到我们,在介绍高斯分布的时候,我们就有详细说过,比如如下OpenCV做归一化的几种方法,感受一下:

  • NORM_L1:

注释:当前元素在整体元素累加和中占得比例,可以想象对于转换后的数据的和就是alpha。

  • NORM_INF:

注释:当前元素是所有元素中最大元素的比例,这样可以保证转换结果小于等于alpha。

  • NORM_L2:

注释:当前元素与标准差的比值。体现一种趋势。

  • NORM_MINMAX:

注释:当前元素按照

比例缩放后再加上baseline

5. 构建HOG特征描述子

其实就是每个Block向量合并的过程。

为了计算这整个图像的特征向量,需要把36*1(一个Block的描述子)的向量全部合并组成一个巨大的向量。向量的大小可以这么计算:

  1. 我们有多少个16*16的块?水平7个,垂直15个,总共有7*15=105次移动。
  2. 每个16*16的块代表了36*1的向量。所以把他们放在一起也就是36*105=3780维向量。

7. SVM训练

这个也难不倒我们,在上一节中我们就已经有实现过用SVM训练的图片,然后那相似图片做判断。这个无非是把HOG特征描述自做SVM训练对象。

只要记住OpenCV中SVM应用的三步走,妥妥的:

  1. 给数据集打标签;
  2. 丢给设置好参数的SVM直接train;
  3. 拿待预测的数据集去predict即可;

好了,看了半天别人的例子,我们也来写一个试一试!


主要分为三个步骤:

  • 准备数据(提取HOG特征点)并打标签;
  • 利用SVM进行Train;
  • 提取待测图片的HOG,用特征点进行SVM的Predict;

需要注意的是:

1. 一张图片的HOG特征点的大小为3780个float,前提是需要把这个图片映射到64*128个像素点的大小上;
2. 可以用glob接口读出来目录下的所有文件,这样可以根据需要train的图片多少动态定义trainData;
3. SVM preduct后,画图可以只画一个(当然,这个是特殊情况)
  • 本代码源于互联网,略加修改。
#include <opencv2/opencv.hpp>
#include <iostream>

using namespace cv;
using namespace cv::ml;
using namespace std;

string positive_dir = "./elec_watch/positive/";
string negative_dir = "./elec_watch/negative/";
string testPic = "./elec_watch/test/scene_08.jpg";

void get_hog_descripor(Mat &image, vector<float> &desc);
void generate_dataset(Mat &trainData, Mat &labels);
void svm_train(Mat &trainData, Mat &labels);
vector< float > get_svm_detector(const Ptr< SVM >& svm);

int main(int argc, char** argv) {
	Mat trainData, labels;
	generate_dataset(trainData, labels);
	svm_train(trainData, labels);
	
	Ptr<SVM> svm = SVM::load("./hog_elec.yml");
	Mat test = imread(testPic);
	resize(test, test, Size(0, 0), 0.20, 0.20);

	imshow("input", test);
	Rect winRect;
	winRect.width = 64;
	winRect.height = 128;
	int sum_x = 0;
	int sum_y = 0;
	int count = 0;
	for (int row = 64; row < test.rows - 64; row += 4) {
		for (int col = 32; col < test.cols - 32; col += 4) {
			winRect.x = col - 32;
			winRect.y = row - 64;
			vector<float> fv;
			Mat hogRect = test(winRect);
			get_hog_descripor(hogRect, fv);
			Mat one_row = Mat::zeros(Size(fv.size(), 1), CV_32FC1);
			for (int i = 0; i < fv.size(); i++) {
				one_row.at<float>(0, i) = fv[i];
			}
			float result = svm->predict(one_row);
			if (result > 0) {
				// rectangle(test, winRect, Scalar(0, 0, 255), 1, 8, 0);
				sum_x += winRect.x;
				sum_y += winRect.y;
				count++;
			}
		}
	}
	winRect.x = sum_x / count;
	winRect.y = sum_y / count;
	rectangle(test, winRect, Scalar(255, 0, 0), 1, 8, 0);
	imshow("result", test);
	waitKey(0);
	return 0;
}

vector< float > get_svm_detector(const Ptr< SVM >& svm)
{
	// get the support vectors
	Mat sv = svm->getSupportVectors();
	const int sv_total = sv.rows;
	// get the decision function
	Mat alpha, svidx;
	double rho = svm->getDecisionFunction(0, alpha, svidx);

	CV_Assert(alpha.total() == 1 && svidx.total() == 1 && sv_total == 1);
	CV_Assert((alpha.type() == CV_64F && alpha.at<double>(0) == 1.) ||
		(alpha.type() == CV_32F && alpha.at<float>(0) == 1.f));
	CV_Assert(sv.type() == CV_32F);

	vector< float > hog_detector(sv.cols + 1);
	memcpy(&hog_detector[0], sv.ptr(), sv.cols * sizeof(hog_detector[0]));
	hog_detector[sv.cols] = (float)-rho;
	return hog_detector;
}

void svm_train(Mat &trainData, Mat &labels) {
	printf("n start SVM training... n");
	Ptr< SVM > svm = SVM::create();
	/* Default values to train SVM */
	svm->setGamma(5.383);
	svm->setKernel(SVM::LINEAR);
	svm->setC(2.67);
	svm->setType(SVM::C_SVC);
	svm->train(trainData, ROW_SAMPLE, labels);
	clog << "...[done]" << endl;

	// save xml
	svm->save("./hog_elec.yml");
}

void get_hog_descripor(Mat &image, vector<float> &desc) {
	HOGDescriptor hog;
	int h = image.rows;
	int w = image.cols;
	float rate = 64.0 / w;
	Mat img, gray;
	resize(image, img, Size(64, int(rate*h)));
	cvtColor(img, gray, COLOR_BGR2GRAY);
	Mat result = Mat::zeros(Size(64, 128), CV_8UC1);
	result = Scalar(127);
	Rect roi;
	roi.x = 0;
	roi.width = 64;
	roi.y = (128 - gray.rows) / 2;
	roi.height = gray.rows;
	gray.copyTo(result(roi));
	hog.compute(result, desc, Size(8, 8), Size(0, 0));
}

void generate_dataset(Mat &trainData, Mat &labels) {
	vector<string> images;
	vector<vector<float>> vecDec;
	vector<float> fv;
	glob(positive_dir, images);
	int posNum = images.size();
	for (int i = 0; i < posNum; i++)
	{
		Mat image = imread(images[i].c_str());
		vector<float> fv;
		get_hog_descripor(image, fv);
		printf("image path : %s, feature data length: %d n", images[i].c_str(), fv.size());
		vecDec.push_back(fv);
	}
	images.clear();
	glob(negative_dir, images);
	int negNum = images.size();
	for (int i = 0; i < negNum; i++)
	{
		fv.clear();
		Mat image = imread(images[i].c_str());
		get_hog_descripor(image, fv);
		printf("image path : %s, feature data length: %d n", images[i].c_str(), fv.size());
		vecDec.push_back(fv);
	}
	int trainDataNum = posNum + negNum;
	int trainDataLen = fv.size();


	Mat trainDataTemp(trainDataNum, trainDataLen, CV_32FC1);
	Mat trainLabel(trainDataNum, 1, CV_32SC1);


	for (int i = 0; i < trainDataNum; i++)
	{
		for (int j = 0; j < trainDataLen; j++)
		{
			trainDataTemp.at<float>(i, j) = vecDec[i][j];
		}
		if (i < posNum)
		{
			trainLabel.at<int>(i) = 1;
		}
		else
		{
			trainLabel.at<int>(i) = -1;
		}
	}

	trainData = trainDataTemp.clone();
	labels = trainLabel.clone();

	return;
}

可以在如下链接获取代码和图片资源:

lowkeyway/OpenCV​github.com
9b4bd668587bfbd8e8f337f1a64f5a68.png
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值