看了下yolov4的作者给的操作说明,链接如下:https://github.com/AlexeyAB/darknet#how-to-compile-on-linux-using-make,有兴趣的可以去看看,总结起来,跟yolov3的操作方式基本一样,所以现在记录一下这次的整个操作流程。
在几个月前,一直在准备一个项目,那个项目已经让人用lableme这个标注软件标注好了图片,但是直到现在,这个项目依旧还没有动工的苗头,估计是悬了,之前也用标注好的数据进行过yolov3的训练,但是说实话,那时候对于yolov3是一头雾水,只知道按照他人的给的操作流程去做,但是为什么这样做,我是不清楚的,这也导致我自己都不知道当初的训练模型的流程是否正确,现在好了,前段时间把yolov3,ssd学习了,现在来操作,就知道这样操作的原理了,整个操作流程就知道的很详细了,也知道怎么训练自己的数据了,不再茫然。
环境说明:yolo系列一直都有linux版本和windows版本,我看了哈,很明显linux版本更加简单,windows版本还需要搭配vs来操作,光vs这个软件就十几个g,太大了,所以我就采取在ubuntu上训练数据了。
首先,要感谢下面的这篇文章的作者:https://www.cnblogs.com/Assist/p/11091501.html,这篇文章为我梳理了整个思路,这也为我想处理自己的数据,训练自己的模型有了想法。
一般来说,训练coco, vol这些数据集,网上很多教程来进行数据的预处理,但是训练自己的数据,就得自己根据网上的教程来改代码了,因为coco这些数据集的标注文件和图片存储结果可能跟自己的数据都不一样,对于我用labelme来标注的图像来说,每一张图片会对应一个json文件,这个json文件里面拥有这标注的类别坐标这些信息,所以,我这里值将所有图片放到一个文件夹,所有json文件放到一个文件夹,但是为了区分训练数据和val数据,我将json进行了划分,一部分放到了train文件夹,一部分放到了val文件夹,最后整个文件的结构如下:
接下来咱就可以开始正式的数据预处理了,这一块是最麻烦的,我个人觉得哈。我们运行第一个Py文件后,要创建出下面这几个文件夹:
第一个文件夹用来装xml文件,这个xml文件里面有标签类别,图片的宽高,标签的左上角和右下角坐标这些信息,每一张照片对应一个xml文件,imagesets这个文件夹里面还有一个文件夹:/Main, 在这个Main文件夹下面会产生train.txt和val.txt,这些文件里面的信息只是图片的名字,不带.jpg这些后缀的名字,而在JPEGImages里面就是所有图片了。
我们假设这第一个py文件叫convertvoc.py,我下面提供我的代码,大家不能照搬,有些地方要改。
'''author:nike hu'''
# -*- coding: utf-8 -*-
import shutil
import os
import json
import cv2
headstr = """\
<annotation>
<folder>VOC2007</folder>
<filename>%06d.jpg</filename>
<source>
<database>My Database</database>
<annotation>PASCAL VOC2007</annotation>
<image>flickr</image>
<flickrid>NULL</flickrid>
</source>
<owner>
<flickrid>NULL</flickrid>
<name>company</name>
</owner>
<size>
<width>%d</width>
<height>%d</height>
<depth>%d</depth>
</size>
<segmented>0</segmented>
"""
objstr = """\
<object>
<name>%s</name>
<pose>Unspecified</pose>
<truncated>0</truncated>
<difficult>0</difficult>
<bndbox>
<xmin>%d</xmin>
<ymin>%d</ymin>
<xmax>%d</xmax>
<ymax>%d</ymax>
</bndbox>
</object>
"""
tailstr = '''\
</annotation>
'''
# 上面的不用改
def writexml(idx, head, bbxes, tail):
filename = ("Annotations/%06d.xml" % (idx))
f = open(filename, "w")
f.write(head)
for bbx in bbxes:
f.write(objstr % (bbx[0], bbx[1], bbx[2], bbx[3], bbx[4]))
# 这里就是将文件中标签类别,左上角和右下角坐标存进去
f.write(tail)
f.close()
# 这个函数不用改
def clear_dir():
if shutil.os.path.exists(('Annotations')):
shutil.rmtree(('Annotations'))
if shutil.os.path.exists(('ImageSets')):
shutil.rmtree(('ImageSets'))
if shutil.os.path.exists(('JPEGImages')):
shutil.rmtree(('JPEGImages'))
shutil.os.mkdir(('Annotations'))
shutil.os.makedirs(('ImageSets/Main'))
shutil.os.mkdir(('JPEGImages'))
def excute_datasets(json_path, tr, idx):
json_path = os.path.join(json_path, tr)
json_file = os.listdir(json_path) # 读取文件夹下的所有json文件
savename = open(('ImageSets/Main/' + tr + '.txt'), 'a') # 写入图片名字
for file in json_file:
file_path = os.path.join(json_path, file) # 找到json文件路径
with open(file_path, 'r', encoding='utf-8') as f: # 开始读取json文件
file_json = json.load(f)
imagename = file_json["imagePath"].split('\\')[-1] # 找到当前json文件对应的图片名字
image_path = os.path.join('./images', imagename) # 找到图片的路径,这里根据不一样的情况,也不一样,需要改
image = cv2.imread(image_path)
if image is None: # 如果没有这种照片,跳过
continue
label_shape_type = file_json['shapes'][0]['shape_type']
if label_shape_type != 'rectangle': # 暂时不考虑其它形状的标签,在labelme这个软件打标签的时候有rectangle,还有多边形圆形这些,这些需要另外处理
continue
head = headstr % (idx, image.shape[1], image.shape[0], image.shape[2])
# 这里是把对应图片路径,图片长,维度,宽存进去
shapes = file_json['shapes'] # 这里是存储标签和坐标的位置,是一个列表,列表里面有很多字典,每个字典就代表一个标签
boxes = []
for i in range(len(shapes)):
classname = file_json['shapes'][i]['label'] # 类别
'''接下来转化类别为英文,因为labelme在标注的时候,为了标注人员的遍历,类别是中文,但是我们训练模型的时候必须是英文,这里就需要转化了,这里得xxxx不代表真的是xxxx,是你自己训练的类别,为了不让我老板看出我做的是他的项目,这里隐藏了'''
if 'xxxxxxx' in classname:
classname = 'xxxxxxx'
if 'xxxxxxx' in classname:
classname = 'xxxxxxx'
if 'xxxxxxx' in classname:
classname = 'xxxxxxx'
if 'xxxxxxx' in classname:
classname = 'xxxxxxx'
if 'xxxxxxx' in classname:
classname = 'xxxxxxx'
if 'xxxxxxx' in classname:
classname = 'xxxxxxx'
box = [classname, file_json['shapes'][i]['points'][0][0],
file_json['shapes'][i]['points'][0][1], file_json['shapes'][i]['points'][1][0],
file_json['shapes'][i]['points'][1][1]] # 这里存储的意义是[标签类别, x1,y1, x2, y2]
boxes.append(box)
writexml(idx, head, boxes, tailstr)
cv2.imwrite('JPEGImages/%06d.jpg' % (idx), image)
savename.write('%06d\n' % (idx)) # 写入图片编号
idx += 1
savename.close()
return idx
if __name__ == '__main__':
clear_dir()
idx = 1
idx = excute_datasets('./jsonhot', 'train', idx) # 其实主要改的地方是这,'./jsonhot'代表json文件的目录
idx = excute_datasets('./jsonhot', 'val', idx) # 还有这也要改,再就是上面那个函数有个地方也要改
print('Complete...')
大概需要改的地方我都标注出来了,大家根据自己的实际情况去改上面的代码。上面的代码借鉴了我最上面给的作者的一些代码,毕竟原理都这样,只需要改一改细节上的东西。之前写过一篇文章,在构造xml结构的时候用的是xml这个库区一个一个节点的构造,现在看来,还是直接向上面的代码那样做简单很多。
我们运行这个代码之后,就会达到我们上面说的目的,生成annotations这些文件夹和数据,这个py文件运行完后大家一定要去看看产生了什么数据,不然对后面的操作会懵逼的,接下来,我们要写一个py文件,我们假设这个文件叫getdata.py,我们要达到的目的是:在主目录下生成两个文件夹train.txt和val.txt,这文件名字随意,这两个文件里面存储的是图片的绝对路径,比如:/media/yunyi/file/code/darknet/data/voc/VOCface/JPEGImages/000003.jpg,这个Py文件运行后还会生成一个label文件夹,这个文件夹里面也会生成以图片名称命名的txt文件,这些文件保存的是图片中的标签类别,中心点的x,中心点的y,标签的宽,边框的高,接下来,我们看看代码:
import xml.etree.ElementTree as ET
import pickle
import os
from os import listdir, getcwd
from os.path import join
sets=['train', 'val']#
classes = ['......'] # 这里是你要处理的数据的类别总数
# 这里是将左上角和右下角的坐标转化为中心点和宽高
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(image_id):
in_file = open(wd + '/Annotations/%s.xml'%(image_id))
out_file = open( wd + '/labels/%s.txt'%(image_id), 'w')
tree=ET.parse(in_file) # 导入xml数据
root = tree.getroot() # 得到跟节点
size = root.find('size') # 找到根节点下面的size节点
w = int(size.find('width').text) # 得到图片的尺寸
h = int(size.find('height').text)
for obj in root.iter('object'): # 对根节点下面的'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')
if __name__=='__main__':
wd = getcwd() # 获取当前文件的路径
wd = wd.replace('\\', '/')
for image_set in sets: # image_set是train或者val
if not os.path.exists(wd + '/labels/'): # 创建一个label文件夹来存放图片对应的类别和坐标
os.makedirs(wd + '/labels/')
image_ids = open(wd +'/ImageSets/Main/%s.txt' % image_set).read().strip().split()
list_file = open('%s.txt' % image_set, 'w')
for image_id in image_ids:
list_file.write(wd + '/JPEGImages/%s.jpg\n' % image_id)
convert_annotation(image_id)
list_file.close()
代码里面很多xml.find()啥的函数,就是找xml里面的节点用的,这些代码,我个人感觉当我们第一个py文件执行之后,这个py文件改一改类别就可以运行了,当然,有可能会出现意外情况,这时候根据不同的报错解决吧,我这反正是没问题的。
好了,最麻烦的预处理弄完了,接下来咱看看怎么配置yolov4了,直接看图片,形象:
在这个yolov4.cfg里面,我们一般要更改的就是batchsize,subdivisions, classes,这几个参数,对于filters=255这一行,根据上面的说明进行更改,max_batches = 500200,steps=400000,450000这两行大家根据实际情况看该不该,电脑配置给力就没必要改,配置不行就改一下,一般是改为 max_batches = 2000,steps=1600,1800,大家更改的时候直接ctrl+f进行定位就行。
对cfg文件进行更改之后,我们就要创建两个文件了,比如叫face.data,face.names,名字你随意,在face.data里面,你的内容范本如下:
classes 就是你的数据的类别总数,train就是你上面getdata.py生成的train.txt文件的路径,这里是相对路径,val同理,names就是刚刚创建的face.names的路径,backup就是训练过程保存模型的路径。至于face.names,里面的内容就是你的类别名字了,类似下面这张:
做了上面那些工作,终于可以开始训练了,只需要一行命令:./darknet detector train data/xxx.data cfg/yolov4.cfg yolov4.conv.137 -map,其中yolov4.conv.137是预训练模型,需要另外进行下载,我有空去下载好然后分享一下,如果没有这个文件,那么只需要执行./darknet detector train data/xxx.data cfg/yolov4.cfg -map(没有-map也可以)即可,yolov4和yolov3训练的时候区别就出来了,yolov4训练的时候会用一张动态图来显示训练的效果,如下所示:
至于训练的效果,哎,不提了,我的gtx2060还在学校,现在还没有返校,只能用多年前的笔记本了,这笔记本显卡是720m,但是不知道是不是cuda这些版本没有选择好,yolo不能使用我这台的gpu,以前折腾了很久,最后放弃了,所以,我只能用cpu来跑,但是我这cpu还是四代i5的,跑不动,所以。。。。。。。。没有训练效果。
好了,训练过程终于理清并且写完了,觉得有用的,留个赞再走呗。
补充:刚刚把作者给的预训练模型下载了,免费分享链接如下:https://blog.csdn.net/yapifeitu/article/details/105756274,拿走的时候记得点赞哦。
ps:5.7号用百度的显卡跑了一下yolov3的模型,果然出问题了,最后,补上上面没有说到的地方:我们在训练的时候,对于cfg文件,我们要将test注释掉,类似于下面这种:
哎,就这里没注释然后去跑模型,让我纠结了好久,希望大家后面注意。
5.8号我在aistudio上用上面的tesla v100跑了一个人脸检测的yolov4模型(由于这篇文章涉及到的数据是我老板发钱买来的,不适合放到公开平台去,所以就用公开数据集跑了人脸检测的模型),显卡给力,跑模型的速度就给力,模型跑的avgloss在3左右后一直在这附近徘徊,知道应该是降不下去多少了,然后把模型下载了,放到自己的的电脑上来测试了几张照片,效果挺不错的,给大家看看效果吧:
很明显这里误检测了一个地方,其余的16张人脸都检测到了,还别说,那个误检测的还真有些像。
至于这张图片,用yolov4训练的模型,检测完美,但是用yolov3训练好的模型然后用yolov3去测试,会出现误检的情况,不知道是不是模型训练不够的原因。
2020 4.25
版权声明:本文为CSDN博主「雅痞匪徒(Nike)」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/yapifeitu/article/details/105749693
暂无评论