OpenCV DNN 模块的示例输出 - 右侧的图像分类和左侧的对象检测。
使用深度学习和 OpenCV 的 DNN 模块进行图像分类(左)和对象检测(右)的示例。

计算机视觉领域自 1960 年代后期就已存在。图像分类和目标检测是计算机视觉中一些最古老的问题,研究人员几十年来一直试图解决这些问题。使用神经网络和深度学习,我们已经达到了计算机可以开始以高精度真正理解和识别物体的阶段,在许多情况下甚至超过了人类。要了解神经网络和计算机视觉深度学习,OpenCV DNN 模块是一个很好的起点。凭借其高度优化的 CPU 性能,初学者即使没有非常强大的 GPU 支持系统也可以轻松上手。

希望这篇博文将成为最好的起点之一。

什么是 OpenCV DNN 模块?

我们都知道 OpenCV 是最好的计算机视觉库之一。此外,它还具有运行深度学习推理的功能。最好的部分是支持加载来自不同框架的不同模型,使用它我们可以执行多种深度学习功能。从 3.3 版本开始,支持来自不同框架的模型的特性已经成为 OpenCV 的一部分。尽管如此,该领域的许多新手仍未意识到 OpenCV 的这一强大功能。因此,他们往往会错过很多有趣和好的学习机会。

为什么选择 OpenCV DNN 模块?

OpenCV DNN 模块仅支持对图像和视频进行深度学习推理。它不支持微调和训练。尽管如此,OpenCV DNN 模块仍然是任何初学者进入基于深度学习的计算机视觉并进行尝试的完美起点。 

OpenCV DNN 模块的优点之一是它针对英特尔处理器进行了高度优化。在针对对象检测和图像分割应用程序对实时视频进行推理时,我们可以获得良好的 FPS。当使用使用特定框架预训练的模型时,我们通常会通过 DNN 模块获得更高的 FPS。例如,让我们看看不同框架的图像分类推理速度。

不同框架在 CPU 上的图像分类推理速度比较。

以上结果是 DenseNet121 模型的推理时序。令人惊讶的是,OpenCV 比 TensorFlow 的原始实现快得多,同时略微落后于 PyTorch。事实上,TensorFlow 的推理时间接近 1 秒,而 OpenCV 不到200 毫秒

上述基准测试是使用撰写本文时的最新版本完成的。它们是 PyTorch 1.8.0、OpenCV 4.5.1 和 TensorFlow 2.4。所有测试均在具有 Intel Xeon 处理器 2.3Ghz 处理器的 Google Colab 上完成。

即使在物体检测的情况下也是如此。

不同框架在 CPU 上的对象检测速度比较。

上图显示了在原始 Darknet 框架和 OpenCV 上使用 Tiny YOLOv4 的视频 FPS 结果。基准测试是在时钟速度为 2.6Ghz 的第 8 代英特尔 i7 笔记本电脑 CPU 上完成的。我们可以看到,在同一视频中,OpenCV 的 DNN 模块以 35 FPS 的速度运行,而使用 OpenMP 和 AVX 编译的 Darknet 以 15 FPS 的速度运行。而Darknet(没有 OpenMP 或 AVX)Tiny YOLOv4 是最慢的,运行速度仅为 3 FPS。考虑到我们在这两种情况下都使用原始的 Darknet Tiny YOLOv4 模型,这是一个巨大的差异。

上图显示了 OpenCV DNN 模块在使用 CPU 时的实际用途和功能。由于其快速的推理时间,即使在 CPU 上,它也可以作为计算能力有限的边缘设备上的优秀部署工具。基于 ARM 处理器的边缘设备就是最好的例子。下图就是很好的证明。

比较在 Raspberry Pi 3B 上运行的不同框架和不同神经网络模型的 FPS(每秒帧数)的条形图。

上图显示了在 Raspberry Pi 3B 上运行的不同框架和模型的 FPS。结果令人印象深刻。对于 SqueezeNet 和 MobileNet 模型,OpenCV 在 FPS 方面超越了所有其他框架。对于 GoogLeNet,OpenCV 位居第二,TensorFlow 是最快的。对于 Network in Network,OpenCV Raspberry FPS 是最慢的。

上面几张图显示了优化的 OpenCV,以及神经网络推理的速度。这些数据是选择详细了解 OpenCV DNN 模块的完美理由。

OpenCV DNN 模块支持的不同深度学习功能

我们已经确定,通过使用 OpenCV DNN 模块,我们可以对图像和视频进行基于深度学习的计算机视觉推理。让我们来看看它支持的所有功能。有趣的是,大多数我们能想到的深度学习和计算机视觉任务都得到了支持。以下列表将使我们对这些功能有一个很好的了解。

  1. 图像分类。
  2. 物体检测。
  3. 图像分割。
  4. 文本检测和识别。
  5. 姿势估计。
  6. 深度估计。
  7. 人脸验证和检测。
  8. 人瑞德。

