0%

HOG算法

前言

在学习和阅读相关多模态目标检测的论文时因为对其中的很多相关名词和核心方法不了解需要额外的学习补充为了防止大量的方法在学习之后忘记所以将他们每次学习的时候汇总到本章节中,利用博客自带的搜索功能进行快速查找复习方便后续的学习阅读。

目标检测的图像特征提取之HOG特征

方向梯度直方图(Histogram of Oriented Gradient, HOG)特征是一种在计算机视觉和图像处理中用来进行物体检测的特征描述子。它通过计算和统计图像局部区域的梯度方向直方图来构成特征。Hog特征结合SVM分类器已经被广泛应用于图像识别中,尤其在行人检测中获得了极大的成功。需要提醒的是,HOG+SVM进行行人检测的方法是法国研究人员Dalal在2005的CVPR上提出的,而如今虽然有很多行人检测算法不断提出,但基本都是以HOG+SVM的思路为主。

核心算法

  • 灰度化(将图像看做一个x,y,z(灰度)的三维图像)

  • 采用Gamma校正法对输入图像进行颜色空间的标准化(归一化);目的是调节图像的对比度,降低图像局部的阴影和光照变化所造成的影响,同时可以抑制噪音的干扰;

公式:$I_{\text{norm}} = \sqrt{I}$

  • 计算图像每个像素的梯度(包括大小和方向);主要是为了捕获轮廓信息,同时进一步弱化光照的干扰。

使用中心差分法:
水平梯度 $G_x$:$I(x+1,y) - I(x-1,y)$
垂直梯度 $G_y$:$I(x,y+1) - I(x,y-1)$

梯度幅值:$\sqrt{G_x + G_y} $
梯度方向:$\arctan(G_x/G_y) $

  • 将图像划分成小cells(例如6*6像素/cell)

  • 统计每个cell的梯度直方图(不同梯度的个数),即可形成每个cell的descriptor

  • 将每几个cell组成一个block(例如3*3个cell/block),一个block内所有cell的特征descriptor串联起来便得到该block的HOG特征descriptor。

  • 将图像image内的所有block的HOG特征descriptor串联起来就可以得到该image(你要检测的目标)的HOG特征descriptor了。这个就是最终的可供分类使用的特征向量了。

这是对目标检测经典论文Histograms of Oriented Gradients for Human Detection的学习和复刻下面是代码复现过程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
#include <iostream>
#include <opencv2/opencv.hpp>
using namespace std;

/*
* @param cv::Mat src 输入图像
* @param cv::Mat& dst 输出图像
* @param float c 常数,用于控制变化幅度
* @param float gamma 指数
* @breif 伽马变换
*/
void GammaGrayTrans(const cv::Mat& src, cv::Mat& dst, float c, float gamma)
{
//建立灰度映射表
float grayTable[256];
for (int i = 0; i < 256; i++)
{
grayTable[i] = c * pow(i, gamma);
}

//遍历修改灰度值
int temp = 0;
for (int i = 0; i < src.rows; i++)
for (int j = 0; j < src.cols; j++)
{
temp = src.at<uchar>(i, j);
dst.at<float>(i, j) = grayTable[temp];
}

//将数据缩放到0-255
cv::normalize(dst, dst, 0, 255, cv::NORM_MINMAX);
//dst的数据类型还原为CV_8UC1
dst.convertTo(dst, CV_8UC1);
}

