opencv-python的cv.dnn介绍
文章目录
OPenCV自3.3版本开始,加入了对深度学习网络的支持,即DNN模块,它支持主流的深度学习框架生成与到处模型的加载。
1.DNN模块
1.1. 模块简介
OpenCV中的深度学习模块(DNN)只提供了推理功能,不涉及模型的训练,支持多种深度学习框架,比如TensorFlow,Caffe,Torch和Darknet。
OpenCV那为什么要实现深度学习DNN模块?
-
轻量型。DNN模块只实现了推理功能,代码量及编译运行开销远小于其他深度学习模型框架。
-
使用方便。DNN模块提供了内建的CPU和GPU加速,无需依赖第三方库,若项目中之前使用了OpenCV,那么通过DNN模块可以很方便的为原项目添加深度学习的能力。
-
通用性。DNN模块支持多种网络模型格式,用户无需额外的进行网络模型的转换就可以直接使用,支持的网络结构涵盖了常用的目标分类,目标检测和图像分割的类别,如下图所示:
DNN模块支持多种类型网络层,基本涵盖常见的网络运算需求。
也支持多种运算设备(CPU,GPU等)和操作系统(Linux,windows,MacOS等)。
1.2.模块架构
DNN模块的架构如下图所示:
从上往下依次是:
- 第一层:语言绑定层,主要支持Python和Java,还包括准确度测试、性能测试和部分示例程序。
- 第二层:C++的API层,是原生的API,功能主要包括加载网络模型、推理运算以及获取网络的输出等。
- 第三层:实现层,包括模型转换器、DNN引擎以及层实现等。模型转换器将各种网络模型格式转换为DNN模块的内部表示,DNN引擎负责内部网络的组织和优化,层实现指各种层运算的实现过程。
- 第四层:加速层,包括CPU加速、GPU加速、Halide加速和Intel推理引擎加速。CPU加速用到了SSE和AVX指令以及大量的多线程元语,而OpenCL加速是针对GPU进行并行运算的加速。Halide是一个实验性的实现,并且性能一般。Intel推理引擎加速需要安装OpenVINO库,它可以实现在CPU、GPU和VPU上的加速,在GPU上内部会调用clDNN库来做GPU上的加速,在CPU上内部会调用MKL-DNN来做CPU加速,而Movidius主要是在VPU上使用的专用库来进行加速。
除了上述的加速方法外,DNN模块还有网络层面的优化。这种优化优化分两类,一类是层融合,还有一类是内存复用。
-
层融合
层融合通过对网络结构的分析,把多个层合并到一起,从而降低网络复杂度和减少运算量。
如上图所示,卷积层后面的BatchNorm层、Scale层和RelU层都被合并到了卷积层当中。这样一来,四个层运算最终变成了一个层运算。
如上图所示,网络结构将卷积层1和Eltwise Layer和RelU Layer合并成一个卷积层,将卷积层2作为第一个卷积层新增的一个输入。这样一来,原先的四个网络层变成了两个网络层运算。
-
如上图所示,原始的网络结构把三个层的输出通过连接层连接之后输入到后续层,这种情况可以把中间的连接层直接去掉,将三个网络层输出直接接到第四层的输入上面,这种网络结构多出现SSD类型的网络架构当中。
-
内存复用
深度神经网络运算过程当中会占用非常大量的内存资源,一部分是用来存储权重值,另一部分是用来存储中间层的运算结果。我们考虑到网络运算是一层一层按顺序进行的,因此后面的层可以复用前面的层分配的内存。
下图是一个没有经过优化的内存重用的运行时的存储结构,红色块代表的是分配出来的内存,绿色块代表的是一个引用内存,蓝色箭头代表的是引用方向。数据流是自下而上流动的,层的计算顺序也是自下而上进行运算。每一层都会分配自己的输出内存,这个输出被后续层引用为输入。
对内存复用也有两种方法:
第一种内存复用的方法是输入内存复用。
如上图所示,如果我们的层运算是一个in-place模式,那么我们无须为输出分配内存,直接把输出结果写到输入的内存当中即可。in-place模式指的是运算结果可以直接写回到输入而不影响其他位置的运算,如每个像素点做一次Scale的运算。类似于in-place模式的情况,就可以使用输入内存复用的方式。
第二种内存复用的方法是后续层复用前面层的输出。
如上图所示,在这个例子中,Layer3在运算时,Layer1和Layer2已经完成了运算。此时,Layer1的输出内存已经空闲下来,因此,Layer3不需要再分配自己的内存,直接引用Layer1的输出内存即可。由于深度神经网络的层数可以非常多,这种复用情景会大量的出现,使用这种复用方式之后,网络运算的内存占用量会下降30%~70%。
2.常用方法简介
DNN模块有很多可直接调用的Python API接口,现将其介绍如下:
2.1.dnn.blobFromImage
作用:根据输入图像,创建维度N(图片的个数),通道数C,高H和宽W次序的blobs
blobs是cv.dnn的输入数据格式,和tensorflow的tensor格式差不多的意思
原型:
blobFromImage(image,
scalefactor=None,
size=None,
mean=None,
swapRB=None,
crop=None,
ddepth=None):
参数:
- image:cv2.imread 读取的图片数据
- scalefactor: 缩放像素值,如 [0, 255] - [0, 1]
- size: 输出blob(图像)的尺寸,如 (netInWidth, netInHeight)
- mean: 从各通道减均值. 如果输入 image 为 BGR 次序,且swapRB=True,则通道次序为 (mean-R, mean-G, mean-B).
- swapRB: 交换 3 通道图片的第一个和最后一个通道,如 BGR - RGB
- crop: 图像尺寸 resize 后是否裁剪. 如果
crop=True
,则,输入图片的尺寸调整resize后,一个边对应与 size 的一个维度,而另一个边的值大于等于 size 的另一个维度;然后从 resize 后的图片中心进行 crop. 如果crop=False
,则无需 crop,只需保持图片的长宽比 - ddepth: 输出 blob 的 Depth. 可选: CV_32F 或 CV_8U
例子
import cv2
from cv2 import dnn
import numpy as np
import matplotlib.pyplot as plt
plt.rcParams['font.sans-serif']=['SimHei'] #用来正常显示中文标签
plt.rcParams['axes.unicode_minus']=False #用来正常显示负号
img_cv2 = cv2.imread("girl.jpg")# 读取图片
print("原图像大小: ", img_cv2.shape)# 原图像大小: (1191, 797, 3)
inWidth = 256
inHeight = 256
outBlob1 = cv2.dnn.blobFromImage(img_cv2,
scalefactor=1.0 / 255, # 归一化,
size=(inWidth, inHeight),
mean=(0, 0, 0),
swapRB=False, # 不转换为RGB
crop=False)
print("未裁剪输出: ", outBlob1.shape) # 输出为:(图像数,通道数,高,宽) 未裁剪输出: (1, 3, 256, 256)
outimg1 = np.transpose(outBlob1[0], (1, 2, 0)) # outBlob1[0]是第一副图像数据,后面是把通道数,高,宽做一个转换位置--》高,宽,通道数
outBlob2 = cv2.dnn.blobFromImage(img_cv2,
scalefactor=1.0 / 255,
size=(inWidth, inHeight),
mean=(0, 0, 0),
swapRB=False,
crop=True) # 进行裁剪,不缩放
print("裁剪输出: ", outBlob2.shape) # 裁剪输出: (1, 3, 256, 256)
outimg2 = np.transpose(outBlob2[0], (1, 2, 0))
plt.figure(figsize=[10, 10])
plt.subplot(1, 3, 1)
plt.title('输入图像', fontsize=16)
plt.imshow(cv2.cvtColor(img_cv2, cv2.COLOR_BGR2RGB))
plt.axis("off")
plt.subplot(1, 3, 2)
plt.title('输出图像 - 未裁剪', fontsize=16)
plt.imshow(cv2.cvtColor(outimg1, cv2.COLOR_BGR2RGB))
plt.axis("off")
plt.subplot(1, 3, 3)
plt.title('输出图像 - 裁剪', fontsize=16)
plt.imshow(cv2.cvtColor(outimg2, cv2.COLOR_BGR2RGB))
plt.axis("off")
plt.show()")
plt.show()
另外一个API与上述API类似,是进行批量图片处理的,其原型如下所示:
blobFromImages(images,
scalefactor=None,
size=None, mean=None,
swapRB=None,
crop=None,
ddepth=None):
作用:批量处理图片,创建4维的blob,其它参数类似于 dnn.blobFromImage
。
注:plt可以直接显示归一化后的图片
由上面的代码可以看到,outBlob1是经过归一化的,但还是可以通过plt显示
比如打印outimg1
print(outimg1)
array([[[0.07058824, 0.12156864, 0.29411766],
[0.06666667, 0.09803922, 0.2392157 ],
[0.05490196, 0.09019608, 0.18823531],
...,
[0.0509804 , 0.07843138, 0.20784315],
[0.05490196, 0.07450981, 0.20392159],
[0.04313726, 0.06666667, 0.18823531]],
.....
...
2.2.dnn.NMSBoxes
作用:根据给定的检测boxes和对应的scores进行NMS(非极大值抑制)处理
原型:
NMSBoxes(bboxes,
scores,
score_threshold,
nms_threshold,
eta=None,
top_k=None)
参数:
-
boxes: 待处理的边界框 bounding boxes
-
scores: 对于于待处理边界框的 scores
-
score_threshold: 用于过滤 boxes 的 score 阈值
-
nms_threshold: NMS 用到的阈值
-
indices: NMS 处理后所保留的边界框的索引值
-
eta: 自适应阈值公式中的相关系数:
-
top_k: 如果 top_k>0,则保留最多 top_k 个边界框索引值.
NMS介绍
检测器预测完结果,一张图像中会有很多预测框,预测的结果间可能存在高冗余(即同一个目标可能被预测多个矩形框),进行NMS是在同一Ground Truth的多个预测结果时,我们只取置信度分数最高的那个预测结果,其余的我们都不要。
缺点:NMS时,置信度分高但是位置不够准的框可能会把置信度分低但是位置很准的框去掉。
NMS过程
NMS的思路是:对于一张图片中的每一个预测框来说,模型为其每一个类别都预测了一个置信度分数(一般多分类,模型输出后接softmax,每一个类别都得到了一个置信度分数,包括背景类)我们取置信度最高的那一个类别作为预测框中对象所属的类别。1. 首先我们将置信度分数低于置信度阈值a的所有预测框去掉 。2. 然后在同一张图片上,我们按照类别(除开背景类,因为背景类不需要进行NMS),将所有预测框按照置信度从高到低排序,将置信度最高的框作为我们要保留的此类别的第1个预测框,3. 然后按照顺序计算剩下其他预测框与其的IoU,4. 去掉与其IoU(IoU作为置信度分数)大于IoU阈值b的预测框(其实代码实现里是将这些要去掉的预测框其置信度分数置为0),5. 第一次迭代结束,我们已经剔除了与第一个框重合度较高的框。
接着从剩下的预测框中取置信度分数最高的检测框作为我们要保留的第2个预测框,进行第2次迭代。反复下去,我们就过滤掉此类别与同一GT重叠度较高的预测框了,然后对下一个类别处理,直至处理完所有的类别。
IoU 的全称为交并比(Intersection over Union),通过这个名称我们大概可以猜到 IoU 的计算方法。IoU 计算的是 “预测的边框” 和 “真实的边框” 的交集和并集的比值。
2.3. dnn.readNet
作用:加载深度学习网络及其模型参数
原型:
readNet(model, config=None, framework=None)
参数:
- model: 训练的权重参数的模型二值文件,支持的格式有:
*.caffemodel
(Caffe)、*.pb
(TensorFlow)、*.t7
或*.net
(Torch)、*.weights
(Darknet)、*.bin
(DLDT). - config: 包含网络配置的文本文件,支持的格式有:
*.prototxt
(Caffe)、*.pbtxt
(TensorFlow)、*.cfg
(Darknet)、*.xml
(DLDT). - framework: 所支持格式的框架名
该函数自动检测训练模型所采用的深度框架,然后调用 readNetFromCaffe
、readNetFromTensorflow
、readNetFromTorch
或 readNetFromDarknet
中的某个函数完成深度学习网络模型及模型参数的加载。
下面我们看下对应于特定框架的API:
1 Caffe
readNetFromCaffe(prototxt, caffeModel=None)
作用:加载采用Caffe的配置网络和训练的权重参数
2 Darknet
readNetFromDarknet(cfgFile, darknetModel=None)
作用:加载采用Darknet的配置网络和训练的权重参数
3 Tensorflow
readNetFromTensorflow(model, config=None)
作用:加载采用Tensorflow 的配置网络和训练的权重参数
参数:
- model: .pb 文件
- config: .pbtxt 文件
4 Torch
readNetFromTorch(model, isBinary=None)
作用:加载采用 Torch 的配置网络和训练的权重参数
参数:
- model: 采用
torch.save()
函数保存的文件
5 ONNX
readNetFromONNX(onnxFile)
作用:加载 .onnx 模型网络配置参数和权重参数
作用:加载采用Tensorflow 的配置网络和训练的权重参数
参数:
- model: .pb 文件
- config: .pbtxt 文件
4 Torch
readNetFromTorch(model, isBinary=None)
作用:加载采用 Torch 的配置网络和训练的权重参数
参数:
- model: 采用
torch.save()
函数保存的文件
5 ONNX
readNetFromONNX(onnxFile)
作用:加载 .onnx 模型网络配置参数和权重参数