该列表内容广泛,提供了大量实用的深度学习用例。通过访问 OpenCV 存储库的OpenCV Wiki 页面中的深度学习,详细了解所有这些内容。

令人印象深刻的事实是,根据系统的硬件和计算能力,有许多模型可供选择(我们稍后会看到)。我们可以为每个用例找到一个模型,从用于最先进结果的真正计算密集型模型到可以在低功耗边缘设备上运行的模型。

请注意,不可能在一篇博文中介绍上述所有用例。因此,我们将详细讨论对象检测和人体姿势估计,以了解使用 OpenCV DNN 选择不同模型的工作原理。

播放视频

掌握 CV 生成 AI

获得专家指导、内部提示和技巧。创建令人惊叹的图像,学习微调扩散模型,高级图像编辑技术,如 In-Painting、Instruct Pix2Pix 等等

OpenCV DNN 模块支持的不同模型

为了支持我们上面讨论的所有应用程序,我们需要大量预训练模型。此外,还有许多最先进的模型可供选择。下表根据不同的深度学习应用列出了一些模型。

图片分类物体检测图像分割文字检测与识别人体姿势估计人脸检测
亚历克斯网MobileNet 固态硬盘深度实验室简单的OCR开放式开脸
谷歌网VGG固态硬盘网络神经网络阿尔法姿势火炬手
VGG更快的 R-CNNFCN移动脸网
ResNet高效OpenCV
人脸检测器
挤压网
密集网络
洗牌网
高效网络

上述模型并不详尽。存在更多模型。如前所述,在单个博客中完成列表或详细讨论每一个几乎是不可能的。上面的列表让我们很好地了解了 DNN 模块在探索计算机视觉深度学习方面的实用性。

OpenCV DNN模块支持的不同框架

综观以上所有模型,我想到的一个问题是,“所有这些模型都由一个框架支持吗”?实际上,不。

OpenCV DNN 模块支持许多流行的深度学习框架。以下是 OpenCV DNN 模块支持的深度学习框架。

Caffe

我们需要两件事才能将预训练的 Caffe 模型与 OpenCV DNN 结合使用。一个是包含预训练权重的 model.caffemodel 文件。另一个是扩展名为 .prototxt 的模型架构文件。它就像一个纯文本文件,具有类似 JSON 的结构,其中包含所有神经网络层的定义。要清楚地了解此文件的外观,请访问此链接

TensorFlow

为了加载预训练的 TensorFlow 模型,我们还需要两个文件。模型权重文件和protobuf文本文件包含模型配置。权重文件的扩展名为 .pb,它是一个包含所有预训练权重的 protobuf 文件。如果你以前使用过 TensorFlow,你就会知道 .pb 文件是我们在保存模型并冻结权重后得到的模型检查点。模型配置保存在 protobuf 文本文件中,文件扩展名为 .pbtxt。

笔记:在较新版本的 TensorFlow 中,模型权重文件可能不是 .pb 格式。如果您尝试使用自己保存的模型之一(可能是 .ckpt 或 .h5 格式),这也是正确的。在这种情况下,在模型可以与 OpenCV DNN 模块一起使用之前,需要执行一些中间步骤。在这种情况下,将模型转换为 ONNX 格式,然后再转换为 .pb 格式是确保一切按预期工作的最佳方法。

Torch and PyTorch

为了加载 Torch 模型文件,我们需要包含预训练权重的文件。通常,此文件的扩展名为 .t7 或 .net。但是对于具有 .pth 扩展名的最新 PyTorch 模型,首先转换为 ONNX 是继续进行的最佳方式。转换为 ONNX 后,您可以直接加载它们,因为 OpenCV DNN 支持 ONNX 模型。

Darknet

OpenCV DNN 模块也支持著名的 Darknet 框架。如果他们使用带有 Darknet 框架的官方 YOLO 模型,可能会认识到这一点。

通常,要加载 Darknet 模型,我们需要一个具有 .weights 扩展名的模型权重文件。网络配置文件将始终是 Darknet 模型的 .cfg 文件。

使用已从 Keras 和 PyTorch 等不同框架转换为 ONNX 格式的模型

通常,在 PyTorch 或 TensorFlow 等框架中训练的模型可能无法直接与 OpenCV DNN 模块一起使用。在这些情况下,通常,我们将模型转换为 ONNX 格式(开放神经网络交换),然后可以按原样使用,甚至可以转换为 TensorFlow 或 PyTorch 等其他框架支持的格式。

