YOLOV5详细解读

YOLOV5检测算法详解

学习前言

本文主要是对基于深度学习的目标检测算法进行细节解读,以YOLOV5为例;

基于深度学习的目标检测整体流程

基于深度学习的目标检测主要包括训练和测试两个部分。
YOLOV5详细解读_第1张图片
YOLOV5详细解读_第2张图片

训练阶段

训练的目的是利用训练数据集进行检测网络的参数学习,其中训练数据集包含大量的视觉图像和标注信息(物体位
置及类别)。训练阶段的主要过程包括数据预处理、检测网络以及标签匹配与损失计算等部分。

1.数据预处理

数据预处理的目的在于增强训练数据多样性,进而提升检测网络的检测能力。
YOLOV5所采用的预处理方式主要有:翻转、缩放、扭曲、色域变换、Mosaic
翻转:

image = image.transpose(Image.FLIP_LEFT_RIGHT)  #利用PIL库对图片直接翻转
box[:, [0,2]] = iw - box[:, [2,0]] #翻转图片后要对目标框同时进行调整

缩放:

#由于实际图像w和h不是相等的,所以采用不失真的resize,将长边resize到和输入尺寸一样大小,然后其余部分
放上灰条

scale = min(w/iw, h/ih) #iw、ih是数据集中图像实际尺寸,w,h为网络输入的图像尺寸,scale为图像缩放因子
nw = int(iw*scale) #图像宽缩放后尺寸
nh = int(ih*scale) #图像长缩放后尺寸
dx = (w-nw)//2  #缩放后图像放在灰度图像上的位置
dy = (h-nh)//2 #缩放后图像放在灰度图像上的位置
image   = image.resize((nw,nh), Image.BICUBIC)  #将输入图像插值到实际缩放后的尺寸大小
new_image   = Image.new('RGB', (w,h), (128,128,128)) #生成一个三通的,大小为(w,h)的灰度图像
new_image.paste(image, (dx, dy)) #将缩放后的实际图像放在灰度图像 (dx, dy)的位置上
image_data  = np.array(new_image, np.float32) #再转换成数组格式

扭曲:

new_ar = iw/ih * self.rand(1-jitter,1+jitter) / self.rand(1-jitter,1+jitter) #iw、ih是数据集中图像实际尺寸,jitter扭曲因子
scale = self.rand(.25, 2)
if new_ar < 1:
   nh = int(scale*h)
   nw = int(nh*new_ar)
else:
   nw = int(scale*w)
   nh = int(nw/new_ar)
   image = image.resize((nw,nh), Image.BICUBIC)

色域变换:

r  = np.random.uniform(-1, 1, 3) * [hue, sat, val] + 1
hue, sat, val   = cv2.split(cv2.cvtColor(image_data, cv2.COLOR_RGB2HSV)) #j将图片转换成HSV格式,再把每个通道分离开来
dtype  = image_data.dtype
x  = np.arange(0, 256, dtype=r.dtype)
lut_hue = ((x * r[0]) % 180).astype(dtype)
lut_sat = np.clip(x * r[1], 0, 255).astype(dtype)
lut_val = np.clip(x * r[2], 0, 255).astype(dtype)
image_data = cv2.merge((cv2.LUT(hue, lut_hue), cv2.LUT(sat, lut_sat), cv2.LUT(val, lut_val))) image_data = cv2.cvtColor(image_data, cv2.COLOR_HSV2RGB)

Mosaic:

train_annotation_path = '1.txt'
with open(train_annotation_path, encoding='utf-8') as f:
    train_lines = f.readlines()
jitter = 0.3
h, w = [640,640]
min_offset_x = rand(0.3, 0.7)
min_offset_y = rand(0.3, 0.7)
image_datas = []
box_datas   = []
index       = 0
lines = sample(train_lines, 3)
lines.append(train_lines[index])
shuffle(lines)  #从训练集中随机取4张图片进行拼接
for line in lines:
    line_content = line.split()
    image = Image.open(line_content[0])
    image = cvtColor(image)
    iw, ih = image.size
    box = np.array([np.array(list(map(int,box.split(',')))) for box in line_content[1:]])
    new_ar = iw / ih * rand(1 - jitter, 1 + jitter) / rand(1 - jitter, 1 + jitter)
    scale = rand(.4, 1)
    if new_ar < 1:
        nh = int(scale * h)
        nw = int(nh * new_ar)
    else:
        nw = int(scale * w)
        nh = int(nw / new_ar)
    image = image.resize((nw, nh), Image.BICUBIC)
    if index == 0:  #分别计算出四张图片分别摆放的位置
        dx = int(w * min_offset_x) - nw
        dy = int(h * min_offset_y) - nh
    elif index == 1:
        dx = int(w * min_offset_x) - nw
        dy = int(h * min_offset_y)
    elif index == 2:
        dx = int(w * min_offset_x)
        dy = int(h * min_offset_y)
    elif index == 3:
        dx = int(w * min_offset_x)
        dy = int(h * min_offset_y) - nh
    new_image = Image.new('RGB', (w, h), (128, 128, 128))
    new_image.paste(image, (dx, dy))
    image_data = np.array(new_image)

    index = index + 1
    box_data = []
    if len(box) > 0:  #对box重新进行处理,超出边界,都要将其限制在图像里面
        np.random.shuffle(box)
        box[:, [0, 2]] = box[:, [0, 2]] * nw / iw + dx
        box[:, [1, 3]] = box[:, [1, 3]] * nh / ih + dy
        box[:, 0:2][box[:, 0:2] < 0] = 0
        box[:, 2][box[:, 2] > w] = w
        box[:, 3][box[:, 3] > h] = h
        box_w = box[:, 2] - box[:, 0]
        box_h = box[:, 3] - box[:, 1]
        box = box[np.logical_and(box_w > 1, box_h > 1)]
        box_data = np.zeros((len(box), 5))
        box_data[:len(box)] = box

    image_datas.append(image_data)
    box_datas.append(box_data)
