YOLO v4和YOLO-Fastest在Darknet上的训练

简单的一批, 就是电脑性能不太够用。

安装Darknet

https://github.com/AlexeyAB/darknet

反正我是在配置好cuda和cudnn, visual studio C++, visual studio English Package, vcpkg之后
直接挂上梯子

1
vcpkg install darknet[opencv-base,cuda,cudnn]:x64-windows

就完事的。

如果遇到问题, 可以参考这篇博客
https://uint128.com/2021/02/13/%E8%88%B9%E8%88%B6%E7%9B%AE%E6%A0%87%E6%A3%80%E6%B5%8B-%E4%BD%BF%E7%94%A8Darknet%E8%AE%AD%E7%BB%83yolo%E6%A8%A1%E5%9E%8B%E7%9A%84%E8%BF%87%E7%A8%8B%E8%AE%B0%E5%BD%95/

数据集转换

现在比较流行的数据集无非就那几种: VOC, tfrecord等。
个人比较推荐一款数据集标注软件: VoTT, 如果想要自己标注的话建议使用这一个工具, 因为他标注一次能导出很多种格式。
用的比较多的是VOC, VoTT也支持VOC, 但是不支持Darknet所需要的 YOLO Mark 格式。
如果没有什么类似于裁剪数据集的特殊需求, 直接用Darknet自带的转化脚本就行了(忘了是哪个了)。

转化数据集以VOC举例。
但是正如上一篇文章附带的Video, 我们正在做一个垃圾分类的项目。
所以我需要找到并优化一个垃圾分类的数据集。
这里推荐几个好用的网址
https://aistudio.baidu.com/aistudio/datasetoverview

我使用的是华为的一个比赛的数据集
https://aistudio.baidu.com/aistudio/datasetdetail/70484

这个数据集一共有这么多种垃圾:

1
['书籍纸张', '金属厨具', '砧板', '污损塑料', '筷子', '陶瓷器皿', '洗护用品', '塑料玩具', '鞋', '果皮果肉', '玻璃器皿', '毛绒玩具', '污损用纸', '插头电线', '塑料器皿', '纸盒纸箱', '花盆', '包', '金属器皿', '干电池', '调料瓶', '菜帮菜叶', '锅', '食用油桶', '饮料瓶', '充电宝', '易拉罐', '牙签', '剩饭剩菜', '大骨头', '鱼骨', '垃圾桶', '酒瓶', '金属食品罐', '一次性快餐盒', '烟蒂', '旧衣服', '塑料衣架', '枕头', '过期药物', '茶叶渣', '软膏', '蛋壳', '快递纸袋']

看了一下, 这个数据集里面还有其他人添加的种类, 有些能合并到一起的label没有被合并, 有些label的质量太差。
根据我们的比赛需求

1
2
3
4
1)有害垃圾:电池(1号、2号、5号)
2)可回收垃圾:易拉罐、小号矿泉水瓶
3)厨余垃圾:完整或切割过的水果、蔬菜
4)其他垃圾:砖瓦陶瓷、烟头等

我只需要其中的

干电池 陶瓷器皿 易拉罐 金属食品罐 烟蒂 果皮果肉 菜帮菜叶 饮料瓶

然后把 易拉罐 金属食品罐 这两个合并即可。

所以我就用了Python的字典来完成这个任务

点击展开转换脚本 这个脚本请去

更新

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
# _*_ coding:utf-8 _*_
import xml.etree.ElementTree as ET
import os
import shutil
import random

classes = {"干电池" : 0, "陶瓷器皿" : 1, "易拉罐" : 2, "金属食品罐" : 2, "烟蒂" : 3, "果皮果肉" : 4, "菜帮菜叶" : 5, "饮料瓶" : 6}

Base_dir = "E:/TensorFlow/datasets/Garbage_VOC2007/"

Blank_probability = 0.05703599 #7种则为 610张
Blank_probability *= 1.25

def convert_annotation(image_id):
in_file = open('%sAnnotations/%s.xml' % (Base_dir, image_id),encoding='UTF-8')

tree = ET.parse(in_file)
root = tree.getroot()
size = root.find('size')
for obj in root.iter('object'):


cls = obj.find('name').text
if(cls in classes):
out_file = open('./data/%s.txt' % image_id, 'a')
cls_id = classes[cls]
xmlbox = obj.find('bndbox')
xmin = float(xmlbox.find('xmin').text) / float(size.find('width').text)
xmax = float(xmlbox.find('xmax').text) / float(size.find('width').text)
ymin = float(xmlbox.find('ymin').text) / float(size.find('height').text)
ymax = float(xmlbox.find('ymax').text) / float(size.find('height').text)