要加载 ONNX 模型,我们需要 OpenCV DNN 模块的 .onnx 权重文件。

请访问官方 OpenCV 文档以了解不同的框架、它们的权重文件和配置文件。

上面的列表很可能涵盖了所有著名的深度学习框架。要全面了解 OpenCV DNN 模块支持的所有框架和模型,请访问官方 Wiki 页面

我们在这里看到的所有模型都经过测试,可以与 OpenCV DNN 模块完美配合。理论上,上述框架中的任何模型都应该与 DNN 模块一起工作。我们只需要找到正确的权重文件和对应的神经网络架构文件即可。当我们开始本教程的编码部分时,事情会更清楚。 

我们已经涵盖了足够多的理论。让我们深入了解本教程的编码部分。首先,我们将使用 OpenCV DNN 模块对图像分类进行完整演练。然后我们将使用 DNN 模块进行对象检测。

使用 OpenCV DNN模块进行图像分类的完整指南

本节将使用 OpenCV DNN 模块对图像进行分类。我们将详细介绍每个步骤,以在本节结束时清除所有内容。

我们将使用使用 Caffe 框架在非常著名的 ImageNet 数据集上训练的神经网络模型。具体来说,我们将使用 DensNet121 深度神经网络模型进行分类任务。优点是它在 ImageNet 数据集中的 1000 个类上进行了预训练。我们可以预期该模型已经看到了我们想要分类的任何图像。这使我们能够从范围广泛的图像中进行选择。

我们将使用下面的老虎图像进行图像分类任务。

老虎的示例图像。 这将用作使用 OpenCV 的 DNN 模块进行分类的输入
我们将使用 OpenCV 的 DNN 模块进行分类的老虎图像。

简而言之,以下是我们在对图像进行分类时将遵循的步骤。

  1. 从磁盘加载类名文本文件并提取所需的标签。
  2. 从磁盘加载预训练的神经网络模型。
  3. 从磁盘加载图像并准备图像,使其采用深度学习模型的正确输入格式。
  4. 通过模型前向传播输入图像并获得输出。

现在让我们详细了解每个步骤以及代码。

导入模块和加载类文本文件

我们需要为 Python 代码导入 OpenCV 和 Numpy 模块。对于 C++,我们需要包括 OpenCV 和 OpenCV 的 DNN 库。

Python:

12importcv2importnumpy as np

C++

123456789#include <iostream>#include <fstream>#include <opencv2/opencv.hpp>#include <opencv2/dnn.hpp>#include <opencv2/dnn/all_layers.hpp> usingnamespacestd;usingnamespacecv;usingnamespacednn;

请记住,我们讨论过我们将使用的 DenseNet121 模型已经过 1000 个 ImageNet 类的训练。我们需要一些方法来将这 1000 个类加载到内存中并轻松访问它们。这些类通常在文本文件中可用。一个这样的文件称为 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

每个新行都包含特定于单个图像的所有标签或类名。例如,第一行包含tench, Tinca Tinca. 这是属于同一种鱼的两个名字。同样,第二行有两个属于金鱼的名字。通常,名字是几乎每个人都认识的最常见的名字。

让我们看看如何加载这样的文本文件并从每一行中提取名字,以便在对图像进行分类时将它们用作标签。

Python:

12345# read the ImageNet class nameswith open('../../input/classification_classes_ILSVRC2012.txt', 'r') as f:   image_net_names =f.read().split('\n')# final class names (just the first word of the many ImageNet names for one image)class_names =[name.split(',')[0] forname inimage_net_names]

C++

1234567std::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现在,我们将按照以下格式将所有类存储在列表中。

[‘tench, Tinca tinca’, ‘goldfish, Carassius auratus’, ‘great white shark, white shark, man-eater, man-eating shark’, ...]

但是,我们只需要每一行的名字。这就是第二行代码的作用。对于 image_net_names 列表中的每个元素,我们使用逗号 (,) 作为分隔符拆分元素,并且只保留这些元素中的第一个。这些名称保存在class_names列表中。现在,列表将如下所示。

['tench', 'goldfish', 'great white shark', 'tiger shark', 'hammerhead', …]

从磁盘加载预训练的 DenseNet121 模型

如前所述,我们将使用预训练的 DenseNet121 模型,该模型已使用 Caffe 深度学习框架进行训练。

我们将需要模型权重文件 ( .caffemodel) 和模型配置文件 ( .prototxt)。

让我们看看代码,然后进入模型加载的解释部分。

Python:

12# load the neural network modelmodel =cv2.dnn.readNet(model='../../input/DenseNet_121.caffemodel', config='../../input/DenseNet_121.prototxt', framework='Caffe')

C++