cutx = int(w * min_offset_x)
cuty = int(h * min_offset_y)
new_image = np.zeros([h, w, 3])
new_image[:cuty, :cutx, :] = image_datas[0][:cuty, :cutx, :]
new_image[cuty:, :cutx, :] = image_datas[1][cuty:, :cutx, :]
new_image[cuty:, cutx:, :] = image_datas[2][cuty:, cutx:, :]
new_image[:cuty, cutx:, :] = image_datas[3][:cuty, cutx:, :] 
new_image       = np.array(new_image, np.uint8)
merge_bbox = []
for i in range(len(bboxes)): #在四张拼接图上面对框进行调整,防止其超出界限
    for box in bboxes[i]:
          tmp_box = []
          x1, y1, x2, y2 = box[0], box[1], box[2], box[3]

          if i == 0:
             if y1 > cuty or x1 > cutx:
                  continue
             if y2 >= cuty and y1 <= cuty:
                        y2 = cuty
             if x2 >= cutx and x1 <= cutx:
                        x2 = cutx

           if i == 1:
                    if y2 < cuty or x1 > cutx:
                        continue
                    if y2 >= cuty and y1 <= cuty:
                        y1 = cuty
                    if x2 >= cutx and x1 <= cutx:
                        x2 = cutx

            if i == 2:
                    if y2 < cuty or x2 < cutx:
                        continue
                    if y2 >= cuty and y1 <= cuty:
                        y1 = cuty
                    if x2 >= cutx and x1 <= cutx:
                        x1 = cutx

             if i == 3:
                    if y1 > cuty or x2 < cutx:
                        continue
                    if y2 >= cuty and y1 <= cuty:
                        y2 = cuty
                    if x2 >= cutx and x1 <= cutx:
                        x1 = cutx
             tmp_box.append(x1)
             tmp_box.append(y1)
             tmp_box.append(x2)
             tmp_box.append(y2)
             tmp_box.append(box[-1])
             merge_bbox.append(tmp_box)

2.检测网络

检测网络一般包括主干特征提取网络、特征融合网络以及预测网络

主干特征提取网络
YOLOV5采用CSPDarknet作为特征提取网络,结构如图所示:
YOLOV5详细解读_第3张图片
(1)Focus结构
实际上就是矩阵的切片索引操作,在每个通道上的w和h方向上分别每隔一个像素点进行取值,最终一个通道图像变成四个通道图像,最终3通道变成12通道,将图片空间信息转换到通道维度,
YOLOV5详细解读_第4张图片

class Focus(nn.Module):
    def __init__(self, c1, c2, k=1, s=1, p=None, g=1, act=True):  # ch_in, ch_out, kernel, stride, padding, groups
        super(Focus, self).__init__()
        self.conv = Conv(c1 * 4, c2, k, s, p, g, act)

    def forward(self, x):
        return self.conv(torch.cat([x[..., ::2, ::2], x[..., 1::2, ::2], x[..., ::2, 1::2], x[..., 1::2, 1::2]], 1))

(2)Csplayer结构
借鉴了CSPnet结构,实际上是残差结构里面嵌套着残差结构,self.cv2是是大残差边,self.m是嵌套的残差结构,

class C3(nn.Module):
    # CSP Bottleneck with 3 convolutions
    def __init__(self, c1, c2, n=1, shortcut=True, g=1, e=0.5):  # ch_in, ch_out, number, shortcut, groups, expansion
        super(C3, self).__init__()
        c_ = int(c2 * e)  # hidden channels
        self.cv1 = Conv(c1, c_, 1, 1)
        self.cv2 = Conv(c1, c_, 1, 1)
        self.cv3 = Conv(2 * c_, c2, 1)  # act=FReLU(c2)
        self.m = nn.Sequential(*[Bottleneck(c_, c_, shortcut, g, e=1.0) for _ in range(n)])

    def forward(self, x):
        return self.cv3(torch.cat((self.m(self.cv1(x)), self.cv2(x)), dim=1))

