Yolov3+C+++opencv+VS2015训练过程及检测(很详细)

  1. 运行环境

    我的运行环境是C+++opencv+VS2015+yolov3,切记opencv的版本最好是opencv 3.4.2版以上的,这个版本以后才有了DNN函数库来实现机器学习的相关内容,否则程序会报未定义错误。

    yolo的一些思想,网上非常多,大家可以自行搜索。
     

  2. 训练过程

  • 制作自己的数据集

     下载数据标记工具labellmg     下载labellmg  

所有文件下载的网盘地址:链接:https://pan.baidu.com/s/19YTUuBmte8drb_ePdjA6og     提取码:32j1

  • 采集图片

   图片大小最好是416*416,这个方便后面作为网络的输入。用摄像头直接采集图片,程序可以参考下,用VS2015调用opencv库写的。当然你也可以在网上下载现成的数据集来标注,我做交通标志识别,所以用的现成的数据集。我是从这篇博客下载的https://blog.csdn.net/u010323150/article/details/84341291

  • 标记图片

采集完图片或者下载好数据集后,就是用labellmg软件进行标记得到.xml文件和.txt文件。具体步骤如下:

       解压了之后,可以直接打开data文件夹,可以把采集的图片都添加进来,然后在predefined_classes.txt文件中添加自己需要标记的类别,一个类别一行。(也就是你要检测物体的名字)

resources.py要放入上面的文件夹

      这里我们打开labellmg软件需要通过labellmg.py,因为它是python写的,所以需要配置环境,具体可以见我的另一篇博客

tensorflow-gpu1.14 + Win10 + CUDA10.0 + CUDNN7.5.0 + Python3.6 + VS2015安装装完之后在按下面说明,里面遇到的问题及解决方法也都有详细记录。

//因为无labelImg.exe文件,需要打开labelImg.py,在Anaconda prompt上按下面操作

(base) C:\Users\Wang>activate TensorFlow
Could not find conda environment: TensorFlow
You can list all discoverable environments with `conda info --envs`.


(base) C:\Users\Wang>conda info --envs
# conda environments:
#
base                  *  C:\Users\Wang\Anaconda3
TensorFlow--GPU          C:\Users\Wang\Anaconda3\envs\TensorFlow--GPU


(base) C:\Users\Wang>activate TensorFlow--GPU

(TensorFlow--GPU) C:\Users\Wang>cd ..

(TensorFlow--GPU) C:\Users>cd..//退回上一级目录

(TensorFlow--GPU) C:\>cd d:
D:\

(TensorFlow--GPU) C:\>cd d
系统找不到指定的路径。

(TensorFlow--GPU) C:\>d://进入D盘

(TensorFlow--GPU) D:\>cd D:\Yolo\model\labelImg-master

(TensorFlow--GPU) D:\Yolo\model\labelImg-master>python labelImg.py//这里执行完就可以打开标注软件
Traceback (most recent call last):
  File "labelImg.py", line 40, in <module>
    from libs.labelFile import LabelFile, LabelFileError
  File "D:\Yolo\model\labelImg-master\libs\labelFile.py", line 10, in <module>
    from libs.pascal_voc_io import PascalVocWriter
  File "D:\Yolo\model\labelImg-master\libs\pascal_voc_io.py", line 6, in <module>
    from lxml import etree
ModuleNotFoundError: No module named 'lxml'

(TensorFlow--GPU) D:\Yolo\model\labelImg-master>pip install lxml//安装1xml
Looking in indexes: https://pypi.mirrors.ustc.edu.cn/simple/
Collecting lxml
  Downloading https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/ab/4e/fd747f8948b107147a0e618a75bdad3a0d1de6227d856261008fd8f850af/lxml-4.5.0-cp36-cp36m-win_amd64.whl (3.7MB)
     |████████████████████████████████| 3.7MB 2.2MB/s
Installing collected packages: lxml
Successfully installed lxml-4.5.0

