YOLOv3调试心得

前言

  1. 需要将图像进行归一化操作,即"/255",将数值从0~255转换到0~1
  2. OpenCV和PIL读取的图像shape为 h,w,c,需要将其转换为c,w,h;并且,当模型存在batch时,还需要在前面加上一维B,从而形成(b,c,w,h)的格式。
  3. OpenCV读取的图像颜色通道为(BGR),最好将其转换为RGB在投入模型(虽然有时候影响不是很大,但最好还是转换一下)。
  4. YOLOV3、tiny-YOLOV3在精度方面,前者优于后者;但在速率方面,听说后者更快,但在我的电脑并没有体现出来。
  5. 参考文章:https://blog.csdn.net/chandanyan8568/article/details/81089083https://blog.csdn.net/leviopku/article/details/82660381

一、制作自己的数据集(VOC数据集方式)

1、新建文件夹:

-VOCdevkit
 --VOC2018
  —Annotations
  —ImageSets
   ----Layout
   ----Main
  —JPEGImages
  —labels
  —test.py
 --2018_train.txt
 --2018_val.txt
 --vol_label.py
下面分析一下各个文件夹和文件的作用:
-VOCdevkit
 --VOC2018 存放此年份的数据集信息
  —Annotations 存放xml文件
  —ImageSets
   ----Layout 未使用
   ----Main 存放分类后的train/val/test数据集的每个数据图像的名称(运行test.py后自动生成)
  —JPEGImages 真正用来存放数据图像的文件夹
  —labels 存放用于YOLOv3识别的标签(运行test.py后自动生成,每行为带后缀的图像名称;包含train/val/test中所有数据图像的label)
  —test.py 用于将xml转换为YOLOv3识别的txt文件
 --2018_train.txt 存放train数据集中的数据的路径信息
 --2018_val.txt 存放val数据集中的数据的路径信息
 --vol_label.py 用于生成2018_train/val.txt文件

2、关于辅助工具和几个.py的使用

1、标签labels生成工具: 精灵标注助手
1)安装后的样子如下,中间是我建立的两个项目。
精灵标注助手
2)点击“新建”,根据的自己的需求选择左侧不同的功能、文件夹路径、分类值;然后点击“创建”
在这里插入图片描述
3)进入项目后,就可进行位置标注,可以选择“矩形框、曲线框、多边形框”;
每张图像位置标注后,点击下方的“对勾”和选定后侧的分类信息后才视为本图像的标注完成;
在这里插入图片描述
4)当所有图像标注完成后,点击上图左侧“导出”,即可跳出下图选项,选择默认选项即可生成与图像名字对应的xml文件。
注:即使没标注的图像,也会生成相应的xml文件,可通过后续的.py文件去掉这些无效的xml文件。
在这里插入图片描述
2、test.py
用途:分别选取train/val/test数据集的图像;
生成文件:train.txt/val.txt/test.txt。

根据上面生成的xml文件,将它们对应的图像名字写入train.txt/val.txt/test.txt文件中,所以txt文件中存在是图像名称(无后缀名,每个名字独占一行),如“photo_0001”。

import os
import random

trainval_percent = 0.15     # 设置验证集的比例
train_percent = 0.85
xmlfilepath = 'Annotations'
txtsavepath = 'ImageSets\Main'
total_xml = os.listdir(xmlfilepath)

num = len(total_xml)
list = range(num)
tv = int(num * trainval_percent)
tr = int(tv * train_percent)
val = random.sample(list, tv)       # 随机选择验证集的图像
train = random.sample(val, tr)

ftest = open('ImageSets/Main/test.txt', 'w')
ftrain = open('ImageSets/Main/train.txt', 'w')
fval = open('ImageSets/Main/val.txt', 'w')

for i in list:
    name = total_xml[i][:-4] + '\n'
    if i in val:
        fval.write(name)    # 设置验证集,将选为验证集的图像名称写入valid.txt中
        # if i in train:
        #     ftest.write(name)
        # else:
        #     fval.write(name)
    else:
        ftrain.write(name)  # 设置训练集

