今天要整理的是OpenCV中dnn模块对于YOLOv3模型的加载调用,以及在此基础上实现图像中的对象检测。OpenCV4.0版本以上支持YOLOv3版本模型的对象检测网络,该网络模型支持80种类别对象的检测,而且现在YOLO发布了v4版本,但是具体的我还没有去尝试过,之前上YOLO的网站看好像还没更新v4版本。YOLOv3版本同时还发布了支持移动端的轻量型网络模型YOLOv3-tiny版本,其速度可以在CPU端实时运行,下次再来整理以YOLOv3-tiny模型为基础实现的实时对象检测功能。
OpenCV中通过对DarkNet框架的集成来实现YOLO网络的加载与调用,下面通过代码逐步进行整理。
首先加载模型,并且设置计算后台和目标设备,我这里使用的计算后台是openVINO,如果没有的话可以使用opencv默认的计算后台。(PS:当只对一张图像进行检测时,如果使用openVINO的话速度反而会更慢,而当对视频流进行检测时,速度就有明显的提升,这应该是在切换计算后台时导致时间消耗的缘故)
string YOLOv3_cfg = "D:/opencv_c++/opencv_tutorial/data/models/yolo/yolov3/yolov3.cfg";
string YOLOv3_model = "D:/opencv_c++/opencv_tutorial/data/models/yolo/yolov3/yolov3.weights";
Net YOLOv3_net = readNetFromDarknet(YOLOv3_cfg, YOLOv3_model);
YOLOv3_net.setPreferableBackend(DNN_BACKEND_INFERENCE_ENGINE);
YOLOv3_net.setPreferableTarget(DNN_TARGET_CPU);
接下来我们需要获取YOLOv3网络的所有输出层名称,这一步是必须的。因为YOLOv3对象检测网络是多个层的合并输出,所以在OpenCV中进行前向传播的时候必须显式声明输出层,也就是在进行forward
的时候声明输出层的名称。
std::vector<String> outNames = YOLOv3_net.getUnconnectedOutLayersNames();
for (int i = 0; i < outNames.size(); i++) {
printf("output layer name : %s\n", outNames[i].c_str());
}
// 加载COCO数据集标签
string YOLOv3_labels = "D:/opencv_c++/opencv_tutorial/data/models/yolo/yolov3/object_detection_classes_yolov3.txt";
vector<string> classNamesVec;
ifstream fp(YOLOv3_labels);
if (fp.is_open())
{
string className = "";
while (getline(fp, className))
{
classNamesVec.push_back(className);
}
}
然后我们就可以加载要进行检测的图像,并将图像从Mat类型转换为blob类型,用以传入神经网络模型。注意在进行转换的时候,缩放因子、输入尺寸、均值等等参数是由该网络自身所决定的,我们可以从其他相关资料上获取这些参数值。
Mat test_image = imread("D:/opencv_c++/opencv_tutorial/data/images/CC4.jpg");
//resize(test_image, test_image, Size(800, 800));
Mat inputBlob = blobFromImage(test_image, 1 / 255.F, Size(416, 416), Scalar(), true, false);
然后将图像blob传入YOLOv3模型的输入层,并进行前向传播,注意这里需要显式声明输出层,并且输出的结果矩阵总共有3个,分别对应一个输出层。并且计算一下该网络在进行推理的时候消耗的时间,可以与Tiny版本进行运行速度对比。
YOLOv3_net.setInput(inputBlob);
std::vector<Mat> outs;
YOLOv3_net.forward(outs, outNames);
vector<double> layersTime;
double freq = getTickFrequency() / 1000;
double time = YOLOv3_net.getPerfProfile(layersTime) / freq;
string runtime = "detection time:" + to_string(time) + "ms";
putText(test_image, runtime, Point(20, 20), 0, 0.5, Scalar(0, 0, 255));
最后就是最最最重要的一部分了,那就是对前向传播后得到的结果矩阵进行解码,得到我们需要的检测结果。
每个输出层产生形状为 [ NxC ] 的输出Blob,其中N是检测到的对象数,也就是每一行表示一个检测到的对象;C等于85表示该检测对象所在位置的矩形框以及所属类别,85 = 类别数80+ 5,其中前4列数字为矩形框的参数[center_x,center_y,width,height],这里的宽度和高度也是以比例来表示的,所以使用的时候还需要乘上原图像的宽和高。
在对每一行、也就是每一个检测对象进行判断的时候,我们可以从该行的第六列到最后一列的数值中来寻找最大值,找到的最大值就是该检测对象最可能属于某个类别的置信度。
然后对置信度进行阈值判断,如果大于阈值,则表示该检测对象被正确检测出来的几率是比较大的,那么就把这个检测对象所在位置的矩形框参数获取出来,并把该对象的矩形框和置信度加入候补对象框列表和候补置信度列表中,用于后续的非最大值抑制。
vector<Rect> boxes;
vector<int> classIds;
vector<float> confidences;
for (size_t i = 0; i < outs.size(); ++i)
{
//网络产生形状为NxC的输出Blob,其中N是检测到的对象,C是类别数+ 5,总共85列,其中前4列数字为[center_x,center_y,宽度,高度]
float* data = (float*)outs[i].data;
for (int j = 0; j < outs[i].rows; ++j, data += outs[i].cols)
{
Mat scores = outs[i].row(j).colRange(5, outs[i].cols);
Point classIdPoint;
double confidence;
minMaxLoc(scores, 0, &confidence, 0, &classIdPoint);
if (confidence > 0.5)
{
int centerX = (int)(data[0] * test_image.cols);
int centerY = (int)(data[1] * test_image.rows);
int width = (int)(data[2] * test_image.cols);
int height = (int)(data[3] * test_image.rows);
int left = centerX - width / 2;
int top = centerY - height / 2;
classIds.push_back(classIdPoint.x);
confidences.push_back((float)confidence);
boxes.push_back(Rect(left, top, width, height));
}
}
}
由于在上一部的解码操作中,我们得到了很多个对象矩形框,可能会出现同一个对象却存在多个矩形框的情况,如果把所有的矩形框绘制出来如下图所示:
可以看到,对于同一个目标却出现了多个矩形框,那么这时候我们只需要其中置信度最高的一个就可以了。此时在解码操作中保留下来的候补矩形框列表和候补置信度列表就派上了用场,我们使用这两个候选列表来筛选出其中置信度最高的一个检测对象,也就是进行一步非最大值抑制操作,只保留其中的最大值。
我们使用OpenCV中的NMSBoxes()
这个API来进行非最大值抑制,Non - maximum - suppression(NMS)通过空间距离结合并交比(IOU)完成聚类划分,对每个矩形簇cluster只保留得分最高的Bounding Box,从而实现非最大值抑制,保留置信度最高的矩形框。其主要参数含义如下:
(1)参数bbox:一组用于 NMS非最大值抑制 的边界框;
(2)参数scores:和边界框相对应的置信度;
(3)参数score_threshold: 用于按置信度筛选边界框的阈值;
(4)参数nms_threshold:非最大抑制中使用的阈值
(5)参数indices:进行NMS非最大值抑制之后参数bbox中所保留下来的边界框的索引。
最后利用非最大值抑制后的得到的边界框索引来获取对应对象的预测类别和矩形框。
// 非最大抑制操作
vector<int> indices;
NMSBoxes(boxes, confidences, 0.5, 0.2, indices);
for (size_t i = 0; i < indices.size(); ++i)
{
int idx = indices[i];
Rect box = boxes[idx];
String className = classNamesVec[classIds[idx]];
putText(test_image, className.c_str(), box.tl(), FONT_HERSHEY_SIMPLEX, 1.0, Scalar(255, 0, 0), 2, 8);
rectangle(test_image, box, Scalar(0, 0, 255), 2, 8, 0);
}
namedWindow("YOLOv3-Detections", WINDOW_FREERATIO);
imshow("YOLOv3-Detections", test_image);
对象检测效果如下:
那到这里我们就实现了基于YOLOv3网络实现的对象检测啦,下面再使用YOLOv3-Tiny网络来实现实时对象检测。
由于同样是使用YOLO网络结构的模型,所以Tiny版本在加载调用的时候,同样是经过获取输出层名称、blob传入输入层、显式声明输出层、前向传播、解码、非极大值抑制等等这些步骤,这里就不再赘述了。Tiny版本和标准版本的差别主要在于运行速度上,Tiny版本是为了移动端实时检测而诞生的,所以其运行速度有了质的飞跃,当然了在检测精度上肯定要做出一定的牺牲。
下面直接给出基于YOLOv3-Tiny模型的实时对象检测的代码演示:
//基于YOLOv3-tiny版本模型的实时对象检测
string YOLOv3_tiny_cfg = "D:/opencv_c++/opencv_tutorial/data/models/yolo/yolov3-tiny-coco/yolov3-tiny.cfg";
string YOLOv3_tiny_model = "D:/opencv_c++/opencv_tutorial/data/models/yolo/yolov3-tiny-coco/yolov3-tiny.weights";
string YOLOv3_tiny_labels = "D:/opencv_c++/opencv_tutorial/data/models/yolo/yolov3-tiny-coco/object_detection_classes_yolov3.txt";
Net net = readNetFromDarknet(YOLOv3_tiny_cfg, YOLOv3_tiny_model);
net.setPreferableBackend(DNN_BACKEND_CUDA);
net.setPreferableTarget(DNN_TARGET_CUDA);
vector<string>outputLayerName = net.getUnconnectedOutLayersNames();
for (int i = 0;i < outputLayerName.size();i++)
{
cout << outputLayerName[i] << endl;
}
ifstream labels_file(YOLOv3_tiny_labels);
if (!labels_file.is_open())
{
cout << "can't open labels file" << endl;
exit(-1);
}
string label;
vector<string>labels;
while (getline(labels_file, label))
{
labels.push_back(label);
}
VideoCapture capture;
capture.open(0, CAP_DSHOW);
//capture.open("http://192.168.43.1:8081");
if (!capture.isOpened())
{
cout << "can't open camera" << endl;
exit(-1);
}
Mat frame;
while (capture.read(frame))
{
double start = getTickCount();
flip(frame, frame, 1);
Mat inputBlob = blobFromImage(frame, 1 / 255.F, Size(416, 416), Scalar(), true, false);
net.setInput(inputBlob);
vector<Mat> prob;
net.forward(prob,outputLayerName);
vector<Rect>boxes;
vector<int>classID;
vector<float>confidences;
for (int i = 0;i < prob.size();i++)
{
for (int row = 0;row < prob[i].rows;row++)
{
Mat scores = prob[i].row(row).colRange(5, prob[i].cols);
double confidence;
Point maxloc;
minMaxLoc(scores, NULL, &confidence, NULL, &maxloc);
if (confidence > 0.5)
{
int center_x = prob[i].at<float>(row, 0) * frame.cols;
int center_y = prob[i].at<float>(row, 1) * frame.rows;
int width = prob[i].at<float>(row, 2) * frame.cols;
int height = prob[i].at<float>(row, 3) * frame.rows;
int x = center_x - width / 2;
int y = center_y - height / 2;
Rect box(x, y, width, height);
boxes.push_back(box);
classID.push_back(maxloc.x);
confidences.push_back(float(confidence));
}
}
}
vector<int>indices;
NMSBoxes(boxes, confidences, 0.5, 0.2, indices);
for (int i = 0;i < indices.size();i++)
{
int index = indices[i];
Rect box = boxes[index];
string className = labels[classID[index]];
rectangle(frame, box, Scalar(0, 255, 0), 1, 8);
putText(frame, className, box.tl(), FONT_HERSHEY_SIMPLEX, 0.5, Scalar(0, 0, 255), 1, 8);
}
double end = getTickCount();
double run_time = (end - start) / getTickFrequency();
double fps = 1 / run_time;
putText(frame, format("FPS: %0.2f", fps), Point(20, 20), FONT_HERSHEY_SIMPLEX, 0.5, Scalar(0,0,255),1, 8);
imshow("YOLOv3-tiny", frame);
char ch = waitKey(1);
if (ch == 27)
{
break;
}
}
好的那本次笔记到此结束,谢谢阅读~
PS:本人的注释比较杂,既有自己的心得体会也有网上查阅资料时摘抄下的知识内容,所以如有雷同,纯属我向前辈学习的致敬,如果有前辈觉得我的笔记内容侵犯了您的知识产权,请和我联系,我会将涉及到的博文内容删除,谢谢!