(TensorFlow--GPU) D:\Yolo\model\labelImg-master>python labelImg.py//成功进入

打开软件如下所示

可以通过Open Dir 导入文件夹的所有图片,然后标注完一张就点Next Image进行下一张标注。标注过程如下点击Create\nRectBox,用框把目标框住,然后选择类别名字,OK即可,然后点击save 保存,这里在Save下面Pasca1VOC是保存为xml文件,点击此处切换为YOLO,点击Verify Image,保存为txt文件,在图片的文件夹下会生成同名的XML和txt文件。

 

这里说名一下TXT的内容,第一项数字5为我的类别,我在labelImg-master\date文件下predefined_classes.txt这个文件建立的,总共有8个类别。如下图。第2,3项为标注目标的框的中心像素在图片的位置,4,5项为框的大小。

  •  整理数据集

    (1)在D:\Yolo\model\darknet-master\scripts下创建一个新的文件夹:VOCdevkit文件夹。再在VOCdevkit文件夹下面分别创建这几个文件夹:

VOC2020文件夹          ------文件名称可以改

JPEGImages文件夹     -----存放所有的.jpg图片

(2)在VOC2020文件夹下创建这几个文件夹:

Annotations文件夹       -------存放所有的.xml文件

ImageSets文件夹        

Labels文件夹              --------存放所有的.txt文件

(3)在ImageSets文件夹下创建一个Main文件

Main文件夹

(4)在Main文件夹下创建两个.txt文本:train.txt、val.txt

train.txt        ------存放图片的名称,不带后缀,如下图所示

val.txt

import os
from os import listdir, getcwd
from os.path import join
if __name__ == '__main__':

    source_folder='D:\Yolo\model\darknet-master\scripts\VOCdevkit\JPEGImages'

   dest='D:\Yolo\model\darknet-master\scripts\VOCdevkit\VOC2020\ImageSets\Main\train.txt' 

   dest2='D:\Yolo\model\darknet-master\scripts\VOCdevkit\VOC2020\ImageSets\Main\val.txt'  

    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_name,file_extend=os.path.splitext(file_obj)

        file_num=int(file_name) 



        if(file_num<776):                     


            train_file.write(file_name+'\n')  

        else :

          val_file.write(file_name+'\n')    

    train_file.close() 

val_file.close()

这个写图片名,其实有个python程序可以直接生成,如上所示,记得修改到对应的路径。

修改voc_label.py生成对应Main中train.txt和val.txt,记得修改对应的路径。


import xml.etree.ElementTree as ET

import pickle

import os

from os import listdir, getcwd

from os.path import join

 

sets=[('2020', 'train'), ('2020', 'val')]  #这里需要修改成对应的Main中

 

classes = ["mandatory", "vehicle", "nonVehicle", "minspeedlimit", "maxspeedlimit", "warning", "prohibitory", "heightlimit"] #这里需要改成对应的类别,可以有多类

 

 

def convert(size, box):

    dw = 1./(size[0])

    dh = 1./(size[1])

    x = (box[0] + box[1])/2.0 - 1

    y = (box[2] + box[3])/2.0 - 1

    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()

os.system("type 2020_train.txt 2020_val.txt > train.txt")


 
  • 下载预先训练的权重

点击:darknet53.conv.74下载

  • 整理所有的文件到对应的地方

其实最终我们只需要.jpg、.xml、2020_train.txt、2020_val.txt、darknet53.conv.74

(1)在D:\Yolo\model\darknet-master\build\darknet\x64\data下创建两个文件夹:


obj文件夹             -----存放所有的.jpg图片和.xml文件

weights文件夹      -------存放训练后的权重文件


(2)对于2020_train.txt和2020_val.txt文件,我们用记事本打开,会发现就是图片的绝对路径而已,在这里需要修改成现在图片的路径,如下所示,其实在运行那个voc_lable.py时可以修改下图片的路径为这里的obj路径。

  • 修改其他的文件

