Darknet 训练与部署 YoLov3 模型

基于 YoLo 的目标检测

本项目主要针对灰度图像 .bmp 进行目标检测, 要求高效、准确地从灰度图像中检测出目标物体.

环境

  • Windows 10
  • Visual Studio 2017
  • NVIDIA Geforce RTX 2060
  • CUDA 10.2
  • cuDNN

数据集构建

首先需要采集足够多的图片, 然后使用 labelImg 工具进行图片标注.

从 Github 官方仓库 https://github.com/tzutalin/labelImg/releases/tag/v1.8.1 下载 windows_v1.8.1.zip, 解压后可以直接运行其中的labelImg.exe 打开软件, 解压路径不可以有中文.

软件使用可以参考 labelImg 使用教程 图像标定工具

注意, 软件中存储格式要从 PascalVOC(标签文件存储为 xml 格式) 改为yolo(标签文件存储为 txt 格式).

构建好的数据集包括若干张 .bmp 图片及其对应的同名 .txt 文件.

YoLo 模型训练

darknet 框架

官方版本(作者: Joseph Redmon)

Darknet 是一个用 C 和 CUDA 编写的开源神经网络框架。速度快,安装方便,支持 CPU 和 GPU 计算.

官网: https://pjreddie.com/darknet/
官网安装教程: https://pjreddie.com/darknet/install/

1
2
3
4
5
6
Darknet is easy to install with only two optional dependancies:

OpenCV if you want a wider variety of supported image types.
CUDA if you want GPU computation.

Both are optional so lets start by just installing the base system. I've only tested this on Linux and Mac computers. If it doesn't work for you, email me or something?

该框架的作者 Joseph Redmon 仅在 linux 和 mac 系统中测试过, 要在 windows 系统下使用还要想其他办法.

windows 适配版(作者: AlexeyAB)

AlexeyAB 在原项目的基础上增加了darknet 在 windows 上的适配.

官方 Github 仓库: https://github.com/AlexeyAB/darknet
Release 版: https://github.com/AlexeyAB/darknet/releases/tag/darknet_yolo_v3
AlexeyAB-DarkNet 源码解析: https://github.com/BBuf/Darknet

从上述 Release 地址下载 darknet.zip 解压后即可运行.

手动编译 Darknet

安装 CUDA, cuDNN, OpenCV 后下载 Darknet 源码进行编译得到 darknet.exe.

AlexeyAB Release 版

既然 AlexeyAB 提供了他构建好的二进制包, 那么我们直接用就好了

There are attached compiled binary files of Darknet for Windows x64 (559 MB): https://github.com/AlexeyAB/darknet/releases/download/darknet_yolo_v3/darknet.zip

从上述网址下载 darknet.zip 文件, 解压缩后在 darknet\build\ 路径下的 darknet 就是编译好的包, 其子目录 x64\ 下的 darknet.exedarknet_no_gpu.exe就是编译得到的可执行文件. 在该路径下同时还有一个 yolov3.weights 文件就是预训练的权重, 以便直接用于模型推理, 同时该文件也可以在 darknet 官网找到. 另一个 darknet53.conv.74 也是权重文件, 在训练自己的数据集时会用到.

预训练权重:

https://pjreddie.com/media/files/yolov3.weights
https://pjreddie.com/media/files/darknet53.conv.74

测试
打开 Powershell, 进入 darknet\build\darknet\x64\ 路径下, 运行如下命令:

.\darknet.exe detect .\cfg\yolov3.cfg .\yolov3.weights .\data\dog.jpg

运行结束后会弹出一张图, 其中框选出了图中的truck,bicycle,dog.

训练自己的数据集

可以参考 AlexeyAB 的 github 仓库:
https://github.com/AlexeyAB/darknet
或者他的中文翻译版:
https://github.com/BBuf/Darknet

数据集组织

开始训练之前需要准备: 数据集图片文件、使用 LabelImg 标注的标签文件、训练集和验证集划分文件、预训练模型参数、数据集类别文件、数据集说明文件、yolov3 配置文件.

例如对于一个从 .bmp 格式图片中提取出标定板 board 的项目来说, 需要组织如下结构的文件, 其中大多数文件名其实是可以自定义的,但是务必要统一.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- darknet_data\
- dataset\
- image1.bmp #相机拍照产生
- image1.txt #labelImg 标注得到
- image2.bmp
- image2.txt
- ...
- train.txt # 由 Generatefilename.py 脚本生成
- val.txt # 由 Generatefilename.py 脚本生成
- pretrainedweight\ # 预训练模型权重
- darknet53.conv.74
- trainedweights\ # 即将要训练得到的模型权重文件
- board.data # 数据集说明文件
- board.names # 数据集标签名文件
- Generatefilename.py # 自动化脚本, 读取数据集中图片文件名,将其中一部分作为训练集写入 train.txt, 另一部分作为验证集写入 val.txt
- yolov3-board.cfg # yolov3 配置文件,由官方配置文件复制修改而成

