YOLOv2训练自己的数据集(VOC格式)

最近在用yolo来做视频中的人员检测,选择YOLO是从速度考虑,当然也可以用ssd。YOLO相关可看主页Darknet,有相关代码和使用方法。由于之前做自己的数据训练过程中出现各种问题,参照了各种博客才跑通,现在记录下以防后面忘记,也方便自己总结一下。

YOLO本身使用的是VOC的数据集,所以可以按照VOC数据集的架构来构建自己的数据集。

1.构建VOC数据集

1.准备数据

首先准备好自己的数据集,最好固定格式,此处以VOC为例,采用jpg格式的图像,在名字上最好使用像VOC一样类似000001.jpg、000002.jpg这样。可参照下面示例代码
// Getfile.cpp : 重命名文件夹内的所有图像并写入txt,同时也可通过重写图像修改格式
//配用Opencv2.4.10

#include "stdafx.h"
#include <stdio.h>
#include <string.h>
#include<io.h> 
#include <opencv2\opencv.hpp>
#define IMGNUM 20000 //图片所在文件夹中图片的最大数量 
char img_files[IMGNUM][1000];

using namespace cv;
int getFiles(char *path)
{
int num_of_img = 0;
long  hFile = 0;
struct _finddata_t fileinfo;
char p[700] = { 0 };
strcpy(p, path);
strcat(p, "\\*");
if ((hFile = _findfirst(p, &fileinfo)) != -1)
{
do
{
if ((fileinfo.attrib & _A_SUBDIR))
{
if (strcmp(fileinfo.name, ".") != 0 && strcmp(fileinfo.name, "..") != 0)
continue;
}
else
{
strcpy(img_files[num_of_img], path);
strcat(img_files[num_of_img], "\\");
strcat(img_files[num_of_img++], fileinfo.name);
}
} while (_findnext(hFile, &fileinfo) == 0);
_findclose(hFile);
}
return num_of_img;
}
int main()
{
char path[] = "SrcImage";//source image
char dstpath[] = "DstImage";//destination image
int num = getFiles(path);
int i;
char order[1000];
FILE *fp = fopen("train.txt", "w");
for (i = 0; i<num; ++i)
{
printf("%s\n", img_files[i]);
IplImage *pSrc = cvLoadImage(img_files[i]);
sprintf(order, "DstImage\\%05d.jpg", i);
fprintf(fp, "%05d\n", i);
cvSaveImage(order, pSrc);
printf("Saving %s!\n", order);
cvReleaseImage(&pSrc);
}
fclose(fp);
return 0;
}
读取某文件夹下的所有图像然后统一命名,用了opencv所以顺便还可以改格式。
准备好了自己的图像后,需要按VOC数据集的结构放置图像文件。VOC的结构如下
--VOC
  --Annotations
  --ImageSets
   --Main
   --Layout
   --Segmentation
  --JPEGImages
  --SegmentationClass
  --SegmentationObject
这里面用到的文件夹是Annotation、ImageSets和JPEGImages。其中文件夹Annotation中主要存放xml文件,每一个xml对应一张图像,并且每个xml中存放的是标记的各个目标的位置和类别信息,命名通常与对应的原始图像一样;而ImageSets我们只需要用到Main文件夹,这里面存放的是一些文本文件,通常为train.txt、test.txt等,该文本文件里面的内容是需要用来训练或测试的图像的名字(无后缀无路径);JPEGImages文件夹中放我们已按统一规则命名好的原始图像。
因此,首先
1.新建文件夹VOC2007(通常命名为这个,也可以用其他命名,但一定是名字+年份,例如MYDATA2016,无论叫什么后面都需要改相关代码匹配这里,本例中以VOC2007为例)
2.在VOC2007文件夹下新建三个文件夹Annotation、ImageSets和JPEGImages,并把准备好的自己的原始图像放在JPEGImages文件夹下
3.在ImageSets文件夹中,新建三个空文件夹Layout、Main、Segmentation,然后把写了训练或测试的图像的名字的文本拷到Main文件夹下,按目的命名,我这里所有图像用来训练,故而Main文件夹下只有train.txt文件。上面说的小代码运行后会生成该文件,把它拷进去即可。
 

2.标记图像目标区域

因为做的是目标检测,所以接下来需要标记原始图像中的目标区域。相关方法和工具有很多,这里需用labelImg,相关用法也有说明,基本就是框住目标区域然后双击类别,标记完整张图像后点击保存即可。操作界面如下:
YOLOv2训练自己的数据集(VOC格式)
通常save之后会将标记的信息保存在xml文件,其名字通常与对应的原始图像一样。最后生成的画风是这样的YOLOv2训练自己的数据集(VOC格式)
 
