1、环境配置
2、ubuntu16.04下安装OpenCV
参考:ubuntu16.04安装C++版本的OpenCV3.4.5
1、使用labelImg标注软件进行数据集中图像的标注
参考:LabelImg的详细使用和图像标注
2、具体改动的标注步骤
注:不同的是,我们在标注的时候就让每张图片生成的是txt文件,而不是xml文件,这样就省去了xml转txt这个步骤
(1)红色方框处选择yolo,而非默认的PascalVOC
(2)每标注一张图片点击保存,就会生成对应的图片名的txt文件,在保存txt文件夹中也会存在一个所有类别的文件
(3)对于每个标注后生成的txt文件,其格式是如下所示
1、建立层次文件夹
一级:darknet-master
二级:——backup (存储训练好的模型)
二级:——cfg (用于存放.cfg和.data文件)
二级:——data (存放标签文件、图片、名字文件(.name))
三级:————data下的目录 :images(存放图片和yolo的txt文件,图片名和txt文本名要对应,但是后面我们在 JPEGImages中这样做了,因此此步就做了)
三级:————data下的目录:labels (存放用于opencv回执图片框的图片文件)
二级:——voc (voc数据集目录)
三级:————VOCdevkit
四级: ——————VOC2019(这个年号可以自行更换,根据自己的需求吧)
五级: ————————Annotations (存放使用labelImg软件标注所有数据集图片时生成的txt标注文件)
五级: ————————ImageSets(用来存放训练和测试数据的名称,其里面存放的是Main文件夹)
五级: ———————— labels(存放Annotations中相同的内容)
六级: ——————————Main(Main文件夹中存放的是4个txt文件,分别是存训练集图片名的train.txt、存验证集图片 名的val.txt、存测试集图片名的test.txt、存测试和验证集图片名的trainval.txt,但是 自己只建了两个,接下来会说明如何生成的)
五级: ————————JPEGImages (用于存放数据集中的图片,后期还要将labels中的txt全部放到其中)
2、一些txt文件的生成
#####
前提说明:因为我们在标注时就已经生成了每个图片对应的txt文档,因此我们就不使用darknet-master/scripts下的voc_label.py文件了,而是自己写脚本生成我们需要的文件。
######
(1)根据JPEGImages中的数据集图片生成我们需要的ImageSets/Main目录下的train.txt和val.txt,这两个文件存的是每个图片的名字,即.jpg之前的内容
import os
from os import listdir, getcwd
from os.path import join
if __name__ == '__main__':
source_folder='/*****(自己的路径)/darknet-master/voc/VOCdevkit/VOC2019/JPEGImages/'#地址是所有图片的保存地点
dest='/*****(自己的路径)/darknet-master/voc/VOCdevkit/VOC2019/ImageSets/Main/train.txt' #保存train.txt的地址,对于train.txt(在ImageSets/Main/下)和2019_train.txt(在VOCdevkit下)是不同的路径
dest2='/*****(自己的路径)/darknet-master/voc/VOCdevkit/VOC2019/ImageSets/Main/val.txt' #保存val.txt的地址,对于val.txt(在ImageSets/Main/下)和2019_val.txt(在VOCdevkit下)是不同的路径
file_list=os.listdir(source_folder) #赋值图片所在文件夹的文件列表
train_file=open(dest,'a') #打开文件
val_file=open(dest2,'a') #打开文件
for file_obj in file_list: #访问文件列表中的每一个文件
file_path=os.path.join(source_folder,file_obj)
#file_path保存每一个文件的完整路径
file_name,file_extend=os.path.splitext(file_obj)
#file_name 保存文件的名字,file_extend保存文件扩展名
file_num=int(file_name)
if(file_num<620): #保留620个文件用于训练
train_file.write(file_name+'\n') #用于训练前620个的图片路径保存在train.txt里面,结尾加回车换行/////////生成train.txt是file_name;生成2019_train.txt是file_path
else :
val_file.write(file_name+'\n') #其余的文件保存在val.txt里面/////////生成val.txt是file_name;生成2019_val.txt是file_path
train_file.close()#关闭文件
val_file.close()
python3 make_train_val.py
a)train.txt文件部分内容(保存的是图片名字)
b)val.txt文件中的内容(保存的是图片名字)
(2)在VOCdevkit同级目录下生成2019_train.txt和2019_val.txt两个文件,yolo训练时需要这两个文件
import os
from os import listdir, getcwd
from os.path import join
if __name__ == '__main__':
source_folder='/*****(自己的路径)/darknet-master/voc/VOCdevkit/VOC2019/JPEGImages/'#地址是所有图片的保存地点
dest='/*****(自己的路径)/darknet-master/voc/2019_train.txt' #保存train.txt的地址,对于train.txt(在ImageSets/Main/下)和2019_train.txt(在VOCdevkit下)是不同的路径
dest2='/*****(自己的路径)/darknet-master/voc/2019_val.txt' #保存val.txt的地址,对于val.txt(在ImageSets/Main/下)和2019_val.txt(在VOCdevkit下)是不同的路径
file_list=os.listdir(source_folder) #赋值图片所在文件夹的文件列表
train_file=open(dest,'a') #打开文件
val_file=open(dest2,'a') #打开文件
for file_obj in file_list: #访问文件列表中的每一个文件
file_path=os.path.join(source_folder,file_obj)
#file_path保存每一个文件的完整路径
file_name,file_extend=os.path.splitext(file_obj)
#file_name 保存文件的名字,file_extend保存文件扩展名
file_num=int(file_name)
if(file_num<620): #保留620个文件用于训练
train_file.write(file_path+'\n') #用于训练前620个的图片路径保存在train.txt里面,结尾加回车换行/////////生成train.txt是file_name;生成2019_train.txt是file_path
else :
val_file.write(file_path+'\n') #其余的文件保存在val.txt里面/////////生成val.txt是file_name;生成2019_val.txt是file_path
train_file.close()#关闭文件
val_file.close()
python3 make_2019_train_val.py
a)2019_train.txt文件中的部分内容如下(保存绝对路径)
b)2019_val.txt文件中的部分内容如下(保存绝对路径)
3、将Annotations中的txt文件(除了classes外)全部复制到labels中
如果不在darknet-master/voc/VOCdevkit/VOC2019/下建立labels文件,则会出现以下错误,因此一定要这样做
4、将labels中的txt全部复制到JPEGImages文件夹中,做到图片和txt一一对应
一开始是要吧JPEGImages文件中的图片放到darknet-master/data/images目录下,现在直接把yolo的标签文件txt即labels中的内容复制到JPEGImages文件夹中就可以了。
1、下载Imagenet上的预训练的权重(将下载的权重文件存在darknet-master/scripts目录下)
wget https://pjreddie.com/media/files/darknet53.conv.74
https://pjreddie.com/media/files/darknet53.conv.74
以上两种方式哪个快下载用哪种方式
2、修改darknet-master/cfg/voc.data文件
文件在:
voc.data修改为:
classes= 9 #classes为训练样本集的类别总数,本实验选的为9类标签
train = /****(自己的路径自己改)/darknet-master/voc/2019_train.txt #train的路径为训练样本集所在的路径
valid = /****(自己的路径自己改)/darknet-master/voc/2019_val.txt #valid的路径为验证样本集所在的路径
names = data/voc.names #names的路径为data/voc.names文件所在的路径
backup = backup
3、修改darknet-master/data/voc.names文件,更换默认的类别为自己的类别,然后保存
文件在:
voc.data修改为:
4、修改darknet-master/cfg/yolov3-voc.cfg文件,根据自己的情况进行修改
文件在:
yolov3-voc.cfg修改为:
Testing注释掉 打开Training
[net]
# Testing
# batch=1 #这里的batch跟subdivisions原来不是注释掉的,但是训练后没成功,有的blog上说为1的时候太小难以收敛,但是不知道下面训练模式的 batch=64 subdivisions=8 会不会覆盖掉,总之注释掉后就成功了,不过这个脚本不是很明白,还来不及验证
# subdivisions=1
# Training ### 训练模式,每次前向的图片数目 = batch/subdivisions
batch=64 #根据自己的情况进行修改
subdivisions=8 #根据自己的情况进行修改
width=416 ### 网络的输入宽、高、通道数
height=416
channels=3
momentum=0.9 ### 动量
decay=0.0005 ### 权重衰减
angle=0
saturation = 1.5 ### 饱和度
exposure = 1.5 ### 曝光度
hue=.1 ### 色调
learning_rate=0.001 ### 学习率
burn_in=1000 ### 学习率控制的参数
max_batches = 50200 ### 迭代次数
policy=steps ### 学习率策略
steps=40000,45000 ### 学习率变动步长
scales=.1,.1 ### 学习率变动因子
......
......
......
[convolutional]
size=1
stride=1
pad=1
filters=42 #---------------修改为3*(classes+5)即3*(9+5)=42
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=9 #---------------修改为标签类别个数,9类
num=9
jitter=.3
ignore_thresh = .5
truth_thresh = 1
random=0 #1,如果显存很小,将random设置为0,关闭多尺度训练;(转自别的blog,还不太明白)
......
[convolutional]
size=1
stride=1
pad=1
filters=42 #---------------修改同上
activation=linear
[yolo]
mask = 3,4,5
anchors = 10,13, 16,30, 33,23, 30,61, 62,45, 59,119, 116,90, 156,198, 373,326
classes=9 #---------------修改同上
num=9
jitter=.3
ignore_thresh = .5
truth_thresh = 1
random=0
......
[convolutional]
size=1
stride=1
pad=1
filters=42 #---------------修改同上
activation=linear
[yolo]
mask = 0,1,2
anchors = 10,13, 16,30, 33,23, 30,61, 62,45, 59,119, 116,90, 156,198, 373,326
classes=9 #---------------修改同上
num=9
jitter=.3
ignore_thresh = .5
truth_thresh = 1
random=0
解决方法:出现以下错误,删除注释后可以训练了
错误提示:
##############
81 Type not recognized: [convolutional]#接下来从此处开始修改
Unused field: 'size = 1'
Unused field: 'stride = 1'
Unused field: 'pad = 1'
Unused field: 'filters = 42#原来是75,现在修改成3*(classes+5)=3*(9+5)=42'
Unused field: 'activation = linear'
82 Cuda malloc failed
: File exists
darknet: ./src/utils.c:256: error: Assertion `0' failed.
已放弃 (核心已转储)
###############
5、参数详解
以下参数是yolov3-voc.cfg参数说明:
1 、在darknet-master终端运行如下代码进行训练
./darknet detector train cfg/voc.data cfg/yolov3-voc.cfg scripts/darknet53.conv.74
#注意文件路径
2、训练默认的是前1000轮每100轮保存一次模型,1000轮后每10000轮保存一次模型。可以修改examples/detector.c文件的138行。修改完重新编译一下,在darknet目录下执行make。
make
3、训练的过程
./darknet detector train cfg/voc.data cfg/yolov3-voc.cfg scripts/darknet53.conv.74
./darknet detector train cfg/voc.data cfg/yolov3-voc.cfg scripts/darknet53.conv.74 | tee train_yolov3-voc.log
4、训练日志参数说明
5、调参中遇到的问题
前提说明:在训练过程中,nan的屏幕占比30%是正常的,如果太大,全是nan,则就是训练出了问题
解决方法一:在显存允许的情况下,可以适当增加batch(darknet-master/yolov3-voc.cfg中的batch)的大小(要视自己数据集 的大小情况来增加batch,不能盲目的改大),这样能够一定程度减少nan的出现。
解决方法二:增加数据集的规模。若是对于10类以内的图片,500张以内的训练集未必是太少了,因此可以增加数据集,实在不 行的话就进行数据增强,把数据集扩展到原来的几倍到几十倍不等。
解决方法:显存不够,调小batch,关闭多尺度训练:random=0.(*************亲测有用**************)
random所在(darknet-master/cfg/yolov3-voc.cfg):
更多问题可以参考此篇大佬的详解:YOLOV3训练调参详解
6、训练结果
7、在avg低于0.06以后,如何停止训练
ubuntu系统下,在训练终端处,使用ctrl+c终止训练。
8、如何接着上一步停止处的训练状态继续训练
在终端输入:
./darknet detector train cfg/voc.data cfg/yolov3-voc.cfg backup/yolov3-voc.backup
如下:
9、训练日志的可视化
因为我们在训练的时候已经保存了训练日志,因此我们使用以下python脚本对训练日志进行可视化,得到loss变化曲线和Avg IOU曲线(Avg IOU是标记的框和预测的框重复的部分除以他们的和,这个值越接近1越好)
(1)visualization_train_yolov3-voc_log.py
# -*- coding: utf-8 -*-
# @Func :yolov3 训练日志可视化,把该脚本和日志文件放在同一目录下运行。
import pandas as pd
import matplotlib.pyplot as plt
import os
# ==================可能需要修改的地方=====================================#
g_log_path = "train_yolov3-voc.log" # 此处修改为你的训练日志文件名
# ==========================================================================#
def extract_log(log_file, new_log_file, key_word):
'''
:param log_file:日志文件
:param new_log_file:挑选出可用信息的日志文件
:param key_word:根据关键词提取日志信息
:return:
'''
with open(log_file, "r") as f:
with open(new_log_file, "w") as train_log:
for line in f:
# 去除多gpu的同步log
if "Syncing" in line:
continue
# 去除nan log
if "nan" in line:
continue
if key_word in line:
train_log.write(line)
f.close()
train_log.close()
def drawAvgLoss(loss_log_path):
'''
:param loss_log_path: 提取到的loss日志信息文件
:return: 画loss曲线图
'''
line_cnt = 0
for count, line in enumerate(open(loss_log_path, "rU")):
line_cnt += 1
result = pd.read_csv(loss_log_path, skiprows=[iter_num for iter_num in range(line_cnt) if ((iter_num < 500))],
error_bad_lines=False,
names=["loss", "avg", "rate", "seconds", "images"])
result["avg"] = result["avg"].str.split(" ").str.get(1)
result["avg"] = pd.to_numeric(result["avg"])
fig = plt.figure(1, figsize=(6, 4))
ax = fig.add_subplot(1, 1, 1)
ax.plot(result["avg"].values, label="Avg Loss", color="#ff7043")
ax.legend(loc="best")
ax.set_title("Avg Loss Curve")
ax.set_xlabel("Batches")
ax.set_ylabel("Avg Loss")
def drawIOU(iou_log_path):
'''
:param iou_log_path: 提取到的iou日志信息文件
:return: 画iou曲线图
'''
line_cnt = 0
for count, line in enumerate(open(iou_log_path, "rU")):
line_cnt += 1
result = pd.read_csv(iou_log_path, skiprows=[x for x in range(line_cnt) if (x % 39 != 0 | (x < 5000))],
error_bad_lines=False,
names=["Region Avg IOU", "Class", "Obj", "No Obj", "Avg Recall", "count"])
result["Region Avg IOU"] = result["Region Avg IOU"].str.split(": ").str.get(1)
result["Region Avg IOU"] = pd.to_numeric(result["Region Avg IOU"])
result_iou = result["Region Avg IOU"].values
# 平滑iou曲线
for i in range(len(result_iou) - 1):
iou = result_iou[i]
iou_next = result_iou[i + 1]
if abs(iou - iou_next) > 0.2:
result_iou[i] = (iou + iou_next) / 2
fig = plt.figure(2, figsize=(6, 4))
ax = fig.add_subplot(1, 1, 1)
ax.plot(result_iou, label="Region Avg IOU", color="#ff7043")
ax.legend(loc="best")
ax.set_title("Avg IOU Curve")
ax.set_xlabel("Batches")
ax.set_ylabel("Avg IOU")
if __name__ == "__main__":
loss_log_path = "train_log_loss.txt"
iou_log_path = "train_log_iou.txt"
if os.path.exists(g_log_path) is False:
exit(-1)
if os.path.exists(loss_log_path) is False:
extract_log(g_log_path, loss_log_path, "images")
if os.path.exists(iou_log_path) is False:
extract_log(g_log_path, iou_log_path, "IOU")
drawAvgLoss(loss_log_path)
drawIOU(iou_log_path)
plt.show()
(2) 将上述python脚本文件和训练日志放在同一目录下,打开此目录下的终端,运行上述.py文件可以得到loss变化曲线和Avg IOU变化曲线。同时,在当前目录下生成了train_log_iou.txt和train_log_loss.txt文件。
训练日志可视化参考博客: YOLO-V3可视化训练过程中的参数,绘制loss、IOU、avg Recall等的曲线图
YOLOv3训练自己的数据集(3)——小技巧和训练日志可视化
注:测试的时候要更改cfg/yolov3-voc.cfg文件,将Testing启用,Training禁用
1、单张图片测试
./darknet detector test cfg/voc.data cfg/yolov3-voc.cfg backup/yolov3-voc_20000.weights voc/VOCdevkit/VOC2019/JPEGImages/0000629.jpg
2、摄像头实时测试
./darknet detector demo cfg/voc.data cfg/yolov3-voc.cfg backup/yolov3-voc_20000.weights
测试集:130张图片
3、批量测试一下,并输出记录目标位置信息的txt文件存在darknet/result下,可以使用valid命令
./darknet detector valid cfg/voc.data cfg/yolov3-voc.cfg backup/yolov3-voc_50000.weights
在detector.c文件中,validate_detector函数下,有个阈值设置thresh,默认是0.005,根据个人情况进行设置
./darknet detector valid cfg/voc.data cfg/yolov3-voc.cfg backup/yolov3-voc_50000.weights
参考博客
4、针对测试集,批量测试图片并将测试的图片显示结果保存在自定义的文件夹下
(1)用下面的代码替换detector.c文件(example文件夹下)的void test_detector函数(此处有三处需要改成自己的路径)
void test_detector(char *datacfg, char *cfgfile, char *weightfile, char *filename, float thresh, float hier_thresh, char *outfile, int fullscreen)
{
list *options = read_data_cfg(datacfg);
char *name_list = option_find_str(options, "names", "data/names.list");
char **names = get_labels(name_list);
image **alphabet = load_alphabet();
network *net = load_network(cfgfile, weightfile, 0);
set_batch_network(net, 1);
srand(2222222);
double time;
char buff[256];
char *input = buff;
float nms=.45;
int i=0;
while(1){
if(filename){
strncpy(input, filename, 256);
image im = load_image_color(input,0,0);
image sized = letterbox_image(im, net->w, net->h);
//image sized = resize_image(im, net->w, net->h);
//image sized2 = resize_max(im, net->w);
//image sized = crop_image(sized2, -((net->w - sized2.w)/2), -((net->h - sized2.h)/2), net->w, net->h);
//resize_network(net, sized.w, sized.h);
layer l = net->layers[net->n-1];
float *X = sized.data;
time=what_time_is_it_now();
network_predict(net, X);
printf("%s: Predicted in %f seconds.\n", input, what_time_is_it_now()-time);
int nboxes = 0;
detection *dets = get_network_boxes(net, im.w, im.h, thresh, hier_thresh, 0, 1, &nboxes);
//printf("%d\n", nboxes);
//if (nms) do_nms_obj(boxes, probs, l.w*l.h*l.n, l.classes, nms);
if (nms) do_nms_sort(dets, nboxes, l.classes, nms);
draw_detections(im, dets, nboxes, thresh, names, alphabet, l.classes);
free_detections(dets, nboxes);
if(outfile)
{
save_image(im, outfile);
}
else{
save_image(im, "predictions");
#ifdef OPENCV
cvNamedWindow("predictions", CV_WINDOW_NORMAL);
if(fullscreen){
cvSetWindowProperty("predictions", CV_WND_PROP_FULLSCREEN, CV_WINDOW_FULLSCREEN);
}
show_image(im, "predictions");
cvWaitKey(0);
cvDestroyAllWindows();
#endif
}
free_image(im);
free_image(sized);
if (filename) break;
}
else {
printf("Enter Image Path: ");
fflush(stdout);
input = fgets(input, 256, stdin);
if(!input) return;
strtok(input, "\n");
list *plist = get_paths(input);
char **paths = (char **)list_to_array(plist);
printf("Start Testing!\n");
int m = plist->size;
if(access("/home/FENGsl/darknet/data/out",0)==-1)//"/home/FENGsl/darknet/data"修改成自己的路径
{
if (mkdir("/home/FENGsl/darknet/data/out",0777))//"/home/FENGsl/darknet/data"修改成自己的路径
{
printf("creat file bag failed!!!");
}
}
for(i = 0; i < m; ++i){
char *path = paths[i];
image im = load_image_color(path,0,0);
image sized = letterbox_image(im, net->w, net->h);
//image sized = resize_image(im, net->w, net->h);
//image sized2 = resize_max(im, net->w);
//image sized = crop_image(sized2, -((net->w - sized2.w)/2), -((net->h - sized2.h)/2), net->w, net->h);
//resize_network(net, sized.w, sized.h);
layer l = net->layers[net->n-1];
float *X = sized.data;
time=what_time_is_it_now();
network_predict(net, X);
printf("Try Very Hard:");
printf("%s: Predicted in %f seconds.\n", path, what_time_is_it_now()-time);
int nboxes = 0;
detection *dets = get_network_boxes(net, im.w, im.h, thresh, hier_thresh, 0, 1, &nboxes);
//printf("%d\n", nboxes);
//if (nms) do_nms_obj(boxes, probs, l.w*l.h*l.n, l.classes, nms);
if (nms) do_nms_sort(dets, nboxes, l.classes, nms);
draw_detections(im, dets, nboxes, thresh, names, alphabet, l.classes);
free_detections(dets, nboxes);
if(outfile){
save_image(im, outfile);
}
else{
char b[2048];
sprintf(b,"/home/FENGsl/darknet/data/out/%s",GetFilename(path));//"/home/FENGsl/darknet/data"修改成自己的路径
save_image(im, b);
printf("save %s successfully!\n",GetFilename(path));
#ifdef OPENCV
cvNamedWindow("predictions", CV_WINDOW_NORMAL);
if(fullscreen){
cvSetWindowProperty("predictions", CV_WND_PROP_FULLSCREEN, CV_WINDOW_FULLSCREEN);
}
show_image(im, "predictions");
cvWaitKey(0);
cvDestroyAllWindows();
#endif
}
free_image(im);
free_image(sized);
if (filename) break;
}
}
}
}
(2)在detector.c文件的最前面加上*GetFilename(char *p)函数(根据自己的情况更改注释处的内容)
#include "darknet.h"
static int coco_ids[] = {1,2,3,4,5,6,7,8,9,10,11,13,14,15,16,17,18,19,20,21,22,23,24,25,27,28,31,32,33,34,35,36,37,38,39,40,41,42,43,44,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,67,70,72,73,74,75,76,77,78,79,80,81,82,84,85,86,87,88,89,90};
char *GetFilename(char *p)
{
static char name[20]={""};
char *q = strrchr(p,'/') + 1;
strncpy(name,q,7);//注意后面的7,如果你的测试集的图片的名字字符(不包括后缀)是其他长度,请改为你需要的长度(官方的默认的长度是6)
return name;
}
(3)在darknet下重新进行make一下
注:在make的时候可能会出现如下错误
#ifdef OPENCV cvNamedWindow("predictions", CV_WINDOW_NORMAL); 提示找不到CV_WINDOW_NORMAL的定义? 报错: error: ‘CV_WINDOW_NORMAL’ undeclared (first use in this function)
解决方法:
找到darknet下的Makefile文件将OPENCV=0
(4)开始批量测试
./darknet detector test cfg/voc.data cfg/yolov3-voc.cfg backup/yolov3-voc_50000.weights
后面输入你的txt文件路径(你准备好的所有测试图片的路径全部存放在一个txt文件里),你可以复制voc.data文件里的valid后面的路径,就可以了。
在darknet下的data中生成out文件夹,里面存放的是批量测试后的图片标注结果