我们在之前实现YOLOv2的基础上,加入了多级检测及FPN,快速的实现了YOLOv3的网络架构,并且实现了前向推理过程。
经典目标检测YOLO系列(三)YOLOV3的复现(1)总体网络架构及前向处理过程
我们继续进行YOLOv3的复现。
官方YOLOv2的正样本匹配思路是根据预测框和目标框的IoU
来确定中心点所在的网格,哪一个预测框是正样本。
大体上,官方YOLOv3也沿用这一思路,但是细节上有差距。官方YOLOv3也会出现之前所说的三种情况:
我们继续沿用之前复现YOLOv2的做法。对于第3种情况,我们不忽略,还是标记为正样本。
先验框决定哪些预测框会参与到何种损失的计算中去
。一个目标会被匹配上多个正样本
。由于YOLOv3中添加了多级检测,因此部分代码细节有所差异。
pytorch读取VOC数据集:
一批图像数据的维度是 [B, 3, H, W] ,分别是batch size,色彩通道数,图像的高和图像的宽。
标签数据是一个包含 B 个图像的标注数据的python的list变量(如下所示),其中,每个图像的标注数据的list变量又包含了 M 个目标的信息(类别和边界框)。
获得了这一批数据后,图片是可以直接喂到网络里去训练的,但是标签不可以,需要再进行处理一下。
[
{
'boxes': torch.tensor([[120., 0., 408., 23.],
[160., 59., 416., 256.],
[172., 24., 218., 128.],
[408., 35., 416., 75.],
[ 0., 64., 8., 186.]]), # bbox的坐标(xmin, ymin, xmax, ymax
'labels': torch.tensor([ 6, 6, 14, 6, 19]), # 标签
'orig_size': [416, 416] # 图片的原始大小
},
{
'boxes': torch.tensor([[367., 255., 416., 416.],
[330., 302., 416., 416.]]),
'labels': torch.tensor([14, 13]),
'orig_size': [416, 416]
}
]
标签处理主要包括3个部分,
正样本位置(anchor_idx)
的置信度置为1,其他默认为0正样本位置(anchor_idx)
的标签类别为1(one-hot格式),其他类别设置为0正样本位置(anchor_idx)
的bbox信息设置为真实框的bbox信息。# 处理好的shape如下:
# gt_objectness
torch.Size([2, 10647, 1]) # 10647=52×52×3 + 26×26×3 + 13×13×3
# gt_classes
torch.Size([2, 10647, 20])
# gt_bboxes
torch.Size([2, 10647, 4])
iou_ind // self.num_anchors
确定这个先验框来自哪个尺度。
# RT-ODLab/models/detectors/yolov3/matcher.py
import numpy as np
import torch
class Yolov3Matcher(object):
def __init__(self, num_classes, num_anchors, anchor_size, iou_thresh):
self.num_classes = num_classes
self.num_anchors = num_anchors
self.iou_thresh = iou_thresh
self.anchor_boxes = np.array(
[[0., 0., anchor[0], anchor[1]]
for anchor in anchor_size]
) # [KA, 4]
def compute_iou(self, anchor_boxes, gt_box):
"""
函数功能: 计算目标框和9个先验框的IoU值
anchor_boxes : ndarray -> [KA, 4] (cx, cy, bw, bh).
gt_box : ndarray -> [1, 4] (cx, cy, bw, bh).
返回值: iou变量,类型为ndarray类型,shape为[9,], iou[i]就表示该目标框和第i个先验框的IoU值
"""
# 1、计算9个anchor_box的面积
# anchors: [KA, 4]
anchors = np.zeros_like(anchor_boxes)
anchors[..., :2] = anchor_boxes[..., :2] - anchor_boxes[..., 2:] * 0.5 # x1y1
anchors[..., 2:] = anchor_boxes[..., :2] + anchor_boxes[..., 2:] * 0.5 # x2y2
anchors_area = anchor_boxes[..., 2] * anchor_boxes[..., 3]
# 2、gt_box复制9份,计算9个相同gt_box的面积
# gt_box: [1, 4] -> [KA, 4]
gt_box = np.array(gt_box).reshape(-1, 4)
gt_box = np.repeat(gt_box, anchors.shape[0], axis=0)
gt_box_ = np.zeros_like(gt_box)
gt_box_[..., :2] = gt_box[..., :2] - gt_box[..., 2:] * 0.5 # x1y1
gt_box_[..., 2:] = gt_box[..., :2] + gt_box[..., 2:] * 0.5 # x2y2
gt_box_area = np.prod(gt_box[..., 2:] - gt_box[..., :2], axis=1)
# 3、计算计算目标框和9个先验框的IoU值
# intersection
inter_w = np.minimum(anchors[:, 2], gt_box_[:, 2]) - \
np.maximum(anchors[:, 0], gt_box_[:, 0])
inter_h = np.minimum(anchors[:, 3], gt_box_[:, 3]) - \
np.maximum(anchors[:, 1], gt_box_[:, 1])
inter_area = inter_w * inter_h
# union
union_area = anchors_area + gt_box_area - inter_area
# iou
iou = inter_area / union_area
iou = np.clip(iou, a_min=1e-10, a_max=1.0)
return iou
@torch.no_grad()
def __call__(self, fmp_sizes, fpn_strides, targets):
"""
fmp_size: (List) [fmp_h, fmp_w]
fpn_strides: (List) -> [8, 16, 32, ...] stride of network output.
targets: (Dict) dict{'boxes': [...],
'labels': [...],
'orig_size': ...}
"""
assert len(fmp_sizes) == len(fpn_strides)
# prepare
bs = len(targets)
gt_objectness = [
torch.zeros([bs, fmp_h, fmp_w, self.num_anchors, 1])
for (fmp_h, fmp_w) in fmp_sizes
]
gt_classes = [
torch.zeros([bs, fmp_h, fmp_w, self.num_anchors, self.num_classes])
for (fmp_h, fmp_w) in fmp_sizes
]
gt_bboxes = [
torch.zeros([bs, fmp_h, fmp_w, self.num_anchors, 4])
for (fmp_h, fmp_w) in fmp_sizes
]
# 第一层for循环遍历每一张图像
for batch_index in range(bs):
targets_per_image = targets[batch_index]
# [N,] N表示一个图像中有N个目标对象
tgt_cls = targets_per_image["labels"].numpy()
# [N, 4]
tgt_box = targets_per_image['boxes'].numpy()
# 第二层for循环遍历这张图像标签的每一个目标数据
for gt_box, gt_label in zip(tgt_box, tgt_cls):
# get a bbox coords
x1, y1, x2, y2 = gt_box.tolist()
# xyxy -> cxcywh
xc, yc = (x2 + x1) * 0.5, (y2 + y1) * 0.5
bw, bh = x2 - x1, y2 - y1
gt_box = [0, 0, bw, bh]
# check target
if bw < 1. or bh < 1.:
# invalid target
continue
# 1、计算该目标框和9个先验框的IoU值
# compute IoU
iou = self.compute_iou(self.anchor_boxes, gt_box)
iou_mask = (iou > self.iou_thresh)
# 2、基于先验框的标签分配策略
label_assignment_results = []
# 第一种情况:所有的IoU值均低于阈值,选择IoU最大的先验框
if iou_mask.sum() == 0:
# We assign the anchor box with highest IoU score.
iou_ind = np.argmax(iou)
# 确定选择的先验框在pyramid上的level及anchor index
level = iou_ind // self.num_anchors # pyramid level
anchor_idx = iou_ind - level * self.num_anchors # anchor index
# get the corresponding stride
stride = fpn_strides[level]
# compute the grid cell
# 计算该目标框在level尺度的网格坐标
xc_s = xc / stride
yc_s = yc / stride
grid_x = int(xc_s)
grid_y = int(yc_s)
# 存下网格坐标、尺度level以及anchor_idx
label_assignment_results.append([grid_x, grid_y, level, anchor_idx])
else:
# 第二种和第三种情况:至少有一个IoU值大于阈值
for iou_ind, iou_m in enumerate(iou_mask):
if iou_m:
level = iou_ind // self.num_anchors # pyramid level
anchor_idx = iou_ind - level * self.num_anchors # anchor index
# get the corresponding stride
stride = fpn_strides[level]
# compute the gride cell
xc_s = xc / stride
yc_s = yc / stride
grid_x = int(xc_s)
grid_y = int(yc_s)
label_assignment_results.append([grid_x, grid_y, level, anchor_idx])
# label assignment
# 获取到被标记为正样本的先验框,我们就可以为这次先验框对应的预测框制作学习标签
for result in label_assignment_results:
grid_x, grid_y, level, anchor_idx = result
fmp_h, fmp_w = fmp_sizes[level]
if grid_x < fmp_w and grid_y < fmp_h:
# objectness标签,采用0,1离散值(gt_objectness为list,存3个尺度的正样本)
gt_objectness[level][batch_index, grid_y, grid_x, anchor_idx] = 1.0
# classification标签,采用one-hot格式
cls_ont_hot = torch.zeros(self.num_classes)
cls_ont_hot[int(gt_label)] = 1.0
gt_classes[level][batch_index, grid_y, grid_x, anchor_idx] = cls_ont_hot
# box标签,采用目标框的坐标值
gt_bboxes[level][batch_index, grid_y, grid_x, anchor_idx] = torch.as_tensor([x1, y1, x2, y2])
# [B, M, C]
gt_objectness = torch.cat([gt.view(bs, -1, 1) for gt in gt_objectness], dim=1).float()
gt_classes = torch.cat([gt.view(bs, -1, self.num_classes) for gt in gt_classes], dim=1).float()
gt_bboxes = torch.cat([gt.view(bs, -1, 4) for gt in gt_bboxes], dim=1).float()
return gt_objectness, gt_classes, gt_bboxes
if __name__ == '__main__':
anchor_size = [[10, 13], [16, 30], [33, 23],
[30, 61], [62, 45], [59, 119],
[116, 90], [156, 198], [373, 326]]
matcher = Yolov3Matcher(iou_thresh=0.5, num_classes=20, anchor_size=anchor_size, num_anchors=3)
fmp_sizes = [torch.Size([52, 52]), torch.Size([26, 26]), torch.Size([13, 13])]
fpn_strides = [8, 16, 32]
targets = [
{
'boxes': torch.tensor([[120., 0., 408., 23.],
[160., 59., 416., 256.],
[172., 24., 218., 128.],
[408., 35., 416., 75.],
[ 0., 64., 8., 186.]]), # bbox的坐标(xmin, ymin, xmax, ymax
'labels': torch.tensor([ 6, 6, 14, 6, 19]), # 标签
'orig_size': [416, 416] # 图片的原始大小
},
{
'boxes': torch.tensor([[367., 255., 416., 416.],
[330., 302., 416., 416.]]),
'labels': torch.tensor([14, 13]),
'orig_size': [416, 416]
}
]
gt_objectness, gt_classes, gt_bboxes = matcher(fmp_sizes=fmp_sizes, fpn_strides=fpn_strides, targets=targets)
print(gt_objectness.shape)
print(gt_classes.shape)
print(gt_bboxes.shape)
我们现在已经知道,在多级检测框架时候,先验框自身尺度在标签分配环节起到了重要的作用。
自Faster R-CNN工作问世以来,anchor box几乎成为了大多数先进的目标检测器的标准配置之一。但是anchor box的缺陷也是十分明显的,比如以下几点:
但是,如果没有先验框,能否做多级检测呢?
没有先验框进行多级检测,即anchor-free架构,首先要解决哪个目标框应该被来自哪个尺度的预测框学习
,即多尺度标签匹配问题。
在2019年,FCOS检测器被提出,其最大的特点就是彻底抛去了一直以来的anchor box,那么FCOS如何解决多尺度匹配问题呢?
FCOS一共使用五个特征图 P3、P4、P5、P6和P7 ,其输出步长stride分别为 8、16、32、64和128。FCOS为这每一个尺度都设定了一个尺度范围,即对于特征图 P_i ,其尺度范围是 (m_i−1,m_i) ,这五个尺度范围分别为 (0,64) 、(64,128)、(128,256)、(256,512),以及(512,∞)。
首先,我们去遍历特征图Pi上的每一个anchor,假设每一个anchor的坐标为 (xs_a+0.5,ys_a+0.5) ,其中(xs_a,ys_a)为anchor的左上角点坐标,也就是我们以前熟悉的网格左上角坐标的概念,但我们又为之加上了0.5亚像素坐标,即网格的中心点。我们求出特征图P_i上的anchor在输入图像上的坐标 (x_a,y_a) ,计算公式如下所示: 若是目标框的尺寸偏小,那它内部的anchor就会更多地落在较小的范围内,比如: (0,64),反之,则会更多地落在较大的范围内,如: (256,512) 。 换言之,FCOS设置的五个范围本质上是一种和目标自身大小相关的尺度范围,是基于一种
x a = x s a ∗ s + s / 2 y a = y s a ∗ s + s / 2 x_a=xs_a∗s+s/2 \\ y_a=ys_a∗s+s/2 xa=xsa∗s+s/2ya=ysa∗s+s/2
然后,我们求出处在边界框内的每一个anchor
到边界框的四条边的距离:
l ∗ = x a − x 1 t ∗ = y a − y 1 r ∗ = x 2 − x a b ∗ = y 2 − y a l^∗=x_a−x_1 \\ t^∗=y_a−y_1 \\ r^∗=x_2−x_a \\ b^∗=y_2−y_a l∗=xa−x1t∗=ya−y1r∗=x2−xab∗=y2−ya
我们取其中的最大值 m=max(l∗,t∗,r∗,b∗) ,如果 m 满足 m_i−1小的目标框更应该让输出步长小的也就是更大的特征图去学习,大的目标框则应该让输出步长更大的特征图去学习
的直观理解。
但这个尺度还需要人工设计,没有摆脱人工先验的超参。
旷视科技在YOLOX种提出了SimOTA,摆脱了人工先验的超参,实现了真正意义的anchor-free,具体细节以后再讲。