其中部分文件是从官方文件复制修改而来的, 根据实际情况, 作如下修改:

  • train.txt/val.txt

    如下所示的文本文件, 每行列出一张图片相对 darknet.exe 的路径. 当然也可以写出绝对路径. 后面将用 python 脚本来自动生成.

1
2
3
4
./darknet_data/dataset/image1.bmp
./darknet_data/dataset/image10.bmp
./darknet_data/dataset/image11.bmp
./darknet_data/dataset/image12.bmp
  • board.data

    总类别数 classes=1
    train 和 valid 描述了训练接和验证集的文件名, 写出其相对于 darknet.exe 的路径 (注意,darknet_data 整个文件夹要放置到和darknet.exe 相同路径下).
    names 指出标签名文件位置
    backup 表示训练后的权重文件放置位置, 模型每迭代 100 次保存当前最好的参数, 每 1000 次保存一下当前参数.

1
2
3
4
5
6
classes= 1
train = darknet_data/dataset/train.txt
valid = darknet_data/dataset/val.txt
#difficult = darknet_data/dataset/difficult_2007_test.txt
names = darknet_data/board.names
backup = darknet_data/trainedweights/
  • board.names

    标签文件, 文件内每个标签名占一行

1
board
  • Generatefilename.py

    将图片文件名分成训练集和验证集两类, 分别写入 train.txt 和 val.txt 文件内. 可以用绝对路径表示, 也可以用相对于 darknet.exe 的相对路径表示. 如下的 python 脚本可以自动化地生成这两个文件. 注意根据实际情况修改 rootdir 的路径和分类时的阈值.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import os
import random

rootdir = './dataset/'
pathname_prefix = "./darknet_data/dataset/"
def split_files():
# 获取当前文件夹下所有图片的文件名
files = [f for f in os.listdir(rootdir) if os.path.isfile(f) and f.endswith('.jpg') or f.endswith('.png') or f.endswith('.bmp')]

# 计算 1:4 的比例
n = len(files)
m = int(n * 0.2)

# 随机选择 m 个文件名
selected_files = random.sample(files, m)

# 将选中的文件名写入两个文件内
with open('val.txt', 'w') as f1, open('train.txt', 'w') as f2:
for i, file in enumerate(files):
if file in selected_files:
f1.write(pathname_prefix + file + '\n')
else:
f2.write(pathname_prefix + file + '\n')

split_files()

  • yolov3-board.cfg

    从官方 yolov3-voc.cfg 文件复制修改而来.
    要修改的地方包括

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
subdivisions=32 # 这里可以设置为 16,32 或 64. 如果设置得太小则可能会内存不足. 视情况增大即可.

max_batches = 4000 # 最大迭代次数, 视情况而定, 一般可以设置为 classes*2000 但是不要低于 4000.

steps=3200,3600 #一般为 80% 和 90% 的 max_batches

# 然后全文检索 yolo, 可以检索到三个地方.
# 每个地方下方有个 classes 设置为 1
# 对应的上方有个 filters 设置为(classes+5)*3=18. 如下所示.

[convolutional]
size=1
stride=1
pad=1
filters=18
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=1
num=9
jitter=.3
ignore_thresh = .5
truth_thresh = 1
random=1

训练

在 powershell 中执行如下命令:

1
PS C:\Users\Username>  .\darknet.exe detector train .\darknet_data\board.data .\darknet_data\yolov3-board.cfg .\darknet_data\pretrainedweight\darknet53.conv.74 .\darknet_data\trainedweights >> .\darknet_data\yolov3-board.log

测试

基于 OpenCV 的模型部署

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
#include <fstream>
#include <vector>
#include <iostream>
#include <sstream>
#include <opencv2/dnn.hpp>
#include <opencv2/imgproc.hpp>
#include <opencv2/highgui.hpp>


std::vector<std::string> classes;

