OpenCV4入门教程142:yolov3实时目标检测

索引地址:系列索引

Darknet接口

本文使用OpenCV获取视频数据,其他部分有darknet库完成。

darknet库GitHub地址:darknet

darknet主页:darknet

说明:因为部分人对darknet使用领域的无限扩张,为了爱与和平,原作者已不在更新,现在的所有内容由他人维护。

在darknet源码中由用于编译的Makefile文件,调整里面的参数用于使能openmpi/cuda等等。

编译结果为:

1
2
3
darknet.h
libdarknet.a
libdarknet.so

就是头文件和动态库。

首先新建一个纯c++的Qt项目(cmake也可以,我比较喜欢Qt)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
TEMPLATE = app
CONFIG += console c++11
CONFIG -= app_bundle
CONFIG -= qt

unix:{
INCLUDEPATH += /usr/include/opencv4
LIBS += `pkgconf --libs opencv4` \
-L$$PWD/libs/ \
-ldarknet
}

SOURCES += \
main.cpp

主要是用Qt来链接动态库。

开发流程为:

flowchart TB

subgraph recog
direction TB
F --Yes--> H[数据格式转换]
H --> I[预测数据]
I --> J[获取结果]
J --> K[处理结果]
K --> L[显示结果]
L --下一帧--> F 
end

subgraph main
direction TB
A{开始} --> B[设置参数]
B --> C[读取标签]
C --> D[对象初始化]
D --> E[打开设备]
E --> F{读取帧}
F --No--> G{结束}
end

获取标签

预测结果的类别是数字,我们需要将其按照对应关系进行解码。

1
2
3
4
5
6
7
8
9
10
11
12
std::vector<std::string> getClassesName(char path[]){
std::vector<std::string> names;
if(!path){
return names;
}
std::ifstream readIn(path);
std::string str;
while(getline(readIn,str)){
names.push_back(str);
}
return names;
}

读取标签列表至vector中,这样就可以通过类似数组(names[0]=person)的方式直接访问数据值。

对象初始化

本程序主要用到两个对象:

1
2
3
4
network *net = load_network(cfgfile,weightfile,0);
set_batch_network(net,1);

cv::VideoCapture cap(0);

net用于处理网络权重,cap用于获取摄像头的数据。

图像格式转换

获取到图像之后,格式为cv::Mat,而net使用的图像格式为image,此为darknet自定义的格式,需要转换一下。当然,图像处理完显示的时候又要变为Mat,否则也不需要使用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
image mat2image(const cv::Mat &mat){
cv::Mat dst;
cv::cvtColor(mat, dst, cv::COLOR_RGB2BGR);

int w = mat.cols;
int h = mat.rows;
int c = mat.channels();
image im = make_image(w, h, c);
unsigned char * imageData = (unsigned char *)dst.data;
int step = dst.step;
for (int y = 0; y < h; ++y) {
for (int k = 0; k < c; ++k) {
for (int x = 0; x < w; ++x) {
im.data[k*w*h + y*w + x] = imageData[y*step + x*c + k] / 255.0f;
}
}
}
return im;
}

cv::Mat image2Mat(image im){
image copy = copy_image(im);
constrain_image(copy);
if(im.c==3) rgbgr_image(copy);

int x,y,c;
cv::Mat m;
switch(im.c){
case 3:
m = cv::Mat(im.w,im.h,CV_8UC3);
break;
case 4:
m=cv::Mat(im.w,im.h,CV_8UC4);
break;
}

int step = m.step;
for(y=0;y<im.h;++y){
for(x=0;x<im.w;++x){
for(c=0;c<im.c;++c){
float val=im.data[c*im.h*im.w+y*im.w+x];
m.data[y*step+x*im.c+c]=(unsigned char)(val*255);
}
}
}

free_image(copy);
cv::cvtColor(m,m,cv::COLOR_BGR2RGB);
return m;
}

如果认真学习OpenCV的话应该不难理解。

获取结果

获取原数据,将Mat处理成image,预测图像

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
image in = mat2image(frame);//格式转换
image in_s = letterbox_image(in,net->w,net->h);//讲图像数据转换为网络需要的尺寸
layer l = net->layers[net->n-1];//预测层

int nboxes=0;//可能的结果数量
float *X=in_s.data;//需要预测的图像数据
time=what_time_is_it_now();
network_predict(net,X);//预测中
//以下是方便人类阅读的部分
std::ostringstream str;
str<< "Prediction spent "<<what_time_is_it_now()-time<<"seconds";
//使用OpenCV的函数来绘制文字
cv::putText(frame,str.str(),cv::Point(0,20),1,1,cv::Scalar(255,0,255),1,8,false);

//获取所有预测的结果
detection *dets = get_network_boxes(net,in.w,in.h,thresh,0.5,0,1,&nboxes);
//利用阈值过滤一部分结果
if(nms) do_nms_sort(dets,nboxes,l.classes,nms);

结果处理

比如说在一次检测中我们获取了10个可能的检测结果,那么我们要对每个结果进行处理。

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
for(i=0;i<nboxes;++i){//处理每一个结果
for(int j=0;j<l.classes;j++){
//对于每一个结果,对于读取的每一个标签类别进行判断
if(dets[i].prob[j]>thresh){
//如果预测的可能性大于我们设置的阈值
box b = dets[i].bbox;
int left = (b.x-b.w/2.)*in.w;
int right = (b.x+b.w/2.)*in.w;
int top = (b.y-b.h/2.)*in.h;
int bot = (b.y+b.h/2.)*in.h;

if(left < 0) left = 0;
if(right > in.w-1) right = in.w-1;
if(top < 0) top = 0;
if(bot > in.h-1) bot = in.h-1;

//根据计算的left,right,top,bot绘制矩形框
cv::rectangle(frame,cv::Rect(left,top,right-left,bot-top),cv::Scalar(255,255,0),2,cv::LINE_8,0);
//记录日志
std::ostringstream text;
text<<names[j]<<": "<<dets[i].prob[j]*100<<"%";
//在界面绘制文字
cv::putText(frame,text.str(),cv::Point(left,top),1,1,cv::Scalar(255,255,0),2,8,false);
}
}
}

