Github上已经有YOLOv3 Pytorch版本的实现,转到GitHub
直接使用固然方便,但是如果自己动手实现一遍对于整个视觉检测任务的理解将会更加深刻,因此我决定分多个阶段将YOLO V3版本的各个模块逐一进行实现。在本系列文章中我会将YOLO V3网络自己搭建一遍,然后使用官方提供的预训练权重进行预测。
今天我们将会介绍什么是目标检测的任务,并且了解并实现目标检测的任务中的两个基本概念边界框和锚框。
什么是目标检测的任务
当涉及到计算机视觉和人工智能的应用时,目标检测是一个非常重要的任务。它是指识别和定位数字图像或视频中的特定对象,通常是在图像中用矩形边界框标识出来。目标检测在许多实际应用中都有着广泛的应用,包括自动驾驶、安防监控、智能交通、医疗影像分析等领域。本文将介绍目标检测的基本概念、算法和应用,帮助读者深入了解目标检测技术。
不同于图像识别任务,目标检测是计算机视觉中一项更具有挑战性的任务,其主要目标是在给定的图像或视频中,自动地识别出目标物体的存在、位置和类别信息。目标检测主要涉及到三种不同的深度学习技术,包括对象检测任务(识别是否包含物体,及识别是物体还是背景的二分类任务)、位置定位任务(定位目标所在的位置并用矩形框标注出来,其本质是一个回归任务,回归矩形框的位置和长宽)和分类识别任务(识别矩形框中的物体种类,本质是一个图像识别任务)等。例如,针对下面这张照片,目标检测的任务就是要自动识别图中的物体种类、位置,进而用矩形边界框将其标注出来。这个过程需要综合运用图像处理、机器学习、深度学习等多种技术手段来实现。
对于图片识别任务很好理解,基本流程是先使用卷积神经网络提取图像特征,然后再用这些特征预测分类概率,根据训练样本标签建立起分类损失函数,最后进行训练,如下图所示。
对于目标检测任务,假设我们现在有某种方式可以在输入图片上生成一系列可能包含物体的区域,这些区域称为候选区域,在一张图上可以生成很多个候选区域。然后对每个候选区域,可以把它单独当成一幅图像来看待,使用图像分类模型对它进行分类,看它属于哪个类别或者背景(即不包含任何物体的类别)。但是当我们拿到一张图片,如何确定候选区域即图中的矩形框的位置和大小呢?如何能保证生成的矩形框刚好就能框住我们需要检测的物体呢?这似乎难以入手。
我们从简单的角度出发,做算法题时,我们能想到的最简单的解决思路就是暴力枚举,同理我们页可以使用穷举法来产生候选区域,如下图所示。
A为图像上的某个像素点,B为A右下方另外一个像素点,A、B两点可以确定一个矩形框,记作AB。如上图所示:当A在图片左上角位置,B遍历除A之外的所有位置,生成矩形框A1B1, …, A1Bn, …。当A在图片中间某个位置,B遍历A右下方所有位置,生成矩形框AkB1, …, AkBn, …。当A遍历图像上所有像素点,B则遍历它右下方所有的像素点,最终生成的矩形框集合{AiBj}将会包含图像上所有可以选择的区域。
只要我们对每个候选区域的分类足够的准确,则一定能找到跟实际物体足够接近的区域来。穷举法也许能得到正确的预测结果,但其计算量也是非常巨大的,其所生成的总候选区域数目约为 W 2 + H 2 4 \frac{W^2+H^2}{4} 4W2+H2,假设 H = W = 100 H=W=100 H=W=100,总数将会达到 2.5 × 1 0 7 2.5×10^7 2.5×107个候选区域,正如同算法中的暴力枚举,在数据规模较小情况下还能运行,对于图片数据如此多的候选区域使得这种方法几乎没有什么实用性。
但是通过这种方式,我们可以看出,假设分类任务完成的足够完美,从理论上来讲检测任务也是可以解决的,亟待解决的问题是如何设计出合适的方法来产生候选区域。科学家们先后设计了一系列的解决方案,如传统图像算法Selective Search。两阶段的的检测算法R-CNN,Fast R-CNN,FastER R-CNN,Mask R-CNN等;两阶段的检测算法SSD和本文学习的YOLO系列感兴趣的可以去看原论文。
目标检测基础概念
在学习YOLO系列目标检测算法之前,先介绍一些跟检测相关的重要概念,包括边界框、锚框和交并比等。
边界框(bounding box)
检测任务需要同时预测物体的类别和位置,因此需要引入一些跟位置相关的概念。通常使用边界框(bounding box,bbox)来表示物体的位置,边界框是正好能包含物体的矩形框,如下图所示,图中猫和狗分别对应2个边界框。值得注意的是在检测任务中,训练数据集的标签里会给出目标物体真实边界框所对应的 ( x 1 , y 1 , x 2 , y 2 ) (x_1, y_1, x_2, y_2) (x1,y1,x2,y2),这样的边界框也被称为真实框(ground truth box),如下图所示,图中画出了猫和狗所对应的真实框。模型会对目标物体可能出现的位置进行预测,由模型预测出的边界框则称为预测框(prediction box)。
通常边界框的位置表示有两种格式:
- 1.
x
y
,
x
y
xy, xy
xy,xy即
(
x
1
,
y
1
,
x
2
,
y
2
)
(x_1,y_1,x_2,y_2)
(x1,y1,x2,y2),其中
(
x
1
,
y
1
)
(x_1, y_1)
(x1,y1)是矩形框左上角的坐标,
(
x
2
,
y
2
)
(x_2, y_2)
(x2,y2)是矩形框右下角的坐标。如果上图中使用
x
y
,
x
y
xy, xy
xy,xy标注:
- 狗:(50,40)(380,520),估算的大概位置。
- 猫:(400,110)(650,480)。
- 2.
x
y
,
w
h
xy,wh
xy,wh,即
(
x
,
y
,
w
,
h
)
(x, y, w, h)
(x,y,w,h),其中
(
x
,
y
)
(x,y)
(x,y)是矩形框中心点的坐标,
w
w
w是矩形框的宽度,
h
h
h是矩形框的高度。如果上图中使用
x
y
,
x
y
xy, xy
xy,xy标注:
- 狗:(200,300)(340,460),估算的大概位置。
- 猫:(520,300)(210,380)。
下面实现边界框的可视化展示
导入相关库
import numpy as np # 可能用到的数据值计算库
import os # 可能用到的文件操作
import matplotlib.pyplot as plt # 图形绘制
import matplotlib.patches as patches # 添加矩形框
import matplotlib.image as image # 读取图像数据
坐标格式的转换
def BoundingBox_Denote(bbox=[], mode=True):
'''边界框的表示形式的转换
bbox: 包含(x1, y1, x2, y2)四个位置信息的数据格式
mode: 边界框数据表示的模式
True: to (x1,y1,x2,y2)
False: to (x,y,w,h)
return: 返回形式转换后的边界框数据
'''
denote_bbox = [] # 转换表示的边界框
if mode is True: # 保持原形式
denote_bbox = bbox
else: # 转换为(center_x, center_y, w, h)
center_x = (bbox[0]+bbox[2]) / 2.0
center_y = (bbox[1]+bbox[3]) / 2.0
w = bbox[2] - bbox[0]
h = bbox[3] - bbox[1]
denote_bbox = [center_x, center_y, w, h]
# 返回表示转换的边界框表示
denote_bbox = np.asarray(denote_bbox, dtype='float32')
return denote_bbox
绘制边界框
def draw_rectangle(bbox=[], mode=True, color='k', fill=False):
'''绘制矩形框
bbox:边界框数据(默认框数据不超过图片边界)
mode: 边界框数据表示的模式
True: to (x1,y1,x2,y2)
False: to (x,y,w,h)
color: 边框颜色
fill: 是否填充
'''
if mode is True: # to (x1,y1,x2,y2)
x = bbox[0]
y = bbox[1]
w = bbox[2] - bbox[0] + 1
h = bbox[3] - bbox[1] + 1
else: # to (x,y,w,h)
# 默认绘制的框不超出边界
x = bbox[0] - bbox[2] / 2.0
y = bbox[1] - bbox[3] / 2.0
w = bbox[2]
h = bbox[3]
# 绘制边界框
# patches.Rectangle需要传入左上角坐标、矩形区域的宽度、高度等参数
# 获取绘制好的图形的返回句柄——用于添加到当前的图像窗口中
rect = patches.Rectangle((x, y), w, h,
linewidth=1, # 线条宽度
edgecolor=color, # 线条颜色
facecolor='y', #
fill=fill, linestyle='-')
return rect
将边界框绘制到图片上
def img_draw_bbox(bbox=[10, 20, 90, 100], mode=True):
'''将边界框绘制到实际图片上
bbox: 需要绘制的边界框
mode: 边界框数据表示的转换模式
True: to (x1,y1,x2,y2)
False: to (x,y,w,h)
'''
fig = plt.figure(figsize=(12, 8))
ax = plt.gca() # 窗口句柄
# 图片路径
img_path = os.path.join(os.getcwd(), 'img', '1.jpg')
img = image.imread(img_path) # 读取图片数据
plt.imshow(img) # 展示图片
# 边界框数据转换
denote_mode = mode # 边界框表示形式——确定数据格式
# 经过转换后的边界框数据
bbox1 = BoundingBox_Denote(bbox=bbox, mode=denote_mode)
# 绘制表示模式2的边界框
rect1 = draw_rectangle(bbox=bbox1, mode=denote_mode, color='r')
ax.add_patch(rect1) # 将矩形添加到当前的图片上
plt.show()
展示效果
# 边界框真实数据
test_bbox = [160, 60, 460, 260]
# 边界框数据表示模式——输入的bbox数据必须是[x1,y1,x2,y2]
# True: to (x1,y1,x2,y2)
# False: to (x,y,w,h)
# denote_mode = True
denote_mode = False
# 测试边界框的转换是否成功
test_bbox_denote(bbox=test_bbox, mode=denote_mode)
# 测试边界框的绘制
img_draw_bbox(bbox=test_bbox, mode=denote_mode)
锚框(Anchor box)
锚框与物体边界框不同,是由人们假想出来的一种框。先设定好锚框的大小和形状,再以图像上某一个点为中心画出矩形框。在下图中,以像素点[300, 500]为中心可以使用下面的程序生成3个框,如图中蓝色框所示,其中锚框A1跟人像区域非常接近。
绘制锚框
# 绘制锚框
def draw_anchor_box(center, length, scales, ratios, img_height, img_width):
"""
以center为中心,产生一系列锚框
其中length指定了一个基准的长度
scales是包含多种尺寸比例的list
ratios是包含多种长宽比的list
img_height和img_width是图片的尺寸,生成的锚框范围不能超出图片尺寸之外
"""
bboxes = []
for scale in scales:
for ratio in ratios:
h = length*scale*math.sqrt(ratio)
w = length*scale/math.sqrt(ratio)
x1 = max(center[0] - w/2., 0.)
y1 = max(center[1] - h/2., 0.)
x2 = min(center[0] + w/2. - 1.0, img_width - 1.0)
y2 = min(center[1] + h/2. - 1.0, img_height - 1.0)
print(center[0], center[1], w, h)
bboxes.append([x1, y1, x2, y2])
for bbox in bboxes:
draw_rectangle(currentAxis, bbox, edgecolor = 'b')
img_height = im.shape[0]
img_width = im.shape[1]
draw_anchor_box([300., 500.], 100., [2.0], [0.5, 1.0, 2.0], img_height, img_width)
上图中红色框为真实边界框,蓝色框则是在中间人物的中心位置根据输入生成的三个不同尺寸的锚框,注意锚框与预测框不同,锚框是根据固定规则生成的用于尽量包含物体的假设框。而预测框还要在这些锚框的基础上进行参数学习,实现一个回归任务,即使蓝色框的坐标与高宽回归到红色框的位置。机器学习完需要回归的参数后输出的框才是预测框。
在目标检测任务中,通常会以某种规则在图片上生成一系列锚框,将这些锚框当成可能的候选区域。模型对这些候选区域是否包含物体进行预测,如果包含目标物体,则还需要进一步预测出物体所属的类别。还有更为重要的一点是,由于锚框位置是固定的,它不大可能刚好跟物体边界框重合,所以需要在锚框的基础上进行微调以形成能准确描述物体位置的预测框,模型需要预测出微调的幅度。在训练过程中,模型通过学习不断的调整参数,最终能学会如何判别出锚框所代表的候选区域是否包含物体,如果包含物体的话,物体属于哪个类别,以及物体边界框相对于锚框位置需要调整的幅度。
不同的模型往往有着不同的生成锚框的方式,在后面的内容中,会详细介绍YOLOv3算法里面产生锚框的规则,理解了它的设计方案,也很容易类推到其它模型上。
交并比
上面我们画出了以点(300,500)(300, 500)(300,500)为中心,生成的三个锚框,我们可以看到锚框A1 与真实框 G1的重合度比较好。那么如何衡量这三个锚框跟真实框之间的关系呢?在检测任务中,使用交并比(Intersection of Union,IoU)作为衡量指标。这一概念来源于数学中的集合,用来描述两个集合AAA和BBB之间的关系,它等于两个集合的交集里面所包含的元素个数,除以它们的并集里面所包含的元素个数,具体计算公式如下:
I
o
U
=
A
∩
B
A
∪
B
IoU= \frac{A\cap B}{A \cup B}
IoU=A∪BA∩B
我们将用这个概念来描述两个框之间的重合度。两个框可以看成是两个像素的集合,它们的交并比等于两个框重合部分的面积除以它们合并起来的面积。下图“交集”中青色区域是两个框的重合面积,图“并集”中蓝色区域是两个框的相并面积。用这两个面积相除即可得到它们之间的交并比,如下图所示。
假设两个矩形框A和B的位置分别为:
A:
[
x
a
1
,
y
a
1
,
x
a
2
,
y
a
2
]
[x_{a1}, y_{a1}, x_{a2}, y_{a2}]
[xa1,ya1,xa2,ya2]
B:
[
x
b
1
,
y
b
1
,
x
b
2
,
y
b
2
]
[x_{b1}, y_{b1}, x_{b2}, y_{b2}]
[xb1,yb1,xb2,yb2]
假如位置关系如下图所示:
如果二者有相交部分,则
相交部分左上角坐标为:
x
1
=
m
a
x
(
x
a
1
,
x
b
1
)
x_1 = max(x_{a1}, x_{b1})
x1=max(xa1,xb1),
y
1
=
m
a
x
(
y
a
1
,
y
b
1
)
y_1 = max(y_{a1}, y_{b1})
y1=max(ya1,yb1)
相交部分右下角坐标为:
x
2
=
m
i
n
(
x
a
2
,
x
b
2
)
x_2 = min(x_{a2}, x_{b2})
x2=min(xa2,xb2),
y
2
=
m
i
n
(
y
a
2
,
y
b
2
)
y_2 = min(y_{a2}, y_{b2})
y2=min(ya2,yb2)
计算先交部分面积:
i
n
t
e
r
s
e
c
t
i
o
n
=
m
a
x
(
x
2
−
x
1
+
1.0
,
0
)
⋅
m
a
x
(
y
2
−
y
1
+
1.0
,
0
)
intersection = max(x_2 - x_1 + 1.0, 0) \cdot max(y_2 - y_1 + 1.0, 0)
intersection=max(x2−x1+1.0,0)⋅max(y2−y1+1.0,0)
矩形框A和B的面积分别是:
S
A
=
(
x
a
2
−
x
a
1
+
1.0
)
⋅
(
y
a
2
−
y
a
1
+
1.0
)
S_A = (x_{a2} - x_{a1} + 1.0) \cdot (y_{a2} - y_{a1} + 1.0)
SA=(xa2−xa1+1.0)⋅(ya2−ya1+1.0)
计算相并部分面积:
u
n
i
o
n
=
S
A
+
S
B
−
i
n
t
e
r
s
e
c
t
i
o
n
union = S_A + S_B - intersection
union=SA+SB−intersection
计算交并比:
I
o
U
=
i
n
t
e
r
s
e
c
t
i
o
n
u
n
i
o
n
IoU = \frac{intersection}{union}
IoU=unionintersection
IOU计算实现
# 计算IoU,矩形框的坐标形式为xyxy
def box_iou_xyxy(box1, box2):
# 获取box1左上角和右下角的坐标
x1min, y1min, x1max, y1max = box1[0], box1[1], box1[2], box1[3]
# 计算box1的面积
s1 = (y1max - y1min + 1.) * (x1max - x1min + 1.)
# 获取box2左上角和右下角的坐标
x2min, y2min, x2max, y2max = box2[0], box2[1], box2[2], box2[3]
# 计算box2的面积
s2 = (y2max - y2min + 1.) * (x2max - x2min + 1.)
# 计算相交矩形框的坐标
xmin = np.maximum(x1min, x2min)
ymin = np.maximum(y1min, y2min)
xmax = np.minimum(x1max, x2max)
ymax = np.minimum(y1max, y2max)
# 计算相交矩形行的高度、宽度、面积
inter_h = np.maximum(ymax - ymin + 1., 0.)
inter_w = np.maximum(xmax - xmin + 1., 0.)
intersection = inter_h * inter_w
# 计算相并面积
union = s1 + s2 - intersection
# 计算交并比
iou = intersection / union
return iou
bbox1 = [100., 100., 200., 200.]
bbox2 = [120., 120., 220., 220.]
iou = box_iou_xyxy(bbox1, bbox2)
print('IoU is {}'.format(iou))
# 计算IoU,矩形框的坐标形式为xywh
def box_iou_xywh(box1, box2):
x1min, y1min = box1[0] - box1[2]/2.0, box1[1] - box1[3]/2.0
x1max, y1max = box1[0] + box1[2]/2.0, box1[1] + box1[3]/2.0
s1 = box1[2] * box1[3]
x2min, y2min = box2[0] - box2[2]/2.0, box2[1] - box2[3]/2.0
x2max, y2max = box2[0] + box2[2]/2.0, box2[1] + box2[3]/2.0
s2 = box2[2] * box2[3]
xmin = np.maximum(x1min, x2min)
ymin = np.maximum(y1min, y2min)
xmax = np.minimum(x1max, x2max)
ymax = np.minimum(y1max, y2max)
inter_h = np.maximum(ymax - ymin, 0.)
inter_w = np.maximum(xmax - xmin, 0.)
intersection = inter_h * inter_w
union = s1 + s2 - intersection
iou = intersection / union
return iou
bbox1 = [100., 100., 200., 200.]
bbox2 = [120., 120., 220., 220.]
iou = box_iou_xywh(bbox1, bbox2)
print('IoU is {}'.format(iou))
为了直观的展示交并比的大小跟重合程度之间的关系,下图示意了不同交并比下两个框之间的相对位置关系,从 IoU = 0.95 到 IoU = 0.0
以上简单介绍了目标检测算法,和目标检测算法中使用到的一些基本概念,后续我们会基于YOLOV3实现整个目标检测任务。