/*
* @param cv::Mat angleImg 梯度方向矩阵
* @param cv::Mat magnitudeImg 梯度幅值矩阵
* @param cv::Size cellSize cell的尺寸
* @param std::vector<std::vector<double>> cellVector 存储每个cell的特征向量
* @brief 计算每个cell的特征向量
*/
void getCellVector(cv::Mat& angleImg,cv::Mat& magnitudeImg,cv::Size cellSize, std::vector<std::vector<double>>& cellVector)
{
for(int cell_i=0; cell_i < angleImg.rows; cell_i=cell_i+cellSize.height)
for (int cell_j = 0; cell_j < angleImg.cols; cell_j=cell_j + cellSize.width)
{
//映射直方图
//[0, 180] 度以20度为一个bin,平均分成9份
std::vector<double> table(9);
int temp_floor = 0;
double scale = 0;
for (int i = cell_i; i < cell_i + cellSize.height; i++)
for (int j = cell_j; j < cell_j + cellSize.width; j++)
{
temp_floor = std::floor(angleImg.at<float>(i, j) / 20); //向下取整
switch (temp_floor) {
case 0:
scale = (angleImg.at<float>(i, j) - 0) / 20;
table[0] += magnitudeImg.at<float>(i, j) * scale;
table[1] += magnitudeImg.at<float>(i, j) * (1 - scale);
break;
case 1:
scale = (angleImg.at<float>(i, j) - 20) / 20;
table[1] += magnitudeImg.at<float>(i, j) * scale;
table[2] += magnitudeImg.at<float>(i, j) * (1 - scale);
break;
case 2:
scale = (angleImg.at<float>(i, j) - 40) / 20;
table[2] += magnitudeImg.at<float>(i, j) * scale;
table[3] += magnitudeImg.at<float>(i, j) * (1 - scale);
break;
case 3:
scale = (angleImg.at<float>(i, j) - 60) / 20;
table[3] += magnitudeImg.at<float>(i, j) * scale;
table[4] += magnitudeImg.at<float>(i, j) * (1 - scale);
break;
case 4:
scale = (angleImg.at<float>(i, j) - 80) / 20;
table[4] += magnitudeImg.at<float>(i, j) * scale;
table[5] += magnitudeImg.at<float>(i, j) * (1 - scale);
break;
case 5:
scale = (angleImg.at<float>(i, j) - 100) / 20;
table[5] += magnitudeImg.at<float>(i, j) * scale;
table[6] += magnitudeImg.at<float>(i, j) * (1 - scale);
break;
case 6:
scale = (angleImg.at<float>(i, j) - 120) / 20;
table[6] += magnitudeImg.at<float>(i, j) * scale;
table[7] += magnitudeImg.at<float>(i, j) * (1 - scale);
break;
case 7:
scale = (angleImg.at<float>(i, j) - 140) / 20;
table[7] += magnitudeImg.at<float>(i, j) * scale;
table[8] += magnitudeImg.at<float>(i, j) * (1 - scale);
break;
case 8:
table[8] += magnitudeImg.at<float>(i, j);
break;
}
}
cellVector.push_back(table);
}
}

/*
* @param cv::Mat src 输入图像(CV_8U),灰度图
* @param cv::Mat dst 输出图像
* @param int thresh OTSU最大类间方差的灰度值
* @brief 计算HOG描述子
*/
void getHOGDescriptor(cv::Mat& src, std::vector<double>& HOGVector,
cv::Mat& gammaOut, cv::Mat& magOut, cv::Mat& angOut)
{
//gamma变换
cv::Mat gammaImg(src.size(),CV_32F);
GammaGrayTrans(src, gammaImg, 1, 0.5);
gammaOut = gammaImg.clone();

//计算梯度
cv::Mat sobel_x, sobel_y;
cv::Sobel(gammaImg, sobel_x, CV_32F, 1, 0);
cv::Sobel(gammaImg, sobel_y, CV_32F, 0, 1);
cv::Mat magnitudeImg(sobel_x.size(),CV_32F), angleImg(sobel_x.size(), CV_32F);
double temp_mag = 0, temp_angle = 0;
for (int i = 0; i < sobel_x.rows; i++)
for (int j = 0; j < sobel_x.cols; j++)
{
temp_mag = cv::abs(sobel_x.at<float>(i, j)) + cv::abs(sobel_y.at<float>(i, j));
temp_angle = cv::fastAtan2(sobel_y.at<float>(i, j), sobel_x.at<float>(i, j));

magnitudeImg.at<float>(i, j) = temp_mag;

if ((temp_angle > 180)) temp_angle -= 180; //将角度映射到0-180°,成为“无符号”梯度
angleImg.at<float>(i, j) = temp_angle;
}

// 保存中间结果
magOut = magnitudeImg.clone();
angOut = angleImg.clone();

//构建直方图
//设置cell的尺寸8*8,获得每个cell的向量
cv::Size cellSize(8,8);
std::vector<std::vector<double>> cellVector;
getCellVector(angleImg, magnitudeImg, cellSize, cellVector);

// 计算每个block向量,选用2*2的block
std::vector < std::vector < double >> blockVector;
int block_i_end = angleImg.rows / cellSize.height;
int block_j_end = angleImg.cols / cellSize.width;
for (int block_i = 0; block_i < block_i_end-1; block_i++) {
for (int block_j = 0; block_j < block_j_end-1; block_j++)
{
std::vector<double> block;
block.insert(block.end(), cellVector[block_i*block_j_end+block_j].begin(), cellVector[block_i * block_j_end + block_j].end());
block.insert(block.end(), cellVector[block_i * block_j_end + block_j + 1].begin(), cellVector[block_i * block_j_end + block_j + 1].end());
block.insert(block.end(), cellVector[(block_i+1) * block_j_end + block_j].begin(), cellVector[(block_i+1) * block_j_end + block_j].end());
block.insert(block.end(), cellVector[(block_i+1) * block_j_end + block_j + 1].begin(), cellVector[(block_i+1) * block_j_end + block_j + 1].end());
cv::normalize(block, block, 1, cv::NORM_L1);
blockVector.push_back(block);
}
}

//构建HOG特征描述子,合并每个block
for (int i = 0; i < blockVector.size(); i++)
{
for (int j = 0; j < blockVector[i].size(); j++)
{
HOGVector.push_back(blockVector[i][j]);
}
}
}