矩形框的计算过程来自官方的代码。

最终效果为:

result

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
150
151
152
153
154
155
156
157
158
159
160
161
162
163
#include <iostream>
#include <string>
#include <vector>
#include <fstream>

#include "opencv2/opencv.hpp"

extern "C"{
#include "darknet.h"
}

image mat2image(const cv::Mat &mat){
cv::Mat dst;
cv::cvtColor(mat, dst, cv::COLOR_RGB2BGR);

int w = mat.cols;
int h = mat.rows;
int c = mat.channels();
image im = make_image(w, h, c);
unsigned char * imageData = (unsigned char *)dst.data;
int step = dst.step;
for (int y = 0; y < h; ++y) {
for (int k = 0; k < c; ++k) {
for (int x = 0; x < w; ++x) {
im.data[k*w*h + y*w + x] = imageData[y*step + x*c + k] / 255.0f;
}
}
}
return im;
}

cv::Mat image2Mat(image im){
image copy = copy_image(im);
constrain_image(copy);
if(im.c==3) rgbgr_image(copy);

int x,y,c;
cv::Mat m;
switch(im.c){
case 3:
m = cv::Mat(im.w,im.h,CV_8UC3);
break;
case 4:
m=cv::Mat(im.w,im.h,CV_8UC4);
break;
}

int step = m.step;
for(y=0;y<im.h;++y){
for(x=0;x<im.w;++x){
for(c=0;c<im.c;++c){
float val=im.data[c*im.h*im.w+y*im.w+x];
m.data[y*step+x*im.c+c]=(unsigned char)(val*255);
}
}
}

free_image(copy);
cv::cvtColor(m,m,cv::COLOR_BGR2RGB);
return m;
}

std::vector<std::string> getClassesName(char path[]){
std::vector<std::string> names;
if(!path){
return names;
}
std::ifstream readIn(path);
std::string str;
while(getline(readIn,str)){
names.push_back(str);
}
return names;
}

int main()
{
//参数初始化
char cfgfile[]="yolov3-tiny.cfg";
char weightfile[]="yolov3-tiny.weights";
char namefile[]="coco.names";
float thresh =0.5;

std::vector<std::string> names=getClassesName(namefile);

//模型初始化
network *net = load_network(cfgfile,weightfile,0);
set_batch_network(net,1);

cv::VideoCapture cap(0);
cv::Mat frame;

if(!cap.isOpened()){
error("Couldn't connect to cam.");
}
int i;
float nms = 0.45;
double time;

while(1){
frame.release();
cap>>frame;
if(frame.empty()){
std::cout<<"Frame is empty"<<std::endl;
break;
}
image in = mat2image(frame);//格式转换
image in_s = letterbox_image(in,net->w,net->h);//讲图像数据转换为网络需要的尺寸
layer l = net->layers[net->n-1];//预测层

int nboxes=0;//可能的结果数量
float *X=in_s.data;//需要预测的图像数据
time=what_time_is_it_now();
network_predict(net,X);//预测中
//以下是方便人类阅读的部分
std::ostringstream str;
str<< "Prediction spent "<<what_time_is_it_now()-time<<"seconds";
//使用OpenCV的函数来绘制文字
cv::putText(frame,str.str(),cv::Point(0,20),1,1,cv::Scalar(255,0,255),1,8,false);

//获取所有预测的结果
detection *dets = get_network_boxes(net,in.w,in.h,thresh,0.5,0,1,&nboxes);
//利用阈值过滤一部分结果
if(nms) do_nms_sort(dets,nboxes,l.classes,nms);

for(i=0;i<nboxes;++i){//处理每一个结果
for(int j=0;j<l.classes;j++){
//对于每一个结果,对于读取的每一个标签类别进行判断
if(dets[i].prob[j]>thresh){
//如果预测的可能性大于我们设置的阈值
box b = dets[i].bbox;
int left = (b.x-b.w/2.)*in.w;
int right = (b.x+b.w/2.)*in.w;
int top = (b.y-b.h/2.)*in.h;
int bot = (b.y+b.h/2.)*in.h;

if(left < 0) left = 0;
if(right > in.w-1) right = in.w-1;
if(top < 0) top = 0;
if(bot > in.h-1) bot = in.h-1;

//根据计算的left,right,top,bot绘制矩形框
cv::rectangle(frame,cv::Rect(left,top,right-left,bot-top),cv::Scalar(255,255,0),2,cv::LINE_8,0);
//记录日志
std::ostringstream text;
text<<names[j]<<": "<<dets[i].prob[j]*100<<"%";
//在界面绘制文字
cv::putText(frame,text.str(),cv::Point(left,top),1,1,cv::Scalar(255,255,0),2,8,false);
}
}
}
cv::imshow("result",frame);

free_detections(dets,nboxes);
free_image(in_s);
free_image(in);

if(cv::waitKey(20)==27){
break;
}
}
return 0;
}

测试效果视频:OpenCV/YOLOv3实时视频目标检测效果


OpenCV4入门教程142:yolov3实时目标检测
https://blog.jackeylea.com/opencv/realtime-object-detect-with-opencv-darknet-yolov3/
作者
JackeyLea
发布于
2020年12月21日
许可协议