1234// load the neural network model   automodel = readNet("../../input/DenseNet_121.prototxt",                       "../../input/DenseNet_121.caffemodel",                       "Caffe");

readNet()你可以看到,我们正在使用一个从 OpenCV DNN 模块调用的函数,它接受三个输入参数。

  • model:这是预训练权重文件的路径。在我们的例子中,它是预训练的 Caffe 模型。
  • config:这是模型配置文件的路径,在本例中是 Caffe 模型的 .prototxt 文件。
  • framework: 最后,我们需要提供我们正在加载模型的框架名称。对我们来说,它是 Caffe 框架。

除了readNet()函数之外,DNN 模块还提供了从特定框架加载模型的函数,我们不必提供参数framework。以下是这些功能。

  1. readNetFromCaffe():这用于加载预训练的 Caffe 模型并接受两个参数。它们是prototxt文件的路径Caffe模型文件的路径
  2. readNetFromTensorflow():我们可以使用这个函数直接加载TensorFlow预训练模型。这也接受两个参数。一个是冻结模型图的路径,另一个是模型架构 protobuf 文本文件的路径
  3. readNetFromTorch(): 我们可以使用它来加载使用 () 函数保存的 Torch 和 PyTorch 模型torch.save。我们需要提供模型路径作为参数
  4. readNetFromDarknet(): 用于加载使用 DarkNet 框架训练的模型。我们也需要在这里提供两个论点。其中一个是模型权重的路径,另一个是模型配置文件的路径
  5. readNetFromONNX(): 我们可以使用它来加载 ONNX 模型,我们只需要提供 ONNX 模型文件的路径。

这篇博文将坚持使用readNet() 函数来加载预训练模型。我们也将在对象检测部分使用相同的函数。

读取图像并为模型输入做准备

我们将像往常一样使用 OpenCV 的 () 函数从磁盘读取图像imread。请注意,还有一些我们需要注意的其他细节。我们使用 DNN 模块加载的预训练模型不会直接将读取的图像作为输入。在此之前我们需要做一些预处理。 

让我们先写代码,然后进入技术细节会容易得多。

Python:

1234# load the image from diskimage =cv2.imread('../../input/image_1.jpg')# create blob from imageblob =cv2.dnn.blobFromImage(image=image, scalefactor=0.01, size=(224, 224), mean=(104, 117, 123))

C++

1234// load the image from diskMat image = imread("../../input/image_1.jpg");// create blob from imageMat blob = blobFromImage(image, 0.01, Size(224, 224), Scalar(104, 117, 123));

在读取图像时,我们假设它是当前目录之前和input文件夹内的两个目录。接下来的几个步骤是必不可少的。我们有一个blobFromImage() 函数,它以正确的格式准备要输入模型的图像。让我们回顾所有的论点并详细了解它们。

  • image:这是我们刚刚使用 imread() 函数读取的输入图像。
  • scalefactor:此值按提供的值缩放图像。它的默认值为 1,表示不执行缩放。
  • size:这是图像将调整到的大小。我们提供的尺寸为 224×224,因为大多数在 ImageNet 数据集上训练的分类模型都只期望这个尺寸。
  • mean: 平均论点非常重要。这些实际上是从图像的 RGB 颜色通道中减去的平均值。这对输入进行了归一化,并使最终输入对不同的光照尺度具有不变性。

这里还有一件事要注意。所有深度学习模型都需要批量输入。但是,我们这里只有一张图片。尽管如此,我们在此处获得的 blob 输出实际上具有[1, 3, 224, 224]. 观察 () 函数添加了一个额外的批次维度blobFromImage。这将是神经网络模型的最终和正确的输入格式。

通过模型向前传播输入

现在,当我们的输入准备就绪时,我们可以做出预测。

Python

1234# set the input blob for the neural networkmodel.setInput(blob)# forward pass image blog through the modeloutputs =model.forward()

进行预测有两个步骤。 

  • 首先,我们必须将输入 blob 设置为我们从磁盘加载的神经网络模型。
  • 第二步是使用 forward() 函数通过模型向前传播 blob,这为我们提供了所有输出。

我们正在执行上述代码块中的两个步骤。

是一个数组outputs,包含所有预测。但是在我们可以正确地看到输出和类标签之前,我们需要完成一些预处理步骤。

目前,outputs具有 ( ) 的形状1, 1000, 1, 1,很难按原样提取类标签。因此,以下代码块重塑了outputs,之后我们可以轻松获得正确的类标签并将标签 ID 映射到类名称。

Python:

