前言
- 需要将图像进行归一化操作,即"/255",将数值从0~255转换到0~1
- OpenCV和PIL读取的图像shape为 h,w,c,需要将其转换为c,w,h;并且,当模型存在batch时,还需要在前面加上一维B,从而形成(b,c,w,h)的格式。
- OpenCV读取的图像颜色通道为(BGR),最好将其转换为RGB在投入模型(虽然有时候影响不是很大,但最好还是转换一下)。
- YOLOV3、tiny-YOLOV3在精度方面,前者优于后者;但在速率方面,听说后者更快,但在我的电脑并没有体现出来。
- 参考文章:https://blog.csdn.net/chandanyan8568/article/details/81089083、https://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
- 在model内部,没有直接使用img_size这个变量,而是通过.shape()函数读取等来的;
- 唯一使用此变量的地方是在读取数据集函数ListDataset;
- 唯一定义此变量的地方就在开头的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、一些变量说明:
- bw, bh - target box的w/h;
- pw, ph - 与target box的IoU最大的那个预设边界框anchor box在特征图上的w/h
- tx, ty - target box中心位置在其对应cell中的比例坐标
- σ() - sigmod函数,目的是将预测偏移量缩放到0到1之间(这样能够将预设边界框的中心坐标固定在一个cell当中,作者说这样能够加快网络收敛)
- Cx, Cy - 该网格区域的下标号
- 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的相应数据: 分别表示此框内物体是对应类别的可能性
经过预处理后,各数据的意义:
- x, y - 中心位置,0~1(相对于gride cell)
- w,h - 宽高比,>=0(这时的比例很奇怪,因为 exp(w) 才是pre_box相对于anchor_w的比例系数)
- pred_conf - 存在目标的置信度,0~1
- 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上的边框高度
转换后,各数据的意义:
- grid_x/y - feature map中各网格点的相对偏移量
- pred_boxes[…, 0:0] - 中心位置,0~gride(在feature map上的绝对坐标)
- anchor_w/h - anchor在feature map上对应的w/h
- pred_boxes[…, 2:4] - 预测边框在feature map上对应的w/h值
- 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,
)
各数据的意义:
- self.stride=img_size/img_gride - feature mpa相对于原图的缩放比例,即为
32/16/8
- pred_boxes.view(num_samples, -1, 4) * self.stride - 转换为原始图像中具体的中心坐标值和边框的宽/高值
- 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计算
前言
- YOLOv3中的loss总共包含3个部分:bbox部分的loss、目标是否存在的置信度loss、分类的loss。即:
total_loss = (loss_x + loss_y + loss_w + loss_h) + (loss_conf) + (loss_cls)
obj_mask
维度:[B, 3, gride, gride],只有与目标IoU最大的那个anchor且中心坐标对应的地方置"1";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
- 问:为什么此处的多分类问题使用二值交叉熵损失函数?
答:把多分类任务当做class_num个二分类任务来处理,当且仅当所有的分类都预测对时,loss最小。
3.3 predict
前言
- 预测时,把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 文件最后一定要多出一行
疑惑
- 对于52*52的yolo层,因为使用的是小尺寸的anchor,这样不会导致大目标在此处的loss很大而影响最后的收敛吗?