-
运行环境
我的运行环境是C+++opencv+VS2015+yolov3,切记opencv的版本最好是opencv 3.4.2版以上的,这个版本以后才有了DNN函数库来实现机器学习的相关内容,否则程序会报未定义错误。
yolo的一些思想,网上非常多,大家可以自行搜索。
-
训练过程
- 制作自己的数据集
下载数据标记工具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-处理每一帧,写的很详细。
- 最后的检查结果
整体结果还算不错,这也是我的一点总结,大家有问题,欢迎随时交流。