std::vector<cv::String> getOutputsNames(cv::dnn::Net& net)
{
static std::vector<cv::String> names;
if (names.empty())
{
//Get the indices of the output layers, i.e. the layers with unconnected outputs
std::vector<int> outLayers = net.getUnconnectedOutLayers();

//get the names of all the layers in the network
std::vector<cv::String> layersNames = net.getLayerNames();

// Get the names of the output layers in names
names.resize(outLayers.size());
for (size_t i = 0; i < outLayers.size(); ++i)
names[i] = layersNames[outLayers[i] - 1];
}
return names;
}
void drawPred(int classId, float conf, int left, int top, int right, int bottom, cv::Mat& frame)
{
//Draw a rectangle displaying the bounding box
rectangle(frame, cv::Point(left, top), cv::Point(right, bottom), cv::Scalar(255, 178, 50), 3);

//Get the label for the class name and its confidence
std::string label = cv::format("%.5f", conf);
if (!classes.empty())
{
CV_Assert(classId < (int)classes.size());
label = classes[classId] + ":" + label;
}

//Display the label at the top of the bounding box
int baseLine;
cv::Size labelSize = cv::getTextSize(label, cv::FONT_HERSHEY_SIMPLEX, 0.5, 1, &baseLine);
top = std::max(top, labelSize.height);
rectangle(frame, cv::Point(left, top - round(1.5*labelSize.height)), cv::Point(left + round(1.5*labelSize.width), top + baseLine), cv::Scalar(255, 255, 255), cv::FILLED);
putText(frame, label, cv::Point(left, top), cv::FONT_HERSHEY_SIMPLEX, 0.75, cv::Scalar(0, 0, 0), 1);
}
void postprocess(cv::Mat& frame, const std::vector<cv::Mat>& outs, float confThreshold, float nmsThreshold)
{
std::vector<int> classIds;
std::vector<float> confidences;
std::vector<cv::Rect> boxes;

for (size_t i = 0; i < outs.size(); ++i)
{
// Scan through all the bounding boxes output from the network and keep only the
// ones with high confidence scores. Assign the box's class label as the class
// with the highest score for the box.
float* data = (float*)outs[i].data;
for (int j = 0; j < outs[i].rows; ++j, data += outs[i].cols)
{
cv::Mat scores = outs[i].row(j).colRange(5, outs[i].cols);
cv::Point classIdPoint;
double confidence;
// Get the value and location of the maximum score
minMaxLoc(scores, 0, &confidence, 0, &classIdPoint);
if (confidence > confThreshold)
{
int centerX = (int)(data[0] * frame.cols);
int centerY = (int)(data[1] * frame.rows);
int width = (int)(data[2] * frame.cols);
int height = (int)(data[3] * frame.rows);
int left = centerX - width / 2;
int top = centerY - height / 2;

classIds.push_back(classIdPoint.x);
confidences.push_back((float)confidence);
boxes.push_back(cv::Rect(left, top, width, height));
}
}
}

// Perform non maximum suppression to eliminate redundant overlapping boxes with
// lower confidences
std::vector<int> indices;
cv::dnn::NMSBoxes(boxes, confidences, confThreshold, nmsThreshold, indices);
for (size_t i = 0; i < indices.size(); ++i)
{
int idx = indices[i];
cv::Rect box = boxes[idx];
drawPred(classIds[idx], confidences[idx], box.x, box.y,
box.x + box.width, box.y + box.height, frame);
}
}

int main()
{
std::string darknet_path = "../darknet_data/";
std::string names_file = darknet_path + "board.names";
cv::String model_def = darknet_path + "yolov3-board.cfg";
cv::String weights = darknet_path + "yolov3-board_final.weights";

int in_w, in_h;
double thresh = 0.5;
double nms_thresh = 0.25;
in_w = in_h = 608;


//read names

std::ifstream ifs(names_file.c_str());
std::string line;
while (getline(ifs, line)) classes.push_back(line);

//init model
cv::dnn::Net net = cv::dnn::readNetFromDarknet(model_def, weights);
net.setPreferableBackend(cv::dnn::DNN_BACKEND_OPENCV);
net.setPreferableTarget(cv::dnn::DNN_TARGET_CPU);

std::string img_path = darknet_path + "dataset/centers_0.bmp";

cv::Mat frame, blob;
frame = cv::imread(img_path, cv::IMREAD_COLOR);
cv::dnn::blobFromImage(frame, blob, 1 / 255.0, cv::Size(in_w, in_h), cv::Scalar(), true, false);

//Sets the input to the network
net.setInput(blob);

// Runs the forward pass to get output of the output layers
std::vector<cv::Mat> outs;
net.forward(outs, getOutputsNames(net));

postprocess(frame, outs, thresh, nms_thresh);

std::vector<double> layersTimes;
double freq = cv::getTickFrequency() / 1000;
double t = net.getPerfProfile(layersTimes) / freq;
std::string label = cv::format("Inference time for a frame : %.2f ms", t);
putText(frame, label, cv::Point(0, 15), cv::FONT_HERSHEY_SIMPLEX, 0.5, cv::Scalar(0, 0, 255));

//imshow("res", frame);
//waitKey();
imwrite(img_path + ".detected.bmp", frame);

return 0;
}

Darknet 训练与部署 YoLov3 模型

https://luosiyou.cn/blogs/darknet_yolov3/

作者

Luo Siyou

发布于

2023-01-09

更新于

2023-05-08

许可协议