1 视觉四大任务
(1)图像相关任务
- 分类-Classification:解决“是什么?”的问题
- 定位-Location:解决“在哪⾥?”的问题
- 检测-Detection:解决“是什么?在哪⾥?”的问题
- 分割-Segmentation:解决“每⼀个像素属于哪个⽬标物或场景”的问题
(2)分类任务应用
- 手写识别
- 病虫害识别
(3)检测任务应用
- 人脸检测
- 行人检测
- 车辆检测
- 遥感检测
(4)分割任务应用
- 农作物种植
- 医疗影像处理
2 HOG算法
HOG的全称是方向梯度直方图(Histogram of Oriented Gradient),通过计算和统计图像局部区域的梯度方向直方图来构成图像的局部特征,它是计算机视觉中用于物体检测的一种特征描述子(Feature Descriptor)。特征描述子的作用是提取有用的信息,抛弃冗余的信息。对于一个物体而言,能够区分它的特征的往往是它的形状——也就是它的边界。而在边界处灰度一般有突变,所以我们考察图像的梯度就可以知道边界在什么地方。在深度学习出现前,HOG是最好进行目标检测的方式。
2.1 梯度(Gradient)
首先,我们假设输入图片是一张灰度图(其实我们一般处理的是图像的一部分,即window,而不是整个图像)。它可以看作是行
(
r
)
(r)
(r)和列
(
c
)
(c)
(c)的二元函数:
I
(
r
,
c
)
I(r,c)
I(r,c),其中
I
I
I代表第
r
r
r行、
c
c
c列的像素点的灰度(取值范围为0~255 )。在研究二元函数时,我们常常会考虑它的梯度。在这里,我们需要知道
I
I
I在
x
x
x、
y
y
y 方向的梯度。我们采取的办法是:使用相邻格子的灰度之差做近似。
I
I
I在
x
x
x、
y
y
y 方向的梯度公式如下:
I
x
(
r
,
c
)
=
I
(
r
,
c
+
1
)
−
I
(
r
,
c
−
1
)
I
y
(
r
,
c
)
=
I
(
r
+
1
,
c
)
−
I
(
r
−
1
,
c
)
I _x ( r , c ) = I ( r , c + 1 ) − I ( r , c − 1 ) \\ I _y ( r , c ) = I ( r + 1 , c ) − I ( r − 1 , c )
Ix(r,c)=I(r,c+1)−I(r,c−1)Iy(r,c)=I(r+1,c)−I(r−1,c)
按理来说,上面的式子应该都除以2,但因为后面要做归一化处理,所以这些常数就无关紧要了。它也可以理解为用向量[−1, 0 ,1 ]和[−1 ,0 ,1 ]T 对原图做卷积运算。接下来我们把梯度转换为极坐标,其中角度被限制在0°~180 ° :
μ
=
I
x
2
+
I
y
2
θ
=
180
π
(
arctan
I
y
I
x
)
\mu = \sqrt{I_x^2+I_y^2}\\ \theta=\frac{180}{\pi}(\arctan \frac{I_y}{I_x})
μ=Ix2+Iy2θ=π180(arctanIxIy)
这里我们把
arctan
\arctan
arctan定义为
arctan
x
=
{
tan
−
1
x
,
x
≥
0
tan
−
1
x
+
π
,
x
<
0
\arctan x= \left \{ \begin{aligned} &\tan ^{-1}x,&x\geq0\\ &\tan ^{-1}x+\pi,&x<0 \end{aligned} \right.
arctanx={tan−1x,tan−1x+π,x≥0x<0
并且用
θ
\theta
θ用角度制表示
2.2 格子(Cell)
我们继续将图像分割为 C × C C\times C C×C大小的格子(一般 C = 8 C=8 C=8)。下图演示了这样的一个分割,每个绿框是 8 × 8 8\times 8 8×8大小的格子:
每个格子有 C 2 C^2 C2(一般为64)个像素点。每个像素点有一个梯度,我们要统计这些像素点梯度方向(即角度 θ \theta θ)的分布规律。上图中某个格子的梯度模长、方向如下:
在直方图中,对于 θ \theta θ,将其范围0°~180° ,分成 B B B个bin。一般取 B = 9 B = 9 B=9 ,也就是说每个区间的宽度是 w = 180 B = 20 ° w=\frac{180}{B}=20 ° w=B180=20°。我们把每个bin从0到 B − 1 B-1 B−1进行编号。第 i i i个bin的范围是 [ w i , w ( i + 1 ) ] [wi,w(i+1)] [wi,w(i+1)],中心是 w ( i + 1 2 ) w\!\left(i+\frac{1}{2}\right) w(i+21)。例如,当 B = 9 B=9 B=9时,第3个bin( i = 3 i = 3 i=3)的范围是 [ 60 ° , 80 ° ) [60°,80°) [60°,80°),中心是 70 ° 70° 70°。最后,不会简单的把每个像素点根据 θ \theta θ所在的范围放进bin里,而是需要考虑 μ \mu μ的大小,把它按一定比例放进相邻的两个bin里。每个bin中的值其实不是像素点个数,而是一种“贡献”(contribution)的度量。一个像素点对一个bin的贡献不仅取决于梯度的模长 μ \mu μ,还取决于它的角度 θ \theta θ与该bin的中心的距离。模长越长,贡献越大;距离越远,贡献越小。具体地说,对于一个梯度模长为 μ \mu μ、方向角为 θ \theta θ的像素点,设 j = ⌊ θ w − 1 2 ⌋ j=\left\lfloor\cfrac{\theta}{w}-\cfrac{1}{2}\right\rfloor j=⌊wθ−21⌋,则它
- 对编号为 j m o d b j\bmod b jmodb的bin的贡献为 v j = μ c j + 1 − θ w v_j=\mu\cfrac{c_{j+1}-\theta}{w} vj=μwcj+1−θ;
- 对编号为 ( j + 1 ) m o d b (j+1)\bmod b (j+1)modb的bin的贡献为 v j + 1 = μ θ − c j w v_{j+1}=\mu\cfrac{\theta-c_j}{w} vj+1=μwθ−cj
最后,每个格子会得到一个直方图,直方图的每个条目是所有这个格子中的像素点对这个bin的贡献之和。有趣的是,每个像素点对两个bin的贡献之和一定是 μ \mu μ。
下图是一个例子。首先,我们把0°~180° 分成B=9份,每一份的中心分别为10°、30° 、…、170°。现在我们有一个
θ
=
77
°
\theta=77°
θ=77°、模长为
μ
\mu
μ的梯度,它对3号bin(范围是60°~80° 、中心为70° )的贡献为
0.65
μ
0.65\mu
0.65μ,对4号bin(范围是80°~100° 、中心为90° )的贡献为
0.35
μ
0.35 \mu
0.35μ。
对于运动员的那张图,下图展示了如何计算一个梯度模长为85、角度为165的像素点的贡献:
这个格子的直方图如下:
2.3 块归一化(Block Normalization)
虽然我们已经获得了一个直方图,但是整体而言,直方图的高度与图像的亮度有很大关系,我们不希望白天拍的照片和晚上拍的照片整体的直方图高度差距很大。于是乎,我们需要对其进行归一化。把格子打包成块(block),每块有 2 × 2 2\times 2 2×2个格子,且块之间是可以重合的。显然,每块包含像素点的个数为2 2 C × 2 C 2C\times 2C 2C×2C。我们按照滑动窗口的方式把整个window扫描一遍,每次移动一个块。这样就保证了每个不在边缘的格子都被四个块覆盖。
上图的大小为 64 × 128 64\times 128 64×128,即 8 × 16 8\times 16 8×16个格子,每个块的水平位置有7个,竖直位置有15个。
现在,既然每个块有4个格子,每个格子的直方图有9个条目,我们可以把这些直方图的条目连接起来,形成36 维的向量
b
\boldsymbol{b}
b。现在,我们利用欧几里得范数(Euclidean norm)把每个块的
b
\boldsymbol{b}
b归一化,使其模长接近于1:
b
:
=
b
∥
b
∥
2
+
ε
,
ε
是为了防止除以
0
加上的一个非常小的正数
b: =\frac{b}{ \sqrt{∥b∥^2 +ε}},\varepsilon是为了防止除以0加上的一个非常小的正数
b:=∥b∥2+εb,ε是为了防止除以0加上的一个非常小的正数
你可能会问:为什么不对每个格子进行归一化呢?答案是,格子之间直方图高度的整体差异是携带了一部分信息的,不能把这部分信息彻底抹去。而对每个
2
×
2
2\times 2
2×2的块进行归一化,可以在一定程度上保留不同格子之间平均灰度差异所代表的信息。
2.4 HOG特征(HOG Feature)
接下来,我们把每个块的 b \boldsymbol{b} b向量全部连接起来,形成一个巨大的向量 h \boldsymbol{h} h,然后进行如下三步操作:
(1) 进行一个初步的归一化: h : = h ∥ h ∥ 2 + ε \boldsymbol{h}:=\cfrac{\boldsymbol{h}}{\sqrt{{\|\boldsymbol{h}\|}^2+\varepsilon}} h:=∥h∥2+εh;
(2) 使得 h \boldsymbol{h} h中每个数的大小不超过一个正的阈值 τ \tau τ,即对 h \boldsymbol{h} h的第 n n n维 h n h_n hn,令 h n : = min ( h n , τ ) h_n:=\min(h_n,\tau) hn:=min(hn,τ);
(3) 最后再进行一次归一化: h : = h ∥ h ∥ 2 + ε \boldsymbol{h}:=\cfrac{\boldsymbol{h}}{\sqrt{{\|\boldsymbol{h}\|}^2+\varepsilon}} h:=∥h∥2+εh。这样我们就大功告成啦。
对于一个 Y Y Y行、 X X X列的window,它的格子数是 Y C × X C \cfrac{Y}{C}\times\cfrac{X}{C} CY×CX ,块数是 ( Y C − 1 ) × ( X C − 1 ) \left(\cfrac{Y}{C}-1\right)\times\left(\cfrac{X}{C}-1\right) (CY−1)×(CX−1),最后HOG特征 h \boldsymbol{h} h的维数是 4 B × ( Y C − 1 ) × ( X C − 1 ) 4B\times\left(\cfrac{Y}{C}-1\right)\times\left(\cfrac{X}{C}-1\right) 4B×(CY−1)×(CX−1)。那张运动员图片的HOG特征维数是 4 × 9 × 15 × 7 = 3780 4\times 9\times 15\times 7=3780 4×9×15×7=3780。
2.5 使用skimage.feature.hog
提取HOG特征
skimage
的安装方法:
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple scikit-image
对于这张 96 × 160 96\times 160 96×160的图片(名为hog_test.png)
下面的代码提取了它的HOG特征:
# encoding: UTF-8
# 文件: hog.py
# 描述: 提取图片的HOG特征
from skimage.io import imread
from skimage.feature import hog
def extract_hog_feature(filename):
# 提取filename文件的HOG特征
image = imread(filename, as_gray=True)
# 读取图片,as_gray=True表示读取成灰度图
feature = hog( # 提取HOG特征
image, # 图片
orientations=9, # 方向的个数,即bin的个数B
pixels_per_cell=(8, 8), # 格子的大小,C×C
cells_per_block=(2, 2), # 一块有2×2个格子
block_norm='L2-Hys', # 归一化方法
visualize=False # 是否返回可视化图像
)
return feature
if __name__ == '__main__':
feature = extract_hog_feature('hog_test.png')
print(feature) # 显示HOG特征
print(feature.shape) # 显示HOG特征的维数
# 输出结果为:
# [0.24284172 0.24284172 0.21779826 ... 0.1942068 0.25568547 0.10666346]
# (7524,)
也就是说,我们获得的HOG特征是一个7524 维的向量。此处
7524
=
4
×
9
×
(
96
8
−
1
)
×
(
160
8
−
1
)
7524=4\times 9\times\left(\cfrac{96}{8}-1\right)\times\left(\cfrac{160}{8}-1\right)
7524=4×9×(896−1)×(8160−1)。如果我们把visualize
设置为True
,则hog
函数会返回一个包含HOG特征和可视化图片的元组,调用得到:
import matplotlib.pyplot as pltimport matplotlib.pyplot as plt
...
feature, visimg = hog(...)
plt.imshow(visimg)
plt.show()
这就是图片hog_test.png
的HOG特征的可视化。
3 HOG+SVM算法实现流程
3.1 图片处理
-
1)图片数据集效果需如下:
-
正负样本数据
-
相同尺寸
-
-
2)算法效果:提取图像特征描述符
-
3)优势:适应不同尺寸特征;粗粒度取样
-
4)分别存储正负样本
-
5)提取图片描述子,存储描述子。
3.2 模型调参及储存
行人检测具有以下痛点:
- 行人姿势各异--------强鲁棒性型
- 训练样本少---------小样本量训练模型
- 行人特征复杂----------核函数升维处理
SVM模型优势:
- 惩罚系数------------防止过拟合
- 核函数-----------复杂特征处理
- 模型存储----------用于实际处理的模型应用
4 图像画框
行人检测难点:行人大小不一致;行人位置不固定
接下来我们介绍在一个图像中框出行人的方法。主要的思想是滑动窗口(Sliding Windows)。即:采用大小不一的窗口在图像上以不同的步长滑过一遍,每次求出窗口中步长的HOG特征。算法有三层循环:
- 第一层枚举窗口的宽度。窗口的长宽比是固定的
(2:1)
,宽度从一个初始的min_width
(默认为48
)开始,每次乘以width_scale
(默认为1.25
)倍,当超过图像宽度时停止。如果你希望取得更好的识别效果,可以将min_width
和width_scale
设置的小一点,代价是识别速度会变慢。 - 第二层枚举窗口左侧的横坐标,从
0
开始,每次增加一个步长coord_step
(默认为16 ),直到右侧达到图像边界为止。如果你希望取得更好的识别效果,可以将coord_step
调小,但识别速度仍然会变慢。 - 第三层枚举窗口上侧的纵坐标,从
0
开始,每次增加一个步长coord_step
。 - 对于每个窗口,将其缩放成
area_width * area_height
(默认为 64 × 128 64\times 128 64×128)的区域,提取其HOG特征。
然后,对于所有窗口的HOG特征,用SVM给出其中有行人的概率。当概率大于阈值threshold
(默认为0.99
)时,视为有行人。但还有一个问题,一个行人可能会被多个方框框起来,我们需要选出其中最合适的方框。这就需要用到非极大值抑制(Non-Maximum Suppression, NMS)。
针对滑动窗口处理后,检测框过多,提出非极大值抑制处理。
NMS的基本思想是,对于两个重叠部分较多的方框,舍弃其中含行人概率较小的那一个,保留概率较大的哪一个。怎么衡量重叠部分的多少呢?我们采用交并比(Intersection over Union, IoU)。IoU
就是两个方框交集面积与并集面积的比值。IoU
越大,重叠部分越多。当IoU
大于等于一个阈值IoU_threshold
时,则舍弃其中一个方框。
非极大值抑制原理
-
只保留同区域得分最高检测框
-
使用SVM模型样本距离边界距离判定
处理效果
-
保留最大得分检测框
计算并集面积时只需要将两个方框面积相加再减去交集面积即可(类似于容斥原理)。代码如下:
def area_of_box(box):
'''
计算框的面积。
参数
---
box: 框,格式为(left, top, width, height)。
返回值
-----
box的面积,即width * height。
'''
return box[2] * box[3]
def intersection_over_union(box1, box2):
'''
两个框的交并比(IoU)。
参数
---
box1: 边框1。
box2: 边框2。
'''
intersection_width = max(0,box1[0] + box1[2] - box2[0])# 相交部分宽度=max(0, box1的右边 - box2的左边)
intersection_height = max(0, box1[1] + box1[3] - box2[1])# 相交部分长度=max(0, box1的下边 - box2的上边)
intersection_area = intersection_width * intersection_height # 相交部分面积
area_box1 = area_of_box(box1) # box1的面积
area_box2 = area_of_box(box2) # box1的面积
union_area = area_box1 + area_box2 - intersection_area
if abs(union_area) < 1:
IoU = 0 # 防止除以0
else:
IoU = intersection_area / union_area # 并集的面积等于二者面积之和减去交集的面积
return IoU
NMS算法的主要流程是:遍历每一个方框,如果它被另一个方框舍弃,则不加入结果列表,否则加入结果列表。最后返回结果列表。代码如下:
def non_maximum_suppression(pos_box_list, pos_prob,
IoU_threshold=0.4):
'''
非极大值抑制(NMS)。
参数
---
pos_box_list: 含有人的概率大于阈值的边框列表。
pos_prob: 对应的概率。
IoU_threshold: 舍弃边框的IoU阈值。
返回值
-----
抑制后的边框列表。
'''
result = [] # 结果
for box1, prob1 in zip(pos_box_list, pos_prob):
discard = False # 是否舍弃box1
for box2, prob2 in zip(pos_box_list, pos_prob):
if intersection_over_union(box1, box2) > IoU_threshold:
# IoU大于阈值
if prob2 > prob1: # 舍弃置信度较小的
discard = True
break
if not discard: # 未舍弃box1
result.append(box1) # 加入结果列表
return result
最后,给单个图像画框的代码如下:
from cv2 import rectangle, imshow, waitKey
from skimage.io import imread
from skimage.transform import resize
def detect_pedestrian(SVM, filename, show_img=False,
threshold=0.99, area_width=64, area_height=128,
min_width=48, width_scale=1.25, coord_step=16,
ratio=2):
'''
用SVM检测file文件中的行人,采用非极大值抑制(NMS)
避免重复画框。
参数
---
SVM: 训练好的SVM模型。
filename: 输入文件名。
show_img: 是否给用户显示已画框的图片。
threshold: 将某一部分视为人的概率阈值。
area_width: 缩放后区域的宽度。
area_height: 缩放后区域的高度。
min_width: 框宽度的最小值,也是初始值。
width_scale: 每一次框宽度增大时扩大的倍数。
coord_step: 坐标变化的步长。
ratio: 框的长宽比。
返回值
-----
一个列表,每个列表项是一个元组
(left, top, width, height), 为行人的边框。
'''
box_list = [] # 行人边框列表
hog_list = [] # HOG特征列表
with open(filename, 'rb') as file:
img = imread(file, as_gray=True) # 读取文件
img_height, img_width = img.shape # 图片长宽
width = min_width # 框的宽度
height = int(width * ratio) # 框的长度
while width < img_width and height < img_height:
for left in range(0, img_width - width,coord_step): # 框的左侧
for top in range(0, img_height - height,coord_step): # 框的上侧
patch = clip_image(img, left, top,width, height) # 截取图像的一部分
resized = resize(patch,(area_height, area_width)) # 缩放图片
hog_feature = extract_hog_feature(resized) # 提取HOG特征
box_list.append((left, top,width, height))
hog_list.append(hog_feature)
width = int(width * width_scale)
height = width * ratio
prob = SVM.predict_proba(hog_list)[:, 1]
# 用SVM模型进行判断
mask = (prob >= threshold)
# 布尔数组, mask[i]代表prob[i]是否等于阈值
pos_box_list = np.array(box_list)[mask]
# 含有人的框
pos_prob = prob[mask] # 对应的预测概率
box_list_after_NMS = non_maximum_suppression(pos_box_list, pos_prob) # NMS处理之后的框列表
if show_img:
shown_img = np.array(img) # 复制原图像,准备画框
for box in box_list_after_NMS:
shown_img = rectangle(shown_img,
pt1=(box[0], box[1]),
pt2=(box[0] + box[2],
box[1] + box[3]),
color=(0, 0, 0),
thickness=2)
imshow('', shown_img)
waitKey(0)
return box_list_after_NMS