ftrain.close()
fval.close()
ftest.close()

3、voc_label.py
用途:将xml文件中的label转换为YOLO能够识别的label,并将分类完成的数据集的路径写入对应txt文件中
生成文件:文件夹labels、2018_train.txt、2018_val.txt

通过程序中的 try…except…来剔除掉为进行标记的数据

import xml.etree.ElementTree as ET
import pickle
import os
from os import listdir, getcwd
from os.path import join

sets = [('2018', 'train'), ('2018', 'val')]      # 根据建立文件夹的名字,和所需要处理的数据集更改
# classes = ["aeroplane", "bicycle", "bird", "boat", "bottle"]
classes = ["tube"]  # (改!)自己要测的目标类别


def convert(size, box):
    dw = 1. / (size[0])
    dh = 1. / (size[1])
    x = (box[0] + box[1]) / 2.0 - 1
    y = (box[2] + box[3]) / 2.0 - 1
    w = box[1] - box[0]
    h = box[3] - box[2]
    x = x * dw
    w = w * dw
    y = y * dh
    h = h * dh
    return (x, y, w, h)


def convert_annotation(year, image_id):
    in_file = open('VOC%s/Annotations/%s.xml' % (year, image_id))  # (改!)自己的图像标签xml文件的路径
    tree = ET.parse(in_file)  # 直接解析xml文件
    root = tree.getroot()  # 获取xml文件的根节点
    try:		# 尝试读取xml中的标签信息
        size = root.find('size')  # 获取指定节点“图像尺寸”
        w = int(size.find('width').text)  # 获取图像宽
        h = int(size.find('height').text)  # 获取图像高
    except:	# 如果查找不到,则return False
        return False

    print(image_id)
    out_file = open('VOC%s/labels/%s.txt' % (year, image_id), 'w')  # (改!)自己的图像标签txt文件要保存的路径
    for obj in root.iter('object'):
        difficult = obj.find('difficult').text  # xml里的difficult参数
        cls = obj.find('name').text  # 要检测的类别名称name
        if cls not in classes or int(difficult) == 1 or cls is None:
            continue
        cls_id = classes.index(cls)
        xmlbox = obj.find('bndbox')
        b = (float(xmlbox.find('xmin').text), float(xmlbox.find('xmax').text), float(xmlbox.find('ymin').text),
             float(xmlbox.find('ymax').text))
        bb = convert((w, h), b)
        out_file.write(str(cls_id) + " " + " ".join([str(a) for a in bb]) + '\n')
    return True

# 获取当前文件的路径
wd = getcwd()
print(wd)

for year, image_set in sets:
    # 用于检查是否存在保存目标文件的文件夹,没有则新建此文件夹
    if not os.path.exists('./VOC%s/labels/' % (year)):
        os.makedirs('./VOC%s/labels/' % (year))
    # 用VOC数据集的话,是将VOCdevkit/VOC2007/ImageSets/Main/文件夹下的所有txt都循环读入了
    # 这里我只读入所有待训练图像的路径train.txt
    image_ids = open('./VOC%s/ImageSets/Main/%s.txt' % (year, image_set)).read().strip().split()
    # 保存数据集的绝对路径至一个txt文件中
    list_file = open('%s_%s.txt' % (year, image_set), 'w')
    for image_id in image_ids:
        res = convert_annotation(year, image_id)
        if res:	# 只有xml文件中存在正确的label时,才将该数据路径写入相应的txt中
            list_file.write('VOCdevkit/VOC%s/JPEGImages/%s.jpg\n' % (year, image_id))
        # image_id = os.path.split(image_id)[1]  # image_id内容类似'0001.jpg'
        # image_id2 = os.path.splitext(image_id)[0]  # image_id2内容类似'0001'
    list_file.close()

4、至此,数据集以处理完毕,下面就是投入到model中进行训练

二、训练

2.1 参数说明

2.1.1 图像尺寸img_size

  1. 在model内部,没有直接使用img_size这个变量,而是通过.shape()函数读取等来的;
  2. 唯一使用此变量的地方是在读取数据集函数ListDataset;
  3. 唯一定义此变量的地方就在开头的paras中(或是在外部执行文件时使用 --epochs=64来更改)。