x_center = (xmin + xmax) / 2
y_center = (ymin + ymax) / 2
width = xmax - xmin
height = ymax - ymin

out_file.write(str(cls_id) + " " + str(x_center) + " " + str(y_center) +" "+str(width)+" "+str(height) + "\n")

# 拷贝图像
shutil.copy(Base_dir + "JPEGImages/" + image_id + ".jpg","./data/" + image_id + ".jpg")
else:
if(random.randint(0,10000) <= 10000*Blank_probability):
out_file = open('./data/%s.txt' % image_id, 'a')
# 拷贝图像
shutil.copy(Base_dir + "JPEGImages/" + image_id + ".jpg","./data/" + image_id + ".jpg")



imgname_list = []
part_name = 'trainval.txt' # test.txt
with open(os.path.join(Base_dir, 'ImageSets/Main/'+part_name)) as f:
all_lines = f.readlines()

for a_line in all_lines:
imgname_list.append(a_line.split()[0].strip())

print(len(imgname_list))
for image_id in imgname_list:
convert_annotation(image_id)

注意需要提前创建data文件夹
注意更改 Base_dir 为你的voc路径

需要注意的是 Blank_probability 变量, 这个变量在后文 数据集调整空label数据 内有讲解。

然后再写一个生成Train.txt和Valid.txt的脚本就行了

点击展开生成脚本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import os
import random

Data_dir = "E:/TensorFlow/datasets/Garbage_YOLO/data"

Valid = 0.15

Train_file = open("./train.txt", 'a')
Valid_file = open("./valid.txt", 'a')

for files in os.listdir(Data_dir):
if(os.path.isfile(Data_dir + "/" + files)):
if(".jpg" in files):
if(random.randint(0,100) < Valid*100):
Valid_file.write(Data_dir + "/" + files + "\n")
else:
Train_file.write(Data_dir + "/" + files + "\n")

注意更改 Data_dir
注意修改 Valid, Valid 为验证集的概率

最后按照
https://github.com/AlexeyAB/darknet#how-to-train-to-detect-your-custom-objects
更改train.data即可。
至于.cfg文件的更改, 看上述网址的教程即可。

数据集调整

空label数据

这个数据集有一些问题, 就是无关物体(包括背景)不是很丰富, 如果只训练我所需要的种类的话, 很容易出现高概率无关物体误判。当然, 这个也可能和我用的是 YOLO-Fastest 而不是 YOLO 有关。
COCO和VOC的 YOLO-Fastest 也出现了这种问题。
比如这些不在训练范围内的:
把大骨头识别为水果的

把纯黑屏幕识别为水果的

把人头识别为水瓶的

点击加载更多

把人识别为易拉罐和碎瓷片的

把药物识别为电池的

把全志V3S识别为电池的

(对了, 只要不是想学Linux移植的千万别买这个板子, 这个板子连镜像都需要自己做, 啥驱动都没有, 无法用OpenCV替代openmv)
(不过我想)

基本上只要是个物体都会被误识别(当然你也可以把误识别的物体也送到图像分类网络里面, 这样准确率甚至比完全体yolo更高)

所以我选择加入了一些没有被标记的图片做负向样本。
但是不能把所有不用的图片都放到负向样本里, 负向样本过多会导致判断条件过于苛刻, 即使都快怼上去了也无法识别。甚至由于摄像头型号, 色温等的不同都无法识别。
所有负向样本的数量适中即可。在我的测试中, 没有背景或无关物干扰的训练集, 加入 1–2 倍每个class平均数量的图片就行了。
转换脚本的
Blank_probability 即为此作用。但是他的作用并不直观。
在精简VOC数据集的时候, 无关图片会以 Blank_probability 的概率被添加到数据集。
其实这里写的并不好谁让我懒呢, Blank_probability 需要计算才行, 比较麻烦。

一些注意事项

  1. 训练的时候注意开map输出 这样能看到你训练效果以及是否过拟合
  2. 第一次使用自己的数据集训练时, 注意留意当前文件夹下是否有 badlist.txt 如果有, 请先去解决问题。
  3. 第一次使用自己的数据集训练时, 请在大概1000代之后, 就测试一下效果 以证明操作有效。
  4. 介意Darknet的那个UI的横竖线会偶尔消失的问题的话, 建议开浏览器去显示实时图表。

性能测试

测试环境树莓派4B+ 2G
YOLO-Fastest:
ncnn: 450ms
darknet 3000ms

YOLO-V4:
darknet 30s

比K210差太多了。。。

准备攒钱上Jetson!

TODO:

  1. 整理details里面的脚本