最近在用yolo來做視頻中的人員檢測,選擇YOLO是從速度考慮,當然也可以用ssd。YOLO相關可看主頁Darknet,有相關代碼和使用方法。由於之前做自己的數據訓練過程中出現各種問題,參照了各種博客才跑通,現在記錄下以防后面忘記,也方便自己總結一下。
YOLO本身使用的是VOC的數據集,所以可以按照VOC數據集的架構來構建自己的數據集。
1.構建VOC數據集
1.准備數據
- // 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;
- }
- --VOC
- --Annotations
- --ImageSets
- --Main
- --Layout
- --Segmentation
- --JPEGImages
- --SegmentationClass
- --SegmentationObject
2.標記圖像目標區域

通常save之后會將標記的信息保存在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>
- find -name '*.xml' |xargs perl -pi -e 's|</filename>|.jpg</filename>|g'
同理修改寬:
- 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'
2.用YOLOv2訓練
1.生成相關文件
- 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()

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
- [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 detector train ./cfg/voc.data .cfg/tiny-yolo-voc.cfg darknet.conv.weights
訓練過程中會根據迭代次數保存訓練的權重模型,然后就可以拿來測試了,測試的命令同理:
./darknet detector test cfg/voc.data cfg/tiny-yolo-voc.cfg results/tiny-yolo-voc_6000.weights data/images.jpg
這樣就完成了整個流程,目前測試感覺同種網絡v2版本似乎比v1版本慢很多,莫非是為了精度的提高犧牲了部分速度。然而我需要的是速度,這個就尷尬了。