2.2 参数更改

2.2.1 类别数目(必须修改)

yolov3 需要更改三处;
tiny 需要修改的有两处。
每一处需要修改两个地方:分别在1)[yolo] 层的class = **,修改为实际类别数 ;2)[yolo]层上面的[conv]层中的filter,修改为 (class+5)*3,必须为数字,不能为公式。

2.2.2 batch_size、epoches、

上面这些数据,如果直接在程序中存在,则不会使用.cfg中的参数。

三、YOLO层解析

3.0 说明

1、YOLOv3总共有三个yolo层,每个yolo层对应一种尺度,分别为:输入图像尺寸的1/32、1/16、1/8(原文中分别为13/26/52,因为输入图像为214)。
2、整个YOLO共有9个anchor,分配后每个yolo层有3个anchor。
3、yolo层的输入数据维度:[B, C, G, G] -> [Batch, Channel, gride_w, gride_h];
4、一些变量说明:
在这里插入图片描述

  1. bw, bh - target box的w/h;
  2. pw, ph - 与target box的IoU最大的那个预设边界框anchor box在特征图上的w/h
  3. tx, ty - target box中心位置在其对应cell中的比例坐标
  4. σ() - sigmod函数,目的是将预测偏移量缩放到0到1之间(这样能够将预设边界框的中心坐标固定在一个cell当中,作者说这样能够加快网络收敛)
  5. Cx, Cy - 该网格区域的下标号
  6. gride cell - 将图像划分为网格后的一块儿区域。注:“划分网格”是一种被动操作,每个cell是通过卷积网络缩放后的feature map中像素点自动对应得到的。

6、labels中的数据说明:
注:(x,y,w,h) 都是相对于整个图像进行的归一化。
在这里插入图片描述

3.1 数据处理

3.1.1 相对于gride cell的坐标(比例系数)

prediction = (
			  x.view(num_samples, self.num_anchors, self.num_classes + 5, grid_size, grid_size)
			  .permute(0, 1, 3, 4, 2)
			  .contiguous()
			)
# Get outputs
x = torch.sigmoid(prediction[..., 0])  # Center x
y = torch.sigmoid(prediction[..., 1])  # Center y
w = prediction[..., 2]  # Width
h = prediction[..., 3]  # Height
pred_conf = torch.sigmoid(prediction[..., 4])  # Conf
pred_cls = torch.sigmoid(prediction[..., 5:])  # Cls pred.

将yolo的输入x重新整合为:prediction = [B, 3, w, h, (5 + class_num)]
# 其中,3表示每个yolo层有3个anchor
# 5表示 center_x, center_y, w, h, obj_conf
# class_num的相应数据: 分别表示此框内物体是对应类别的可能性

经过预处理后,各数据的意义:

  1. x, y - 中心位置,0~1(相对于gride cell)
  2. w,h - 宽高比,>=0(这时的比例很奇怪,因为 exp(w) 才是pre_box相对于anchor_w的比例系数)
  3. pred_conf - 存在目标的置信度,0~1
  4. pred_cls - 此框内物体是对应类别的置信度

3.1.2 feature map上的bbox(绝对坐标)

# If grid size does not match current we compute new offsets
if grid_size != self.grid_size:
    self.compute_grid_offsets(grid_size, cuda=x.is_cuda)

# Add offset and scale with anchors
pred_boxes = FloatTensor(prediction[..., :4].shape)
pred_boxes[..., 0] = x.data + self.grid_x
pred_boxes[..., 1] = y.data + self.grid_y
pred_boxes[..., 2] = torch.exp(w.data) * self.anchor_w      # 处理后的结果为在feature map上的边框宽度
pred_boxes[..., 3] = torch.exp(h.data) * self.anchor_h      # 处理后的结果为在feature map上的边框高度

