yolov4训练自己的数据模型

看了下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张人脸都检测到了很明显这里误检测了一个地方,其余的16张人脸都检测到了,还别说,那个误检测的还真有些像。

在这里插入图片描述至于这张图片,用yolov4训练的模型,检测完美,但是用yolov3训练好的模型然后用yolov3去测试,会出现误检的情况,不知道是不是模型训练不够的原因。

2020 4.25

版权声明:本文为CSDN博主「雅痞匪徒(Nike)」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/yapifeitu/article/details/105749693

雅痞匪徒(Nike)

我还没有学会写个人说明!

暂无评论

发表评论

相关推荐