时间为友,记录点滴。
初学武术的时候肯定不知道扎马步、打木桩这些基本功到底能有什么作用,就像我们讲过这么多特征点检测的方法Harris、SIFT、ORB、LBP,这些到底有什么用呢?算法最终给出来的一堆特征值如何用呢?所谓的尺度不变、旋转不变在实际应用中如何体现呢?总不能跟Demo中一样,就是为了划线吧。
再了解一个特征值算法HOG,尝试用它跟刚刚聊完的SVM结合,做一些有实际意义用途的事情。
写在前面
HOG(方向直方图梯度)不是什么新的算法,比较成熟了。本篇内容主要借鉴了如下两篇文章内容:
1、2005年CVPR论文,使用HOG+SVM做行人检测:
https://hal.inria.fr/file/index/docid/548512/filename/hog_cvpr2005.pdfhal.inria.fr2、自带OpenCV官方属性的Satya 文章:
Histogram of Oriented Gradientswww.learnopencv.comHOG(Histogram of Oriented Gradients)
HOG直译过来就是方向梯度直方图法,是一种特征值检测的方式。它主要是利用了图片中特征点的梯度信息作为特征值,可以用来做行人、一些物品的检测。
作为一名严谨的理工男,当然是要先扣题目字眼。
1. Histogram 是什么?
这个难不到我们,我们在OpenCV的专栏和数字图像处理专栏中都提到过。但是,我们貌似都只提到过如何统计、绘制一幅图的灰度或者RGB的直方图。
实际中,直方图仅仅是个工具,横轴坐标当然可以随意定,纵轴用来统计横轴坐标的度量范围结果即可。所以在横轴定义的时候:
- 首先是个闭区间,要最大限度囊括集合;
- 其次对这个区间划分,以“段”为等级梯度;
2. Oriented Gradients
这个的核心就是梯度了。至于梯度,貌似也没有脱离我们的知识范畴。我们在《用梯度(一阶微分)实现图像锐化》和《锐化空间滤波器之梯度》也都讲过。
我们再回顾一下。
2.1 先从一维连续函数的导数说起
一维函数的求导过程:
它对应的几何意义:
所以在一维函数中,梯度可以先认为是一阶导数。
需要注意的是,这里仅仅是认为,因为梯度是一个标量,存在大小+方向两个维度,只存在多元函数中。
2.2 多元连续函数的偏导
问题来了,要是我们都只生活在
-
或来表示这个函数在y方向不变,函数值沿着x轴方向的导数;
-
或来表示这个函数在x方向不变,函数值沿着y轴方向的导数;
它的几何意义:
2.3 多元连续函数的方向导数
如果我们看出了偏导的门道,就是对切面求导数。那么是不是切面只能是X/Y轴方向呢?当然不是,理论上来说,可以沿着任何方向做垂直于X/Y平面的切面,这就是任意方向的变化率,也称方向导数(Directional Derivative).
如果函数
其中
它的几何意义:
2.4 多元连续函数的梯度
了解了方向导数,梯度的概念就呼之欲出了。方向导数取最大值的那个就是梯度(Gradients ), 也就是函数变化率最大的方向。
问题来了,如何求方向导数的最大值呢?总不能再求导数吧。。。
我们先把方向导数的表达式拿过来:
其中就是切面投影跟X轴的夹角,就是切面投影跟Y轴的夹角;
我们令:
所以:
其中:是两个向量的夹角
很明显,当
它的模
2.5 数字图像中的梯度
数字图像处理中的一阶微分是用梯度的幅值来实现的。对于f(x, y), f在坐标(x, y)处的梯度定义为二维列向量:
同模拟函数
完美,这样我们知道理解了HOG标题中隐藏的内容,这样不至于在实现的时候懵逼。
实现过程
提纲挈领,先把总流程贴一下:
翻译过来就是:
- 图片Gamma和颜色的归一化;
- 计算梯度;
- 构建直方图;
- Block混叠空间块的归一化;
- 构建HOG特征描述子;
- SVM训练;
拆开看看如何实现:
1. 图片Gamma和颜色的归一化
就是用来对图片做Gamma均衡的,但是作者也坦言,他们尝试了对灰度、RGB和LAB的幂律均衡,但是收到的效果不是很明显。
所以,Satya 在实验过程中,直接跳过了一步。
结合实际例子看一看:
Satya 的思路是从一张图片中扣出来行人作为建立HOG描述子的目标图像,resize成一张64x128像素的图像,需要指出的是抠图对宽高是有1:2的要求的;相对来说Naveet和Bill的论文中就是用了MIT pedestrian database和INRIA(貌似是他们自己搞出来的图像集合)。
2. 计算梯度
这个可难不倒我们,刚讲过了什么是梯度,以及如何计算梯度。论文作者也提到了用3x3的sobel算子来做梯度运算。然后通过公式求出梯度幅值和方向:
这对OpenCV简直小菜一碟。
需要指出的是,如果是灰度图,只计算目标像素的梯度,如果是彩色图,那么就要计算各个RGB分量上的梯度;
结合实际例子看一看:
还记的Sobel算子(kernel)吗?
3. 构建直方图
这一步会稍微复杂一些,简单的说就是把上面每个像素的梯度的两个维度映射成直方图,其中以梯度的角度为横轴,梯度的模为纵轴。
还是直接结合例子吧:
3.1 选择Cell分割图片
Satya 选择了8x8个像素作为一个cell,这样把图像分割成了8x16个cell。
一个8*8的Cell有8*8*3=192个像素值,每个像素有两个值(幅值magnitude和方向direction,三个channel取最大magnitude那个),加起来就是8*8*2=128个数。
3.2 每个Cell的探究
还是要深入到其中一个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份;
纵轴:按照像素梯度的方向找对应的bin,然后把该像素梯度对应的幅值按照比例放入到相应的bin中。
太抽象?看例子:
先来看蓝色圈圈出来的像素点,它的角度是80,幅值是2,所以它在第五个bin里面加了2,再来看红色的圈圈出来的像素点,它的角度是10,幅值是4,因为角度10介于0-20度的中间(正好一半),所以把幅值一分为二地放到0和20两个bin里面去。
3.4 构建最终直方图
大功告成!别忘了每个Bin的横轴数字和纵轴数字代表什么含义奥。
4. Block混叠空间块的归一化
为什么要做混叠空间的归一化?论文作者说这样可以很好应对光线和前景-后景对比度的变化,而且评估了一系列的混叠空间大小(Block Size)和方格像素大小(Cell Size)组合下的错误率。
哈哈,Satya 在一开始选择Cell大小的时候就没想过论文作者的感受吗?
不管他们,看例子:
选用了2x2的Block,即4个9*1的直方图组合成一个36*1的向量,然后做归一化,然后间隔一个Cell依次向后循环,直到扫描完整张图像。
至于什么叫归一化,也难不到我们,在介绍高斯分布的时候,我们就有详细说过,比如如下OpenCV做归一化的几种方法,感受一下:
- NORM_L1:
注释:当前元素在整体元素累加和中占得比例,可以想象对于转换后的数据的和就是alpha。
- NORM_INF:
注释:当前元素是所有元素中最大元素的比例,这样可以保证转换结果小于等于alpha。
- NORM_L2:
注释:当前元素与标准差的比值。体现一种趋势。
- NORM_MINMAX:
注释:当前元素按照
5. 构建HOG特征描述子
其实就是每个Block向量合并的过程。
为了计算这整个图像的特征向量,需要把36*1(一个Block的描述子)的向量全部合并组成一个巨大的向量。向量的大小可以这么计算:
- 我们有多少个16*16的块?水平7个,垂直15个,总共有7*15=105次移动。
- 每个16*16的块代表了36*1的向量。所以把他们放在一起也就是36*105=3780维向量。
7. SVM训练
这个也难不倒我们,在上一节中我们就已经有实现过用SVM训练的图片,然后那相似图片做判断。这个无非是把HOG特征描述自做SVM训练对象。
只要记住OpenCV中SVM应用的三步走,妥妥的:
- 给数据集打标签;
- 丢给设置好参数的SVM直接train;
- 拿待预测的数据集去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/OpenCVgithub.com