(1)修改voc.data

记住这里的voc.data是在D:\Yolo\model\darknet-master\build\darknet\x64\data这下面的,做如下修改:

类别数为对应你想检测的类别总数,train和valid对应的路径,backup就是最后训练好的权重存放位置。
 

classes= 8
train  = data/2020_train.txt
valid  = data/2020_val.txt
names = data/voc.names
backup = data/weights

(2)修改yolov3-voc.cfg

记住这里的yolov3-voc.cfg是在D:\Yolo\model\darknet-master\build\darknet\x64下的,做如下修改:

一共需要改三处,每处有两个地方需要修改,每次都是先修改[yolo]下的classes为对应的类别数,再修改[yolo]对应上面的[convolutional]下的filters为(classes+5)*3
 

[convolutional]
size=1
stride=1
pad=1
filters=39   #(classes+5)*3
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=8    #类别数
num=9
jitter=.3
ignore_thresh = .5
truth_thresh = 1
random=1

(3)修改voc.names

记住这里的voc.names是在D:\Yolo\model\darknet-master\build\darknet\x64\data下的,做如下修改:

写上自己需要检测的类别名即可,一行一个,我这里有八个

 

 

  • 生成darknet.exe

配置好opencv环境,具体见上面博客或者自己搜索,在VS下打开D:\Yolo\model\darknet-master\build\darknet下darknet.sln,运行后在D:\Yolo\model\darknet-master\build\darknet\x64文件夹下会看到darknet.exe

  • 开始训练

    我这里是在win10系统下进行训练的,所有打开cmd,cd D:\Yolo\model\darknet-master\build\darknet\x64,到这个路径下,然后输入:


darknet.exe detector train data/voc.data yolov3-voc.cfg darknet53.conv.74 data/weights  >> yolov3.log


这里需要等待很长时间,大概1000次就会生成一个yolov3-voc_last.weights。一般根据数据样本的多少,可能得学习5-10个小时,学习完成后会生成权重文件,注意这里对电脑的配置要求比较高CPU,内存,和GPU显卡。这里采用CPU训练,至少是I7 标压版,否则可能无法进行训练>>yolov3.log :这是生成日志

  • 测试

   测试,最后生成权重文件,测试会用到下面4个加上D:\Yolo\model\darknet-master\build\darknet\x64\data\obj的txt和图片文件

  测试在VS2015+OpenCV3.4.2的环境下进行,整个代码采用C++语言来写的,构造了一个Recognition的类。创建了yolov3.h和yolov3.cpp还有main.cpp三个文件。下面只针对关键代码进行解释,代码上也有注释。

(1)首先对yolov3进行配置,即把三个重要的文件分别加载进来,即cocoB.names,yolov3-vocB.cfg,yolov3-voc_3000B.weights文件,注意这里的路径,写绝对路径,一直没有加载进来而报错。就用自己生成的文件。
yolov3.h

#pragma once
#ifndef YOLOV3_H
#define YOLOV3_H

#include <fstream>
#include <sstream>
#include <iostream>

#include <opencv2/dnn.hpp>
#include <opencv2/imgproc.hpp>
#include <opencv2/highgui.hpp>
#include <CL/cl.h>


using namespace cv;
using namespace dnn;
using namespace std;

class Recognition//建立识别的类
{
private:
	// 初始化参数
	float confThreshold; // 置信度阈值
	float nmsThreshold;  // 非极大值的抑制阈值
	const int inpWidth = 416;  // 网络输入图像的宽度
	const int inpHeight = 416; // 网络输入图像的高度
	
	
	VideoCapture cap;//打开摄像头
	vector<string> class_names; //用来存放所有类别的名字
public:
	Recognition();
	~Recognition();

	//初始化Yolov3的配置
	Net InitYolov3();

	//得到输出层的名称
	vector<String> getOutputsNames(const Net& net);

