本文轉載自:
http://blog.csdn.net/ch_liu23/article/details/53558549
最近在用yolo來做視頻中的人員檢測,選擇YOLO是從速度考慮,當然也可以用ssd。YOLO相關可看主頁Darknet,有相關代碼和使用方法。由於之前做自己的數據訓練過程中出現各種問題,參照了各種博客才跑通,現在記錄下以防後面忘記,也方便自己總結一下。
YOLO本身使用的是VOC的數據集,所以可以按照VOC數據集的架構來構建自己的數據集。
1.構建VOC數據集
1.準備數據
首先準備好自己的數據集,最好固定格式,此處以VOC爲例,採用jpg格式的圖像,在名字上最好使用像VOC一樣類似000001.jpg、000002.jpg這樣。可參照下面示例代碼
-
-
-
-
#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";
-
char dstpath[] = "DstImage";
-
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,相關用法也有說明,基本就是框住目標區域然後雙擊類別,標記完整張圖像後點擊保存即可。操作界面如下:
通常save之後會將標記的信息保存在xml文件,其名字通常與對應的原始圖像一樣。最後生成的畫風是這樣的
其中每個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'
在對應目錄下執行即可,這樣就可以把後綴添上。這樣就做按照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=[('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))
-
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,該文件夾下的畫風是這樣的
這裏包含了類別和對應歸一化後的位置(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似乎擯棄了這種做法,所以訓練的命令也與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討論交流.