1234567891011121314151617final_outputs =outputs[0]# make all the outputs 1Dfinal_outputs =final_outputs.reshape(1000, 1)# get the class labellabel_id =np.argmax(final_outputs)# convert the output scores to softmax probabilitiesprobs =np.exp(final_outputs) /np.sum(np.exp(final_outputs))# get the final highest probabilityfinal_prob =np.max(probs) *100.# map the max confidence to the class label namesout_name =class_names[label_id]out_text =f"{out_name}, {final_prob:.3f}"# put the class name text on top of the imagecv2.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++

1234567891011121314// set the input blob for the neural networkmodel.setInput(blob);// forward pass the image blob through the modelMat outputs = model.forward(); Point classIdPoint;doublefinal_prob;minMaxLoc(outputs.reshape(1, 1), 0, &final_prob, 0, &classIdPoint);intlabel_id = classIdPoint.x; // Print predicted class.string out_text = format("%s, %.3f", (class_names[label_id].c_str()), final_prob);// put the class name text on top of the imageputText(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 个标签都有 1000 行。每行包含与类标签对应的分数,如下所示。

[[-1.44623446e+00]
[-6.37421310e-01]
 [-1.04836571e+00]
 [-8.40160131e-01]

]

从这些中,我们提取最高的标签索引并将其存储在label_id. 但是,这些分数实际上并不是概率分数。我们需要获得 softmax 概率以了解模型预测得分最高标签的概率。 

在上面的 Python 代码中,我们使用 将分数转换为 softmax 概率np.exp(final_outputs) / np.sum(np.exp(final_outputs))。然后我们将最高概率分数乘以 100 以获得预测分数百分比。

最后的步骤是在图像顶部注释类名称和百分比。然后我们将图像可视化并将结果保存到磁盘。

执行代码后,我们将得到以下输出。

DenseNet121 将图像正确预测为老虎。
DenseNet121 模型以接近 91% 的置信度将图像预测为老虎图像,这非常好。

DenseNet121 模型正确地将图像预测为老虎的图像,而且置信度也达到了 91%。结果相当不错。 

在上面的部分中,我们了解了如何使用 OpenCV DNN 模块使用 DenseNet121 神经网络模型进行图像分类。我们还详细介绍了每个步骤,以更好地理解 DNN 模块的工作原理。

在以下部分中,我们将在图像和视频中使用 OpenCV DNN 和对象检测。

使用 OpenCV DNN 进行目标检测

使用 OpenCV DNN 模块,我们可以轻松开始深度学习和计算机视觉中的对象检测。与分类一样,我们将加载图像、适当的模型并通过模型前向传播输入。对象检测中正确可视化的预处理步骤会有所不同。随着我们在博客文章的其余部分中取得进展,我们将了解所有这些内容。

让我们从图像中的对象检测开始。

使用 OpenCV DNN 在图像中进行目标检测

就像分类一样,在这里,我们也将利用预训练模型。这些模型已经在MS COCO数据集上进行了训练,MS COCO 数据集是基于深度学习的目标检测模型的当前基准数据集。

MS COCO 有近 80 类对象,从人到汽车,再到牙刷。该数据集包含 80 类日常物品。我们还将使用文本文件加载 MS COCO 数据集中的所有标签以进行对象检测。 

对于物体检测,我们将使用下图。

用于物体检测的输入图像,车辆停在交通信号灯处,注意不同物体的数量以及它们的杂乱程度。
用于物体检测的输入图像。这将是对模型的一个很好的测试,因为有许多物体杂乱无章,人、自行车和自行车。

我们将使用 MobileNet SSD(单次检测器),它已经使用 TensorFlow 深度学习框架在 MS COCO 数据集上进行了训练。与其他对象检测模型相比,SSD 模型通常更快。此外,MobileNet 骨干网还降低了它们的计算密集度。因此,开始使用 OpenCV DNN 学习对象检测是一个很好的模型。

让我们从编码部分开始。

Python

importcv2importnumpy as np

C++

#include <iostream>#include <fstream>#include <opencv2/opencv.hpp>#include <opencv2/dnn.hpp>#include <opencv2/dnn/all_layers.hpp> usingnamespacestd;usingnamespacecv;usingnamespacednn;
  • 在 Python 代码中,首先我们导入 cv2 和 numpy 模块。
  • 对于 C++,我们需要包括 OpenCV 和 OpenCV DNN 库。

Python

123456# load the COCO class nameswith open('object_detection_classes_coco.txt', 'r') as f:   class_names =f.read().split('\n') # get a different color array for each of the classesCOLORS =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);}

接下来我们读取object_detection_classes_coco.txt文件,其中包含由新行分隔的所有类名。我们将每个类名存储在class_names列表中。

class_names 列表将类似于以下内容。

['person', 'bicycle', 'car', 'motorcycle', 'airplane', 'bus', 'train', 'truck', 'boat', 'traffic light', … 'book', 'clock', 'vase', 'scissors', 'teddy bear', 'hair drier', 'toothbrush', '']

