学习这一篇课程之前,就听说过YOLO的大名。在疫情期间,有up主就通过yolo训练出了对行人口罩的检测并且大获成功,没成想今天有幸学习到。废话不多说,直接开始学习。
在开始学习YOLO之前首先行该学习目标检测的一些基础概念,毕竟YOLO也时目标检测的模型啊。
边界框(bounding box,bbox)就是图像上恰好可以框住检测物体的矩形框。
例如图中绿色的矩形框,都是恰好框住被检测物体。
在图象中边界框位置的表示方法有两种:
(x1,y1) = (x - w / 2 , y - h / 2)
(x2,y2) = (x + w / 2 , y + h / 2)
切记在编写代码时确定是哪一种表示方式。
锚框(Anchor box),是由人们假想出来的一种框。先设定好锚框的大小和形状,再以图像上某一个点为中心画出矩形框。就和他的名字一样,锚,把框固定在图片上某处。这么做有什么意义呢?其实就是为了之后模型方便在我们框出的众多锚框中找到目标,并且学习如何对锚框进行微调使其恰好可以框住目标。
每种模型都有自己的锚框的生成方式,YOLO-v3也不例外。
这是目标检测里很重要的概念。我们模型预测的边界框,该如何去评价它的好坏即它与真实框的重合程度。这就要引入交并比的概念。
这一概念来源于数学中的集合,用来描述两个集合A和B之间的关系,它等于两个集合的交集里面所包含的元素个数,除以它们的并集里面所包含的元素个数,具体计算公式如下:
IoU=A∪B / A∩B
我们将用这个概念来描述两个框之间的重合度。
下面让我们通过一段代码来加深理解
# 计算IoU,矩形框的坐标形式为xyxy,这个函数会被保存在box_utils.py文件中
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
其中有几点需要注意:
1.计算面积时公式中边长都加了1,这是由于交并比这个概念是从数学中引入的。数学中自然数矩形面积,但是在像素图中也可以使用坐标点的个数来计算。因此加不加这个1理论上都可以。
2.考虑到各种情况,在计算相交矩形的宽高时不要忘了与0取最大值,毕竟两个矩形不一定相交。
既然了解了基本的概念,那就让我们来解开YOLO的神秘面纱吧!
Joseph Redmon等人在2015年提出YOLO(You Only Look Once,YOLO)算法,通常也被称为YOLO-V1;2016年,他们对算法进行改进,又提出YOLO-V2版本;2018年发展出YOLO-V3版本。
主要涵盖如下内容:
YOLO-V3模型设计思想
⚪产生候选区域
生成锚框
生成预测框
标注候选区域
⚪卷积神经网络提取特征
⚪建立损失函数
获取样本标签
建立各项损失函数
⚪多层级检测
⚪预测输出
计算预测框得分和位置
非极大值抑制
YOLO-V3算法的基本思想可以分成两部分:
⚪按一定规则在图片上产生一系列的候选区域,然后根据这些候选区域与图片上物体真实框之间的位置关系对候选区域进行标注。跟真实框足够接近的那些候选区域会被标注为正样本,同时将真实框的位置作为正样本的位置目标。偏离真实框较大的那些候选区域则会被标注为负样本,负样本不需要预测位置或者类别。
⚪使用卷积神经网络提取图片特征并对候选区域的位置和类别进行预测。这样每个预测框就可以看成是一个样本,根据真实框相对它的位置和类别进行了标注而获得标签值,通过网络模型预测其位置和类别,将网络预测值和标签值进行比较,就可以建立起损失函数。
下面让我们以YOLO-V3模型设计思想的顺序来学习吧
机器和我们人不一样,我们想看图片上某一个目标,我们的眼睛会一下抓住它,但机器一开始可不会呀。机器是通过预先生成大量的固定的候选区域,这些候选区域覆盖了整张图片。在这些候选区域中去检测是否存在物体以及物体的类别。再通过一遍遍的学习对一开始固定的框进行微调使得其越来越适合目标物体。
因此,如何产生候选区域是目标检测很重要的一步。
1.生成锚框
锚框就是上面说的预先生成的固定的框。YOLO-V3的生成锚框的方式是:将原始图片划分成m×n个区域,并在分割后的每个小区域的中心,生成一系列锚框。下面举例说明一下。
原始图片高度H=640, 宽度W=480,如果我们选择小块区域的尺寸为32×32,则m和n分别为20和15。分割后就是图中黑色的方框。
之后以每个小框中心生成锚框,生成锚框的图片如下
图中蓝色的就是锚框,可以看到数量蛮多的,而且覆盖了整张图片。
那么这些锚框的大小是如何确定的呢?是我们预先通过训练集中目标物体的大小通过聚类得出的。通俗的来说就是看训练集中边界框大小集中在那些数值处,把这个集中的数值定义为锚框的大小。很好理解,这样做可以是到我们之后生成预测框时能进行尽量小的调整,方便模型学习。YOLO-V3针对Coco数据集进行的聚类后共产生了9种大小的锚框。
2.生成预测框
由于锚框它大小以及中心都是固定的,难免与目标物体“配合得不是很好”。由于我们最初是分为了许多小区域,每个小区域都有各自的一组锚框。因此,我们应该对锚框的中心坐标xy以及锚框宽高wh进行微调。我们选择的方法是:
#锚框中心微调
bx=cx+σ(tx)
by=cy+σ(ty)
#cx,cy是小区域左上角坐标
#σ(x)即sigmoid函数
σ(x) = 1 / (1+exp(−x))
#锚框大小调整
bh = ph * e^th
bw = pw * e^tw
#ph,pw为原锚框大小
是不是有小伙伴感觉疑惑,为什么要这么麻烦。听了课我才明白,这么做是为了我们训练考虑。我们知道每个小区域都是以左上角cx,cy为原点,原点向右向下一个单位视为该区域。如果变为一下这样:
bx=cx + tx
by=cy + ty
bh = ph * th
bw = pw * tw
看上去简单了许多,但要保证所做的微调tx,ty,th,tw均为正值才可以,并且tx,ty小于1。机器一开始是无法知道这个条件的,还需要学习参数的范围,增加了模型学习的复杂度.
数据集给我们的数据往往是真实框的绝对位置,我们需要通过他们来标注我们想要的数据形式。
对于产生的候选框,我们需要知道:
a. 候选框中是否存在目标物体,我们通过标签objectness来表示。objectness=1时,表示候选框中存在物体。objectness=0时则不存在。
b. 如果包含物体,我们要对锚框进行如何的调整来更完美的框住目标,也就是上一步预测框所做的,这里的参数即就是tx,ty,tw,th。
c. 如果包含物体,我们还需要得知候选区域中目标的种类,一般目标检测任务中存在很多物体,因此分类必不可少。我们使用变量label来表示其所属类别的标签。
综上所述,我们要标注一个锚框需要 [objectness,(tx,ty,tw,th),label] 这些标签。因此就需要用真实框标注出list里的标签。方法如下:
#标注预测框的objectness
def get_objectness_label(img, gt_boxes, gt_labels, iou_threshold = 0.7,
anchors = [116, 90, 156, 198, 373, 326],
num_classes=7, downsample=32):
"""
img 是输入的图像数据,形状是[N, C, H, W]
gt_boxes,真实框,维度是[N, 50, 4],其中50是真实框数目的上限,当图片中真实框不足50个时,不足部分的坐标全为0
真实框坐标格式是xywh,这里使用相对值
gt_labels,真实框所属类别,维度是[N, 50]
iou_threshold,当预测框与真实框的iou大于iou_threshold时不将其看作是负样本
anchors,锚框可选的尺寸
anchor_masks,通过与anchors一起确定本层级的特征图应该选用多大尺寸的锚框
num_classes,类别数目
downsample,特征图相对于输入网络的图片尺寸变化的比例
"""
img_shape = img.shape
batchsize = img_shape[0]
num_anchors = len(anchors) // 2
input_h = img_shape[2]
input_w = img_shape[3]
# 将输入图片划分成num_rows x num_cols个小方块区域,每个小方块的边长是 downsample
# 计算一共有多少行小方块
num_rows = input_h // downsample
# 计算一共有多少列小方块
num_cols = input_w // downsample
label_objectness = np.zeros([batchsize, num_anchors, num_rows, num_cols])
label_classification = np.zeros([batchsize, num_anchors, num_classes, num_rows, num_cols])
label_location = np.zeros([batchsize, num_anchors, 4, num_rows, num_cols])
scale_location = np.ones([batchsize, num_anchors, num_rows, num_cols])
# 对batchsize进行循环,依次处理每张图片
for n in range(batchsize):
# 对图片上的真实框进行循环,依次找出跟真实框形状最匹配的锚框
for n_gt in range(len(gt_boxes[n])):
gt = gt_boxes[n][n_gt]
gt_cls = gt_labels[n][n_gt]
gt_center_x = gt[0]
gt_center_y = gt[1]
gt_width = gt[2]
gt_height = gt[3]
if (gt_height < 1e-3) or (gt_height < 1e-3):
continue
i = int(gt_center_y * num_rows)
j = int(gt_center_x * num_cols)
ious = []
for ka in range(num_anchors):
bbox1 = [0., 0., float(gt_width), float(gt_height)]
anchor_w = anchors[ka * 2]
anchor_h = anchors[ka * 2 + 1]
bbox2 = [0., 0., anchor_w/float(input_w), anchor_h/float(input_h)]
# 计算iou
iou = box_iou_xywh(bbox1, bbox2)
ious.append(iou)
ious = np.array(ious)
inds = np.argsort(ious)
k = inds[-1]
label_objectness[n, k, i, j] = 1
c = gt_cls
label_classification[n, k, c, i, j] = 1.
# for those prediction bbox with objectness =1, set label of location
dx_label = gt_center_x * num_cols - j
dy_label = gt_center_y * num_rows - i
dw_label = np.log(gt_width * input_w / anchors[k*2])
dh_label = np.log(gt_height * input_h / anchors[k*2 + 1])
label_location[n, k, 0, i, j] = dx_label
label_location[n, k, 1, i, j] = dy_label
label_location[n, k, 2, i, j] = dw_label
label_location[n, k, 3, i, j] = dh_label
# scale_location用来调节不同尺寸的锚框对损失函数的贡献,作为加权系数和位置损失函数相乘
scale_location[n, k, i, j] = 2.0 - gt_width * gt_height
# 目前根据每张图片上所有出现过的gt box,都标注出了objectness为正的预测框,剩下的预测框则默认objectness为0
# 对于objectness为1的预测框,标出了他们所包含的物体类别,以及位置回归的目标
return label_objectness.astype('float32'), label_location.astype('float32'), label_classification.astype('float32'), \
scale_location.astype('float32')
代码结合注释很好理解,将一个批次每张图片中的真实框的中心点求出,得到所在的小方块区域。然后遍历各个大小锚框与之计算IoU,求出最大的锚框将其objectness标记为1。需要注意的是,这里再求IoU的时候只去考虑锚框与真实框的形状,因此中心点的偏差不做考虑,因此box的坐标均为(0,0).
之后再标记处类别(one-hot形式)以及预测框偏差[dx,dy,tw,th](由于tx,ty需要求sigmoid反函数不方便,因此使用dx,dy)。
到此,我们就把标注的任务做完了,也就是得到了label,那么我们该如何将它与图像进行联系呢?由于篇幅原因,我们下次学习。