class Bottleneck(nn.Module):
    # Standard bottleneck
    def __init__(self, c1, c2, shortcut=True, g=1, e=0.5):  # ch_in, ch_out, shortcut, groups, expansion
        super(Bottleneck, self).__init__()
        c_ = int(c2 * e)  # hidden channels
        self.cv1 = Conv(c1, c_, 1, 1)
        self.cv2 = Conv(c_, c2, 3, 1, g=g)
        self.add = shortcut and c1 == c2

    def forward(self, x):
        return x + self.cv2(self.cv1(x)) if self.add else self.cv2(self.cv1(x))

(3)SPP结构
通过不同池化核大小的最大池化进行特征提取,提高网络的感受野。

class SPP(nn.Module):
    # Spatial pyramid pooling layer used in YOLOv3-SPP
    def __init__(self, c1, c2, k=(5, 9, 13)):
        super(SPP, self).__init__()
        c_ = c1 // 2  # hidden channels
        self.cv1 = Conv(c1, c_, 1, 1)
        self.cv2 = Conv(c_ * (len(k) + 1), c2, 1, 1)
        self.m = nn.ModuleList([nn.MaxPool2d(kernel_size=x, stride=1, padding=x // 2) for x in k])

    def forward(self, x):
        x = self.cv1(x)
        return self.cv2(torch.cat([x] + [m(x) for m in self.m], 1))

YOLOV5详细解读_第5张图片

特征聚合网络
YOLOV5是采用FPN+PAN的结构进行特征聚合,将主干网络后三部分提取的特征层进行聚合,浅层特征通过下采样,与深层特征进行拼接,深层特征也会采用上采样,与浅层特征进行拼接,同时还会采用借鉴CSPnet设计的CSP2结构,加强网络特征融合的能力。

预测网络
YOLOV5采用一个卷积层得到最后的结果

yolo_head = nn.Conv2d(c1, len(anchors_mask[2]) * (5 + num_classes), 1) #最后的输出通道数为3个特征层上的预测结果,包括预测框四个参数、置信度、,所有类别的概率

3.标签分配与损失计算

标签分配主要是为检测器预测提供真实值,在目标检测中,标签分配的准则包括交并比准则、距离准则、似然估计准则、二分匹配。然后基于标签分类的结果,采用损失函数计算分类和回归等任务的损失,并利用反向传播算法更新检测网络的权重,常用的分类损失函数有交叉熵损失函数、聚焦损失函数、平滑L1损失函数‘、交并比IOU损失函数、GIOU损失函数

YOLOV5
训练时正样本的选取分为两步:寻找最优先验框;匹配特征点;
寻找最优先验框
YOLOV5在三个特征层上设置了9个先验框,用这9个先验框和GT进行宽高比的计算,将先验框宽高除以GT宽高,同时GT宽高除以先验框宽高,取二者中的大值,然后将这9个比值和提前设置好的阈值进行对比,小于阈值,就说明该先验框的大小是和真实框比较接近的,可以用来作为正样本进行训练的

匹配特征点
我们在上一步选取了最优先验框,确定好了尺寸,但是还没有确定先验框的位置,所以我们计算真实框落在哪个网格内,则该网格的左上角特征点则为一个负责预测的特征点,同时为了增加正样本的数量,找出离真实框中心点最近的两个网格,因此一个真实框会对应三个特征点,每个特征点上的先验框大小由上一步确定。

损失计算
损失由三部分组成,回归损失、置信度损失、分类损失;
回归损失:利用网络得到的调整参数,对之前取得的先验框进行就算修正,得到预测框,利用真实框和预测框计算IOU损失;

置信度损失:根据特征点和正负样本是否包含物体计算交叉熵损失;

分类损失:根据真实框的种类和预测结果的种类计算交叉熵损失;

测试阶段

将测试图像输入训练好的检测网络中,得到预测结果,然后进行解码、非极大值抑制等后处理操作,最终识别出图像中存在物体的类别及位置信息;

NMS
非极大值抑制实际上跟冒泡排序原理是一样的,只不过,nms要先取出某一类别的置信度最大的那个预测框,然后将它和其他剩下的预测框进行IOU计算,如果重叠较大,则将该预测框删掉,如果重叠较小,也会同时将该框进行输出;知道所有重叠较大的预测框被剔除掉;

参考了Bubbliiiing大佬的代码和博客,十分感谢

你可能感兴趣的:(计算机视觉,目标检测,深度学习)