除此之外,我们还有一个 COLORS 数组,其中包含三个整数值的元组。我们可以在为每个类绘制边界框时应用这些随机颜色。最好的部分是我们将为每个类别设置不同颜色的边界框,这样我们就可以轻松区分最终结果中的类别。 

加载 MobileNet SSD 模型并准备输入

我们将使用我们之前也使用过的 readNet() 函数加载 MobileNet SSD 模型。

Python

# load the DNN modelmodel =cv2.dnn.readNet(model='frozen_inference_graph.pb',                config='ssd_mobilenet_v2_coco_2018_03_29.pbtxt.txt',framework='TensorFlow')

C++

// load the neural network modelautomodel = readNet("../../../input/frozen_inference_graph.pb","../../../input/ssd_mobilenet_v2_coco_2018_03_29.pbtxt.txt", "TensorFlow");

在上面的代码块中:

  • 模型参数接受冻结推理图路径作为输入,这是一个包含权重的预训练模型。
  • config参数接受作为 protobuf 文本文件的模型配置文件的路径。
  • 最后,我们指定framework,在本例中为 TensorFlow。

接下来,我们将从磁盘读取图像并准备输入的 blob 文件。

Python

# read the image from diskimage =cv2.imread('../../input/image_2.jpg')image_height, image_width, _ =image.shape# create blob from imageblob =cv2.dnn.blobFromImage(image=image, size=(300, 300), mean=(104, 117, 123), swapRB=True)# set the blob to the modelmodel.setInput(blob)# forward pass through the model to carry out the detectionoutput =model.forward()

C++

// read the image from diskMat image = imread("../../../input/image_2.jpg");intimage_height = image.cols;intimage_width = image.rows;//create blob from imageMat blob = blobFromImage(image, 1.0, Size(300, 300), Scalar(127.5, 127.5, 127.5),true, false);//create blob from imagemodel.setInput(blob);//forward pass through the model to carry out the detectionMat output = model.forward();Mat detectionMat(output.size[2], output.size[3], CV_32F, output.ptr<float>());

对于对象检测,我们在 () 函数中使用了一些不同的参数值blobFromImage

  • 我们指定size300×300 是 SSD 模型在几乎所有框架中通常期望的输入大小。TensorFlow 也是如此。
  • swapRB这次我们也使用了参数。通常,OpenCV 以 BGR 格式读取图像,而对于对象检测,模型期望输入为 RGB 格式。因此,该swapRB参数将交换图像的 R 和 B 通道,使其成为 RGB 格式。

然后我们将 blob 设置为 MobileNet SSD 模型并使用forward() 函数对其进行前向传播。

我们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 边界框坐标,最后一个是边界框的宽度和高度。

循环检测并绘制边界框

我们都准备好循环 中的检测output,并在每个检测到的对象周围绘制边界框。以下是循环检测的代码。

Python

# loop over each of the detectionfordetection inoutput[0, 0, :, :]:   # extract the confidence of the detection   confidence =detection[2]   # draw bounding boxes only if the detection confidence is above...   # ... a certain threshold, else skip   ifconfidence > .4:       # get the class id       class_id =detection[1]       # map the class id to the class       class_name =class_names[int(class_id)-1]       color =COLORS[int(class_id)]       # get the bounding box coordinates       box_x =detection[3] *image_width       box_y =detection[4] *image_height       # get the bounding box width and height       box_width =detection[5] *image_width       box_height =detection[6] *image_height       # draw a rectangle around each detected object       cv2.rectangle(image, (int(box_x), int(box_y)), (int(box_width), int(box_height)), color, thickness=2)       # put the FPS text on top of the frame       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(inti = 0; i < detectionMat.rows; i++){       intclass_id = detectionMat.at<float>(i, 1);       floatconfidence = detectionMat.at<float>(i, 2);             // Check if the detection is of good quality       if(confidence > 0.4){           intbox_x = static_cast<int>(detectionMat.at<float>(i, 3) * image.cols);           intbox_y = static_cast<int>(detectionMat.at<float>(i, 4) * image.rows);           intbox_width = static_cast<int>(detectionMat.at<float>(i, 5) * image.cols - box_x);           intbox_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();
  • 在循环内部for,首先,我们提取当前检测到的对象的置信度分数。如前所述,我们可以从索引位置 2 获取它。
  • 然后我们有if块来检查检测到的对象的置信度是否高于某个阈值。我们只是向前移动以绘制置信度高于 0.4 的边界框。
  • 我们获取类 ID 并将其映射到 MS COCO 类名。然后我们为当前类获取单一颜色来绘制边界框并将类标签文本放在边界框的顶部。
  • 然后我们提取边界框 x 和 y 坐标以及边界框的宽度和高度。分别将它们与图像的宽度和高度相乘,为我们提供了绘制矩形的正确值。
  • 在最后几个步骤中,我们绘制边界框矩形,在顶部写上类文本并可视化生成的图像。