// 可视化HOG特征
void visualizeHOG(cv::Mat& img, std::vector<double>& hogVector, cv::Size winSize) {
const int CELL_SIZE = 8;
const int BIN_NUM = 9;
const float DEG_PER_BIN = 180.0 / BIN_NUM;
const float MAX_LEN = 6.0; // 箭头最大长度

cv::Mat hogImg = img.clone();
cv::cvtColor(hogImg, hogImg, cv::COLOR_GRAY2BGR);

int cellsInX = winSize.width / CELL_SIZE;
int cellsInY = winSize.height / CELL_SIZE;

for (int cy = 0; cy < cellsInY - 1; cy++) {
for (int cx = 0; cx < cellsInX - 1; cx++) {
int cellOffset = (cy * (cellsInX - 1) + cx) * BIN_NUM * 4;

for (int by = 0; by < 2; by++) {
for (int bx = 0; bx < 2; bx++) {
int cellY = cy * CELL_SIZE + by * CELL_SIZE;
int cellX = cx * CELL_SIZE + bx * CELL_SIZE;

cv::Point center(cellX + CELL_SIZE/2, cellY + CELL_SIZE/2);

for (int bin = 0; bin < BIN_NUM; bin++) {
float binValue = hogVector[cellOffset + (by * 2 + bx) * BIN_NUM + bin];
float angle = bin * DEG_PER_BIN;
float radian = angle * CV_PI / 180.0;

// 计算箭头方向
float dx = cos(radian) * binValue * MAX_LEN;
float dy = sin(radian) * binValue * MAX_LEN;

cv::Point direction(static_cast<int>(dx), static_cast<int>(dy));
cv::line(hogImg, center, center + direction,
cv::Scalar(0, 255, 0), 1, cv::LINE_AA);
}
}
}
}
}

cv::imshow("HOG Visualization", hogImg);
}

int main()
{
//读取图片
string filepath = "/home/song/object_detect_lab/HOGtestCPP/pic/miku.jpg";
cv::Mat src = cv::imread(filepath, cv::IMREAD_GRAYSCALE);
if (src.empty())
{
std::cout << "imread error" << std::endl;
return -1;
}
cv::resize(src, src, cv::Size(64, 128));
cv::imshow("Original Image", src);

// 存储中间结果
cv::Mat gammaImg, magImg, angImg;
std::vector<double> HOGVector;
getHOGDescriptor(src, HOGVector, gammaImg, magImg, angImg);

// 1. 显示伽马变换结果
cv::imshow("1Gamma Transform", gammaImg);

// 2. 显示梯度幅值(归一化)
cv::Mat magShow;
cv::normalize(magImg, magShow, 0, 255, cv::NORM_MINMAX);
magShow.convertTo(magShow, CV_8U);
cv::imshow("2Gradient Magnitude", magShow);

// 3. 显示梯度方向(转换为角度图)
cv::Mat angShow;
angImg.convertTo(angShow, CV_32F);
angShow = angShow * 255.0 / 180.0; // 0-180°映射到0-255
angShow.convertTo(angShow, CV_8U);
cv::imshow("3Gradient Orientation", angShow);

// 4. 打印HOG特征信息
std::cout << "HOG Descriptor Size: " << HOGVector.size() << std::endl;
std::cout << "First 10 values: ";
for(int i = 0; i < 10 && i < HOGVector.size(); i++) {
std::cout << HOGVector[i] << " ";
}
std::cout << std::endl;

// 5. 可视化HOG特征
visualizeHOG(src, HOGVector, src.size());

cv::waitKey(0);
return 0;
}

得到灰度化的原始图片:

original

原始图片灰度化后用Gamma校正法对输入图像进行颜色空间的标准化:

伽马变换结果

梯度方向角度图:

梯度方向(转换为角度图).png)

梯度方向幅值图:

梯度幅值(归一化)

HOG示意图:

HOG

学习思考

如果按照我的复现过程一张维64×128的图片其得到的HOG检测子应该为3780维,而在opencv的HOG特征提取代码getDefaultPeopleDetector()中却得到了3781维我们来看一下原因以免在以后被疯狂困扰。

这是因为另外一维是一维偏移

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