转换后,各数据的意义:

  1. grid_x/y - feature map中各网格点的相对偏移量
  2. pred_boxes[…, 0:0] - 中心位置,0~gride(在feature map上的绝对坐标)
  3. anchor_w/h - anchor在feature map上对应的w/h
  4. pred_boxes[…, 2:4] - 预测边框在feature map上对应的w/h值
  5. compute_grid_offsets(grid_size, cuda=x.is_cuda) - 此函数就是为了准确的获得每个feature map中的gride_x/y、anchor_w/h

3.1.3 原图上的bbox(绝对坐标)

output = torch.cat(
		      (
		             pred_boxes.view(num_samples, -1, 4) * self.stride,
		             pred_conf.view(num_samples, -1, 1),
		             pred_cls.view(num_samples, -1, self.num_classes),
		         ),
		         -1,
		     )

各数据的意义:

  1. self.stride=img_size/img_gride - feature mpa相对于原图的缩放比例,即为32/16/8
  2. pred_boxes.view(num_samples, -1, 4) * self.stride - 转换为原始图像中具体的中心坐标值和边框的宽/高值
  3. torch.cat() - 重新将这些数据组合为[B, 3, w, h, (4 + 1 + class_num)]的格式

3.1.4 输出

yolo层最后的输出值:
4 – bbox:(x, y, w, h),bbox的中心坐标和w/h值,如:左上角坐标值 x1= bbox[0] - bbox[2] / 2
1 – pred_conf,此gride含有目标的置信度
num_classes – pred_cls,每个类别的置信度

3.1.5 ELSE

1、为什么会有相对于(像素)点的比例系数?
答:因为feature map中,像素点即为gride cell,放大后为原始图像中的一块儿区域。这些像素点的坐标值,表示将原始图像划分为gride*gride的网格后每个gride cell(左上角)的坐标。

3.2 Loss计算

前言

  1. YOLOv3中的loss总共包含3个部分:bbox部分的loss、目标是否存在的置信度loss、分类的loss。即:total_loss = (loss_x + loss_y + loss_w + loss_h) + (loss_conf) + (loss_cls)
  2. obj_mask 维度:[B, 3, gride, gride],只有与目标IoU最大的那个anchor且中心坐标对应的地方置"1";
  3. noobj_mask 非obj_mask且Iou<threshold的地方置"1"。

3.2.1 bbox

MSE_Loss:只计算obj_mask对应位置处的(x, y, w, h)的loss

3.2.2 目标是否存在的置信

1、存在地方(obj_mask)的置信度loss(BCE_Loss):target = 1
2、不存在目标地方(noobj_mask)的置信度loss(BCE_Loss):target = 0

3.2.3 分类

BCE_Loss:只计算obj_mask对应地方的分类loss

  1. 问:为什么此处的多分类问题使用二值交叉熵损失函数?
    答:把多分类任务当做class_num个二分类任务来处理,当且仅当所有的分类都预测对时,loss最小。

3.3 predict

前言

  1. 预测时,把3个yolo层的输出torch.cat()后,进行NMS处理,即为最终的输出。

踩到的坑:

1、加载(而非制作)数据集的时候,使用Dataloader函数一直报错:

原因:在读取label时,读取的文件不存在。
解决:

# datasets.py-65行
# path.replace("images", "labels").replace(".png", ".txt").replace(".jpg", ".txt")	#改为下面的内容
path.replace("JPEGImages", "labels").replace(".png", ".txt").replace(".jpg", ".txt")

因为原工程中图像存在于“image”,标签存在于“labels”;而自己制作的数据使用VOC的方式,图像数据在“JPEGImages”文件夹中,label存储在“labels”文件夹中。

2、训练集中的每一张图像都必须存在标注(即图像中至少存在一类对象)

解决:已通过上述的py文件剔除为进行任何标记的数据。

3、如果数据集的类别大于80,则不能使用预训练数据yolov3.weights等

4、.names 文件最后一定要多出一行

在这里插入图片描述

疑惑

  1. 对于52*52的yolo层,因为使用的是小尺寸的anchor,这样不会导致大目标在此处的loss很大而影响最后的收敛吗?
  • 1
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值