这是我们使用 OpenCV DNN 在图像中进行对象检测所需的所有代码。执行代码给我们以下结果。

使用 MobileNet SSD 深度学习模型时的对象检测输出。 值得注意的是,有些检测是不正确的。
使用 MobileNet SSD 的对象检测结果。该模型正在检测图像中的几乎每个对象。但是,请注意某些检测是不正确的。

在上图中,我们可以看到结果看起来不错。该模型正在检测几乎所有可见的对象。但是,也有一些不正确的预测。例如,MobileNet SSD 模型将右侧的自行车检测为摩托车。MobileNet SSD 往往会犯这样的错误,因为它们是为实时应用程序而犯的,并且以准确性换取速度。 

这标志着使用 OpenCV DNN 在图像中进行对象检测。在这篇博文中,我们将做最后一件事来改进学习过程。那就是视频中的对象检测。

使用 OpenCV DNN 在视频中进行对象检测 

视频中的对象检测代码与图像中的对象检测代码非常相似。当我们对视频帧而不是图像进行预测时,会有一些变化。

几行代码与图像中的对象检测相同。让我们先完成那部分。

Python

importcv2importtimeimportnumpy as np # load the COCO class nameswith open('object_detection_classes_coco.txt', 'r') as f:   class_names =f.read().split('\n') # get a different color array for each of the classesCOLORS =np.random.uniform(0, 255, size=(len(class_names), 3)) # load the DNN modelmodel =cv2.dnn.readNet(model='frozen_inference_graph.pb',                        config='ssd_mobilenet_v2_coco_2018_03_29.pbtxt.txt',framework='TensorFlow') # capture the videocap =cv2.VideoCapture('../../input/video_1.mp4')# get the video frames' width and height for proper saving of videosframe_width =int(cap.get(3))frame_height =int(cap.get(4))# create the `VideoWriter()` objectout =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> usingnamespacestd;usingnamespacecv;usingnamespacednn;  intmain(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);   }      // load the neural network model   automodel = readNet("../../../input/frozen_inference_graph.pb","../../../input/ssd_mobilenet_v2_coco_2018_03_29.pbtxt.txt","TensorFlow");    // capture the video   VideoCapture cap("../../../input/video_1.mp4");   // get the video frames' width and height for proper saving of videos   intframe_width = static_cast<int>(cap.get(3));   intframe_height = static_cast<int>(cap.get(4));   // create the `VideoWriter()` object   VideoWriter out("video_result.avi", VideoWriter::fourcc('M', 'J', 'P', 'G'), 30, Size(frame_width, frame_height));

我们可以看到大部分代码都是一样的。我们正在加载相同的 MS COCO 类文件和 MobileNet SSD 模型。

在这里,我们使用 () 对象捕获视频而不是图像VideoCapture。我们还创建了一个 VideoWriter() 对象以正确保存生成的视频帧。

循环播放视频帧并检测每一帧中的对象

截至目前,我们已准备好视频和 MobileNet SSD 模型。下一步是循环每个视频帧并在每个帧中进行对象检测。这样,我们将把每一帧都当作一张图像来对待。

Python

# detect objects in each frame of the videowhilecap.isOpened():   ret, frame =cap.read()   ifret:       image =frame       image_height, image_width, _ =image.shape       # create blob from image       blob =cv2.dnn.blobFromImage(image=image, size=(300, 300), mean=(104, 117, 123), swapRB=True)       # start time to calculate FPS       start =time.time()       model.setInput(blob)       output =model.forward()              # end time after detection       end =time.time()       # calculate the FPS for current frame detection       fps =1/(end-start)       # loop over each of the detections       fordetection inoutput[0, 0, :, :]:           # extract the confidence of the detection           confidence =detection[2]           # draw bounding boxes only if the detection confidence is above...           # ... a certain threshold, else skip           ifconfidence > .4:               # get the class id               class_id =detection[1]               # map the class id to the class               class_name =class_names[int(class_id)-1]               color =COLORS[int(class_id)]               # get the bounding box coordinates               box_x =detection[3] *image_width               box_y =detection[4] *image_height               # get the bounding box width and height               box_width =detection[5] *image_width               box_height =detection[6] *image_height               # draw a rectangle around each detected object               cv2.rectangle(image, (int(box_x), int(box_y)), (int(box_width), int(box_height)), color, thickness=2)               # put the class name text on the detected object               cv2.putText(image, class_name, (int(box_x), int(box_y -5)), cv2.FONT_HERSHEY_SIMPLEX, 1, color, 2)               # put the FPS text on top of the frame               cv2.putText(image, f"{fps:.2f} FPS", (20, 30), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2)             cv2.imshow('image', image)       out.write(image)       ifcv2.waitKey(10) & 0xFF==ord('q'):           break   else:       break cap.release()cv2.destroyAllWindows()

