OpenCV DNN 模块完全指南:从理论基础到实战应用 —— 图像分类与目标检测的深度学习实现(含 Python/C++ 代码与性能分析)
计算机视觉——基于OpenCV DNN模块部署深度学习模型详细指南
计算机视觉领域自20世纪60年代末诞生以来,图像分类和目标检测作为其中最古老的问题之一,历经数十年研究。如今,借助神经网络和深度学习,计算机已能真正理解和识别对象,在诸多场景下准确性甚至超越人类。而OpenCV DNN模块凭借高度优化的CPU性能,成为初学者涉足基于深度学习的计算机视觉领域的绝佳起点,即便没有强大GPU支持的系统,也能轻松上手。
本文将从理论与实践两方面,详细探讨如何利用OpenCV DNN模块在图像和实时视频中实现分类与目标检测,为初学者提供全面且易操作的学习路径。
一、OpenCV DNN模块概述
OpenCV作为卓越的计算机视觉库,自3.3版本起便具备运行深度学习推理的功能,支持加载不同框架的模型,实现多种深度学习功能。然而,许多新手往往忽视这一强大特性,错失了不少学习良机。
1.1 选择OpenCV DNN模块的原因
OpenCV DNN模块仅支持基于图像和视频的深度学习推理,不支持微调和训练,但对于初学者而言,是踏入基于深度学习的计算机视觉领域并进行实践的理想起点。
其一大优势是对Intel处理器高度优化,在实时视频的目标检测和图像分割应用中能实现出色的帧率。例如,在图像分类推理速度上,OpenCV表现远超TensorFlow原始实现,以DenseNet121模型为例,TensorFlow推理时间接近1秒,而OpenCV不到200毫秒(测试环境为PyTorch 1.8.0,OpenCV 4.5.1和TensorFlow 2.4,在具有2.3Ghz处理器的Intel Xeon处理器的Google Colab上完成)。
在目标检测方面,同样优势明显。在时钟速度为2.6Ghz的Intel i7第8代笔记本电脑CPU上,OpenCV的DNN模块运行视频可达35 FPS,而带有OpenMP和AVX的Darknet编译版本为15 FPS,不带OpenMP或AVX的Darknet Tiny YOLOv4仅3 FPS(均使用原始Darknet Tiny YOLOv4模型)。
对于计算能力有限的边缘设备,如基于ARM处理器的设备,OpenCV DNN模块也表现出色。在Raspberry Pi 3B上的测试显示,对于SqueezeNet和MobileNet模型,OpenCV在FPS上超过其他所有框架;对于GoogLeNet,OpenCV排名第二;仅对于Network in Network,OpenCV在Raspberry上的FPS最慢。
这些数据充分证明了OpenCV的优化程度及其在神经网络推理中的快速性,是深入学习OpenCV DNN模块的有力理由。
1.2 OpenCV DNN模块支持的深度学习功能
OpenCV DNN模块支持多种深度学习和计算机视觉任务,涵盖了大部分常见应用场景,主要包括:
- 图像分类
- 目标检测
- 图像分割
- 文本检测和识别
- 姿态估计
- 深度估计
- 人物和面部验证及检测
- 人物Reid(再识别)
更多详细信息可访问OpenCV存储库的计算机视觉中的深度学习Wiki页面。
针对不同的系统硬件和计算能力,有多种模型可供选择,从计算密集型模型到适用于低功耗边缘设备的模型,能满足各类用例需求。本文将重点讨论目标检测和人体姿态估计,以展示使用OpenCV DNN选择不同模型的方法。
1.3 OpenCV DNN模块支持的模型
为支持上述应用,存在大量预训练模型,且有许多最先进的模型可供选用。以下表格按不同深度学习应用列出了部分模型:
图像分类 | 目标检测 | 图像分割 | 文本检测和识别 | 人体姿态估计 | 人物和面部检测 |
---|---|---|---|---|---|
Alexnet | MobileNet SSD | DeepLab | Easy OCR | Open Pose | Open Face |
GoogLeNet | VGG SSD | UNet | CRNN | Alpha Pose | Torchreid |
VGG | Faster R-CNN | FCN | Mobile FaceNet | OpenCV FaceDetector | |
ResNet | EfficientDet | ||||
SqueezeNet | |||||
DenseNet | |||||
ShuffleNet | |||||
EfficientNet |
上述列表并非全部,实际存在更多模型。此列表旨在让读者了解DNN模块在计算机视觉深度学习探索中的实用性。
1.4 OpenCV DNN模块支持的框架
OpenCV DNN模块支持多种流行的深度学习框架,具体如下:
Caffe
加载预训练的Caffe模型需两个文件:一是包含预训练权重的model.caffemodel文件,二是具有.prototxt扩展名的模型架构文件(纯文本文件,类似JSON结构,包含所有神经网络层定义)。
TensorFlow
加载预训练的TensorFlow模型也需两个文件:模型权重文件(.pb扩展名,包含所有预训练权重的protobuf文件)和包含模型配置的protobuf文本文件(.pbtxt扩展名)。
注意:在TensorFlow较新版本中,模型权重文件可能为.ckpt或.h5格式,此时需先将模型转换为ONNX格式,再转换为.pb格式,以确保与OpenCV DNN模块兼容。
Torch和PyTorch
加载Torch模型文件需包含预训练权重的.t7或.net扩展名文件。对于.pth扩展名的最新PyTorch模型,最佳方法是先转换为ONNX格式,因OpenCV DNN支持ONNX模型,转换后可直接加载。
Darknet
加载Darknet模型需.weights扩展名的模型权重文件和.cfg格式的网络配置文件。
ONNX格式模型
对于在PyTorch或TensorFlow等框架中训练的模型,若不直接适用于OpenCV DNN模块,通常先转换为ONNX格式(Open Neural Network Exchange),之后可直接使用或转换为其他框架支持的格式。加载ONNX模型只需.onnx权重文件。
更多关于不同框架、权重文件和配置文件的信息,可参考官方OpenCV文档。
多数上述模型经测试可与OpenCV DNN模块完美配合,理论上上述框架中的任何模型只要找到正确的权重文件和相应神经网络架构文件,都能与DNN模块一起使用。在后续编码部分,这些内容将更加清晰。
二、使用OpenCV DNN模块进行图像分类的完整指南
本节将详细介绍如何使用OpenCV DNN模块对图像进行分类,步骤清晰,便于理解。
我们将采用在著名ImageNet数据集上通过Caffe框架训练的DensNet121深度神经网络模型进行分类任务。该模型在ImageNet数据集的1000个类别上预训练,能识别多种图像,适用范围广。
以老虎图像为例进行分类,如下所示:
图像分类的步骤如下:
- 从磁盘加载类名文本文件并提取所需标签
- 从磁盘加载预训练的神经网络模型
- 从磁盘加载图像并将其准备为深度学习模型的正确输入格式
- 将输入图像前向传播通过模型并获取输出
2.1 导入模块和加载类文本文件
Python:
import cv2
import numpy as np
C++:
#include <iostream>
#include <fstream>
#include <opencv2/opencv.hpp>
#include <opencv2/dnn.hpp>
#include <opencv2/dnn/all_layers.hpp>using namespace std;
using namespace cv;
using namespace dnn;
DenseNet121模型在1000个ImageNet类别上训练,需将这些类别加载到内存中以便访问,通常存储在文本文件(如classification_classes_ILSVRC2012.txt)中,格式如下:
tench, Tinca tinca
goldfish, Carassius auratus
great white shark, white shark, man-eater, man-eating shark, Carcharodon carcharias
tiger shark, Galeocerdo cuvieri
hammerhead, hammerhead shark
…
每行包含单个图像的所有标签或类名,第一个名称通常是最常见的名称。加载并提取第一个名称作为标签的代码如下:
Python:
# 读取ImageNet类名
with open('../../input/classification_classes_ILSVRC2012.txt', 'r') as f:image_net_names = f.read().split('\n')
# 最终类名(仅为每个图像的许多ImageNet名称中的第一个名称)
class_names = [name.split(',')[0] for name in image_net_names]
C++:
std::vector<std::string> class_names;
ifstream ifs(string("../../input/classification_classes_ILSVRC2012.txt").c_str());
string line;
while (getline(ifs, line))
{class_names.push_back(line);
}
加载后,image_net_names
列表包含每行完整内容,class_names
列表仅保留每行第一个名称。
2.2 从磁盘加载预训练的DenseNet121模型
使用OpenCV DNN模块的readNet()
函数加载模型,代码如下:
Python:
# 加载神经网络模型
model = cv2.dnn.readNet(model='../../input/DenseNet_121.caffemodel', config='../../input/DenseNet_121.prototxt', framework='Caffe')
C++:
// 加载神经网络模型
auto model = readNet("../../input/DenseNet_121.prototxt","../../input/DenseNet_121.caffemodel","Caffe");
readNet()
函数参数说明:
model
:预训练权重文件路径(此处为Caffe模型)config
:模型配置文件路径(此处为Caffe模型的.prototxt文件)framework
:加载模型的框架名称(此处为Caffe)
此外,DNN模块还提供了针对特定框架的加载函数,无需提供framework
参数:
readNetFromCaffe()
:加载Caffe模型,参数为.prototxt文件和Caffe模型文件路径readNetFromTensorflow()
:加载TensorFlow模型,参数为frozen model graph和model architecture protobuf text file路径readNetFromTorch()
:加载Torch和PyTorch模型(使用torch.save()
保存),参数为模型路径readNetFromDarknet()
:加载DarkNet框架训练的模型,参数为模型权重和配置文件路径readNetFromONNX()
:加载ONNX模型,参数为ONNX模型文件路径
本文将使用readNet()
函数加载预训练模型。
2.3 读取图像并为模型输入做准备
使用OpenCV的imread()
函数读取图像后,需进行预处理才能作为模型输入,代码如下:
Python:
# 从磁盘加载图像
image = cv2.imread('../../input/image_1.jpg')
# 从图像创建blob
blob = cv2.dnn.blobFromImage(image=image, scalefactor=0.01, size=(224, 224), mean=(104, 117, 123))
C++:
// 从磁盘加载图像
Mat image = imread("../../input/image_1.jpg");
// 从图像创建blob
Mat blob = blobFromImage(image, 0.01, Size(224, 224), Scalar(104, 117, 123));
blobFromImage()
函数参数说明:
image
:读取的输入图像scalefactor
:图像缩放因子,默认1(不缩放)size
:图像调整后的大小,此处224×224(多数在ImageNet数据集上训练的分类模型期望此大小)mean
:从图像RGB颜色通道中减去的平均值,用于规范化输入,使输入对不同照明比例不变
该函数会添加额外的批量维度,输出blob形状为[1, 3, 224, 224]
,符合神经网络模型输入格式。
2.4 将输入前向传播通过模型
输入准备好后,即可进行预测,代码如下:
Python:
# 为神经网络设置输入blob
model.setInput(blob)
# 将图像blob前向传播通过模型
outputs = model.forward()
预测步骤:
- 将输入blob设置为加载的神经网络模型
- 使用
forward()
函数将blob前向传播通过模型,获取所有输出
outputs
为保存所有预测的数组,形状为(1, 1000, 1, 1
),需进一步处理才能提取类标签,代码如下:
Python:
final_outputs = outputs[0]
# 使所有输出成为1D
final_outputs = final_outputs.reshape(1000, 1)
# 获取类标签
label_id = np.argmax(final_outputs)
# 将输出分数转换为softmax概率
probs = np.exp(final_outputs) / np.sum(np.exp(final_outputs))
# 获取最终最高概率
final_prob = np.max(probs) * 100.
# 将最大置信度映射到类标签名称
out_name = class_names[label_id]
out_text = f"{out_name}, {final_prob:.3f}"
# 在图像顶部放置类名文本
cv2.putText(image, out_text, (25, 50), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2)
cv2.imshow('Image', image)
cv2.waitKey(0)
cv2.imwrite('result_image.jpg', image)
C++:
// 为神经网络设置输入blob
model.setInput(blob);
// 前向传播图像blob通过模型
Mat outputs = model.forward();
Point classIdPoint;
double final_prob;
minMaxLoc(outputs.reshape(1, 1), 0, &final_prob, 0, &classIdPoint);
int label_id = classIdPoint.x;
// 打印预测的类。
string out_text = format("%s, %.3f", (class_names[label_id].c_str()), final_prob);
// 在图像顶部放置类名文本
putText(image, out_text, Point(25, 50), FONT_HERSHEY_SIMPLEX, 1, Scalar(0, 255, 0), 2);
imshow("Image", image);
imwrite("result_image.jpg", image);
处理后,outputs
形状为(1000, 1,
),每行对应1000个标签中的一个,保存对应类标签的分数。提取最高标签索引label_id
,并将分数转换为softmax概率,最后在图像上标注类名和概率,可视化并保存结果。
执行代码后,DenseNet121模型以约91%的置信度预测图像为老虎,结果如下:
三、使用OpenCV DNN进行目标检测
使用OpenCV DNN模块可轻松开展基于深度学习的目标检测,流程与分类类似,但预处理步骤略有不同。本节将逐步介绍图像和视频中的目标检测。
3.1 图像中的目标检测
利用预训练模型进行图像目标检测,模型在MS COCO数据集(包含80个类别的日常物体)上训练,需加载该数据集的标签文本文件。
以包含多种物体(人、自行车等)的图像为例进行检测,选用MobileNet SSD(Single Shot Detector)模型,该模型在TensorFlow框架上基于MS COCO数据集训练,速度快且计算需求较低,适合入门学习。
3.1.1 导入模块和加载类名
Python:
import cv2
import numpy as np
C++:
#include <iostream>
#include <fstream>
#include <opencv2/opencv.hpp>
#include <opencv2/dnn.hpp>
#include <opencv2/dnn/all_layers.hpp>using namespace std;
using namespace cv;
using namespace dnn;
加载MS COCO类名和生成颜色数组(用于区分不同类别边界框):
Python:
# 加载COCO类名
with open('object_detection_classes_coco.txt', 'r') as f:class_names = f.read().split('\n')# 为每个类别获取不同的颜色数组
COLORS = np.random.uniform(0, 255, size=(len(class_names), 3))
C++:
std::vector<std::string> class_names;
ifstream ifs(string("../../../input/object_detection_classes_coco.txt").c_str());
string line;
while (getline(ifs, line))
{class_names.push_back(line);
}
class_names
列表包含MS COCO数据集的80个类名,如['person', 'bicycle', 'car', ...]
。
3.1.2 加载MobileNet SSD模型并准备输入
使用readNet()
函数加载模型:
Python:
# 加载DNN模型
model = cv2.dnn.readNet(model='frozen_inference_graph.pb', config='ssd_mobilenet_v2_coco_2018_03_29.pbtxt.txt', framework='TensorFlow')
C++:
// 加载神经网络模型
auto model = readNet("../../../input/frozen_inference_graph.pb",
"../../../input/ssd_mobilenet_v2_coco_2018_03_29.pbtxt.txt", "TensorFlow");
参数说明:
model
:冻结的推理图路径(包含权重的预训练模型)config
:模型配置文件路径(protobuf文本文件)framework
:框架名称(此处为TensorFlow)
读取图像并准备输入blob:
Python:
# 从磁盘读取图像
image = cv2.imread('../../input/image_2.jpg')
image_height, image_width, _ = image.shape
# 从图像创建blob
blob = cv2.dnn.blobFromImage(image=image, size=(300, 300), mean=(104, 117, 123), swapRB=True)
# 将blob设置为模型
model.setInput(blob)
# 通过模型前向传播进行检测
output = model.forward()
C++:
// 从磁盘读取图像
Mat image = imread("../../../input/image_2.jpg");
int image_height = image.cols;
int image_width = image.rows;
// 从图像创建blob
Mat blob = blobFromImage(image, 1.0, Size(300, 300), Scalar(127.5, 127.5, 127.5), true, false);
// 将blob设置为模型
model.setInput(blob);
// 通过模型前向传播进行检测
Mat output = model.forward();
Mat detectionMat(output.size[2], output.size[3], CV_32F, output.ptr<float>());
blobFromImage()
函数参数说明:
size
:300×300(SSD模型通常预期的输入大小)swapRB
:交换R和B通道,将OpenCV读取的BGR格式图像转换为模型期望的RGB格式
output
结构解析:
[[[[0.00000000e+00 1.00000000e+00 9.72869813e-01 2.06566155e-02 1.11088693e-01 2.40461200e-01 7.53399074e-01]]]]
- 索引1:类标签(1-80)
- 索引2:置信度分数(模型对检测结果的置信度)
- 最后四个值:边界框坐标(x、y、宽度、高度)
3.1.3 循环检测并绘制边界框
遍历output
中的检测结果,绘制边界框和类名:
Python:
# 循环遍历每个检测
for detection in output[0, 0, :, :]:# 提取检测的置信度confidence = detection[2]# 仅当检测置信度高于阈值时绘制边界框,否则跳过if confidence > .4:# 获取类IDclass_id = detection[1]# 将类ID映射到类class_name = class_names[int(class_id)-1]color = COLORS[int(class_id)]# 获取边界框坐标box_x = detection[3] * image_widthbox_y = detection[4] * image_height# 获取边界框宽度和高度box_width = detection[5] * image_widthbox_height = detection[6] * image_height# 在每个检测到的物体周围绘制矩形cv2.rectangle(image, (int(box_x), int(box_y)), (int(box_width), int(box_height)), color, thickness=2)# 在边界框上方放置类名文本cv2.putText(image, class_name, (int(box_x), int(box_y - 5)), cv2.FONT_HERSHEY_SIMPLEX, 1, color, 2)cv2.imshow('image', image)
cv2.imwrite('image_result.jpg', image)
cv2.waitKey(0)
cv2.destroyAllWindows()
C++:
for (int i = 0; i < detectionMat.rows; i++){int class_id = static_cast<int>(detectionMat.at<float>(i, 1));float confidence = detectionMat.at<float>(i, 2);// 检查检测是否质量良好if (confidence > 0.4){int box_x = static_cast<int>(detectionMat.at<float>(i, 3) * image.cols);int box_y = static_cast<int>(detectionMat.at<float>(i, 4) * image.rows);int box_width = static_cast<int>(detectionMat.at<float>(i, 5) * image.cols - box_x);int box_height = static_cast<int>(detectionMat.at<float>(i, 6) * image.rows - box_y);rectangle(image, Point(box_x, box_y), Point(box_x+box_width, box_y+box_height), Scalar(255,255,255), 2);putText(image, class_names[class_id-1].c_str(), Point(box_x, box_y-5), FONT_HERSHEY_SIMPLEX, 0.5, Scalar(0,255,255), 1);}}imshow("image", image);imwrite("image_result.jpg", image);waitKey(0);destroyAllWindows();
代码说明:
- 提取置信度分数,仅处理置信度高于0.4的检测结果
- 获取类ID并映射到类名,获取对应颜色
- 计算边界框坐标(乘以图像宽高得到实际坐标)
- 绘制边界框和类名文本,可视化并保存结果
执行代码后,模型能检测到图像中的大部分物体,但也可能存在错误预测(如将自行车检测为摩托车),这是MobileNet SSD为追求速度而牺牲部分准确性导致的。
3.2 视频中的目标检测
视频目标检测代码与图像类似,只需对视频帧进行处理,步骤如下:
3.2.1 导入模块、加载类名和模型
Python:
import cv2
import time
import numpy as np# 加载COCO类名
with open('object_detection_classes_coco.txt', 'r') as f:class_names = f.read().split('\n')# 为每个类别获取不同的颜色数组
COLORS = np.random.uniform(0, 255, size=(len(class_names), 3))# 加载DNN模型
model = cv2.dnn.readNet(model='frozen_inference_graph.pb', config='ssd_mobilenet_v2_coco_2018_03_29.pbtxt.txt', framework='TensorFlow')# 捕获视频
cap = cv2.VideoCapture('../../input/video_1.mp4')
# 获取视频帧的宽度和高度以正确保存视频
frame_width = int(cap.get(3))
frame_height = int(cap.get(4))
# 创建`VideoWriter()`对象
out = cv2.VideoWriter('video_result.mp4', cv2.VideoWriter_fourcc(*'mp4v'), 30, (frame_width, frame_height))
C++:
#include <iostream>
#include <fstream>
#include <opencv2/opencv.hpp>
#include <opencv2/dnn.hpp>
#include <opencv2/dnn/all_layers.hpp>using namespace std;
using namespace cv;
using namespace dnn;int main(int, char**) {std::vector<std::string> class_names;ifstream ifs(string("../../../input/object_detection_classes_coco.txt").c_str());string line;while (getline(ifs, line)){class_names.push_back(line);}// 加载神经网络模型auto model = readNet("../../../input/frozen_inference_graph.pb","../../../input/ssd_mobilenet_v2_coco_2018_03_29.pbtxt.txt","TensorFlow");// 捕获视频VideoCapture cap("../../../input/video_1.mp4");// 获取视频帧的宽度和高度以正确保存视频int frame_width = static_cast<int>(cap.get(3));int frame_height = static_cast<int>(cap.get(4));// 创建`VideoWriter()`对象VideoWriter out("video_result.avi", VideoWriter::fourcc('M', 'J', 'P', 'G'), 30, Size(frame_width, frame_height));
代码与图像目标检测类似,使用VideoCapture()
捕获视频,VideoWriter()
保存结果视频。
3.2.2 循环遍历视频帧并检测目标
对每个视频帧进行处理,视为图像进行目标检测:
Python:
# 在视频中的每个帧中检测目标
while cap.isOpened():ret, frame = cap.read()if ret:image = frameimage_height, image_width, _ = image.shape# 从图像创建blobblob = cv2.dnn.blobFromImage(image=image, size=(300, 300), mean=(104, 117, 123), swapRB=True)# 开始时间以计算FPSstart = time.time()model.setInput(blob)output = model.forward()# 检测后结束时间end = time.time()# 计算当前帧检测的FPSfps = 1 / (end-start)# 循环遍历每个检测for detection in output[0, 0, :, :]:# 提取检测的置信度confidence = detection[2]# 仅当检测置信度高于阈值时绘制边界框,否则跳过if confidence > .4:# 获取类IDclass_id = detection[1]# 将类ID映射到类class_name = class_names[int(class_id)-1]color = COLORS[int(class_id)]# 获取边界框坐标box_x = detection[3] * image_widthbox_y = detection[4] * image_height# 获取边界框宽度和高度box_width = detection[5] * image_widthbox_height = detection[6] * image_height# 在每个检测到的物体周围绘制矩形cv2.rectangle(image, (int(box_x), int(box_y)), (int(box_width), int(box_height)), color, thickness=2)# 在边界框上方放置类名文本cv2.putText(image, class_name, (int(box_x), int(box_y - 5)), cv2.FONT_HERSHEY_SIMPLEX, 1, color, 2)# 在帧上放置FPS文本cv2.putText(image, f"{fps:.2f} FPS", (20, 30), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2)# 显示帧cv2.imshow('frame', image)# 写入帧out.write(image)# 按'q'退出if cv2.waitKey(1) & 0xFF == ord('q'):breakelse:break# 释放资源
cap.release()
out.release()
cv2.destroyAllWindows()
上述代码循环读取视频帧,对每个帧进行与图像相同的预处理和检测操作,计算并显示FPS,将处理后的帧写入结果视频,按’q’键退出。
通过以上步骤,可实现利用OpenCV DNN模块在视频中进行实时目标检测。
四、总结
本文详细介绍了OpenCV DNN模块的基本概念、支持的功能、模型和框架,以及如何使用该模块进行图像分类和目标检测(包括图像和视频)。通过具体的代码示例和步骤说明,展示了从加载模型、预处理输入到获取输出并可视化结果的完整流程。
OpenCV DNN模块凭借其高效的CPU性能和对多种框架、模型的支持,为初学者和开发者提供了便捷的深度学习推理工具,尤其适用于边缘设备等计算资源有限的场景。希望本文能帮助读者快速入门并掌握使用OpenCV DNN模块进行计算机视觉深度学习应用的相关技能。