其中每个xml文件是这样的画风
<?xml version="1.0" ?>
<annotation>
<folder>JPEGImages</folder>
<filename>00000</filename>
<path>/home/kinglch/VOC2007/JPEGImages/00000.jpg</path>
<source>
<database>Unknown</database>
</source>
<size>
<width>704</width>
<height>576</height>
<depth>3</depth>
</size>
<segmented>0</segmented>
<object>
<name>person</name>
<pose>Unspecified</pose>
<truncated>0</truncated>
<difficult>0</difficult>
<bndbox>
<xmin>73</xmin>
<ymin>139</ymin>
<xmax>142</xmax>
<ymax>247</ymax>
</bndbox>
</object>
<object>
<name>person</name>
<pose>Unspecified</pose>
<truncated>0</truncated>
<difficult>0</difficult>
<bndbox>
<xmin>180</xmin>
<ymin>65</ymin>
<xmax>209</xmax>
<ymax>151</ymax>
</bndbox>
</object>
<object>
<name>person</name>
<pose>Unspecified</pose>
<truncated>0</truncated>
<difficult>0</difficult>
<bndbox>
<xmin>152</xmin>
<ymin>70</ymin>
<xmax>181</xmax>
<ymax>144</ymax>
</bndbox>
</object>
</annotation>
注意filename中文件的文件名名没有后缀,因此需要统一加上后缀。只需一段命令即可
find -name '*.xml' |xargs perl -pi -e 's|</filename>|.jpg</filename>|g'
有时候在Windows下用该工具label图像,可能会出现size那里的width和height都为0,如果在label之前已经归一化了图像大小那么就可以用下面两行命令来修改这个0值
同理修改宽:
find -name '*.xml' |xargs perl -pi -e 's|0</width>|448</width>|g'
同理修改高:
find -name '*.xml' |xargs perl -pi -e 's|0</height>|448</height>|g'
在对应目录下执行即可,这样就可以把后缀添上。这样就做按照VOC做好了我们的数据集,接下来就是放到算法中去训练跑起来。

2.用YOLOv2训练

1.生成相关文件

按darknet的说明编译好后,接下来在darknet-master/scripts文件夹中新建文件夹VOCdevkit,然后将整个VOC2007文件夹都拷到VOCdevkit文件夹下。
然后,需要利用scripts文件夹中的voc_label.py文件生成一系列训练文件和label,具体操作如下:
首先需要修改voc_label.py中的代码,这里主要修改数据集名,以及类别信息,我的是VOC2007,并且所有样本用来训练,没有val或test,并且只检测人,故只有一类目标,因此按如下设置
import xml.etree.ElementTree as ET
import pickle
import os
from os import listdir, getcwd
from os.path import join

#sets=[('2012', 'train'), ('2012', 'val'), ('2007', 'train'), ('2007', 'val'), ('2007', 'test')]

#classes = ["aeroplane", "bicycle", "bird", "boat", "bottle", "bus", "car", "cat", "chair", "cow", "diningtable", "dog", "horse", "motorbike", "person", "pottedplant", "sheep", "sofa", "train", "tvmonitor"]

sets=[('2007', 'train')]
classes = [ "person"]


def convert(size, box):
  dw = 1./size[0]
  dh = 1./size[1]
  x = (box[0] + box[1])/2.0
  y = (box[2] + box[3])/2.0
  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('VOCdevkit/VOC%s/Annotations/%s.xml'%(year, image_id)) #(如果使用的不是VOC而是自设置数据集名字,则这里需要修改)
  out_file = open('VOCdevkit/VOC%s/labels/%s.txt'%(year, image_id), 'w') #(同上)
  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'):
    difficult = obj.find('difficult').text
    cls = obj.find('name').text
    if cls not in classes or int(difficult) == 1:
      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')

wd = getcwd()

for year, image_set in sets:
  if not os.path.exists('VOCdevkit/VOC%s/labels/'%(year)):
    os.makedirs('VOCdevkit/VOC%s/labels/'%(year))
  image_ids = open('VOCdevkit/VOC%s/ImageSets/Main/%s.txt'%(year, image_set)).read().strip().split()
  list_file = open('%s_%s.txt'%(year, image_set), 'w')
  for image_id in image_ids:
    list_file.write('%s/VOCdevkit/VOC%s/JPEGImages/%s.jpg\n'%(wd, year, image_id))
    convert_annotation(year, image_id)
  list_file.close()