	//使用非极大值抑制方法删除低置信度的边界框
	void postprocess(Mat& frame, const vector<Mat>& out);

	//画出预测的边界框
	void drawPred(int classId, float conf, int left, int top, int right, int bottom, Mat& frame);

	//执行用Yolov3的过程
	void runYolov3(Net& net, Mat& frame);

};

#endif //yolov3_h#pragma once
#pragma once

 

#include "yolov3.h"


Recognition::Recognition()
{
	confThreshold = 0.5;
	nmsThreshold = 0.4;
}

Recognition::~Recognition()
{

}

/************************************************************************
函数功能:进行Yolov3的各种配置工作
输入:无
输出:返回配置好的网络
************************************************************************/
Net Recognition::InitYolov3()
{
	//加载类名文件
	std::string class_names_string = "D:\\Yolo\\model\\cocoB.names";
	std::ifstream class_names_file(class_names_string.c_str());
	if (class_names_file.is_open())
	{
		std::string name = "";
		while (std::getline(class_names_file, name))
		{
			class_names.push_back(name);
		}
	}
	else
	{
		std::cout << "don't open class_names_file!" << endl;
	}

	//给出模型的配置和权重文件
	cv::String modelConfiguration = "D:\\Yolo\\model\\yolov3-vocB.cfg";
	//cv::String modelWeights = "F:\\VS\\20190309Yolov3\\yolov3-voc_250_1000.weights";
	cv::String modelWeights = "D:\\Yolo\\model\\yolov3-voc_3000B.weights";
	//cv::String modelWeights = "F:\\VS\\20190309Yolov3\\yolov3-voc_1000_1000.weights";
	// 装载网络
	cv::dnn::Net net = cv::dnn::readNetFromDarknet(modelConfiguration, modelWeights);//读取网络模型和参数,初始化网络
	std::cout << "Read Darknet..." << std::endl;
	net.setPreferableBackend(cv::dnn::DNN_BACKEND_OPENCV);

	net.setPreferableTarget(DNN_TARGET_OPENCL_FP16);//没有GPU用CPU
	//net.setPreferableTarget(DNN_TARGET_CPU);
	
	return net;
}

/************************************************************************
函数功能:得到输出层的名称
输入:需要遍历的网络Net
输出:返回输出层的名称
************************************************************************/
std::vector<cv::String> Recognition::getOutputsNames(const Net& net)
{
	static vector<String> names;
	if (names.empty())
	{
		//得到输出层的索引号
		std::vector<int> out_layer_indx = net.getUnconnectedOutLayers();

		//得到网络中所有层的名称
		std::vector<String> all_layers_names = net.getLayerNames();

		//在名称中获取输出层的名称
		names.resize(out_layer_indx.size());
		for (int i = 0; i < out_layer_indx.size(); i++)
		{
			names[i] = all_layers_names[out_layer_indx[i] - 1];
		}
	}
	return names;
}

/************************************************************************
函数功能:使用非极大值抑制方法删除低置信度的边界框
输入:frame:网络的输入图像,out:网络输出层的输出图像
输出:无
************************************************************************/
void Recognition::postprocess(Mat& frame, const vector<Mat>& out)
{
	std::vector<float> confidences;
	std::vector<Rect> boxes;
	std::vector<int> classIds;

	for (int num = 0; num < out.size(); num++)
	{
		double value;
		Point Position;
		//得到每个输出的数据
		float *data = (float *)out[num].data;

		for (int i = 0; i < out[num].rows; i++, data += out[num].cols)
		{
			//得到每个输出的所有类别的分数
			Mat sorces = out[num].row(i).colRange(5, out[num].cols);

			//获取最大得分的值和位置
			minMaxLoc(sorces, 0, &value, 0, &Position);
			//if(value>0)
			//cout << value << endl;
			if (value > confThreshold)
			{
				cout << "value:" << value << " confThreshold:" << confThreshold << endl;
				//data[0],data[1],data[2],data[3]都是相对于原图像的比例
				int center_x = (int)(data[0] * frame.cols);
				int center_y = (int)(data[1] * frame.rows);
				int width = (int)(data[2] * frame.cols);
				int height = (int)(data[3] * frame.rows);
				int box_x = center_x - width / 2;
				int box_y = center_y - height / 2;

				classIds.push_back(Position.x);
				confidences.push_back((float)value);
				boxes.push_back(Rect(box_x, box_y, width, height));
			}
		}
	}

	//执行非极大值抑制,以消除具有较低置信度的冗余重叠框 
	std::vector<int> perfect_indx;
	NMSBoxes(boxes, confidences, confThreshold, nmsThreshold, perfect_indx);
	for (int i = 0; i < perfect_indx.size(); i++)
	{
		int idx = perfect_indx[i];
		Rect box = boxes[idx];
		drawPred(classIds[idx], confidences[idx], box.x, box.y,
			box.x + box.width, box.y + box.height, frame);
	}

}

