六、实时对象检测
在上一章中,我们了解了光学字符识别(OCR)技术。 我们借助 Tesseract 库和预训练的深度学习模型(EAST 模型)来识别扫描文档和照片中的文本,该模型已随 OpenCV 一起加载。 在本章中,我们将继续进行对象检测这一主题。 我们将讨论 OpenCV 以及其他库和框架提供的几种对象检测方法。
本章将涵盖以下主题:
- 训练和使用级联分类器检测对象
- 使用深度学习模型进行对象检测
技术要求
与前面的章节一样,要求读者至少安装版本 5 和 OpenCV 4.0.0 的 Qt。 具备一些有关 C++ 和 Qt 编程的基本知识也是一个基本要求。
尽管我们专注于 OpenCV 4.0.0,但在本章中还需要 OpenCV3.4.x。 您应该已经安装了多个版本的 OpenCV(4.0.0 和 3.4.5),才能与本章一起学习。 稍后我将解释原因。
由于我们将使用深度学习模型来检测对象,因此拥有深度学习知识也将有助于理解本章的内容。
使用 OpenCV 检测对象
OpenCV 中有许多方法可以进行对象检测。 这些方法可以分类如下:
- 基于颜色的算法,例如均值移位和连续自适应均值移位(CAMshift)
- 模板匹配
- 特征提取与匹配
- 人工神经网络(人工神经网络)
- 级联分类器
- 预先训练的深度学习模型
前三个是传统的对象检测方法,后三个是机器学习方法。
基于颜色的算法(例如均值偏移和 CAMshift)使用直方图和反投影图像以惊人的速度在图像中定位对象。 模板匹配方法将感兴趣的对象用作模板,并尝试通过扫描给定场景的图像来找到对象。 特征提取和匹配方法首先从感兴趣的对象和场景图像中提取所有特征,通常是边缘特征和角点特征,然后使用这些特征进行匹配以找到对象。 所有这些方法在简单和静态的场景中都能很好地工作,并且非常易于使用。 但是它们通常无法在复杂而动态的情况下正常工作。
ANN,级联分类器和深度学习方法被归类为机器学习方法。 他们都需要在使用之前训练模型。 借助 OpenCV 提供的功能,我们可以训练 ANN 模型或级联分类器模型,但目前尚无法使用 OpenCV 训练深度学习模型。 下表显示了这些方法是否可以与 OpenCV 库一起训练或使用,以及它们的表现(在查全率和准确率上)水平:
方法 | 可以由 OpenCV 训练 | 可以由 OpenCV 加载 | 效果 |
---|---|---|---|
人工神经网络 | 是 | 是 | 中 |
级联分类器 | 是 | 是 | 中 |
深度学习模型 | 没有 | 是(多种格式) | 高 |
实际上,人工神经网络和深度学习都是神经网络。 它们之间的区别在于,ANN 模型具有简单的架构,并且只有很少的隐藏层,而深度学习模型可能具有复杂的架构(例如 LSTM,RNN,CNN 等)以及大量的隐藏层 。 在上个世纪,人们使用人工神经网络是因为它们没有足够的计算能力,因此不可能训练复杂的神经网络。 现在,由于过去十年中异构计算的发展,训练复杂的神经网络成为可能。 如今,我们使用深度学习模型是因为它们比简单的 ANN 模型具有更高的表现(在召回率和准确率方面)。
在本章中,我们将重点介绍级联分类器和深度学习方法。 尽管无法使用当前版本的 OpenCV 库训练深度学习模型,但将来可能会实现。
使用级联分类器检测对象
首先,让我们看看如何使用级联分类器检测对象。 实际上,本书已经使用了级联分类器。 在第 4 章,“面部表情”中,我们使用了预训练的级联分类器来实时检测面部。 我们使用的预训练级联分类器是 OpenCV 内置级联分类器之一,可以在 OpenCV 安装的数据目录中找到:
$ ls ~/programs/opencv/share/opencv4/haarcascades/
haarcascade_eye_tree_eyeglasses.xml haarcascade_lefteye_2splits.xml
haarcascade_eye.xml haarcascade_licence_plate_rus_16stages.xml
haarcascade_frontalcatface_extended.xml haarcascade_lowerbody.xml
haarcascade_frontalcatface.xml haarcascade_profileface.xml
haarcascade_frontalface_alt2.xml haarcascade_righteye_2splits.xml
haarcascade_frontalface_alt_tree.xml haarcascade_russian_plate_number.xml
haarcascade_frontalface_alt.xml haarcascade_smile.xml
haarcascade_frontalface_default.xml haarcascade_upperbody.xml
haarcascade_fullbody.xml
如您所见,haarcascade_frontalface_default.xml
文件是我们在第 4 章,面对人脸中使用的文件。
在本章中,我们将尝试自己训练级联分类器。 在此之前,我们将首先构建一个应用来测试级联分类器。 我将这个应用称为 Detective。
该应用与我们在第 3 章,“家庭安全应用”(Gazer 应用)和第 4 章中内置的 FacesFaces(Facetious)应用非常相似 ,因此我们将通过应对其中一种应用来快速构建它。
您还记得我们在第 4 章,“人脸上的乐趣”开头所做的事情吗? 我们从第 3 章,“家庭安全应用”复制了 Gazer 应用,然后将其简化为一个基本应用,可以使用该应用播放网络摄像头中的视频供稿并拍照。 我们可以在这个页面上找到该提交的基本应用。 让我们将其复制到终端中:
$ pwd
/home/kdr2/Work/Books/Qt-5-and-OpenCV-4-Computer-Vision-Projects
$ git checkout 744d445
Note: checking out '744d445'.
You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by performing another checkout.
If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -b with the checkout command again. Example:
git checkout -b <new-branch-name>
HEAD is now at 744d445 Facetious: take photos
$ mkdir Chapter-06
# !!! you should copy it to a different dir
$ cp -r Chapter-04/Facetious Chapter-06/Detective
$ ls Chapter-06
Detective
$git checkout master
$ cd Chapter-06/Detective/
在我们的代码存储库中,我们签出到744d445
提交,为此章创建一个新目录,然后将该版本的 Facetious 项目的源树复制到Chapter-06
目录下一个名为Detective
的新目录中。然后,我们切换回master
分支。
在编写本书时,我已将基本应用复制到Chapter-06/Detective
目录,因此在您阅读本书时该目录已经存在。 如果您按照说明进行编码,则可以将基本应用复制到另一个新目录并在该目录下工作。
获得基本应用后,我们对其进行一些小的更改:
- 将
Facetious.pro
项目文件重命名为Detective.pro
。 - 在以下文件中将单词 Facetious 更改为 Detective:
Detective.pro
main.cpp
mainwindow.cpp
utilities.cpp
好的,现在我们有一个基本的侦探应用。 此阶段中的所有更改都可以在这个页面的提交中找到。
接下来的事情是使用预训练的级联分类器检测某种对象。 这次,我们将使用 OpenCV 库中包含的haarcascade_frontalcatface_extended.xml
文件来检测猫的脸。
首先,我们打开capture_thread.h
文件以添加一些行:
class CaptureThread : public QThread
{
// ...
private:
// ...
void detectObjects(cv::Mat &frame);
private:
// ...
// object detection
cv::CascadeClassifier *classifier;
};
然后,在capture_thread.cpp
文件中,我们实现detectObjects
方法,如下所示:
void CaptureThread::detectObjects(cv::Mat &frame)
{
vector<cv::Rect> objects;
classifier->detectMultiScale(frame, objects, 1.3, 5);
cv::Scalar color = cv::Scalar(0, 0, 255); // red
// draw the circumscribe rectangles
for(size_t i = 0; i < objects.size(); i++) {
cv::rectangle(frame, objects[i], color, 2);
}
}
在这种方法中,我们通过调用级联分类器的detectMultiScale
方法检测对象,然后在图像上绘制检测到的矩形,就像在第 4 章,“人脸上的乐趣”中所做的一样。
接下来,我们在run
方法中实例化级联分类器,并在视频捕获无限循环中调用detectObjects
方法:
void CaptureThread::run() {
// ...
classifier = new cv::CascadeClassifier(OPENCV_DATA_DIR \
"haarcascades/haarcascade_frontalcatface_extended.xml");
// ...
while(running) {
// ...
detectObjects(tmp_frame);
// ...
}
// ...
delete classifier;
classifier = nullptr;
running = false;
}
如您所见,在无限循环结束后,我们还将销毁级联分类器。
我们更新Detective.pro
项目文件,将opencv_objdetect
模块添加到链接选项,然后定义OPENCV_DATA_DIR
宏:
# ...
unix: !mac {
INCLUDEPATH += /home/kdr2/programs/opencv/include/opencv4
LIBS += -L/home/kdr2/programs/opencv/lib -lopencv_core -lopencv_imgproc -lopencv_imgcodecs -lopencv_video -lopencv_videoio -lopencv_objdetect
}
# ...
DEFINES += OPENCV_DATA_DIR=\\\"/home/kdr2/programs/opencv/share/opencv4/\\\"
# ...
现在我们编译并运行该应用,打开相机,然后将一只猫放到相机的视线中:
也许你没有猫。 不用担心 cv::VideoCapture
类提供了许多其他测试应用的方法。
您可以找到猫的视频并将其放置在本地磁盘上,然后将其路径传递到cv::VideoCapture
的结构,而不传递摄像机 ID(例如cv::VideoCapture cap("/home/kdr2/Videos/cats.mp4")
)。 构造器还接受视频流的 URI,例如http://some.site.com/some-video.mp4
或rtsp://user:password@192.168.1.100:554/camera/0?channel=1
。
如果您有很多猫的图片,那也可以。 cv::VideoCapture
类的构造器还接受由字符串表示的图像序列。 例如,如果将"image_%02d.jpg"
字符串传递给构造器,则cv::VideoCapture
实例将读取名称类似于image_00.jpg
,image_01.jpg
和image_02.jpg
等的图像,然后逐一显示为视频帧。
好的,我们已经设置了 Detective 应用,以使用预训练的层叠分类器检测对象。 在下一节中,我们将尝试自己训练级联分类器。 当获得自训练的级联分类器文件时,我们可以将其路径传递给cv::CascadeClassifier
类的构造器以加载它,并更改cv::VideoCapture
的输入以对其进行测试。
训练级联分类器
OpenCV 提供了一些工具来训练级联分类器,但它们已从 4.0.0 版本中删除。 如我们所提到的,这种删除主要是由于深度学习方法的兴起。 深度学习方法成为现代方法,而其他方法(包括级联分类器)则成为了传统。 但是,世界上仍在使用许多级联分类器,在许多情况下,它们仍然是一个不错的选择。 这些工具可能有一天会重新添加。 您可以在这个页面上找到并参与有关此主题的讨论。
幸运的是,我们可以使用 OpenCV v3.4.x,它提供了这些工具来训练级联分类器。 由 v3.4 训练的结果级联分类器文件与 v4.0.x 兼容。 换句话说,我们可以使用 OpenCV v3.4.x 训练级联分类器,并将其与 OpenCV v4.0.x 一起使用。
首先,我们应该安装 OpenCVv3.4.x。 我们可以使用系统包管理器来安装它,例如yum
或apt-get
。 我们也可以从这里下载它并从源代码构建它。 如果决定从源代码构建它,请记住将-D BUILD_opencv_apps=yes
选项传递给cmake
命令。 否则,将无法构建训练工具。 安装后,我们可以在其二进制目录下找到许多可执行文件:
$ ls ~/programs/opencv-3.4.5/bin/
opencv_annotation opencv_createsamples
opencv_interactive-calibration opencv_traincascade
opencv_version opencv_visualisation
setup_vars_opencv3.sh
我们将用来训练级联分类器的工具是opencv_createsamples
,opencv_traincascade
,有时是opencv_annotation
。
opencv_createsamples
和opencv_annotation
工具用于创建样本,opencv_traincascade
工具用于使用创建的样本训练级联分类器。
在训练级联分类器之前,我们必须准备两种样本:正样本和负样本。 正样本应包含我们要检测的对象,而负样本应包含除我们要检测的对象以外的所有内容。 可以通过 OpenCV 提供的工具opencv_createsamples
生成正样本。 没有任何工具可以生成负样本,因为负样本可以是任何不包含我们要检测的对象的任意图像。
我们如何准备或产生阳性样本? 让我们看一些例子。
禁止进入的交通标志
在此示例中,我们将训练级联分类器,该分类器将用于检测交通标志,即禁止进入标志:
首先要做的是准备负样本-背景图像。 如前所述,负样本可以是不包含感兴趣对象的任意图像,因此我们可以轻松地为此目的收集一些图像。 收集这些图像后,我们将其路径放在文本文件中。 在该文件中,路径以先列后行格式一个路径,并且该路径可以是绝对路径或相对路径。
在本章中,我们将交替使用短语负面样本图像和背景图像,因为它们在上下文中是相同的。
您可以从这里下载许多交通照片,并选择其中一些不包含任何禁止进入的照片用作背景图像的标志:
我们将它们放在名为background
的文件夹中,并将它们的相对路径保存到名为bg.txt
的文件中:
$ ls background/
traffic-sign-bg-0.png traffic-sign-bg-1.png traffic-sign-bg-2.png traffic-sign-bg-3.png
$ ls background/* > bg.txt
$ cat bg.txt
background/traffic-sign-bg-0.png
background/traffic-sign-bg-1.png
background/traffic-sign-bg-2.png
background/traffic-sign-bg-3.png
这些图像可以具有不同的大小。 但是它们都不应该小于训练窗口的大小。 通常,训练窗口的大小是我们感兴趣的对象的平均大小,即禁止进入标志的图像。 这是因为将从这些背景图像中获取具有训练窗口大小作为其维度的负样本。 如果背景图像小于示例图像,则无法执行此操作。
好,负片图像已准备就绪。 让我们继续进行阳性样本的制备。
如前所述,我们将使用opencv_createsamples
工具生成正样本。 正样本将用于训练过程中,以告诉级联分类器感兴趣的对象实际是什么样。
为了创建阳性样本,我们将感兴趣的对象(非进入符号)保存为名为no-entry.png
的文件,位于background
文件夹和bg.txt
文件所在的目录中。 然后我们按以下方式调用opencv_createsamples
工具:
opencv_createsamples -vec samples.vec -img no-entry.png -bg bg.txt \
-num 200 -bgcolor 0 -bgthresh 20 -maxidev 30 \
-maxxangle 0.3 -maxyangle 0.3 -maxzangle 0.3 \
-w 32 -h 32
如您所见,在运行该工具时,我们提供了许多参数,这似乎很可怕。 但是不用担心。 我们将一一解释:
-vec
参数用于指定我们要创建的正样本文件。 在本例中,我们使用samples.vec
作为文件名。-img
参数用于指定我们要检测的对象的图像。 该工具将使用它来生成阳性样本。 正如我们所提到的,在我们的例子中是no-entry.png
。-bg
参数用于指定背景图像的描述文件。 我们的是bg.txt
,其中包含四个选定背景的相对路径。-num
参数是要生成的正样本数。-bgcolor
自变量用于指定感兴趣对象的图像的背景色。 背景色表示透明色,在生成样本时将被视为透明色。 我们在这里使用的感兴趣图像的背景是黑色,因此在这里使用零。 在某些情况下,例如,当图像上出现压缩伪像时,给定图像的背景色将具有多种颜色,而不是单一的颜色值。 为了应对这种情况,还有一个名为-bgthresh
的参数指定背景的颜色容忍度。 如果指定此参数,则颜色在bgcolor - bgthresh
和bgcolor + bgthresh
之间的像素将被解释为透明的。- 正如我们提到的,
-bgthresh
参数指定bgcolor
的阈值。 -maxidev
自变量用于设置生成样本时前景像素值的最大强度偏差。 值 30 表示前景像素的强度可以在其原始值 +30 和其原始值 -30 之间变化。-maxxangle
,-maxyangle
和-maxzangle
自变量对应于创建新样本时,x
,y
和z
方向所允许的最大可能旋转。 这些值以弧度为单位。 在这里,我们使用 0.3、0.3 和 0.3,因为交通标志通常不会在照片中剧烈旋转。-w
和-h
自变量定义了样本的宽度和高度。 我们都使用了 32,因为我们要寻找的对象是训练一个适合正方形的分类器。 这些相同的值将在以后训练分类器时使用。 另外,请注意,这将是稍后您训练有素的分类器中可检测到的最小大小。
命令返回后,将生成示例文件。 我们可以使用相同的工具来查看样本:
opencv_createsamples -vec samples.vec -show
如果运行此命令,将出现一个32 x 32
大小的窗口,其中带有单个样本图像。 您可以按N
查看下一个,或按Esc
键退出。 这些阳性样本在我的计算机上如下所示:
好了,阳性样本已经准备好了。 让我们训练级联分类器:
mkdir -p classifier
opencv_traincascade -data classifier -numStages 10 -featureType HAAR \
-vec samples.vec -bg bg.txt \
-numPos 200 -numNeg 200 -h 32 -w 32
我们首先为输出文件创建一个新目录,然后使用许多参数调用opencv_traincascade
工具:
-data
参数指定输出目录。 训练有素的分类器和许多中间文件将放置在此目录中。 该工具不会为我们创建目录,因此我们应该像运行mkdir
命令一样,在运行命令之前自行创建目录。-vec
参数指定由opencv_createsamples
工具创建的正样本文件。-bg
参数用于指定背景描述文件,在本例中为bg.txt
文件。-numPos
参数指定在每个分类器阶段的训练过程中将使用多少阳性样本。-numNeg
参数指定在每个分类器阶段的训练中将使用多少个负样本。-numStages
参数指定将训练多少个级联阶段。-featureType
参数指定特征的类型。 它的值可以是 HAAR 或 LBP。-w
和-h
自变量指定训练过程中使用的样本的宽度和高度(以像素为单位)。 这些值必须与我们使用opencv_createsamples
工具生成的阳性样本的宽度和高度完全相同。
该命令的运行将花费几分钟到几小时。 一旦返回,我们将在用作-data
参数值的目录(即classifier
目录)中找到许多输出文件:
$ ls classifier/
cascade.xml stage10.xml stage5.xml stage9.xml
params.xml stage2.xml stage6.xml
stage0.xml stage3.xml stage7.xml
stage1.xml stage4.xml stage8.xml
让我们看看这些文件的用途:
params.xml
文件包含用于训练分类器的参数。stage<NN>.xml
文件是在每个训练阶段完成之后创建的检查点。 如果训练过程意外终止,则可以使用它们稍后重新开始训练。cascade.xml
文件是经过训练的分类器,也是由训练工具创建的最后一个文件。
让我们现在测试我们新训练的级联分类器。 打开capture_thread.cpp
文件,在run
方法中找到创建分类器的行,然后将我们新训练的分类器文件的路径传递给它:
classifier = new cv::CascadeClassifier("../no-entry/classifier/cascade.xml");
在detectObjects
方法中,当调用分类器的detectMultiScale
方法时,我们将第四个参数minNeighbors
更改为3
。
好,一切都完成了。 让我们编译并运行该应用。 打开照相机; 您将看到一个这样的窗口:
如果您不方便使用计算机上的网络摄像头捕获包含禁止进入标志的视频,则可以从互联网上搜索并下载此类视频或某些图片,然后将其传递给cv::VideoCapture
实例以执行测试。
我将训练该级联分类器所需的所有命令包装到一个外壳脚本中,并将其放入本书随附的代码存储库中的Chapter-06/no-entry
目录中。 在我的计算机上的该目录中,还有一个名为cascade.xml
的级联分类器文件。 请注意,您的训练结果可能与我的完全不同。 如果在同一环境中重新运行训练,我们甚至会得到不同的结果。 您可以摆弄对象图像,背景图像和训练参数,以自己找到可接受的输出。
在本小节中,我们训练交通标志的分类器,并使用交通标志的特定图像来生成正样本。 这种生成样本的方法非常适用于稳定的对象,例如固定的徽标或固定的交通标志。 但是,一旦给它一些刚度不高的物体(例如人或动物的脸),我们就会发现它是失败的。 在这种情况下,我们应该使用另一种方法来生成阳性样本。 在这种替代方法中,我们应该收集许多真实的对象图像,并使用opencv_annotation
工具对其进行标注。 然后,我们可以使用opencv_createsamples
工具从带标注的图像中创建正样本。 我们将在下一部分中尝试这种方法。
波士顿公牛队的人脸
在本小节中,我们将训练一个级联分类器,用于一个不太刚性的对象:狗脸。
我们将使用这个页面中的数据集。 该数据集包含 20,580 张狗的图像,分为 120 个类别,每个类别都是一个犬种。 让我们下载并解压缩图像的压缩包:
$ curl -O http://vision.stanford.edu/aditya86/ImageNetDogs/images.tar
$ tar xvf images.tar
# output omitted
$ ls Images/
n02085620-Chihuahua n02091635-otterhound
n02097298-Scotch_terrier n02104365-schipperke
n02109525-Saint_Bernard n02085782-Japanese_spaniel
# output truncated
我们将把波士顿公牛犬种的人脸作为目标。 Images/n02096585-Boston_bull
目录中有 182 张波士顿公牛的图像。 与固定物体(例如交通标志)不同,我们找不到波士顿公牛队脸部的标准图片。 我们应该在刚刚选择的 182 张图像上标注狗的脸。 标注是使用 OpenCV v3.4.x 提供的opencv_annotation
工具完成的:
rm positive -fr
cp -r Images/n02096585-Boston_bull positive
opencv_annotation --annotations=info.txt --images=positive
我们将包含波士顿牛市图像的目录复制到新的positive
目录中,以明显方便地将它们用作正图像。 然后,我们使用两个参数调用opencv_annotation
工具:
--annotations
参数指定标注的输出文件。--images
参数指定一个文件夹,其中包含我们要标注的图像。
调用opencv_annotation
工具将打开一个窗口,显示需要标注的图像。 我们可以使用鼠标和键盘在图像上做标注:
- 左键单击鼠标以标记标注的起点。
- 移动鼠标。 您将看到一个矩形; 通过移动鼠标来调整此矩形以适合狗的脸。
- 当您得到适当的矩形时,停止移动鼠标,然后再次单击鼠标左键。 您将获得一个固定的红色矩形。
- 现在,您可以按键盘上的
D
键删除矩形,或按C
键确认矩形。 如果确认为矩形,它将变为绿色。 - 您可以重复这些步骤以在图像上标记多个矩形。
- 完成当前图像的标注后,请按键盘上的
N
键以转到下一张图像。 - 您可以按
Esc
退出该工具。
这是我标注狗脸时该工具的屏幕截图:
我们应该在 182 张图像中仔细标记所有的狗脸。 这将是一个繁琐的过程,因此我在代码存储库的Chapter-06/boston-bull
目录中提供了标注过程的结果文件info.txt
文件。 该文件的数据格式非常简单:
positive/n02096585_10380.jpg 1 7 4 342 326
positive/n02096585_11731.jpg 1 158 218 93 83
positive/n02096585_11776.jpg 2 47 196 104 120 377 76 93 98
positive/n02096585_1179.jpg 1 259 26 170 165
positive/n02096585_12825.jpg 0
positive/n02096585_11808.jpg 1 301 93 142 174
上面的列表是从info.txt
文件中选取的一些行。 我们可以看到此文件的每一行都是单个图像的信息,并且该信息以PATH NUMBER_OF_RECT RECT0.x RECT0.y RECT0.width RECT0.height RECT1.x RECT1.y RECT1.width RECT1.height ...
格式组织。
借助此标注信息文件,我们可以创建正样本:
opencv_createsamples -info info.txt -vec samples.vec -w 32 -h 32
如您所见,它比上次使用opencv_createsamples
工具要简单。 我们不需要为其提供背景图像,感兴趣对象的图像以及使对象变形的最大角度。 只给它注解数据作为-info
参数就足够了。
调用返回后,我们在samples.vec
文件中获得了正样本。 同样,我们可以使用opencv_createsamples
工具进行查看:
opencv_createsamples -vec samples.vec -show
您可以通过按键盘上的N
在提示窗口中一一查看所有样本。 这些样本如下所示:
现在可以准备好阳性样本了,该准备背景图像了。 Briard 品种的狗与 Boston Bulls 有很大的不同,因此我决定将这些图像用作背景图像:
rm negative -fr
cp -r Images/n02105251-briard negative
ls negative/* >bg.txt
我们将目录Images/n02105251-briard
复制到negative
目录,并将该目录下所有图像的相对路径保存到bg.txt
文件。 bg.txt
文件只是我们的背景描述文件:
negative/n02105251_1201.jpg
negative/n02105251_1240.jpg
negative/n02105251_12.jpg
negative/n02105251_1382.jpg
negative/n02105251_1588.jpg
...
正样本和背景图像都已准备就绪,因此让我们训练分类器:
mkdir -p classifier
opencv_traincascade -data classifier -vec samples.vec -bg bg.txt \
-numPos 180 -numNeg 180 -h 32 -w 32
此步骤与我们训练分类器的禁止进入交通标志的步骤非常相似。 值得注意的是,我们在这里使用-numPos 180
,因为在samples.vec
文件中只有 183 个阳性样本。
训练过程完成后,我们将在classifier
目录下获得训练后的分类器,作为cascade.xml
文件。 让我们现在尝试这个新训练的分类器。
首先,我们以CaptureThread::run()
方法加载它:
classifier = new cv::CascadeClassifier("../boston-bull/classifier/cascade.xml");
然后,在CaptureThread::detectObjects
方法中将detectMultiScale
调用的minNeighbors
参数更改为5
:
int minNeighbors = 5; // 3 for no-entry-sign; 5-for others.
classifier->detectMultiScale(frame, objects, 1.3, minNeighbors);
让我们编译并运行 Detective 应用,并在一些材料上测试我们的新分类器:
好,还不错,我们训练了两个级联分类器。 您可能对在训练过程中如何选择 HAAR 或 LBP 特征感到好奇,所以让我们更深入一些。
OpenCV 提供了一个名为opencv_visualisation
的工具,以帮助我们可视化训练有素的级联。 有了它,我们可以看到在每个阶段选择的级联分类器具有哪些特征:
$ mkdir -p visualisation
$ opencv_visualisation --image=./test-visualisation.png \
--model=./classifier/cascade.xml \
--data=./visualisation/
我们创建一个新目录,并使用许多参数调用opencv_visualisation
工具:
--image
参数用于指定图像的路径。 该图像应该是感兴趣的图像,具有我们在创建样本和训练分类器时使用的尺寸,即,波士顿牛头犬脸的32 x 32
图像。--model
参数是新训练模型的路径。--data
是输出目录。 它必须以斜杠(/
)结尾,并且必须预先手动创建目录。
当此命令返回时,我们将在输出目录中获得许多图像和一个视频文件:
$ ls visualisation/
model_visualization.avi stage_14.png stage_1.png stage_7.png
stage_0.png stage_15.png stage_2.png stage_8.png
stage_10.png stage_16.png stage_3.png stage_9.png
stage_11.png stage_17.png stage_4.png
stage_12.png stage_18.png stage_5.png
stage_13.png stage_19.png stage_6.png
制作了一个名为model_visualization.avi
的视频,用于每个阶段的特征可视化; 您可以播放它以查看级联分类器如何选择特征。 此外,输出目录中的每个阶段都有一个图像。 我们可以检查这些图像以查看特征选择。
我们用于训练该分类器的所有材料,以及经过处理的cascade.xml
,都位于我们代码存储库中的Chapter-06/boston-bull
目录中。 请随便摆弄它们。
使用深度学习模型检测对象
在上一节中,我们学习了如何训练和使用级联分类器来检测对象。 但是,与不断扩展的深度学习方法相比,在召回率和准确率方面都提供了较差的表现。 OpenCV 库已经开始转向深度学习方法。 在 3.x 版中,它引入了深度神经网络(DNN)模块,现在在最新版本 v4.x 中,我们可以加载多种格式的神经网络架构, 以及他们的预训练权重。 另外,正如我们提到的,在最新版本中不推荐使用用于训练级联分类器的工具。
在本节中,我们将继续进行深度学习方法,以了解如何使用 OpenCV 来检测对象是深度学习方法。 我们已经使用过这种方法。 在第 5 章,“光学字符识别”中,我们使用了预训练的 EAST 模型来检测照片上的文本区域。 这是使用 TensorFlow 框架开发和训练的深度学习模型。 除了由 TensorFlow 框架训练的 DNN 模型外,OpenCV 还支持来自许多其他框架的多种格式的模型:
- 来自 Caffe 而非 Caffe2 的
*.caffemodel
格式, - TensorFlow 的
*.pb
格式 - Torch 而非 PyTorch 的
*.t7
或*.net
格式 - Darknet 的
*.weights
格式 - DLDT 中的
*.bin
格式
如您所见,尽管 OpenCV 支持多种 DNN 模型,但是仍然有一些流行的深度学习框架不在前面的列表中;例如, PyTorch 框架,Caffe2 框架,MXNet 和 Microsoft 认知工具包(CNTK)。 幸运的是,存在一种称为开放式神经网络交换(ONNX)的格式,该格式由其社区开发和支持,并且 OpenCV 库现在可以加载此格式的模型 。
大多数流行的深度学习框架(包括我刚才提到的框架)也支持 ONNX 格式。 因此,如果您开发的 DNN 模型的框架不在 OpenCV 支持列表中,则可以将模型架构和经过训练的权重保存为 ONNX 格式。 然后它将与 OpenCV 库一起使用。
OpenCV 库本身不具备构建和训练 DNN 模型的能力,但是由于它可以加载和转发 DNN 模型,并且比其他深度学习框架具有更少的依赖关系,因此,部署 DNN 模型确实是一个很好的解决方案 。 OpenCV 团队和英特尔还创建并维护了其他一些专注于机器学习模型部署的项目,例如 DLDT 和 OpenVINO 项目。
如果我们无法使用 OpenCV 训练模型,我们如何获得可与 OpenCV 一起使用以检测对象的模型? 最简单的方法是找到一个预先训练的模型。 最受欢迎的深度学习框架都有一个model zoo
,它收集了许多使用该框架构建和预训练的模型。 您可以在互联网上搜索框架名称以及关键字model zoo
来找到它们。 这些是我发现的一些:
另外,您可以在这里找到许多开源的预训练模型。
现在,让我们回到对象检测的主题。 通常,有三种基于深度学习的对象检测器:
- 基于 R-CNN 的检测器,包括 R-CNN,Fast R-CNN 和 Faster R-CNN
- 单发检测器(SSD)
- 只看一次(YOLO)
在基于区域特征的 CNN(R-CNN)中,我们首先需要使用一种算法,提出可能包含对象的候选边界框,然后将这些候选框发送到卷积神经网络(CNN)模型进行分类。 因此,这种检测器也称为两级检测器。 这种方法的问题是它非常慢并且不是端到端的深度学习对象检测器(因为我们需要在进入 CNN 模型之前搜索候选框并做一些其他工作)。 尽管 R-CNN 进行了两次改进(使用 Fast R-CNN 和 Faster R-CNN),即使在 GPU 上,这些方法仍然不够快。
R-CNN 方法使用两阶段策略,而 SSD 和 YOLO 方法使用一个阶段策略。 一阶段策略将对象检测视为回归问题,获取给定的输入图像,同时学习边界框坐标和相应的类标签概率。 通常,一级检测器的精度往往不如二级检测器,但要快得多。 例如,由这里引入的单阶段策略 YOLO 的著名实现在 GPU 上具有可飙升至 45 FPS 的性能,其中两级检测器可能仅具有 5-10 FPS 的性能。
在本节中,我们将使用预训练的 YOLOv3 检测器来检测对象。 在 COCO 数据集上训练了该模型; 它可以检测数百种对象。 要在 OpenCV 中使用此模型,我们首先应为其下载一些文件:
现在,让我们将它们下载到我们的侦探应用的data
子目录中:
$ pwd
/home/kdr2/Work/Books/Qt-5-and-OpenCV-4-Computer-Vision-Projects/Chapter-06/Detective
$ mkdir data
$ cd data/
$ curl -L -O https://raw.githubusercontent.com/pjreddie/darknet/master/data/coco.names
# output omitted
$ curl -L -O https://raw.githubusercontent.com/pjreddie/darknet/master/cfg/yolov3.cfg
# output omitted
$ curl -L -O https://pjreddie.com/media/files/yolov3.weights
# output omitted
$ ls -l
total 242216
-rw-r--r-- 1 kdr2 kdr2 625 Apr 9 15:23 coco.names
-rw-r--r-- 1 kdr2 kdr2 8342 Apr 9 15:24 yolov3.cfg
-rw-r--r-- 1 kdr2 kdr2 248007048 Apr 9 15:49 yolov3.weights
准备好这三个文件后,我们可以在应用中加载模型。 首先,让我们打开capture_thread.h
头文件以添加一些方法和字段:
// ...
#include "opencv2/dnn.hpp"
// ...
class CaptureThread : public QThread
{
// ...
private:
// ...
void detectObjectsDNN(cv::Mat &frame);
private:
// ...
cv::dnn::Net net;
vector<string> objectClasses;
};
首先,我们将添加include
指令以包含opencv2/dnn.hpp
头文件,因为我们将使用 DNN 模块。 这些是方法和字段:
- 方法
detectObjectsDNN
用于使用 DNN 模型检测帧中的对象。 - 成员字段
cv::dnn::Net net
是 DNN 模型实例。 - 成员字段
vector<string> objectClasses
将保存 COCO 数据集中的对象的类名称。
让我们打开源代码capture_thread.cpp
,以查看detectObjectsDNN
方法的实现:
void CaptureThread::detectObjectsDNN(cv::Mat &frame)
{
int inputWidth = 416;
int inputHeight = 416;
if (net.empty()) {
// give the configuration and weight files for the model
string modelConfig = "data/yolov3.cfg";
string modelWeights = "data/yolov3.weights";
net = cv::dnn::readNetFromDarknet(modelConfig, modelWeights);
objectClasses.clear();
string name;
string namesFile = "data/coco.names";
ifstream ifs(namesFile.c_str());
while(getline(ifs, name)) objectClasses.push_back(name);
}
// more code here ...
}
在此方法的开头,我们为 YOLO 模型定义了输入图像的宽度和高度。 共有三个选项:320 x 320
、416 x 416
和608 x 608
。在这里,我们选择416 x 416
,所有输入图像都将调整为该尺寸。
我们检查net
字段是否为空。 如果为空,则意味着我们尚未加载模型,因此我们使用模型配置文件和权重文件的路径调用cv::dnn::readNetFromDarknet
函数以加载模型。
之后,我们通过创建ifstream
的实例来打开data/coco.names
文件。 如前所述,该文件包含 COCO 数据集中对象的所有类名称:
$ wc -l data/coco.names
80 data/coco.names
$ head data/coco.names
person
bicycle
car
motorbike
aeroplane
bus
train
truck
boat
traffic light
在 shell 命令和先前的输出中,我们可以看到总共有 80 个类名。 通过使用head
命令查看前十个名称,我们也可以大致了解这些名称。 让我们继续我们的 C++ 代码。 我们逐行读取打开的文件,然后将读取的名称(即每一行)推送到成员字段objectClasses
。 完成此操作后,objectClasses
字段将保存所有 80 个名称。
好的,模型和类名都已加载。 接下来,我们应该转换输入图像并将其传递给 DNN 模型以进行正向传播以获得输出:
cv::Mat blob;
cv::dnn::blobFromImage(
frame, blob, 1 / 255.0,
cv::Size(inputWidth, inputHeight),
cv::Scalar(0, 0, 0), true, false);
net.setInput(blob);
// forward
vector<cv::Mat> outs;
net.forward(outs, getOutputsNames(net));
转换通过调用cv::dnn::blobFromImage
方法完成。 这个调用有点复杂,所以让我们逐个分析参数:
- 第一个参数是输入图像。
- 第二个参数是输出图像。
- 第三个是每个像素值的比例因子。 我们在这里使用
1 / 255.0
,因为模型要求像素值是0
到1
范围内的浮点数。 - 第四个参数是输出图像的空间大小; 我们在这里使用
416 x 416
,以及我们定义的变量。 - 第五个参数是平均值,应该从每个图像中减去平均值,因为在训练模型时会使用该平均值。 YOLO 不执行均值减法,因此在此我们使用零。
- 下一个参数是我们是否要交换 R 和 B 通道。 这是我们必需的,因为 OpenCV 使用 BGR 格式,而 YOLO 使用 RGB 格式。
- 最后一个参数是我们是否要裁剪图像并进行中心裁剪。 在这种情况下,我们指定
false
。
关键参数是比例因子(第三个)和均值(第五个)。 在转换中,首先从输入图像的每个像素中减去平均值,然后将像素乘以比例因子,即,将输出 BLOB 的像素计算为output_pixel = (input_pixel - mean) * scale_factor
。
但是,我们如何知道应该为模型使用这两个参数的哪些值? 一些模型同时使用均值减法和像素缩放,一些模型仅使用均值减法而不使用像素缩放,而某些模型仅使用像素法缩放而不使用平均减法。 对于特定的模型,了解这些值的详细信息的唯一方法是阅读文档。
获取输入 BLOB 后,通过调用模型的setInput
方法将其传递给 DNN 模型,然后对模型执行转发。 但是我们必须知道在执行前向传递时要通过转发获得哪些层。 这是通过名为getOutputsNames
的辅助函数完成的,我们也在capture_thread.cpp
源文件中实现了该函数:
vector<string> getOutputsNames(const cv::dnn::Net& net)
{
static vector<string> names;
vector<int> outLayers = net.getUnconnectedOutLayers();
vector<string> layersNames = net.getLayerNames();
names.resize(outLayers.size());
for (size_t i = 0; i < outLayers.size(); ++i)
names[i] = layersNames[outLayers[i] - 1];
return names;
}
DNN 模型的输出层的索引可以通过getUnconnectedOutLayers
方法获得,而所有层的名称都可以通过getLayerNames
方法获得。 如果我们监视getLayerNames
方法的结果向量,则将在此 YOLO 模型中发现 254 层。 在我们的函数中,我们获得所有这 254 个名称,然后选择未连接输出层的索引所指示的名称。 实际上,此函数只是cv::dnn::Net.getUnconnectedOutLayersNames()
方法的另一个版本。 在这里,我们使用自制版本来了解有关cv::dnn::Net
类的更多信息。
让我们回到我们的detectObjectsDNN
方法。 转发完成后,我们将在vector<cv::Mat> outs
变量中获取输出层的数据。 所有信息(包括我们检测到的盒子中的对象,以及它们的置信度和类索引)都在此矩阵向量中。 我们在capture_thread.cpp
源文件中编写了另一个辅助函数,以对向量进行解码以获得所需的所有信息:
void decodeOutLayers(
cv::Mat &frame, const vector<cv::Mat> &outs,
vector<int> &outClassIds,
vector<float> &outConfidences,
vector<cv::Rect> &outBoxes
)
{
float confThreshold = 0.5; // confidence threshold
float nmsThreshold = 0.4; // non-maximum suppression threshold
vector<int> classIds;
vector<float> confidences;
vector<cv::Rect> boxes;
// not finished, more code here ...
}
此函数将原点框架和输出层的数据作为其内部参数,并通过其外部参数返回检测到的对象框及其类索引和置信度。 在函数主体的开头,我们定义了几个变量,例如置信度阈值和非最大抑制阈值,以及在过滤之前检测到的所有对象的框信息。
然后,我们遍历输出层中的矩阵:
for (size_t i = 0; i < outs.size(); ++i) {
float* data = (float*)outs[i].data;
for (int j = 0; j < outs[i].rows; ++j, data += outs[i].cols)
{
cv::Mat scores = outs[i].row(j).colRange(5, outs[i].cols);
cv::Point classIdPoint;
double confidence;
// get the value and location of the maximum score
cv::minMaxLoc(scores, 0, &confidence, 0, &classIdPoint);
if (confidence > confThreshold)
{
int centerX = (int)(data[0] * frame.cols);
int centerY = (int)(data[1] * frame.rows);
int width = (int)(data[2] * frame.cols);
int height = (int)(data[3] * frame.rows);
int left = centerX - width / 2;
int top = centerY - height / 2;
classIds.push_back(classIdPoint.x);
confidences.push_back((float)confidence);
boxes.push_back(cv::Rect(left, top, width, height));
}
}
}
让我们以输出向量中的单个矩阵(即代码中的outs[i]
)为例。 矩阵中的每一行代表一个检测到的框。 每行包含(5 + x)
元素,其中x
是coco.names
文件中类名称的数量,即 80,如上所述。
前四个元素表示框的center_x
,center_y
,width
和height
。 第五个元素表示边界框包围对象的置信度。 其余元素是与每个类别相关的置信度。 将该框分配给与该框的最高分数相对应的类别。
换句话说,row[i + 5]
的值是该框是否包含objectClasses[i]
类的对象的置信度。 因此,我们使用cv::minMaxLoc
函数来获得最大的置信度及其位置(索引)。 然后,我们检查置信度是否大于定义的置信度阈值。 如果为true
,则将框解码为cv::Rect
,然后将框及其类索引和置信度推入boxes
,classIds
和confidences
定义的变量。
下一步是将解码后的框和置信度传递给非最大抑制,以减少重叠框的数量。 未消除的框的索引将存储在cv::dnn::NMSBoxes
函数的最后一个参数中,即indices
变量:
// non maximum suppression
vector<int> indices;
cv::dnn::NMSBoxes(boxes, confidences, confThreshold, nmsThreshold, indices);
for (size_t i = 0; i < indices.size(); ++i) {
int idx = indices[i];
outClassIds.push_back(classIds[idx]);
outBoxes.push_back(boxes[idx]);
outConfidences.push_back(confidences[idx]);
}
最后,我们迭代保留的索引,并将相应的框及其类索引和置信度推入外部参数。
decodeOutLayers
函数完成后,让我们再次回到detectObjectsDNN
方法。 通过调用新实现的decodeOutLayers
函数,我们可以获得检测到的对象的所有信息。 现在让我们在原点框架上绘制它们:
for(size_t i = 0; i < outClassIds.size(); i ++) {
cv::rectangle(frame, outBoxes[i], cv::Scalar(0, 0, 255));
// get the label for the class name and its confidence
string label = objectClasses[outClassIds[i]];
label += cv::format(":%.2f", outConfidences[i]);
// display the label at the top of the bounding box
int baseLine;
cv::Size labelSize = cv::getTextSize(label,
cv::FONT_HERSHEY_SIMPLEX, 0.5, 1, &baseLine);
int left = outBoxes[i].x, top = outBoxes[i].y;
top = max(top, labelSize.height);
cv::putText(frame, label, cv::Point(left, top),
cv::FONT_HERSHEY_SIMPLEX, 0.5, cv::Scalar(255,255,255));
}
使用前面的代码,我们绘制了检测到的对象的边界框。 然后在每个框的左上角绘制一个字符串,其中包含类名和相应检测到的对象的置信度。
至此,用 YOLO 检测物体的工作完成了。 但是,在编译和运行应用之前,还有几件事要做。
首先,在CaptureThread::run()
方法中,将调用更改为detectObjects
方法,该方法使用级联分类器来检测对我们新增方法detectObjectsDNN
的调用的对象:
// detectObjects(tmp_frame);
detectObjectsDNN(tmp_frame);
其次,我们将opencv_dnn
模块添加到Detective.pro
项目文件中LIBS
配置的末尾:
unix: !mac {
INCLUDEPATH += /home/kdr2/programs/opencv/include/opencv4
LIBS += -L/home/kdr2/programs/opencv/lib -lopencv_core -lopencv_imgproc -lopencv_imgcodecs -lopencv_video -lopencv_videoio -lopencv_objdetect -lopencv_dnn
}
现在,让我们编译并运行我们的应用以对其进行测试。 这是我通过相机观察桌面时的侦探应用的屏幕截图:
这是运动场景图像的屏幕截图:
如您所见,YOLO 在对象检测方面确实做得很好。 但是仍然有一些错误的预测。 在第一个屏幕截图中,YOLO 将我的 iPad 识别为 0.67 的笔记本电脑。 在第二张屏幕截图中,它以 0.62 的置信度将足球场识别为电视监视器。 为了消除这些错误的预测,我们可以将置信度阈值设置得更高一些,例如 0.70。 我将留给您摆弄参数。
值得注意的是,YOLO 模型有其缺点。 例如,它不能始终很好地处理小对象,尤其是不能处理紧密组合在一起的对象。 因此,如果您要处理小对象或组合在一起的对象的视频或图像,则 YOLO 不是最佳选择。
到目前为止,我们已经讨论了如何找到使用许多深度学习框架构建的预训练 DNN 模型,以及如何在 OpenCV 中使用它们。 但是,如果没有针对您的案例的预训练模型,例如,检测不在COCO
数据集中的某种对象,该怎么办? 在这种情况下,您应该自己构建和训练 DNN 模型。 对于每个深度学习框架,都有一个有关如何在 MNIST 或 CIFAR-10/100 数据集上构建和训练模型的教程。 请遵循这些教程。 您将学习如何为您的用例训练 DNN 模型。 还有一个名为 Keras 的框架,该框架提供了用于构建,训练和运行 DNN 模型的高级 API。 它使用 TensorFlow,CNTK 和 Theano 作为其基础框架。 对于初学者来说,使用 Keras 提供的友好 API 也是一个不错的选择。 本书侧重于 OpenCV,而这些知识已超出其范围,因此我将把学习训练 DNN 模型的任务交给您。
关于实时
当我们处理视频时,无论是视频文件还是来自摄像机的实时视频源,我们都知道视频的帧频通常约为 24-30 FPS。 这意味着我们有 33-40 毫秒来处理每个帧。 如果我们花费的时间更多,则会从实时视频源中丢失一些帧,或者从视频文件中获得较慢的播放速度。
现在,让我们向应用中添加一些代码,以测量检测对象时每帧花费的时间。 首先,在Detective.pro
项目文件中,添加一个新的宏定义:
DEFINES += TIME_MEASURE=1
我们将使用此宏来打开或关闭时间测量代码。 如果要关闭时间测量,只需将这一行注释掉,然后通过运行make clean && make
命令重建应用。
然后,在CaptureThread::run
方法的capture_thread.cpp
文件中,我们在detectObjects
或detectObjectsDNN
方法的调用前后添加一些行:
#ifdef TIME_MEASURE
int64 t0 = cv::getTickCount();
#endif
detectObjects(tmp_frame);
// detectObjectsDNN(tmp_frame);
#ifdef TIME_MEASURE
int64 t1 = cv::getTickCount();
double t = (t1 - t0) * 1000 /cv::getTickFrequency();
qDebug() << "Detecting time on a single frame: " << t <<"ms";
#endif
在前面的代码中,cv::getTickCount
函数返回从头开始的时钟周期数(系统启动的时间)。 我们在检测之前和之后两次调用它。 之后,使用t1 - t0
表达式获取检测对象时的时钟周期。 由于cv::getTickFrequency()
函数返回一秒内有多少个时钟周期,因此我们可以通过(t1 - t0) * 1000 /cv::getTickFrequency()
将经过的时钟周期数转换为毫秒。 最后,我们通过qDebug()
输出与时间使用相关的消息。
如果使用级联分类器方法编译并运行“侦探”应用,则会看到类似以下的消息:
Detecting time on a single frame: 72.5715 ms
Detecting time on a single frame: 71.7724 ms
Detecting time on a single frame: 73.8066 ms
Detecting time on a single frame: 71.7509 ms
Detecting time on a single frame: 70.5172 ms
Detecting time on a single frame: 70.5597 ms
如您所见,我在每个帧上花费了 70 毫秒以上。 该值应大于(33-40)。 就我而言,这主要是因为我的计算机中装有旧的 CPU,大约是十年前购买的,并且没有降低输入帧的分辨率。 为了优化这一点,我们可以使用功能更强大的 CPU,并将输入帧的大小调整为更小,更合适的大小。
在使用前面的代码来衡量 YOLO 方法使用的时间之前,让我们向CaptureThread::detectObjectsDNN
方法中添加一些代码行:
// ...
net.forward(outs, getOutputsNames(net));
#ifdef TIME_MEASURE
vector<double> layersTimes;
double freq = cv::getTickFrequency() / 1000;
double t = net.getPerfProfile(layersTimes) / freq;
qDebug() << "YOLO: Inference time on a single frame: " << t <<"ms";
#endif
在 DNN 模型上执行转发后,我们调用其getPerfPerofile
方法来获取在前向传递中花费的时间。 然后我们将其转换为毫秒并打印。 通过这段代码,我们将获得两次:一次是调用detectObjectsDNN
方法所花费的总时间;另一次是调用detectObjectsDNN
方法所花费的总时间。 另一个是推理时间,即在前向传递上花费的时间。 如果从第一个中减去第二个,我们将得到花在 BLOB 准备和结果解码上的时间。
让我们在run
方法中切换到 YOLO 方法并运行该应用:
YOLO: Inference time on a single frame: 2197.44 ms
Detecting time on a single frame: 2209.63 ms
YOLO: Inference time on a single frame: 2203.69 ms
Detecting time on a single frame: 2217.69 ms
YOLO: Inference time on a single frame: 2303.73 ms
Detecting time on a single frame: 2316.1 ms
YOLO: Inference time on a single frame: 2203.01 ms
Detecting time on a single frame: 2215.23 ms
哦,不,这太慢了。 但是我们说 YOLO 性能飙升至 45 FPS,这意味着它在每个帧上仅花费 22 毫秒。 为什么我们的结果慢 100 倍? 性能可能会飙升至 45 FPS,但这是在 GPU 而非 CPU 上测得的。 深度神经网络需要大规模的计算,这不适合在 CPU 上运行,但很适合在 GPU 上运行。 当前,将计算放到 GPU 上最成熟的解决方案是 CUDA 和 OpenCL,而 OpenCV 库的 DNN 模块目前仅支持 OpenCL 方法。
cv::dnn::Net
类有两种方法来设置其后端和目标设备:
setPreferableBackend()
setPreferableTarget()
如果您有 GPU,并且已正确安装 OpenCL 和 GPU 驱动程序,则可以使用-DWITH_OPENCL=ON
标志构建 OpenCV 以启用 OpenCL 支持。 之后,您可以使用net.setPreferableTarget(cv::dnn::DNN_TARGET_OPENCL)
使用 GPU 进行计算。 这将带来性能上的巨大改进。
总结
在本章中,我们创建了一个名为 Detective 的新应用,以使用不同的方法来检测对象。 首先,我们使用 OpenCV 内置的层叠分类器来检测猫的脸。 然后,我们学习了如何自己训练级联分类器。 我们训练了用于刚性物体(禁止进入的交通标志)的级联分类器和用于不太刚性物体(波士顿公牛队的脸)的级联分类器,然后在我们的应用中对此进行了测试。
我们转向了深度学习方法。 我们讨论了深度学习技术的不断扩展,介绍了许多框架,并了解了 DNN 模型可以使用两阶段检测器和一阶段检测器检测对象的不同方式。 我们结合了 OpenCV 库的 DNN 模块和预训练的 YOLOv3 模型来检测应用中的对象。
最后,我们简要讨论了实时性和检测器的性能。 我们了解了如何将计算移至 GPU,以实现性能的大幅提高。
在本章中,我们检测了多种对象。 在下一章中,我们将讨论如何借助计算机视觉技术来测量它们之间的距离。
问题
尝试这些问题以测试您对本章的了解:
- 当我们为波士顿公牛队的脸训练级联分类器时,我们自己在每个图像上标注了狗脸。 标注过程花费了我们很多时间。 在以下网站上有该数据集的注解数据包。 我们可以通过一段代码从此标注数据生成
info.txt
文件吗? 我们该怎么做? - 尝试找到预训练的(快速/快速)R-CNN 模型和预训练的 SSD 模型。 运行它们,并将其性能与 YOLOv3 进行比较。
- 我们可以使用 YOLOv3 来检测某种对象,但不能检测所有 80 类对象吗?