修改好后在该目录下运行命令:python voc_label.py,之后则在文件夹scripts\VOCdevkit\VOC2007下生成了文件夹lable,该文件夹下的画风是这样的
YOLOv2训练自己的数据集(VOC格式)
这里包含了类别和对应归一化后的位置(i guess,如有错请指正)。同时在scripts\下应该也生成了train_2007.txt这个文件,里面包含了所有训练样本的绝对路径。

2.配置文件修改

做好了上述准备,就可以根据不同的网络设置(cfg文件)来训练了。在文件夹cfg中有很多cfg文件,应该跟caffe中的prototxt文件是一个意思。这里以tiny-yolo-voc.cfg为例,该网络是yolo-voc的简版,相对速度会快些。主要修改参数如下

.
.
.
[convolutional]
size=1
stride=1
pad=1
filters=30 //修改最后一层卷积层核参数个数,计算公式是依旧自己数据的类别数filter=num×(classes + coords + 1)=5×(1+4+1)=30
activation=linear

[region]
anchors = 1.08,1.19, 3.42,4.41, 6.63,11.38, 9.42,5.11, 16.62,10.52
bias_match=1
classes=1 //类别数,本例为1类
coords=4
num=5
softmax=1
jitter=.2
rescore=1

object_scale=5
noobject_scale=1
class_scale=1
coord_scale=1

absolute=1
thresh = .6
random=1
另外也可根据需要修改learning_rate、max_batches等参数。这里歪个楼吐槽一下其他网络配置,一开始是想用tiny.cfg来训练的官网作者说它够小也够快,但是它的网络配置最后几层是这样的画风:
[convolutional]
filters=1000
size=1
stride=1
pad=1
activation=linear

[avgpool]

[softmax]
groups=1

[cost]
type=sse
这里没有类别数,完全不知道怎么修改,强行把最后一层卷积层卷积核个数修改又跑不通会出错,如有大神知道还望赐教。

Back to the point。修改好了cfg文件之后,就需要修改两个文件,首先是data文件下的voc.names。打开voc.names文件可以看到有20类的名称,本例中只有一类,检测人,因此将原来所有内容清空,仅写上person并保存。名字仍然用这个名字,如果喜欢用其他名字则请按一开始制作自己数据集的时候的名字来修改。

接着需要修改cfg文件夹中的voc.data文件。也是按自己需求修改,我的修改之后是这样的画风:

classes= 1 //类别数
train = /home/kinglch/darknet-master/scripts/2007_train.txt //训练样本的绝对路径文件,也就是上文2.1中最后生成的
//valid = /home/pjreddie/data/voc/2007_test.txt //本例未用到
names = data/voc.names //上一步修改的voc.names文件
backup = /home/kinglch/darknet-master/results/ //指示训练后生成的权重放在哪
修改后按原名保存最好,接下来就可以训练了。

ps:yolo v1中这些细节是直接在源代码的yolo.c中修改的,源代码如下

YOLOv2训练自己的数据集(VOC格式)

比如这里的类别,训练样本的路径文件和模型保存路径均在此指定,修改后从新编译。而yolov2似乎摈弃了这种做法,所以训练的命令也与v1版本的不一样。

3.运行训练

上面完成了就可以命令训练了,可以在官网上找到一些预训练的模型作为参数初始值,也可以直接训练,训练命令为

$./darknet detector train ./cfg/voc.data cfg/tiny-yolo-voc.cfg
如果用官网的预训练模型darknet.conv.weights做初始化,则训练命令为
$./darknet detector train ./cfg/voc.data .cfg/tiny-yolo-voc.cfg darknet.conv.weights
不过我没试成功,加上这个模型直接就除了final,不知道啥情况。当然也可以用自己训练的模型做参数初始化,万一训练的时候被人终端了,可以再用训练好的模型接上去接着训练。

训练过程中会根据迭代次数保存训练的权重模型,然后就可以拿来测试了,测试的命令同理:

./darknet detector test cfg/voc.data cfg/tiny-yolo-voc.cfg results/tiny-yolo-voc_6000.weights data/images.jpg
这样就完成了整个流程,目前测试感觉同种网络v2版本似乎比v1版本慢很多,莫非是为了精度的提高牺牲了部分速度。然而我需要的是速度,这个就尴尬了。
初学yolo,如有问题欢迎加我q675143196讨论交流.