/************************************************************************
函数功能:绘制预测边界框
输入:矩形框的对角的两个坐标
输出:无
************************************************************************/
void Recognition::drawPred(int classId, float conf, int left, int top, int right, int bottom, Mat& frame)
{
	//Draw a rectangle displaying the bounding box
	rectangle(frame, Point(left, top), Point(right, bottom), Scalar(255, 178, 50), 3);
	string label = format("%.2f", conf);

	std::string name = class_names[classId] +":" + label;//绘制类名字和得分
	//std::string name = std::to_string(classId) + "  " + label;
	//Get the label for the class name and its confidence
	

	//Display the label at the top of the bounding box
	int baseLine;
	Size labelSize = getTextSize(name, FONT_HERSHEY_SIMPLEX, 0.5, 1, &baseLine);
	top = max(top, labelSize.height);
	rectangle(frame, Point(left, top - round(1.5*labelSize.height)), Point(left + round(1.5*labelSize.width), top + baseLine), Scalar(255, 255, 255), FILLED);
	putText(frame, name, Point(left, top), FONT_HERSHEY_SIMPLEX, 0.75, Scalar(0, 0, 0), 1);
}

/************************************************************************
函数功能:yolov3执行的过程
输入:yolov3的网络net,原始的图像frame
输出:无
************************************************************************/
void Recognition::runYolov3(Net& net, Mat& frame)
{
	// 从一个帧创建一个4D blob
	cv::Mat blob;

	// 1/255:将图像像素值缩放到0到1的目标范围
	// Scalar(0, 0, 0):我们不在此处执行任何均值减法,因此将[0,0,0]传递给函数的mean参数
	cv::dnn::blobFromImage(frame, blob, 1 / 255.0, cv::Size(inpWidth, inpHeight), cv::Scalar(0, 0, 0), true, false);

	// 设置网络的输入
	net.setInput(blob);

	// 运行向前传递以获得输出层的输出
	std::vector<cv::Mat> outs;
	net.forward(outs, getOutputsNames(net));//forward需要知道它的结束层

											// 以较低的置信度移除边界框
	postprocess(frame, outs);//端到端,输入和输出

							 //std::cout << "succeed!!!" << std::endl;

	cv::Mat detectedFrame;
	frame.convertTo(detectedFrame, CV_8U);
	// 显示detectedFrame
	cv::imshow("detectedFrame", detectedFrame);
	waitKey(1);

}


 

以上即是yolov3的关键函数,先进行调用InitYolov3函数进行yolov3的配置,再调用runYolov3进行执行即可这里其实就是在用OpenCV的DNN库函数调用网络,再进行测试,代码也可以参考https://www.aiuai.cn/aifarm822.html#4.5.Step5-处理每一帧,写的很详细。

  • 最后的检查结果

 

整体结果还算不错,这也是我的一点总结,大家有问题,欢迎随时交流。

 

 

 

 

  • 5
    点赞
  • 60
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值