Darknet is an open source neural network framework written in C and CUDA. It is fast, easy to install, and supports CPU and GPU computation.
—— https://pjreddie.com/darknet/
本文是对使用 darknet 进行目标检测的小结,包括:
- 数据集准备:如何使用 labelimage 对数据进行标注,注意事项,文件格式转换
- darknet 使用:如何编译和修改配置文件
- 模型评估:如何查看 loss、计算 IoU、recall、mAP
数据集准备
大多数情况下,数据集决定了任务的成败。一开始我认为标注数据是一件非常枯燥和乏味的事情,但是当模型指标一直上不去,检测和识别效果也一直不好时,回过头才会发现是因为数据标注的有问题。
这只有自己经历过之后才会有体会,得出这样几条经验:
- 保持类间差距大,类内差距小
- 一开始先标注少量的图片并训练模型查看效果,根据结果进行调整,否则等到标注了大量图片之后再回过头修改,得不偿失。
labelimage
图片标注使用的是开源的工具 LabelImg,可以查看文档自行编译,下载之后的文件结构如下:
.
├── build-tools
├── CONTRIBUTING.rst
├── data
|—— predefined_classes.txt
├── labelImg.py
...
- 在
data/predefined_classes.txt
文件配置进行图片的类别
界面展示及注意事项
启动 labelimg 之后的界面如下图所示
注意:
- 存放图片文件夹和存放标记文件的文件夹需要保持一致
- 正确选择标记文件的格式,是需要 yolo 格式还是 xml 格式,默认为 xml 格式
- 右侧可以选择默认标签,当密集标注一个类的时候很实用
- 打完几张标签之后请确认标签是否正确,再去文件目录下确认以下标记文件是否生成且格式正确
快捷键
+------------+--------------------------------------------+
| Space | 保存 |
+------------+--------------------------------------------+
| w | 创建矩形框 |
+------------+--------------------------------------------+
| d | 下一张图片 |
+------------+--------------------------------------------+
| a | 上一张图片 |
+------------+--------------------------------------------+
最常用的快捷键是上面这 4 个,用好快捷键可以调高打标效率。此外按住ctrl+鼠标滚轮
可以调整图片大小,局部放大图片可以提高打标的精准度。
yolo格式和 xml 格式转换
xml2yolo.py
"""
1. 修改 classes 列表中的元素为当前标签列表
2. 修改 list_xml 指向的 xml 文件保存的位置
"""
#import xml.etree.ElementTree as ET
from xml.etree import ElementTree as ET
import pickle
import os
from os import listdir, getcwd
from os.path import join
import glob
classes = ['rabbit']
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(xml_file, txt_file):
in_file = open(xml_file)
tree = ET.parse(in_file)
root = tree.getroot()
size = root.find('size')
w = int(size.find('width').text)
h = int(size.find('height').text)
for obj in root.iter('object'):
cls = obj.find('name').text
if cls not in classes:
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)
txt_file.write(str(cls_id) + " " + " ".join([str(a) for a in bb]) + '\n')
list_xml = []
list_xml = glob.glob(r"/file_path/*.xml")
for xml_file in list_xml:
xml_name = xml_file.split('.')[0]
txt_file = open('%s.txt' % (xml_name), 'w')
convert_annotation(xml_file, txt_file)
txt_file.close()
yolo2xml.py
"""
1. 修改 class_name
2. 修改 src_img_dir/src_txt_dir/src_xml_dir 路径
"""
import os, sys
import glob
from PIL import Image
class_name = ['switch_closed','switch_open','whirled_switch_closed','whirled_switch_open',
'lifting_switch_closed','lifting_switch_open','closure_switch_closed','closure_switch_open',
'color_switch01_red','color_switch01_green','color_switch02_red','color_switch02_green',
'isolation_switch_closed','isolation_switch_open','grounding_knife_switch01_closed','grounding_knife_switch01_open',
'grounding_knife_switch02_closed','grounding_knife_switch02_open']
def convert_yolo_coordinates_to_voc(x_c_n, y_c_n, width_n, height_n, img_width, img_height):
## remove normalization given the size of the image
x_c = float(x_c_n) * img_width
y_c = float(y_c_n) * img_height
width = float(width_n) * img_width
height = float(height_n) * img_height
## compute half width and half height
half_width = width / 2
half_height = height / 2
## compute left, top, right, bottom
## in the official VOC challenge the top-left pixel in the image has coordinates (1;1)
left = int(x_c - half_width) + 1
top = int(y_c - half_height) + 1
right = int(x_c + half_width) + 1
bottom = int(y_c + half_height) + 1
return left, top, right, bottom
# 将标注的txt转换为 voc xml
# VEDAI 图像存储位置
src_img_dir = "./switch_mAP"
# VEDAI 图像的 ground truth 的 txt 文件存放位置
src_txt_dir = "./switch_mAP"
src_xml_dir = "./switch_mAP"
img_Lists = glob.glob(src_img_dir + '/*.txt')
# 文件名(含扩展名)
img_basenames = [] # e.g. 100
for item in img_Lists:
img_basenames.append(os.path.basename(item))
img_names = [] # e.g. 100
for item in img_basenames:
# 文件名与扩展名
temp1, temp2 = os.path.splitext(item)
img_names.append(temp1)
for img in img_names:
im = ""
suffix = ""
# 同一个文件夹下存在多种图片格式,在这里加上格式判断
if os.path.exists(src_img_dir + '/' + img + '.jpg'):
im = Image.open((src_img_dir + '/' + img + '.jpg'))
suffix = '.jpg'
elif os.path.exists(src_img_dir + '/' + img + '.JPG'):
im = Image.open((src_img_dir + '/' + img + '.JPG'))
suffix = '.JPG'
elif os.path.exists(src_img_dir + '/' + img + '.png'):
im = Image.open((src_img_dir + '/' + img + '.png'))
suffix = '.png'
elif os.path.exists(src_img_dir + '/' + img + '.PNG'):
im = Image.open((src_img_dir + '/' + img + '.PNG'))
suffix = '.PNG'
elif os.path.exists(src_img_dir + '/' + img + '.JPEG'):
im = Image.open((src_img_dir + '/' + img + '.JPEG'))
suffix = '.JPEG'
elif os.path.exists(src_img_dir + '/' + img + '.jpeg'):
im = Image.open((src_img_dir + '/' + img + '.jpeg'))
suffix = '.jpeg'
width, height = im.size
"""
以下部分为 xml 解析
"""
# open the crospronding txt file
# 提取每一行并分割
gt = open(src_txt_dir + '/' + img + '.txt').read().splitlines()
print(img + '\n')
# write in xml file
# os.mknod(src_xml_dir + '/' + img + '.xml')
xml_file = open((src_xml_dir + '/' + img + '.xml'), 'a')
xml_file.write('<annotation>\n')
xml_file.write(' <folder>VOC2007</folder>\n')
xml_file.write(' <filename>' + str(img) + suffix + '</filename>\n')
xml_file.write(' <size>\n')
xml_file.write(' <width>' + str(width) + '</width>\n')
xml_file.write(' <height>' + str(height) + '</height>\n')
xml_file.write(' <depth>3</depth>\n')
xml_file.write(' </size>\n')
# write the region of image on xml file
for img_each_label in gt:
spt = img_each_label.split(' ') # 这里如果txt里面是以逗号‘,’隔开的,那么就改为spt = img_each_label.split(',')。
name = class_name[int(spt[0])]
x_c,y_c,width_n,height_n = spt[1:]
xmin,ymin,xmax,ymax = convert_yolo_coordinates_to_voc(x_c,y_c,width_n,height_n,width,height)
xml_file.write(' <object>\n')
xml_file.write(' <name>' + name + '</name>\n')
xml_file.write(' <pose>Unspecified</pose>\n')
xml_file.write(' <truncated>0</truncated>\n')
xml_file.write(' <difficult>0</difficult>\n')
xml_file.write(' <bndbox>\n')
xml_file.write(' <xmin>' + str(xmin) + '</xmin>\n')
xml_file.write(' <ymin>' + str(ymin) + '</ymin>\n')
xml_file.write(' <xmax>' + str(xmax) + '</xmax>\n')
xml_file.write(' <ymax>' + str(ymax) + '</ymax>\n')
xml_file.write(' </bndbox>\n')
xml_file.write(' </object>\n')
xml_file.write('</annotation>')
使用 darknet 训练模型
安装和编译
# 从 Github 下载
git clone https://github.com/pjreddie/darknet
进入darknet
目录中,对编译文件Makefile
进行如下修改
GPU=1
CUDNN=1
OPENCV=0
OPENMP=0
DEBUG=0
注:
- 画图或显示图片等操作需要配置OPENCV并设置为1
- 需要多线程相关操作需要将OPENMP设置为1
修改完成保存并执行make
命令。
目录结构介绍
|
|
编译完成之后,我们关注目录中的这几个文件和文件夹。
- backup 存放训练出来的权值文件
- cfg 保存配置文件,其中两个文件,在后面介绍
- darknet 是可执行文件
- scripts 下是一些脚本
- data/voc.names 存放类别标签
- train.txt 保存用于训练的图片全路径(自建,位置无特殊要求)
- val.txt 保存用于校正的图片全路径(自建,位置无特殊要求)
- test.txt 保存用于校正的图片全路径(自建,位置无特殊要求)
注: train.txt, val.txt, test.txt 中图片数量比例建议为 8:1:1
修改配置文件
类别文件 voc.names
修改data/voc.names
,将我们的类别标签写这个文件,比如说有以下 5 类
dog
cat
magpie
pigeon
nest
注意:
- 类别标签要与训练集包含的图片类别一一对应,训练集中有以上 5 类则
voc.names
包含以上 5 类 - 类别标签需要连续,如果不连续就必须要修改类别标签改成连续的
配置 voc.data
修改cfg/voc.data
文件
classes= class_number
train = /path/train.txt
valid = /path/val.txt
names = data/voc.names
backup = backup
- classes 配置类别数量,与上一步
voc.names
中类别数量一致 - train 配置为目录结构一章介绍的
train.txt
文件的位置 - vaild 配置为目录结构一章介绍的
val.txt
文件的位置 - names 配置为
voc.names
位置(默认不变即可) - backup 配置为权值文件的位置(默认不变即可)
配置 yolov3-voc.cfg
在文件开头位置
[net]
# Testing
# batch=1 # 测试时开启,训练时关闭
# subdivisions=1 # 测试时开启,训练时关闭
# Training
batch=6 # 训练时开启,测试时关闭
subdivisions=2 # 训练时开启,测试时关闭
...
learning_rate=0.0001 # 学习率,可以调整得小一点
burn_in=1000
max_batches = 50200
policy=steps
steps=40000,45000
scales=.1,.1
- batch 和 subdivisions 在测试和训练的时候请按照上面注释开启或者关闭
- batch 一批处理几张图片,没有超过显存的情况下越大越好
- subdivisions 表示在 batch 中再划分的数量
当前配置的意思是每轮迭代从所有训练集中抽取 6 张图片,这 6 张样本图片又被分成 2 次,每次 3 张送入到网络参与训练。
接着在文件中搜索yolo
,会有三条结果。每个yolo
上下都要修改filters
和classes
,总共需要修改3 组共 6 个字段。
[convolutional]
size=1
stride=1
pad=1
filters=30 # 3*(classes+5)
activation=linear
[yolo]
mask = 6,7,8
anchors = 10,13, 16,30, 33,23, 30,61, 62,45, 59,119, 116,90, 156,198, 373,326
classes=5 # 类别(classes)数量
num=9
jitter=.3
ignore_thresh = .5
truth_thresh = 1
random=1
训练
方法一
./darknet detector train cfg/voc.data cfg/yolov3-voc.cfg scripts/darknet53.conv.74 -gpus 0
方法二
nohup ./darknet detector train cfg/voc.data cfg/yolov3-voc.cfg scripts/darknet53.conv.74 -gpus 0 >train.log 2>&1 &
我们需要保留训练的日志,所以用方法二更好。
查看 GPU 使用情况
我们可以使用nvidia-smi
来查看 gpu 的使用情况,注意先查看 gpu 的使用情况。
Fri Mar 29 16:33:31 2019
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 410.78 Driver Version: 410.78 CUDA Version: 10.0 |
|-------------------------------+----------------------+----------------------+
| GPU Name Persistence-M| Bus-Id Disp.A | Volatile Uncorr. ECC |
| Fan Temp Perf Pwr:Usage/Cap| Memory-Usage | GPU-Util Compute M. |
|===============================+======================+======================|
| 0 GeForce GTX 108... Off | 00000000:01:00.0 On | N/A |
| 59% 87C P2 221W / 250W | 4874MiB / 11175MiB | 98% Default |
+-------------------------------+----------------------+----------------------+
| 1 GeForce GTX 108... Off | 00000000:02:00.0 Off | N/A |
| 24% 42C P8 16W / 250W | 5107MiB / 11178MiB | 0% Default |
+-------------------------------+----------------------+----------------------+
+-----------------------------------------------------------------------------+
| Processes: GPU Memory |
|