C++

while(cap.isOpened()) {       Mat image;       boolisSuccess = cap.read(image);       if(! isSucess) break;             intimage_height = image.cols;       intimage_width = image.rows;       //create blob from image       Mat blob = blobFromImage(image, 1.0, Size(300, 300), Scalar(127.5, 127.5, 127.5),                               true, false);       //create blob from image       model.setInput(blob);       //forward pass through the model to carry out the detection       Mat output = model.forward();             Mat detectionMat(output.size[2], output.size[3], CV_32F, output.ptr<float>());             for(inti = 0; i < detectionMat.rows; i++){           intclass_id = detectionMat.at<float>(i, 1);           floatconfidence = detectionMat.at<float>(i, 2);            // Check if the detection is of good quality           if(confidence > 0.4){               intbox_x = static_cast<int>(detectionMat.at<float>(i, 3) * image.cols);               intbox_y = static_cast<int>(detectionMat.at<float>(i, 4) * image.rows);               intbox_width = static_cast<int>(detectionMat.at<float>(i, 5) * image.cols - box_x);               intbox_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);       out.write(image);       intk = waitKey(10);       if(k == 113){           break;       }   } cap.release();destroyAllWindows();}

在上面的代码块中,模型检测每一帧中的对象,直到视频中没有要循环的帧。需要注意的一些重要事项:

  • 我们在变量中存储检测之前的开始时间start和检测结束后的结束时间。 
  • 上述时间变量帮助我们计算 FPS(每秒帧数)。我们正在计算 FPS 并将其存储在fps
  • 在代码的最后部分,我们还在当前帧之上写入计算出的 FPS,以了解在使用 OpenCV DNN 模块运行 MobileNet SSD 模型时我们可以期望什么样的速度。
  • 最后,我们在屏幕上可视化每一帧并将它们保存到磁盘。

执行上面的代码将给出以下输出。

我们在第 8 代 i7 笔记本电脑 CPU 上获得了大约 33 FPS。考虑到检测的数量,这一点也不差。该模型可以检测几乎所有的人、车辆和红绿灯。尽管如此,它在尝试检测手提包和背包等小物体时还是有点吃力。CPU 上的 33 FPS 是我们在准确性和较少检测到较小物体之间进行权衡的回报。

GPU 推理

我们还可以在 GPU 上运行所有的分类和检测推理。我们需要使用 GPU 从源代码编译 OpenCV DNN 模块。 

要在 GPU 上运行推理,我们需要简单地更改 C++ 和 Python 代码。

Python:
net.setPreferableBackend(cv2.dnn.DNN_BACKEND_CUDA)
net.setPreferableTarget(cv2.dnn.DNN_TARGET_CUDA)

C++:
net.setPreferableBackend(DNN_BACKEND_CUDA);
net.setPreferableTarget(DNN_TARGET_CUDA);

从磁盘加载神经网络模型后,我们应该添加上面两行代码。第一行代码确保如果 DNN 模块支持 CUDA GPU 模型,神经网络将使用 CUDA 后端。

第二行代码告诉我们所有的神经网络计算都将发生在 GPU 而不是 CPU 上。使用启用 CUDA 的 GPU,与 CPU 相比,我们应该在对象检测视频推理上获得更高的 FPS。即使是图像,推理时间也应该比 CPU 的情况低得多。

概括

我们介绍了 OpenCV 的 DNN 模块,并讨论了为什么选择 DNN 模块。我们已经看到比较性能的条形图。我们还研究了 OpenCV DNN 支持的不同深度学习功能、模型和框架。

我们讨论了使用 OpenCV 的 DNN 模块进行图像分类和目标检测任务以获得实践经验。我们还看到了使用 OpenCV DNN 在视频中进行对象检测。

关键要点

  1. 神经网络和深度学习已经到了计算机可以准确理解和识别物体的阶段。有时,它们甚至在某些用例中超越人类。
  2. OpenCV DNN 模块:
    • 是模型推理的首选,尤其是在 Intel CPU 上。
    • 易于安装。
    • 它带有适合大多数用例的现成的、随时可用的模型和算法。
    • DNN 模块虽然不具备训练能力,但对边缘设备的部署支持仍然很好。

总览 977 , 今日 3