OpenCV-精要-全-
OpenCV 精要(全)
原文:
annas-archive.org/md5/30ed4628ecbccfa4ab1dd1f1ecb4e3da译者:飞龙
前言
OpenCV,可能是最广泛使用的计算机视觉库,包含了数百个现成的图像和视觉函数,并在学术界和工业界得到广泛应用。随着相机价格的降低和成像需求的增长,利用 OpenCV 的应用范围显著增加,尤其是在移动平台上。
作为计算机视觉库,OpenCV 提供以下两个主要优势:
-
它是开源的,任何人都可以免费使用,无论是学术层面还是实际项目
-
它可能包含了最全面和最新的计算机视觉函数集合
OpenCV 结合了计算机视觉、图像和视频处理以及机器学习的尖端研究成果。
第一本关于 OpenCV 的书籍主要采用理论方法,解释了底层计算机视觉技术。随后的书籍则采取了相反的方法,用大量例子(几乎是完整的应用程序)填满了页面,这些例子难以理解,且难以在读者的项目中重用。占用几页的例子对于一本书来说并不合适。我们认为,例子应该易于理解,并且应该用作构建块,以减少读者项目需要的工作示例的时间。因此,在这本书中,我们也采用了一种实用方法,尽管我们的目标是使用更短、更易于理解的例子来覆盖更广泛的函数范围。根据我们对 OpenCV 的经验,我们可以肯定地说,例子最终是最有价值的资源。
本书涵盖的内容
第一章, 入门,处理了基本的安装步骤,并介绍了 OpenCV API 的基本概念。还提供了读取/写入图像和视频以及从相机捕获它们的第一个示例。
第二章, 我们关注的对象 – 图形用户界面,涵盖了基于 OpenCV 的应用程序的用户界面功能。
第三章, 首要之事 – 图像处理,涵盖了 OpenCV 中最实用的图像处理技术。
第四章, 图像中有什么?分割,探讨了 OpenCV 中至关重要的图像分割问题。
第五章, 关注有趣的二维特征,涵盖了从图像中提取关键点和描述符的功能。
第六章, 沃利在哪里?目标检测,描述了目标检测是计算机视觉中的一个核心问题。本章解释了可用于目标检测的功能。
第七章, 他正在做什么?运动,不仅考虑了单个静态图像。本章讨论了 OpenCV 中的运动和跟踪。
第八章, 高级主题,专注于一些高级主题,例如机器学习和基于 GPU 的加速。
您需要这本书什么
本书采用的方法特别适合那些已经在计算机视觉(或可以在其他地方学习该学科)方面有所了解的读者,并且希望快速开始开发应用程序。每一章都提供了视觉系统最重要阶段的关键可用函数的几个示例。因此,本书的重点是尽快为读者提供一个工作示例,以便他们可以在其基础上开发更多功能。
要使用这本书,只需要免费软件。所有示例都是使用免费可用的 Qt IDE 开发和测试的。第八章(part0066_split_000.html#page "第八章. 高级主题")高级主题中的 GPU 加速示例需要免费可用的 CUDA 工具包。
这本书面向谁
本书既不是 C++教程,也不是计算机视觉教科书。本书旨在为希望学习如何实现 OpenCV 主要技术并快速入门的 C++开发者编写。预期读者之前有计算机视觉/图像处理方面的接触。
惯例
在这本书中,您将找到多种文本样式,用于区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。
文本中的代码单词、文件夹名称、文件名、文件扩展名、路径名、系统变量、URL 和用户输入如下所示:"每个模块都有一个相关的头文件(例如,core.hpp)。"
代码块以如下方式设置:
#include "opencv2/core/core.hpp"
#include "opencv2/highgui/highgui.hpp"
using namespace std;
using namespace cv;
int main(int argc, char *argv[])
{
Mat frame; // Container for each frame
当我们希望您注意代码块中的特定部分时,相关的行或项目将以粗体显示:
#include "opencv2/core/core.hpp"
#include "opencv2/highgui/highgui.hpp"
#include <iostream>
using namespace std;
using namespace cv;
int main(int argc, char *argv[])
{
任何命令行输入或输出都应如下编写:
C:\opencv-buildQt\install
新术语和重要词汇以粗体显示。您在屏幕上看到的单词,例如在菜单或对话框中,在文本中如下所示:"此外,标记为分组和高级的复选框应在 CMake 主窗口中勾选。"
注意
警告或重要提示以如下框中的形式出现。
小贴士
小贴士和技巧看起来像这样。
读者反馈
我们欢迎读者的反馈。请告诉我们您对这本书的看法——您喜欢什么或可能不喜欢什么。读者反馈对我们开发您真正从中受益的标题非常重要。
要发送一般反馈,只需发送电子邮件到 <feedback@packtpub.com>,并在您的邮件主题中提及书籍标题。
如果您在某个主题上具有专业知识,并且您对撰写或为书籍做出贡献感兴趣,请参阅我们的作者指南 www.packtpub.com/authors。
客户支持
现在您是 Packt 书籍的骄傲拥有者,我们有几个可以帮助您从购买中获得最大收益的方法。
下载示例代码
您可以从 www.packtpub.com 下载您购买的所有 Packt 书籍的示例代码文件。如果您在其他地方购买了这本书,您可以访问 www.packtpub.com/support 并注册,以便将文件直接通过电子邮件发送给您。
下载本书的颜色图像
我们还为您提供了一个包含本书中使用的截图/图表的颜色图像的 PDF 文件。颜色图像将帮助您更好地理解输出的变化。您可以从:www.packtpub.com/sites/default/files/downloads/4244OS_Graphics.pdf 下载此文件。
勘误
尽管我们已经尽最大努力确保内容的准确性,但错误仍然可能发生。如果您在我们的书中发现错误——可能是文本或代码中的错误——如果您能向我们报告这一点,我们将不胜感激。通过这样做,您可以避免其他读者的挫败感,并帮助我们改进本书的后续版本。如果您发现任何勘误,请通过访问 www.packtpub.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入您的勘误详情来报告。一旦您的勘误得到验证,您的提交将被接受,勘误将被上传到我们的网站,或添加到该标题的勘误部分下的现有勘误列表中。您可以通过从 www.packtpub.com/support 选择您的标题来查看任何现有勘误。
盗版
互联网上版权材料的盗版是一个跨所有媒体的持续问题。在 Packt,我们非常重视我们版权和许可证的保护。如果您在互联网上发现我们作品的任何非法副本,无论形式如何,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。
请通过 <copyright@packtpub.com> 联系我们,并提供涉嫌盗版材料的链接。
我们感谢您在保护我们作者和我们为您提供有价值内容的能力方面的帮助。
问题
如果您在本书的任何方面遇到问题,可以通过 <questions@packtpub.com> 联系我们,我们将尽力解决。
第一章. 入门
本章介绍了使用 OpenCV 库开发应用程序所需的基本安装步骤和设置。同时,它还介绍了使用库提供的应用程序编程接口(API)和基本数据类型所必需的基本概念。本章包括一个包含完整代码示例的部分,展示了如何读取/写入图像和视频文件,以及如何从实时摄像头访问图像。这些示例还展示了如何获取连接到计算机的摄像头的实时输入。
设置 OpenCV
OpenCV 可以从 opencv.org/ 下载,适用于最流行的操作系统,如 Unix(Linux/Mac)、Microsoft Windows(Windows)、Android 和 iOS。在本书中,使用了适用于 Windows 7(SP1)的 OpenCV 的最后一个稳定版本(2.4.9)。对于 Windows,这个版本以自解压存档(opencv-2.4.9.exe)的形式提供,应该解压到所需的位置(例如,C:\opencv-src 下的 OPENCV_SCR)。需要注意的是,在 Windows 中,强烈建议将源代码和二进制文件分配到没有空格的绝对路径,因为以后可能会出现错误。
解压存档后,获取的文件在 OPENCV_SCR 下的两个子目录中组织:build 和 sources。第一个(build)包括预编译(二进制)版本,使用 Microsoft Visual C++ 编译器(MSVC,版本 10、11 和 12)为 32 位和 64 位架构编译(分别位于 x86 和 x64 子目录中)。sources 子目录包含 OpenCV 库的源代码。此代码可能使用其他编译器(例如,GNU g++)编译。
小贴士
使用预编译版本的 OpenCV 是最简单的方法,只需在 Path 环境变量中设置 OpenCV 的动态库二进制文件(DLL 文件)的位置。例如,在我们的设置中,这个位置可能是 OPENCV_SCR/build/x86/vc12/bin,其中包含用 MS VC 版本 12 编译的 32 位架构的二进制文件。请记住,在 Windows 7(SP1)中更改环境变量可以在 我的电脑 的 属性 下的 高级系统设置 中完成。快速环境编辑器 工具(可在 www.rapidee.com 获取)提供了在 Windows 7 中方便地更改 Path 和其他环境变量的方法。
本章详细介绍了在 Windows 7(SP1)上安装 OpenCV 的过程。对于 Linux 和其他操作系统,您可以查看 OpenCV 在线文档(OpenCV 教程,OpenCV 简介 部分),该文档可在 docs.opencv.org/doc/tutorials/tutorials.html 获取。
编译库与预编译库
OpenCV 的发行版包括库的源代码,当需要编译不同版本的二进制文件时可以编译。这种情况之一是我们需要使用 OpenCV 中可用的基于 Qt 的用户界面函数(这些函数不包括在预编译版本中)。此外,如果我们的编译器(例如,GNU g++)与库的预编译版本不匹配,则需要为 OpenCV 库进行构建过程(编译)。
为了使用 Qt 编译 OpenCV 必须满足以下要求:
-
兼容的 C++ 编译器:我们使用 MinGW(Minimal GNU GCC for Windows)中包含的 GNU g++ 编译器。这是一个在 Unix 上的标准编译器,它适合保证代码兼容性。在构建过程之前,将编译器二进制文件(g++ 和 gmake)的位置添加到 Path 环境变量中非常方便(例如,在我们的本地系统中,位置是
C:\Qt\Qt5.2.1\Tools\mingw48_32\bin)。 -
Qt 库:特别是,Qt 5.2.1 包(可在
qt-project.org/获取)是为了简化设置而定制的,因为它包括了 Qt 库和完整的开发 IDE Qt Creator,以及 MinGW 4.8 和 OpenGL。Qt Creator 是一个功能齐全的 IDE,拥有免费软件许可,我们推荐使用。Qt 二进制文件的位置也必须添加到 Path 环境变量中(例如,C:\Qt\Qt5.2.1\5.2.1\mingw48_32\bin)。 -
CMake 构建系统:这个跨平台构建系统可在
www.cmake.org/获取。它由一组工具组成,帮助用户准备和生成用于构建(编译)、测试和打包大型代码项目(如 OpenCV)的适当配置文件。
使用 CMake 配置 OpenCV
在本节中,我们通过截图展示了使用 CMake 配置 OpenCV 的步骤:
-
第一步涉及选择目录和编译器。一旦启动 CMake,就可以在 CMake 主窗口的适当文本字段中设置源目录(
OPENCV_SCR)和构建目录(OPENCV_BUILD)。此外,在 CMake 主窗口中标记为 Grouped 和 Advanced 的复选框应该被选中。我们继续点击 Configure 按钮。此时,工具提示用户指定所需的编译器,我们使用本地编译器选择 MinGW Makefiles。如果我们选择 指定本地编译器 选项,可以指定编译器和构建工具的特定位置。点击 Finish 按钮后,配置步骤将继续检查系统的设置。以下截图显示了此预配置过程结束时的 CMake 窗口:![使用 CMake 配置 OpenCV]()
预配置步骤结束时的 CMake
注意
为了简化,我们在本文中使用
OPENCV_BUILD和OPENCV_SCR分别表示 OpenCV 本地设置的目标和源目录。请记住,所有目录都应该与当前的本地配置相匹配。 -
下一步是选择构建选项。在主 CMake 窗口的中心,如果需要,红色条目可能会更改。在我们的设置中,我们打开带有WITH标签的条目组,并在那里将WITH_QT条目设置为ON,然后我们再次单击配置以获取一组新的选项。
-
现在,下一步是设置 Qt 目录。在主 CMake 窗口中,有一些条目被标记为红色。这些是构建带有 Qt 的 OpenCV 所需的目录。接下来要设置的条目是:
Qt5Concurrent_DIR、Qt5Core_DIR、Qt5Gui_DIR、Qt5OpenGL_DIR、Qt5Test_DIR和Qt5Widgets_DIR(参见图示)。在我们的设置中,这些目录可以在C:/Qt/Qt5.2.1/5.2.1/mingw48_32/lib/cmake下找到。通过单击一次配置按钮,我们获得没有进一步的红色条目,配置过程最终完成,如图所示:
![使用 CMake 配置 OpenCV]()
为 CMake 设置 Qt 目录
-
最后一步是生成项目。在这一步中,我们点击生成按钮以获取在目标平台上构建 OpenCV 所需的合适项目文件。然后,应该关闭 CMake GUI 以继续编译。
在上述过程中,可以在生成步骤之前多次更改配置选项。以下列出了要设置的一些其他方便的选项:
-
BUILD_EXAMPLES:此选项用于编译分发中包含的几个示例的源代码
-
BUILD_SHARED_LIBS:取消选中此选项以获取库的静态版本
-
CMAKE_BUILD_TYPE:将其设置为调试以获取用于调试目的的版本等
-
WITH_TBB:将此选项设置为激活使用 Intel® Threading Building Block,这使您能够轻松编写并行 C++代码
-
WITH_CUDA:将此选项设置为使用 CUDA 库通过 GPU 进行处理
构建和安装库
编译应该从配置过程中设置的 CMake 目标目录(OPENCV_BUILD)的终端启动(即前述列表中的第 1 步)。命令应如下所示:
OPENCV_BUILD>mingw32-make
此命令使用 CMake 生成的文件启动构建过程。编译通常需要几分钟。如果编译没有错误结束,则安装将继续执行以下命令:
OPENCV_BUILD>mingw32-make install
此命令将 OpenCV 的二进制文件复制到以下目录:
C:\opencv-buildQt\install
如果在编译过程中出现问题,我们应该返回 CMake 来更改之前步骤中选择的选项。通过将库二进制文件(DLL 文件)的位置添加到 Path 环境变量来结束安装。在我们的设置中,此目录位于 OPENCV_BUILD\install\x64\mingw\bin。
要检查安装过程是否成功,可以运行与库一起编译的一些示例(如果设置了 CMake 中的 BUILD_EXAMPLES 选项)。代码示例可在 OPENCV_BUILD\install\x64\mingw\samples\cpp 中找到。

Canny 边缘检测示例
上一张截图显示了示例 cpp-example-edge.exe 文件的输出窗口,该文件演示了在包含在源 OpenCV 分发的 fruits.jpg 输入文件上的 Canny 边缘检测。
在下一节中,我们总结了在 Windows 7-x32 平台上使用 Qt 5.2.1(MinGW 4.8)设置 OpenCV 2.4.9 的配方。
设置 OpenCV 的快速配方
设置 OpenCV 的整个过程可以使用以下步骤完成:
-
下载并安装 Qt5(可在
qt-project.org/找到)。 -
将 MinGW 二进制目录(用于 g++ 和 gmake)添加到 Path 环境变量中(例如,
C:\Qt\Qt5.2.1\Tools\mingw48_32\bin\)。 -
将 Qt 二进制目录(用于 DLL)添加到 Path 环境变量中(例如,
C:\Qt\Qt5.2.1\5.2.1\mingw48_32\bin\)。 -
下载并安装 CMake(可在
www.cmake.org/找到)。 -
下载 OpenCV 存档(可在
opencv.org/找到)。 -
将下载的存档解压到
OPENCV_SRC目录。 -
使用以下步骤使用 CMake 配置 OpenCV 构建项目:
-
选择源目录(
OPENCV_SCR)和目标目录(OPENCV_BUILD)。 -
标记 分组 和 高级 复选框,然后点击 配置。
-
选择一个编译器。
-
设置 BUILD_EXAMPLES 和 WITH_QT 选项,并最终点击 配置 按钮。
-
设置以下 Qt 目录:
Qt5Concurrent_DIR、Qt5Core_DIR、Qt5Gui_DIR、Qt5OpenGL_DIR、Qt5Test_DIR、Qt5Widgets_DIR。然后,再次点击配置。 -
如果没有报告错误(在 CMake 窗口中用红色标记),则可以点击 生成 按钮。如果有错误报告,应纠正错误的选项,并重复 配置 步骤。在 生成 步骤后关闭 CMake。
-
-
在
OPENCV_BUILD目录下打开控制台,并运行mingw32-make命令以开始编译。 -
如果构建过程没有产生错误,请在命令行上运行
mingw32-make install。 -
将 OpenCV 二进制目录(用于 DLL)添加到 Path 环境变量中(例如,
OPENCV_BUILD\install\x64\mingw\bin\)。
要检查 OpenCV 库的正确安装,可以在 OPENCV_BUILD\install\x64\mingw\samples\cpp 中运行一些包含的示例。
API 概念和基本数据类型
安装后,准备一个新的 OpenCV 代码项目是一个相当直接的过程,需要包含头文件并指示编译器查找项目中使用的文件和库。
OpenCV 由几个模块组成,将相关功能分组。每个模块都有一个与之关联的头文件(例如,core.hpp),位于与模块同名的目录中(即,OPENCV_BUILD\install\include\opencv2\<module>)。当前 OpenCV 版本提供的模块如下:
-
core:此模块定义了所有其他模块使用的(基本)函数和基本数据结构,包括密集的多维数组Mat。 -
highgui:此模块提供简单的用户界面(UI)功能和视频和图像捕获的简单接口。使用 Qt 选项构建库允许与这些框架的 UI 兼容性。 -
imgproc:此模块包括图像处理函数,包括滤波(线性和非线性)、几何变换、颜色空间转换等。 -
features2d:此模块包括用于特征检测(角点和平面对象)、特征描述、特征匹配等功能。 -
objdetect:此模块包括用于对象检测和预定义检测类实例(例如,面部、眼睛、微笑、人物、汽车等)的函数。 -
video:此模块提供视频分析功能(运动估计、背景提取和对象跟踪)。 -
gpu:此模块为其他 OpenCV 模块中的某些函数提供了一组 GPU 加速算法。 -
ml:此模块包括实现机器学习工具的函数,如统计分类、回归和数据聚类。 -
一些其他不太常见的模块,如相机标定、聚类、计算摄影、图像拼接、OpenCL 加速 CV、超分辨率等。
所有 OpenCV 类和函数都在cv命名空间中。因此,在我们的源代码中将有以下两种选择:
-
在包含头文件后添加
using namespace cv声明(这是本书中所有代码示例使用的选项)。 -
将
cv::指定符作为前缀添加到我们使用的所有 OpenCV 类、函数和数据结构之前。如果 OpenCV 提供的外部名称与标准模板库(STL)或其他库冲突,则建议使用此选项。
DataType类定义了 OpenCV 的原始数据类型。原始数据类型可以是bool、unsigned char、signed char、unsigned short、signed short、int、float、double,或者这些原始类型值的一个元组。任何原始类型都可以通过以下形式的标识符定义:
CV_<bit depth>{U|S|F}C(<number of channels>)
在前面的代码中,U、S 和 F 分别代表无符号、有符号和浮点。对于单通道数组,以下枚举应用于描述数据类型:
enum {CV_8U=0, CV_8S=1, CV_16U=2, CV_16S=3, CV_32S=4, CV_32F=5, CV_64F=6};
下图展示了单个通道(4 x 4)数组(8 位无符号整数 CV_8U)的图形表示。在这种情况下,每个元素应该有一个从零到 255 的值,这可以由一个灰度图像表示。

灰度图像的 8 位无符号整数单通道数组
我们可以为多通道数组(最多 512 个通道)定义所有上述数据类型。以下图示说明了三个通道 4 x 4 数组(8 位无符号整数 CV_8UC3)的图形表示。在这个例子中,数组由三个元素的元组组成,对应于 RGB 图像。

RGB 图像的 8 位无符号整数三通道数组
注意
这里需要注意的是,以下三个声明是等价的:CV_8U、CV_8UC1 和 CV_8UC(1)。
OpenCV 的 Mat 类用于存储和操作密集的 n 维单通道或多通道数组。它可以存储实值或复值向量矩阵、彩色或灰度图像、直方图、点云等。创建 Mat 对象的方法有很多种,最流行的是构造函数,其中指定了数组的大小和数据类型,如下所示:
Mat(nrows, ncols, type[, fillValue])
数组的初始值可能由 Scalar 类设置为一个典型的四元素向量(用于存储在数组中的图像的 RGB 和透明度组件)。接下来,我们将展示 Mat 的一些使用示例:
Mat img_A(640, 480, CV_8U, Scalar(255)); // white image
// 640 x 480 single-channel array with 8 bits of unsigned integers
// (up to 255 values, valid for a grayscale image, for example,
// 255=white)
…
Mat img_B(Size(800, 600), CV_8UC3, Scalar(0,255,0)); // Green image
// 800 x 600 three channel array with 8 bits of unsigned integers
// (up to 24 bits color depth, valid for a RGB color image)
注意
注意,OpenCV 按照 BGR 顺序将彩色 RGB 图像分配到三通道数组(以及第四个通道用于透明度,即 alpha 通道),其中较高的值对应于亮度更高的像素。
Mat 类是存储和操作图像的主要数据结构。OpenCV 实现了为这些数据结构自动分配和释放内存的机制。然而,程序员在数据结构共享相同的缓冲区内存时仍需特别小心。
OpenCV 中的许多函数处理密集的单通道或多通道数组时通常使用 Mat 类。然而,在某些情况下,不同的数据类型可能更方便,例如 std::vector<>、Matx<>、Vec<> 或 Scalar。为此,OpenCV 提供了代理类 InputArray 和 OutputArray,允许使用任何前面的类型作为函数的参数。
我们的第一个程序——读取和写入图像和视频
为了准备本书的示例,我们使用了包含在 Qt 5.2 捆绑包中的 Qt Creator IDE 和用 MinGW g++ 4.8 和 Qt 功能编译的 OpenCV 2.4.9。Qt Creator 是一个免费的多平台 IDE,具有针对 C++编程非常有用的功能。然而,用户可以选择构建可执行文件的最佳工具链,以满足其需求。
我们第一个使用 OpenCV 的 Qt Creator 项目将是一个非常简单的翻转图像工具,命名为flipImage。这个工具读取彩色图像文件,将其转换为灰度图像,翻转并保存到输出文件中。
对于这个应用程序,我们选择通过导航到文件 | 新建文件或文件 | 项目…来创建一个新的代码项目,然后导航到非 Qt 项目 | 纯 C++项目。然后,我们必须选择项目名称和位置。下一步是选择项目的工具链(即编译器)(在我们的情况下,桌面 Qt 5.2.1 MinGW 32 位)和生成的二进制文件的位置。通常,使用两种可能的构建配置(配置文件):debug和release。这些配置文件设置了适当的标志来构建和运行二进制文件。
当创建 Qt Creator 项目时,会生成两个特殊文件(具有.pro和.pro.user扩展名),用于配置构建和运行过程。构建过程由创建项目时选择的工具链确定。使用桌面 Qt 5.2.1 MinGW 32 位工具链,此过程依赖于qmake和mingw32-make工具。使用.pro文件作为输入,qmake为 Make(即mingw32-make)生成makefiles,驱动每个配置文件(即release和debug)的构建过程。
qmake 项目文件
对于我们的flipImage示例项目,flipImage.pro文件看起来像以下代码:
TARGET: flipImage
TEMPLATE = app
CONFIG += console
CONFIG -= app_bundle
CONFIG -= qt
SOURCES += \
flipImage.cpp
INCLUDEPATH += C:\\opencv-buildQt\\install\\include
LIBS += -LC:\\opencv-buildQt\\install\\x64\mingw\\lib \
-lopencv_core249.dll \
-lopencv_highgui249.dll
前面的文件说明了qmake需要哪些选项来生成适当的 makefiles,以便为我们的项目构建二进制文件。每一行都以一个标签开头,表示一个选项(TARGET、CONFIG、SOURCES、INCLUDEPATH和LIBS),后面跟着一个标记来添加(+=)或删除(-=)该选项的值。在这个示例项目中,我们处理的是非 Qt 控制台应用程序。可执行文件是flipImage.exe(TARGET),源文件是flipImage.cpp(SOURCES)。由于这个项目是 OpenCV 应用程序,最后两个标签指出了该特定项目使用的头文件位置(INCLUDEPATH)和 OpenCV 库(LIBS)(例如,core和highgui)。请注意,行尾的反斜杠表示下一行的延续。在 Windows 中,路径反斜杠应该重复,如前例所示。
以下代码显示了flipImage项目的源代码:
#include "opencv2/core/core.hpp"
#include "opencv2/highgui/highgui.hpp"
#include <iostream>
using namespace std;
using namespace cv;
int main(int argc, char *argv[])
{
int flip_code=0;
Mat out_image; // Output image
if (argc != 4) {//Check args. number
cout << "Usage: <cmd> <flip_code> <file_in> <file_out>\n";
return -1;
}
Mat in_image = imread(argv[2], CV_LOAD_IMAGE_GRAYSCALE);
if (in_image.empty()) { // Check if read
cout << "Error! Input image cannot be read...\n";
return -1;
}
sscanf(argv[1], "%d", &flip_code); // Read flip code
flip(in_image, out_image, flip_code);
imwrite(argv[3], out_image); // Write image to file
namedWindow("Flipped…"); // Creates a window
imshow(win, out_image); // Shows output image on window
cout << "Press any key to exit...\n";
waitKey(); // Wait infinitely for key press
return 0;
}
构建项目后,我们可以从以下命令行运行flipImage应用程序:
CV_SAMPLES/flipImage_build/debug>flipImage.exe -1 lena.jpg lena_f.jpg
以下截图显示了翻转后两个轴(水平和垂直)的输出图像窗口:

输入图像(左侧)和翻转图像工具应用后的输出图像(右侧)
源代码以包含与应用程序使用的模块相关的头文件(core.hpp和highgui.hpp)开始。请注意,也可以只包含opencv.hpp头文件,因为它将依次包含 OpenCV 的所有头文件。
flipImage示例获取翻转代码和两个文件名(输入图像和输出图像)作为命令行参数。这些参数从argv[]变量中获取。以下示例说明了 OpenCV 应用程序中的几个基本任务:
-
从文件中读取图像到
Mat类(imread)并检查目标变量是否不为空(Mat::empty)。 -
使用代理类(例如,
InputArray(in_image) 和OutputArray(out_image))调用过程(例如,flip)。 -
将图像写入文件(
imwrite)。 -
创建一个输出窗口(
namedWindow)并在其上显示图像(imshow)。 -
等待按键(
waitKey)。
以下是对代码的解释:
-
Mat imread(const string& filename, int flags=1): 此函数从指定的文件中加载图像并返回它。如果无法读取图像,它还返回一个空矩阵。它支持文件中最常见的图像格式,通过其内容而不是扩展名来检测。flags参数指示加载到内存中的图像颜色,这可能与文件中存储的原始图像颜色不同。在示例代码中,此函数的使用方式如下:Mat in_image = imread(argv[2], CV_LOAD_IMAGE_GRAYSCALE);在这里,文件名是从命令行参数中获得的(命令名之后的第二个参数)。
CV_LOAD_IMAGE_GRAYSCALE标志表示图像应作为 8 位灰度图像加载到内存中。有关可用标签的描述,建议阅读 OpenCV 在线文档(可在docs.opencv.org/找到)。 -
bool imwrite(const string& filename, InputArray img, const vector<int>& params=vector<int>()): 此函数将图像写入指定的文件,其中在第二个参数之后指定了一些可选的格式参数。输出文件的格式由文件扩展名确定。在我们的示例代码中,此函数使用时没有格式参数,如下所示:imwrite(argv[3], out_image); -
void namedWindow(const string& winname, int flags=WINDOW_AUTOSIZE): 此函数创建一个不显示的窗口。第一个参数是一个用作窗口名称及其标识符的字符串。第二个参数是一个标志或标志组合,它控制一些窗口属性(例如,启用调整大小)。接下来,我们将展示如何使用常量字符串作为创建的窗口名称来使用此函数,如下所示:namedWindow("Flipped …"); // Creates a window使用 Qt 编译 OpenCV 为
highgui模块添加了一些新功能(关于这一点稍后讨论)。然后,使用 Qt 和namedWindow函数创建的窗口使用默认标志:CV_WINDOW_AUTOSIZE、CV_WINDOW_KEEPRATIO或CV_GUI_EXPANDED。 -
void imshow(const string& winname, InputArray mat): 此函数在创建窗口时使用指定标志设置的属性中显示一个数组(图像)。在示例中,此函数的使用如下:imshow(win, out_image); // Shows output image on window -
int waitKey(int delay=0): 此函数等待按键或由delay指定的毫秒数(如果delay大于零)。如果delay小于或等于零,则无限期等待。如果按下按键,则返回按键代码;如果在延迟后未按下按键,则返回-1。此函数必须在创建和激活窗口后使用。在示例代码中,它的使用如下:waitKey(); // Wait infinitely for key press
读取和播放视频文件
视频处理的是动态图像而不是静态图像,即以适当的速率显示帧序列(FPS 或 每秒帧数)。以下 showVideo 示例说明了如何使用 OpenCV 读取和播放视频文件:
//… (omitted for simplicity)
int main(int argc, char *argv[])
{
Mat frame; // Container for each frame
VideoCapture vid(argv[1]); // Open original video file
if (!vid.isOpened()) // Check whether the file was opened
return -1;
int fps = (int)vid.get(CV_CAP_PROP_FPS);
namedWindow(argv[1]); // Creates a window
while (1) {
if (!vid.read(frame)) // Check end of the video file
break;
imshow(argv[1], frame); // Show current frame on window
if (waitKey(1000/fps) >= 0)
break;
}
return 0;
}
代码解释如下:
-
VideoCapture::VideoCapture(const string& filename)– 此类构造函数提供了一个 C++ API,用于从文件和摄像头中抓取视频。构造函数可以有一个参数,即文件名或摄像头的设备索引。在我们的代码示例中,它使用从命令行参数获得的文件名如下:VideoCapture vid(argv[1]); -
double VideoCapture::get(int propId)– 此方法返回指定的VideoCapture属性。如果VideoCapture类使用的后端不支持该属性,则返回的值是0。在以下示例中,此方法用于获取视频文件的帧率:int fps = (int)vid.get(CV_CAP_PROP_FPS);由于该方法返回一个
double值,因此执行显式转换为int。 -
bool VideoCapture::read(Mat& image)– 此方法从VideoCapture对象中抓取、解码并返回一个视频帧。该帧存储在Mat变量中。如果失败(例如,当文件末尾到达时),它返回false。在代码示例中,此方法的使用如下,同时也检查文件末尾条件:if (!vid.read(frame)) // Check end of the video file break;
在前面的示例中,waitKey 函数使用计算出的毫秒数(1000/fps)尝试以与原始录制相同的速率播放视频文件。以比原始速率更快/更慢的速率(更多/更少 fps)播放视频将产生更快/更慢的播放。
来自摄像头的实时输入
通常,我们面临的计算机视觉问题与处理来自一个或多个摄像头的实时视频输入有关。在本节中,我们将描述 recLiveVid 示例,该示例从连接到我们计算机的摄像头中抓取视频流,在窗口中显示该流,并将其记录在文件中(recorded.avi)。默认情况下,在下面的示例中,视频捕获是从具有 cam_id=0 的摄像头中获取的。然而,可以处理第二个摄像头(cam_id=1)并从它那里抓取视频,通过命令行设置一个参数:
//… (omitted for brevity)
int main(int argc, char *argv[])
{
Mat frame;
const char win_name[]="Live Video...";
const char file_out[]="recorded.avi";
int cam_id=0; // Webcam connected to the USB port
double fps=20;
if (argc == 2)
sscanf(argv[1], "%d", &cam_id);
VideoCapture inVid(cam_id); // Open camera with cam_id
if (!inVid.isOpened())
return -1;
int width = (int)inVid.get(CV_CAP_PROP_FRAME_WIDTH);
int height = (int)inVid.get(CV_CAP_PROP_FRAME_HEIGHT);
VideoWriter recVid(file_out, CV_FOURCC('F','F','D','S'), fps, Size(width, height));
if (!recVid.isOpened())
return -1;
namedWindow(win_name);
while (1) {
inVid >> frame; // Read frame from camera
recVid << frame; // Write frame to video file
imshow(win_name, frame); // Show frame
if (waitKey(1000/fps) >= 0)
break;
}
inVid.release(); // Close camera
return 0;
}
代码解释如下:
-
VideoCapture::VideoCapture(int device)– 此类构造函数初始化一个VideoCapture对象,使其从摄像头而不是文件接收视频。在下面的代码示例中,它使用摄像头标识符:VideoCapture inVid(cam_id); // Open camera with cam_id -
VideoWriter::VideoWriter(const string& filename, int fourcc, double fps, Size frameSize, bool isColor=true)– 此类构造函数创建一个对象,用于将视频流写入名为第一个参数传递的文件。第二个参数使用四个字符的代码标识视频编解码器(例如,在先前的示例代码中,FFDS 代表ffdshow)。显然,只有实际安装在本地系统中的编解码器才能使用。第三个参数表示记录的每秒帧数。此属性可以通过VideoCapture::get方法从VideoCapture对象中获取,尽管如果后端不支持该属性,它可能返回0。frameSize参数表示将要写入的视频的每一帧的总大小。此大小应与抓取的输入视频相同。最后,最后一个参数允许以彩色(默认)或灰度写入帧。在示例代码中,构造函数使用ffdshow编解码器和以下视频捕获大小:int width = (int)inVid.get(CV_CAP_PROP_FRAME_WIDTH); int height = (int)inVid.get(CV_CAP_PROP_FRAME_HEIGHT); VideoWriter recVid(file_out, CV_FOURCC('F','F','D','S'), fps,Size(width, height)); -
void VideoCapture::release()– 此方法关闭捕获设备(摄像头)或视频文件。此方法在程序结束时总是隐式调用。然而,在先前的示例中,它是显式调用的,以避免输出文件错误终止(仅在播放录制的视频时才会注意到)。
摘要
本章从如何使用 Qt(使用 CMake、GNU g++ 编译器和 GNU Make)构建和安装 OpenCV 库的解释开始。然后,简要介绍了库的模块组织以及其基本 API 概念的简单解释。本章接着更详细地修订了存储数组和操作图像的基本数据结构。此外,还解释了三个代码示例,例如 flipImage、showVideo 和 recLiveVid,以说明 OpenCV 库的基本用法。下一章将介绍为 OpenCV 程序提供图形用户界面功能的两种主流选项。
第二章. 我们关注的对象 – 图形用户界面
在本章中,我们将介绍 OpenCV 库中包含的主要用户界面功能。我们将从 highgui 模块中包含的用户界面函数开始。然后,我们将处理在显示的窗口上插入对象(如文本和几何形状)以指出图像上的某些特定特征。最后,本章将介绍 OpenCV 中包含的新 Qt 函数,以丰富用户体验。
使用 OpenCV 的 highgui 模块
highgui 模块已被设计用来提供一个简单的方式来可视化结果并尝试使用 OpenCV 开发的应用的功能。正如我们在上一章中看到的,该模块提供了执行以下操作的功能:
-
通过
VideoCapture对象从文件和实时摄像头读取图像和视频(imread)。 -
通过
VideoWriter对象将图像和视频从内存写入磁盘(imwrite)。 -
创建一个可以显示图像和视频帧的窗口(
namedWindow和imshow)。 -
当按键时获取和处理事件(
waitKey)。
当然,该模块包含更多功能以增强用户与软件应用的交互。其中一些将在本章中解释。在下面的 tbContrast 代码示例中,我们可以读取一个图像文件,并创建了两个窗口:第一个显示原始图像,另一个显示在原始图像上应用了简单的缩放操作后增加或减少对比度的结果图像。下面的示例展示了如何在窗口中创建一个滑动条以轻松地更改图像的对比度因子(缩放)。让我们看看代码:
#include "opencv2/core/core.hpp"
#include "opencv2/highgui/highgui.hpp"
#include <iostream>
using namespace std;
using namespace cv;
int main(int argc, char* argv[]) {
const char in_win[]="Orig. image";
const char out_win[]="Image converted...(no saved)";
int TBvalContrast=50; // Initial value of the TrackBar
Mat out_img;
if (argc != 2) {
cout << "Usage: <cmd><input image_file>" << endl;
return -1;
}
Mat in_img = imread(argv[1]); // Open and read the image
if (in_img.empty()) {
cout << "Error!!! Image cannot be loaded..." << endl;
return -1;
}
namedWindow(in_win); // Creates window for orig. image
moveWindow(in_win, 0, 0); // Move window to pos. (0, 0)
imshow(in_win, in_img); // Shows original image
namedWindow(out_win);
createTrackbar("Contrast", out_win, &TBvalContrast, 100);
cout << "Press Esc key to exit..." << endl;
while (true) {
in_img.convertTo(out_img, -1, TBvalContrast/50.0);
imshow(out_win, out_img);
if (waitKey(50) == 27) // If Esc key pressed breaks
break;
}
return 0;
}
下面的截图显示了原始图像(fruits.jpg)和通过 tbContrast 应用程序获得的对比度增加后的相同图像。

原始图像和对比度增加后的图像
注意
为了避免在示例中重复,只解释了代码中显著的新部分。
代码解释如下:
-
void moveWindow(const string& winname, int x, int y): 此函数将窗口移动到指定的屏幕(x, y)位置,其中(0, 0)是屏幕左上角的起始点。当创建并显示窗口时,其默认位置在屏幕中心。如果只显示一个窗口,这种行为相当方便。然而,如果需要显示多个窗口,它们将重叠,并且应该移动以查看其内容。在示例中,此函数的使用如下:moveWindow(in_win,0,0);现在,显示原始图像的窗口在创建后移动到屏幕的左上角(原点),而转换后的图像位于其默认位置(屏幕中心)。
-
int createTrackbar(const string& trackbarname, const string& winname, int* value, int range, TrackbarCallbackonChange=0, void* userdata=0): 此函数创建一个与指定名称和范围关联的 滑块(滑块)。滑块的位置与value变量同步。此外,可以实现一个在滑块移动后调用的 回调 函数。在此调用中,将用户数据指针作为参数传递。在我们的代码中,此函数的使用方式如下:createTrackbar("Contrast", out_win, &TBvalContrast, 100);注意
回调函数是将一个函数作为参数传递给另一个函数的函数。回调函数作为代码指针传递,当发生预期事件时执行。
在此代码中,滑块被命名为
"Contrast",但没有与它链接的回调函数。最初,滑块位于整个范围(100)的中间(50)。此范围允许最大缩放因子为 2.0(100/50)。 -
void Mat::convertTo(OutputArray m, int rtype, double alpha=1, double beta=0 ) const: 此函数将数组转换为另一种数据类型,并可选地进行缩放。如果rtype为负,输出矩阵将与输入具有相同的类型。应用的缩放公式如下:m(x, y) = alfa(*this)(x, y) + beta,在此代码中,应用了最终的隐式转换 (
saturate_cast<>) 以避免可能的溢出。在tbContrast示例中,此函数在无限循环中使用:while (true) { in_img.convertTo(out_img, -1, TBvalContrast/50.0); imshow(out_win, out_img); if (waitKey(50) == 27) // If Esc key pressed breaks break; }
在上一章中,我们看到了使用 waitKey 函数(不带参数)创建隐式无限循环等待按键的代码示例。应用程序主窗口上的事件(例如,滑块、鼠标等)在该循环内部被捕获和处理。相反,在本例中,我们使用 while 语句创建一个无限循环,通过 convertTo 函数应用对比度变化,缩放因子从 0.0(滑块在 0 位置)到 2.0(滑块在 100 位置)。当按下 Esc 键(ASCII 码 27)时,无限循环终止。实现的对比度方法相当简单,因为像素的新值是通过将原始值乘以大于 1.0 的因子来增加对比度,以及乘以小于 1.0 的因子来降低对比度。在此方法中,当像素值超过 255(任何通道)时,必须进行舍入(饱和转换)。
注意
在下一章中,我们将解释一个更复杂的算法,用于通过图像直方图均衡化来提高图像对比度。
然后,在 tbContrastCallB 示例中,我们展示了相同的功能,但使用了一个每次滑块移动时都会调用的 trackbarcallback 函数。注意,当调用 waitKey 函数时处理事件。如果按下任何键,应用程序将结束。代码如下:
//… (omitted for brevity)
#define IN_WIN "Orig. image"
#define OUT_WIN "Image converted...(no saved)"
Mat in_img, out_img;
// CallBack function for contrast TrackBar
void updateContrast(int TBvalContrast, void *userData=0) {
in_img.convertTo(out_img, -1, TBvalContrast/50.0);
imshow(OUT_WIN, out_img);
return;
}
int main(int argc, char* argv[]) {
int TBvalContrast=50; // Value of the TrackBar
// (omitted for simplicity)
in_img = imread(argv[1]); // Open and read the image
// (omitted for simplicity)
in_img.copyTo(out_img); // Copy orig. image to final img
namedWindow(IN_WIN); // Creates window for orig. image
moveWindow(IN_WIN, 0, 0); // Move window to pos. (0, 0)
imshow(IN_WIN, in_img); // Shows original image
namedWindow(OUT_WIN); // Creates window for converted image
createTrackbar("Contrast", OUT_WIN, &TBvalContrast, 100,
updateContrast);
imshow(OUT_WIN, out_img); // Shows converted image
cout << "Press any key to exit..." << endl;
waitKey();
return 0;
}
在此示例中,将 updatedContrast 函数的 void 指针作为参数传递给 createTrackbar 函数:
createTrackbar("Contrast", OUT_WIN, &TBvalContrast, 100,
updateContrast);
回调函数接收的第一个参数是轨迹条中滑块的值以及指向其他用户数据的void指针。图像的新像素值将在该函数中计算。
注意
在这个示例(以及随后的示例)中,为了简洁,省略了一些代码,因为省略的代码与之前的示例中的代码相同。
使用回调函数导致在这段新代码中产生了一些变化,因为此函数内部可访问的数据必须使用全局作用域来定义。然后,为了避免向回调函数传递的数据类型更加复杂,如下所示:
-
窗口名称是定义的符号(例如,
#define IN_WIN)。在之前的示例(tbContrast)中,窗口名称存储在局部变量(字符串)中。 -
在这种情况下,原始图像(
in_img)和转换图像(out_img)的Mat变量被声明为全局变量。
提示
有时在这本书的示例代码中,为了简化,使用了全局变量。由于它们可以在代码的任何地方被更改,因此请务必小心使用全局变量。
在之前的示例中展示的两个不同实现产生了相同的结果。然而,需要注意的是,在使用回调函数之后,生成的应用程序(tbContrastCallB)更加高效,因为图像转换的数学运算仅在轨迹条的滑动变化(当回调被执行时)发生。在第一个版本(tbContrast)中,即使TBvalContrast变量没有变化,convertTo函数也会在while循环内部被调用。
文本和绘图
在上一节中,我们使用一个简单的用户界面通过轨迹条获取输入值。然而,在许多应用中,用户必须指向图像上的位置和区域,并用文本标签进行标记。为此目的,highgui模块提供了一套绘图函数以及鼠标事件处理。
drawThings代码示例展示了如何轻松地在输入图像上标记位置。位置用红色圆圈和旁边的黑色文本标签标记。以下截图显示了包含输入图像及其上标记位置的窗口。为了标记图像上的每个位置,用户必须在其上点击鼠标左键。在其他应用中,标记的位置可能是从应用于输入图像的算法中获得的点或区域。
接下来,我们展示一个示例代码,其中为了简化,省略了一些代码,因为它们在其他之前的示例中是重复的:
// (omitted for simplicity)
#define IN_WIN "Drawing..."
Mat img;
// CallBack Function for mouse events
void cbMouse(int event, int x, int y, int flags, void* userdata) {
static int imark=0;
char textm[] = "mark999";
if (event == EVENT_LBUTTONDOWN) { // Left mouse button pressed
circle(img, Point(x, y), 4, Scalar(0,0,255), 2);
imark++;// Increment the number of marks
sprintf(textm, "mark %d", imark);// Set the mark text
putText(img, textm, Point(x+6, y), FONT_HERSHEY_PLAIN,
1, Scalar(0,0,0),2);
imshow(IN_WIN, img); // Show final image
}
return;
}
int main(int argc, char* argv[]) {
// (omitted for brevity)
img = imread(argv[1]); //open and read the image
// (omitted for brevity)
namedWindow(IN_WIN);
setMouseCallback(IN_WIN, cbMouse, NULL);
imshow(IN_WIN, img);
cout << "Pres any key to exit..." << endl;
waitKey();
return 0;
}
代码解释如下:
-
void setMouseCallback(const string& winname, MouseCallback onMouse, void* userdata=0): 此函数为指定的窗口设置事件鼠标处理程序。在此函数中,第二个参数是每当鼠标事件发生时执行的回调函数。最后一个参数是传递给该函数的void指针数据。在我们的代码中,此函数的使用方式如下:setMouseCallback(IN_WIN, cbMouse, NULL);在这种情况下,而不是使用全局变量来表示窗口名称,更倾向于使用具有全局作用域的已定义符号(
IN_WIN)。![文本和绘图]()
带有圆和文本的图像
鼠标处理程序本身声明如下:
void cbMouse(int event, int x, int y, int flags, void* userdata)在这里,
event表示鼠标事件类型,x和y是事件在窗口中的位置坐标,而flags是事件发生时的特定条件。在这个例子中,唯一捕获的鼠标事件是左键点击(EVENT_LBUTTONDOWN)。以下枚举定义了鼠标回调函数中处理的事件和标志:
enum{ EVENT_MOUSEMOVE =0, EVENT_LBUTTONDOWN =1, EVENT_RBUTTONDOWN =2, EVENT_MBUTTONDOWN =3, EVENT_LBUTTONUP =4, EVENT_RBUTTONUP =5, EVENT_MBUTTONUP =6, EVENT_LBUTTONDBLCLK =7, EVENT_RBUTTONDBLCLK =8, EVENT_MBUTTONDBLCLK =9}; enum { EVENT_FLAG_LBUTTON =1, EVENT_FLAG_RBUTTON =2, EVENT_FLAG_MBUTTON =4, EVENT_FLAG_CTRLKEY =8, EVENT_FLAG_SHIFTKEY =16, EVENT_FLAG_ALTKEY =32}; -
void circle(Mat& img, Point center, int radius, const Scalar& color, int thickness=1, int lineType=8, int shift=0): 这个函数在图像上以指定的radius(以像素为单位)和color在由其center标记的位置绘制一个圆。此外,还可以设置线的thickness值和其他一些附加参数。该函数在示例中的使用方法如下:circle(img, Point(x, y), 4, Scalar(0,0,255), 2);圆的中心是鼠标点击的点。半径为
4像素,颜色为纯红色(Scalar(0, 0, 255)),线粗细为2像素。注意
记住,OpenCV 使用 BGR 颜色方案,
Scalar类用于表示每个像素的三个(或四个,如果考虑不透明通道)通道,亮度更高的值(或不透明度更高)。包含在
highgui模块中的其他绘图函数允许我们绘制椭圆、线条、矩形和多边形。 -
void putText(Mat& image, const string& text, Point org, int fontFace, double fontScale, Scalar color, int thickness=1, int lineType=8, bool bottomLeftOrigin=false): 这个函数在image中指定位置(org)绘制一个text字符串,其属性由参数fontFace、fontScale、color、thickness和lineType设置。可以通过最后一个参数(bottomLeftOrigin)设置坐标原点在左下角。在示例中,此函数的使用方法如下:imark++; // Increment the number of marks sprintf(textm, "mark %d", imark); // Set the mark text putText(img, textm, Point(x+6, y), FONT_HERSHEY_PLAIN, 1.0, Scalar(0,0,0),2);在
drawThings示例中,我们绘制了一个文本"mark",后面跟着一个递增的数字,指出了标记的顺序。为了存储标记顺序,我们使用了一个static变量(imark),它在调用之间保持其值。putText函数在鼠标点击的位置绘制文本,在x轴上有 6 像素的偏移。字体样式由标志FONT_HERSHEY_PLAIN指定,并且没有缩放(1.0),黑色(Scalar(0, 0, 0))和2像素的粗细。字体样式的可用标志由枚举定义:
enum{ FONT_HERSHEY_SIMPLEX = 0, FONT_HERSHEY_PLAIN = 1, FONT_HERSHEY_DUPLEX = 2, FONT_HERSHEY_COMPLEX = 3, FONT_HERSHEY_TRIPLEX = 4, FONT_HERSHEY_COMPLEX_SMALL = 5, FONT_HERSHEY_SCRIPT_SIMPLEX = 6, FONT_HERSHEY_SCRIPT_COMPLEX = 7, FONT_ITALIC = 16};
选择区域
许多计算机视觉应用需要在图像的局部区域内聚焦兴趣。在这种情况下,选择所需感兴趣区域(ROI)是一个非常有用的用户工具。在drawRs示例中,我们展示了如何使用鼠标选择图像中的矩形区域,以在这些区域内局部增加对比度(如下面的截图所示)。为了更好地控制区域选择,我们实现了一个点击并拖拽的行为来调整每个区域的矩形边界。

在某些矩形区域内对比度增加的输出图像
为了简化,只显示了对应于鼠标事件函数回调的代码,因为其余部分与前面的示例非常相似。代码如下:
void cbMouse(int event, int x, int y, int flags, void* userdata) {
static Point p1, p2; // Static vars hold values between calls
static bool p2set = false;
if (event == EVENT_LBUTTONDOWN) { // Left mouse button pressed
p1 = Point(x, y); // Set orig. point
p2set = false;
} else if (event == EVENT_MOUSEMOVE &&
flags == EVENT_FLAG_LBUTTON) {
if (x >orig_img.size().width) // Check out of bounds
x = orig_img.size().width;
else if (x < 0)
x = 0;
if (y >orig_img.size().height) // Check out of bounds
y = orig_img.size().height;
else if (y < 0)
y = 0;
p2 = Point(x, y); // Set final point
p2set = true;
orig_img.copyTo(tmp_img); // Copy orig. to temp. image
rectangle(tmp_img, p1, p2, Scalar(0, 0, 255));
imshow(IN_WIN, tmp_img); // Draw temporal image with rect.
} else if (event == EVENT_LBUTTONUP && p2set) {
Mat submat = orig_img(Rect(p1, p2)); // Set region
submat.convertTo(submat, -1, 2.0); // Compute contrast
rectangle(orig_img, p1, p2, Scalar(0, 0, 255));
imshow(IN_WIN, orig_img); // Show image
}
return;
}
回调函数声明其局部变量为static,因此它们在调用之间保持其值。变量p1和p2存储定义感兴趣矩形区域的点,而p2set保存一个布尔(bool)值,表示点p2是否已设置。当p2set为true时,可以绘制一个新的选定区域并计算其新值。
鼠标回调函数处理以下事件:
-
EVENT_LBUTTONDOWN:此按钮也称为左键按下。初始位置(p1)被设置为事件发生的位置Point(x, y)。此外,将p2set变量设置为false。 -
EVENT_MOUSEMOVE && EVENT_FLAG_LBUTTON:按住左键移动鼠标。首先,应该检查边界,以便我们可以纠正坐标并避免错误,以防最终点超出窗口。然后,临时p2点被设置为鼠标的最终位置,并将p2set设置为true。最后,在窗口中显示一个带有矩形的时间图像。 -
EVENT_LBUTTONUP:此按钮也称为左键释放,并且仅在p2set为true时有效。最终区域被选定。然后可以在原始图像中指向子数组进行进一步计算。之后,在原始图像中绘制围绕最终区域的矩形,并将结果显示在应用程序窗口中。
接下来,我们更仔细地查看代码:
-
Size Mat::size() const:返回矩阵大小(Size(cols, rows)):此函数用于获取图像(orig_img)的边界,如下所示:if (x > orig_img.size().width) // Check out bounds x = orig_img.size().width; else if (x < 0) x = 0; if (y > orig_img.size().height) // Check out bounds y = orig_img.size().height;由于
Mat::size()返回一个Size对象,我们可以访问其成员width和height以获取图像(orig_img)中x和y的最大值,并将这些值与鼠标事件发生的坐标进行比较。 -
void Mat::copyTo(OutputArray m) const:此方法将矩阵复制到另一个矩阵中,如果需要则重新分配新的大小和类型。在复制之前,以下方法被调用:m.create(this->size(), this->type());在示例中,采用以下方法来创建原始图像的时间副本:
orig_img.copyTo(tmp_img); // Copy orig. to temp. image定义选定区域的矩形被绘制在这个临时图像上。
-
void rectangle(Mat& img, Point pt1, Point pt2, const Scalar& color, int thickness=1, int lineType=8, int shift=0): 此函数使用指定的color、thickness和lineType在图像 (img) 上绘制由点pt1和pt2定义的矩形。在代码示例中,此函数被调用了两次。首先,在临时图像 (tmp_img) 中绘制一个红色(Scalar(0, 0, 255))的矩形,围绕选定的区域,然后绘制原始图像 (orig_img) 中最终选定区域的边界:rectangle(tmp_img, p1, p2, Scalar(0, 0 ,255)); //… rectangle(orig_img, p1, p2, Scalar(0, 0, 255)); -
Mat::Mat(const Mat& m, const Rect& roi): 构造函数接受由矩形 (roi) 限定的m的子矩阵,该矩形代表存储在m中的图像中的感兴趣区域。在代码示例中,此构造函数用于获取需要转换对比度的矩形区域:Mat submat = orig_img(Rect(p1, p2));// Set subarray on orig. image
使用基于 Qt 的函数
虽然 highgui 对于大多数用途来说已经足够,但 Qt UI 框架(可在 qt-project.org/ 找到)可以用于 OpenCV 中开发更丰富的用户界面。OpenCV 的许多用户界面函数在幕后使用 Qt 库。为了使用这些函数,OpenCV 必须使用 WITH_QT 选项编译。
注意,Qt 是一个类和 小部件 库,它允许创建具有丰富、事件驱动用户界面的完整应用程序。然而,在本节中,我们将主要关注 OpenCV 中的特定 Qt 基于函数。使用 Qt 进行编程超出了本书的范围。
在启用 Qt 支持的情况下,使用 namedWindow 函数创建的窗口将自动看起来像以下截图所示。有一个带有平移、缩放和保存图像等有用功能的工具栏。窗口还显示底部状态栏,显示当前鼠标位置和该像素下的 RGB 值。在图像上右键单击将显示一个弹出菜单,其中包含与工具栏相同的选项。

启用 Qt 支持显示的窗口
文本叠加和状态栏
文本可以显示在图像顶部的行上。这非常有用,可以显示每秒帧数、检测数量、文件名等。主要函数是 displayOverlay(const string& winname, const string& text, int delayms=0)。该函数期望一个窗口标识符和要显示的文本。通过在文本字符串中使用 \n 字符允许多行。文本将在中心显示,并且具有固定大小。delayms 参数允许文本仅显示指定数量的毫秒(0=永远)。
我们还可以在状态栏中显示用户文本。此文本将替换当前像素下的默认 x 和 y 坐标以及 RGB 值。displayStatusBar(const string& winname, const string& text, int delayms=0) 函数与之前的 displayOverlay 函数具有相同的参数。当延迟时间过去后,将显示默认的状态栏文本。
属性对话框
OpenCV 基于 Qt 的功能中最有用的特性之一是属性对话框窗口。此窗口可以用来放置滑块条和按钮。同样,这在调整我们应用程序的参数时非常有用。可以通过按工具栏中的最后一个按钮(如图所示)或按Ctrl + P来访问属性对话框窗口。窗口只有在分配了滑块条或按钮后才会可用。要为属性对话框创建滑块条,只需使用createTrackbar函数,并将空字符串(不是NULL)作为窗口名称传递。
也可以将按钮添加到属性对话框中。由于原始窗口和对话框窗口可以同时可见,这可以用来激活/停用我们应用程序中的功能并立即看到结果。要向对话框添加按钮,请使用createButton(const string& button_name, ButtonCallback on_change, void* userdata=NULL,inttype=CV_PUSH_BUTTON, bool initial_button_state=0)函数。第一个参数是按钮标签(即按钮中要显示的文本)。每次按钮改变其状态时,on_change回调函数都会被调用。这应该采用void on_change(int state, void *userdata)的形式。传递给createButton的用户数据指针将在每次调用时传递给此回调函数。状态参数表示按钮变化,并且对于每种类型的按钮,它将具有由参数类型给出的不同值:
-
CV_PUSH_BUTTON: 推压按钮 -
CV_CHECKBOX: 复选框按钮;状态将为 1 或 0 -
CV_RADIOBOX: 单选框按钮;状态将为 1 或 0
对于前两种类型,每次按下都会调用一次回调。对于单选框按钮,它既会调用刚刚点击的按钮,也会调用未取消的按钮。
按钮被组织到按钮栏中。按钮栏占据对话框窗口中的一行。每个新的按钮都添加到上一个按钮的右侧。滑块条占据整整一行,因此当添加滑块条时,按钮栏会结束。以下propertyDlgButtons示例显示了按钮和滑块条在属性对话框中的布局:
#include "opencv2/core/core.hpp"
#include "opencv2/highgui/highgui.hpp"
#include <iostream>
using namespace std;
using namespace cv;
Mat image;
const char win[]="Flip image";
void on_flipV(int state, void *p)
{
flip(image, image, 0); // flip vertical
imshow(win, image);
}
void on_flipH(int state, void *p)
{
flip(image, image, 1); // flip horizontal
imshow(win, image);
}
void on_negative(int state, void *p)
{
bitwise_not(image, image); // invert all channels
imshow(win, image);
}
int main(int argc, char *argv[])
{
if (argc != 2) {//Check args.
cout << "Usage: <cmd><file_in>\n";
return -1;
}
image = imread(argv[1]);
if (image.empty()) {
cout << "Error! Input image cannot be read...\n";
return -1;
}
namedWindow(win);
imshow(win, image);
displayOverlay(win, argv[1], 0);
createButton("Flip Vertical", on_flipV, NULL, CV_PUSH_BUTTON);
createButton("Flip Horizontal", on_flipH, NULL, CV_PUSH_BUTTON);
int v=0;
createTrackbar("trackbar1", "", &v, 255);
createButton("Negative", on_negative, NULL, CV_CHECKBOX);
cout << "Press any key to exit...\n";
waitKey();
return 0;
}
此代码与上一章中的flipImage示例类似。在此示例中,将图像文件名作为参数传递。创建一个属性窗口,包含两个按钮用于垂直和水平翻转,一个模拟的滑块条,以及一个复选框按钮用于反转颜色强度。回调函数on_flipV和on_flipH简单地翻转当前图像并显示结果(我们使用一个全局图像变量来完成此操作),而回调函数on_negative逻辑上反转颜色强度并显示结果。请注意,滑块条实际上并没有真正被使用;它被用来显示换行符效果。以下截图显示了结果:

propertyDlgButtons示例
窗口属性
如前所述,默认情况下,所有新窗口将类似于使用基于 Qt 的函数部分中的截图所示。然而,我们可以通过将CV_GUI_NORMAL选项传递给namedWindow来以非 Qt 格式显示窗口。另一方面,可以使用double getWindowProperty(const string& winname, int prop_id)和setWindowProperty(const string& winname, int prop_id, double prop_value)函数检索和设置窗口大小参数。以下表格显示了可以更改的属性:
属性(prop_id) |
描述 | 可能的值 |
|---|---|---|
CV_WND_PROP_FULLSCREEN |
显示全屏或常规窗口 | CV_WINDOW_NORMAL或CV_WINDOW_FULLSCREEN |
CV_WND_PROP_AUTOSIZE |
窗口自动调整大小以适应显示的图像 | CV_WINDOW_NORMAL或CV_WINDOW_AUTOSIZE |
CV_WND_PROP_ASPECTRATIO |
允许调整大小的窗口具有任何比例或固定原始比例 | CV_WINDOW_FREERATIO或CV_WINDOW_KEEPRATIO |
更重要的是,可以保存窗口属性。这包括不仅大小和位置,还包括标志、滑块值、缩放和滚动位置。要保存和加载窗口属性,请使用saveWindowParameters(const string& windowName)和loadWindowParameters(const string& windowName)函数。
Qt 图像
如果我们想在项目中广泛使用 Qt 库(即,超出 OpenCV 的基于 Qt 的函数),我们必须找到一种方法将 OpenCV 的图像转换为 Qt 使用的格式(QImage)。这可以通过以下函数完成:
QImage* Mat2Qt(const Mat &image)
{
Mat temp=image.clone();
cvtColor(image, temp, CV_BGR2RGB);
QImage *imgQt= new QImage((const unsigned char*)(temp.data),temp.cols,temp.rows,QImage::Format_RGB888);
return imgQt;
}
此函数使用 OpenCV 的图像数据创建 Qt 图像。请注意,首先需要进行转换,因为 Qt 使用 RGB 图像,而 OpenCV 使用 BGR 顺序。
最后,为了使用 Qt 显示图像,我们至少有两种选择:
-
创建一个扩展
QWidget类并实现绘制事件的类。 -
创建一个标签并设置它来绘制一个图像(使用
setPixMap方法)。
摘要
在本章中,我们提供了对highgui模块功能的更深入的了解,以丰富用户体验。OpenCV 用于构建图形用户界面的主要元素在以下代码示例中展示。此外,我们还回顾了 OpenCV 内部的新 Qt 功能。
本章的示例涵盖了诸如tbarContrast、tbarContrastCallB、drawThings、drawRs和propertyDlgButtons等主题。
下一章将介绍用于图像处理的常用方法的实现,例如亮度控制、对比度和颜色转换、视网膜过滤和几何变换。
第三章。首要之事 – 图像处理
图像处理是指计算机通过应用信号处理方法对任何二维数据(图像)进行数字处理。图像处理有广泛的应用范围,如图像表示、图像增强或锐化、通过滤波进行图像恢复以及几何校正。这些应用通常是计算机视觉系统后续阶段的第一个阶段和输入。在 OpenCV 中,有一个专门的模块imgproc用于图像处理。在本章中,我们将介绍库中最重要和最常用的方法,即像素级访问、直方图操作、图像均衡化、亮度和对比度建模、颜色空间、滤波以及算术和几何变换。
像素级访问和常见操作
图像处理中最基本的操作之一是像素级访问。由于图像包含在Mat矩阵类型中,因此存在一个使用at<>模板函数的通用访问形式。为了使用它,我们必须指定矩阵单元格的类型,例如:
Mat src1 = imread("stuff.jpg", CV_LOAD_IMAGE_GRAYSCALE);
uchar pixel1=src1.at<uchar>(0,0);
cout << "First pixel: " << (unsigned int)pixel1 << endl;
Mat src2 = imread("stuff.jpg", CV_LOAD_IMAGE_COLOR);
Vec3b pixel2 = src2.at<Vec3b>(0,0);
cout << "First pixel (B):" << (unsigned int)pixel2[0] << endl;
cout << "First pixel (G):" << (unsigned int)pixel2[1] << endl;
cout << "First pixel (R):" << (unsigned int)pixel2[2] << endl;
注意,彩色图像使用Vec3b类型,它是一个包含三个无符号字符的数组。具有第四个 alpha(透明度)通道的图像将使用Vec4b类型访问。Scalar类型表示一个 1 到 4 元素的向量,也可以用于所有这些情况。注意,at<>也可以用于更改像素值(即赋值语句的左侧)。
除了像素访问之外,还有一些常见的操作,我们应该知道相应的代码片段。以下表格显示了这些常见操作:
| 操作 | 代码示例 |
|---|---|
| 获取矩阵大小 |
Size siz=src.size();
cout << "width: " << siz.width << endl;
cout << "height: " << siz.height << endl;
|
| 获取通道数 |
|---|
int nc=src.channels();
|
| 获取像素数据类型 |
|---|
int d=src.depth();
|
| 设置矩阵值 |
|---|
src.setTo(0); //for one-channel src
或者
src.setTo(Scalar(b,g,r)); // for three-channel src
|
| 创建矩阵的副本 |
|---|
Mat dst=src.clone();
|
| 创建矩阵的副本(可选掩码) |
|---|
src.copy(dst, mask);
|
| 引用子矩阵 |
|---|
Mat dst=src(Range(r1,r2),Range(c1,c2));
|
| 从子矩阵创建新矩阵(即图像裁剪) |
|---|
Rect roi(r1,c2, width, height);
Mat dst=src(roi).clone();
|
注意最后两行的区别:在最后一行中,创建了一个新的矩阵。在倒数第二行的情况下,仅创建了对src中子矩阵的引用,但数据实际上并未复制。
提示
最常见的操作,包括基于迭代器的额外像素访问方法,总结在OpenCV 2.4 快速参考卡中,可以从docs.opencv.org/trunk/opencv_cheatsheet.pdf下载。
图像直方图
图像直方图表示图像中各种灰度级别或颜色的出现频率,对于二维和三维直方图分别如此。因此,直方图类似于图像中不同像素值(即灰度级别)的概率密度函数。在 OpenCV 中,可以使用函数 void calcHist(const Mat* images, int nimages, const int* channels, InputArray mask, OutputArray hist, int dims, const int* histSize, const float** ranges, bool uniform=true, bool accumulate=false) 计算图像直方图。第一个参数是指向输入图像的指针。可以计算多个输入图像的直方图。这允许你比较图像直方图并计算多个图像的联合直方图。第二个参数是源图像的数量。第三个输入参数是用于计算直方图的通道列表。可以计算同一颜色图像的多个通道的直方图。因此,在这种情况下,nimages 的值将是 1,而 const int* channels 参数将是一个包含通道号的数组。
通道数从零到二。参数 InputArray mask 是一个可选的掩码,用于指示直方图中计数的数组元素(图像像素)。第五个参数是输出直方图。参数 int dims 是直方图的维数,必须是正数且不超过 32 (CV_MAX_DIMS)。直方图可以是 n-维的,根据用于量化图像像素值的箱子数量。参数 const int* histSize 是每个维度的直方图大小的数组。它允许我们计算具有非均匀箱子(或量化)的直方图。参数 const float** ranges 是每个维度的直方图箱子边界的 dims 数组的数组。最后两个参数具有布尔值,默认分别为 true 和 false。它们表示直方图是均匀的且非累积的。
以下 ImgHisto 示例展示了如何计算和显示二维图像的一维直方图:
#include "opencv2/imgproc/imgproc.hpp" // a dedicated include file
#include "opencv2/highgui/highgui.hpp"
#include <iostream>
using namespace cv;
using namespace std;
int main( int argc, char *argv[])
{
int histSize = 255;
long int dim;
Mat hist, image;
//Read original image
Mat src = imread( "fruits.jpg");
//Convert color image to gray level image
cvtColor(src, image, CV_RGB2GRAY);
//Create three windows
namedWindow("Source", 0);
namedWindow("Gray Level Image", 0);
namedWindow("Histogram", WINDOW_AUTOSIZE);
imshow("Source", src);
imshow("Gray Level Image", image);
calcHist(&image, 1, 0, Mat(), hist, 1, &histSize, 0);
dim=image.rows *image.cols;
Mat histImage = Mat::ones(255, 255, CV_8U)*255;
normalize(hist, hist, 0, histImage.rows, CV_MINMAX, CV_32F);
histImage = Scalar::all(255);
int binW = cvRound((double)histImage.cols/histSize);
for( int i = 0; i < histSize; i++ )
rectangle( histImage, Point(i*binW, histImage.rows), Point((i+1)*binW, histImage.rows – cvRound(hist.at<float>(i))), Scalar::all(0), -1, 8, 0 );
imshow("Histogram", histImage);
cout << "Press any key to exit...\n";
waitKey(); // Wait for key press
return 0;
}
代码解释如下:示例创建了三个窗口,分别显示源图像、灰度图像和一维直方图的结果。一维直方图以条形图的形式显示了 255 个灰度值。因此,首先使用 cvtColor 函数将颜色像素转换为灰度值。然后使用 normalize 函数将灰度值归一化到 0 和最大灰度级别值之间。然后通过将图像中的颜色离散化到多个箱子中并计算每个箱子中的图像像素数量来计算一维直方图。以下截图显示了示例的输出。请注意,需要一个专门用于图像处理的新的包含文件 imgproc.hpp。

直方图示例输出
直方图均衡化
一旦计算了图像直方图,就可以对其进行建模,以便修改图像并使直方图具有不同的形状。这对于改变具有狭窄直方图的图像的低对比度级别很有用,因为这会将灰度级别分散开来,从而增强对比度。直方图建模,也称为直方图转换,是图像增强的一种强大技术。在直方图均衡化中,目标是获得输出图像的均匀直方图。也就是说,一个平坦的直方图,其中每个像素值具有相同的概率。在 OpenCV 中,直方图均衡化是通过函数 void equalizeHist(InputArray src, OutputArray dst) 来实现的。第一个参数是输入图像,第二个参数是具有均衡化直方的输出图像。
以下 EqualizeHist_Demo 示例展示了如何计算和显示直方图均衡化及其对二维图像的影响:
#include "opencv2/highgui/highgui.hpp"
#include "opencv2/imgproc/imgproc.hpp"
#include <iostream>
#include <stdio.h>
using namespace cv;
using namespace std;
int main( int, char *argv[] )
{
Mat src, image, hist;
int histSize = 255;
long int dim;
//Read original image
src = imread( "fruits.jpg");
//Convert to grayscale
cvtColor( src, src, COLOR_BGR2GRAY );
//Apply Histogram Equalization
equalizeHist( src, image );
//Display results
namedWindow("Source image", 0 );
namedWindow("Equalized Image", 0 );
imshow( "Source image", src );
imshow( "Equalized Image", image );
//Calculate Histogram of the Equalized Image and display
calcHist(&image, 1, 0, Mat(), hist, 1, &histSize, 0);
dim=image.rows *image.cols;
Mat histImage = Mat::ones(255, 255, CV_8U)*255;
normalize(hist, hist, 0, histImage.rows, CV_MINMAX, CV_32F);
histImage = Scalar::all(255);
int binW = cvRound((double)histImage.cols/histSize);
for( int i = 0; i < histSize; i++ )
rectangle( histImage, Point(i*binW, histImage.rows), Point((i+1)*binW, histImage.rows – cvRound(hist.at<float>(i))), Scalar::all(0), -1, 8, 0 );
namedWindow("Histogram Equalized Image", WINDOW_AUTOSIZE);
imshow("Histogram Equalized Image", histImage);
waitKey();// Exits the program
return 0;
}
以下是对代码的解释。示例首先读取原始图像并将其转换为灰度图。然后,使用 equalizeHist 函数执行直方图均衡化。最后,显示均衡化图像的直方图以及前两个图像。以下截图显示了示例的输出,其中创建了三个窗口,分别显示灰度图像、均衡化图像及其直方图:

直方图均衡化示例的输出
亮度和对比度建模
物体的亮度是感知的亮度或强度,并取决于环境的亮度。在不同环境中,两个物体可能具有相同的亮度但不同的亮度。原因是人类视觉感知对亮度对比度敏感,而不是绝对亮度。对比度是亮度以及/或颜色之间的差异,使得物体在相同视野内的其他物体中可区分。图像的最大对比度称为对比度比或动态范围。
通过点操作可以修改图像的亮度和对比度。点操作根据预先定义的转换将给定的灰度像素值映射到不同的灰度级别。在 OpenCV 中,可以使用函数 void Mat::convertTo(OutputArray m, int rtype, double alpha=1, double beta=0) 来执行点操作。convertTo 函数可以将图像数组转换为另一种数据类型,并可选择缩放。第一个参数是输出图像,第二个参数是输出矩阵类型,即深度,因为通道数与输入图像相同。因此,源像素值 I(x,y) 被转换为具有新值 (I(x,y) * alpha + beta) 的目标数据类型。
以下 BrightnessContrast 示例展示了如何执行图像像素(点)操作以修改亮度和对比度:
#include "opencv2/imgproc/imgproc.hpp"
#include "opencv2/highgui/highgui.hpp"
#include <iostream>
using namespace cv;
using namespace std;
int init_brightness = 100;
int init_contrast = 100;
Mat image;
/* brightness and contrast function to highlight the image*/
void updateBrightnessContrast(int, void* )
{
int histSize = 255;
int var_brightness = init_brightness - 100;
int var_contrast = init_contrast - 100;
double a, b;
if( var_contrast > 0 )
{
double delta = 127.*var_contrast/100;
a = 255./(255\. - delta*2);
b = a*(var_brightness - delta);
}
else
{
double delta = -128.*var_contrast/100;
a = (256.-delta*2)/255.;
b = a*var_brightness + delta;
}
Mat dst, hist;
image.convertTo(dst, CV_8U, a, b);
imshow("image", dst);
calcHist(&dst, 1, 0, Mat(), hist, 1, &histSize, 0);
Mat histImage = Mat::ones(200, 320, CV_8U)*255;
normalize(hist, hist, 0, histImage.rows, CV_MINMAX, CV_32F);
histImage = Scalar::all(255);
int binW = cvRound((double)histImage.cols/histSize);
for( int i = 0; i < histSize; i++ )
rectangle( histImage, Point(i*binW, histImage.rows), Point((i+1)*binW, histImage.rows – cvRound(hist.at<float>(i))), Scalar::all(0), -1, 8, 0 );
imshow("histogram", histImage);
}
const char* keys = {
"{1| |fruits.jpg|input image file}"
};
int main( int argc, const char** argv )
{
CommandLineParser parser(argc, argv, keys);
string inputImage = parser.get<string>("1");
//Read the input image.
image = imread( inputImage, 0 );
namedWindow("image", 0);
namedWindow("histogram", 0);
createTrackbar("brightness", "image", &init_brightness , 200, updateBrightnessContrast);
createTrackbar("contrast", "image", &init_contrast, 200, updateBrightnessContrast);
updateBrightnessContrast(0, 0);
waitKey();
return 0;
}
代码解释如下:示例创建两个窗口,一个用于灰度图像及其直方图。用户通过createTrackbar函数选择亮度和对比度的新值。此函数将两个滑块或范围控件附加到图像上,用于亮度和对比度。以下截图显示了BrightnessContrast示例的输出,亮度值为 148,对比度值为 81:

亮度对比度图像修改的输出
直方图匹配和查找表
直方图还可以用于修改图像的颜色。直方图匹配是两种颜色图像之间颜色调整的方法。给定一个参考图像和一个目标图像,结果(目标图像)将与目标图像相等,除了其(三个)直方图将看起来像参考图像的直方图。这种效果被称为颜色映射或颜色传递。
直方图匹配算法独立运行在每个三个颜色直方图上。对于每个通道,必须计算累积分布函数(CDF)。对于给定的通道,设Fr为参考图像的 CDF,Ft为目标图像的 CDF。然后,对于参考图像中的每个像素v,我们找到灰度级w,使得Fr(v)=Ft(w)。因此,具有值v的像素被更改为w。
接下来,我们提供另一个使用直方图的示例,其中我们使用一种称为直方图匹配的技术。该示例还使用了查找表(LUT)。查找表变换将新的像素值分配给输入图像中的每个像素(有关 LUT 的详细解释和示例,请参阅docs.opencv.org/doc/tutorials/core/how_to_scan_images/how_to_scan_images.html)。新值由一个表给出。因此,该表的第一项给出像素值 0 的新值,第二项给出像素值 1 的新值,依此类推。假设我们使用源图像和目标图像,变换由Dst(x,y)=LUT(Src(x,y))给出。
OpenCV 执行查找表变换的函数是LUT(InputArray src, InputArray lut, OutputArray dst, int interpolation=0)。参数src是一个 8 位图像。表由参数lut给出,它包含 256 个元素。表包含一个通道或与源图像相同数量的通道。
以下是一个histMatching示例:
#include "opencv2/opencv.hpp"
#include <iostream>
using namespace std;
using namespace cv;
void histMatch(const Mat &reference, const Mat &target, Mat &result){
float const HISTMATCH = 0.000001;
double min, max;
vector<Mat> ref_channels;
split(reference, ref_channels);
vector<Mat> tgt_channels;
split(target, tgt_channels);
int histSize = 256;
float range[] = {0, 256};
const float* histRange = { range };
bool uniform = true;
//For every channel (B, G, R)
for ( int i=0 ; i<3 ; i++ )
{
Mat ref_hist, tgt_hist;
Mat ref_hist_accum, tgt_hist_accum;
//Calculate histograms
calcHist(&ref_channels[i], 1, 0, Mat(), ref_hist, 1, &histSize, &histRange, uniform, false);
calcHist(&tgt_channels[i], 1, 0, Mat(), tgt_hist, 1, &histSize, &histRange, uniform, false);
//Normalize histograms
minMaxLoc(ref_hist, &min, &max);
if (max==0) continue;
ref_hist = ref_hist / max;
minMaxLoc(tgt_hist, &min, &max);
if (max==0) continue;
tgt_hist = tgt_hist / max;
//Calculate accumulated histograms
ref_hist.copyTo(ref_hist_accum);
tgt_hist.copyTo(tgt_hist_accum);
float * src_cdf_data = ref_hist_accum.ptr<float>();
float * dst_cdf_data = tgt_hist_accum.ptr<float>();
for ( int j=1 ; j < 256 ; j++ )
{
src_cdf_data[j] = src_cdf_data[j] + src_cdf_data[j-1];
dst_cdf_data[j] = dst_cdf_data[j] + dst_cdf_data[j-1];
}
//Normalize accumulated
minMaxLoc(ref_hist_accum, &min, &max);
ref_hist_accum = ref_hist_accum / max;
minMaxLoc(tgt_hist_accum, &min, &max);
tgt_hist_accum = tgt_hist_accum / max;
//Result max
Mat Mv(1, 256, CV_8UC1);
uchar * M = Mv.ptr<uchar>();
uchar last = 0;
for ( int j=0 ; j < tgt_hist_accum.rows ; j++ )
{
float F1 = dst_cdf_data[j];
for ( uchar k=last ; k < ref_hist_accum.rows ; k++ )
{
float F2 = src_cdf_data[k];
if ( std::abs(F2 - F1) < HISTMATCH || F2 > F1 )
{
M[j] = k;
last = k;
break;
}
}
}
Mat lut(1, 256, CV_8UC1, M);
LUT(tgt_channels[i], lut, tgt_channels[i]);
}
//Merge the three channels into the result image
merge(tgt_channels, result);
}
int main(int argc, char *argv[])
{
//Read original image and clone it to contain results
Mat ref = imread("baboon.jpg", CV_LOAD_IMAGE_COLOR );
Mat tgt = imread("lena.jpg", CV_LOAD_IMAGE_COLOR );
Mat dst = tgt.clone();
//Create three windows
namedWindow("Reference", WINDOW_AUTOSIZE);
namedWindow("Target", WINDOW_AUTOSIZE);
namedWindow("Result", WINDOW_AUTOSIZE);
imshow("Reference", ref);
imshow("Target", tgt);
histMatch(ref, tgt, dst);
imshow("Result", dst);
// Position windows on screen
moveWindow("Reference", 0,0);
moveWindow("Target", ref.cols,0);
moveWindow("Result", ref.cols+tgt.cols,0);
waitKey(); // Wait for key press
return 0;
}
代码解释如下:示例首先读取参考图像和目标图像。输出图像也被分配。主函数是histMatch。在其中,参考图像和目标图像首先被分割成三个颜色通道。然后,对于每个通道,我们获得参考图像和目标图像的归一化直方图,接着是相应的累积分布函数(CDF)。接下来,执行直方图匹配变换。
最后,我们使用查找表应用新的像素值。请注意,该转换也可以通过遍历结果图像中的每个像素来应用。然而,查找表选项要快得多。以下截图显示了样本的输出。参考图像(baboon.jpg图像)的颜色调色板被转移到目标图像中。

histMatching 示例的输出
从 RGB 转换为其他颜色空间
通过更改颜色空间,图像的颜色也可能被修改。在 OpenCV 中,有六个颜色模型可用,并且可以使用cvtColor函数从一种转换为另一种。
注意
OpenCV 中的默认颜色格式通常被称为 RGB,但实际上是 BGR(通道已反转)。
函数 void cvtColor(InputArray src, OutputArray dst, int code, int dstCn=0) 的输入和输出图像分别作为第一和第二个参数。第三个参数是颜色空间转换代码,最后一个参数是输出图像中的通道数;如果此参数为 0,则自动从输入图像中获取通道数。
以下color_channels示例显示了如何将 RGB 转换为 HSV、Luv、Lab、YCrCb 和 XYZ 颜色空间:
#include "opencv2/highgui/highgui.hpp"
#include "opencv2/imgproc/imgproc.hpp"
using namespace cv;
using namespace std;
int main( ){
Mat image, HSV, Luv, Lab, YCrCb, XYZ;
//Read image
image = imread("HappyFish.jpg", CV_LOAD_IMAGE_COLOR);
//Convert RGB image to different color spaces
cvtColor(image, HSV, CV_RGB2HSV);
cvtColor(image, Luv, CV_RGB2Luv);
cvtColor(image, Lab, CV_RGB2Lab);
cvtColor(image, YCrCb, CV_RGB2YCrCb);
cvtColor(image, XYZ, CV_RGB2XYZ);
//Create windows and display results
namedWindow( "Source Image", 0 );
namedWindow( "Result HSV Image", 0 );
namedWindow( "Result Luv Image", 0 );
namedWindow( "Result Lab Image", 0 );
namedWindow( "Result YCrCb Image", 0 );
namedWindow( "Result XYZ Image", 0 );
imshow( "Source Image", image );
imshow( "Result HSV Image", HSV );
imshow( "Result Luv Image", Luv );
imshow( "Result Lab Image", Lab);
imshow( "Result YCrCb Image", YCrCb );
imshow( "Result XYZ Image", XYZ );
waitKey(); //Wait for key press
return 0; //End the program
}
代码解释如下:第一个示例读取原始图像并将其转换为五种不同的颜色模型。然后显示 RGB 原始图像和结果。以下截图显示了样本的输出:

不同颜色空间的输出
使用视网膜模型进行滤波
图像恢复涉及通过滤波数字图像以最小化退化效果。退化是在图像获取过程中由光学或电子设备中的传感环境产生的。图像滤波的有效性取决于对退化过程的了解程度和准确性以及滤波器设计。
在 OpenCV 中,有几种各向同性滤波器和各向异性滤波器可用于空间和频率域。其中最新的一种滤波器是视网膜滤波器,它基于人类视觉系统的模型。有一个名为Retina的类来执行时空滤波建模,模拟两个主要的视网膜信息通道,即由于黄斑视觉而称为小细胞(parvo)和由于周边视觉而称为大细胞(magno)。parvo通道与细节提取相关,而magno通道专门用于运动分析。
Retina 类可以应用于静态图像、图像序列和视频序列以执行运动分析。在此,我们展示了 OpenCV 中提供的简化版 retinademo 算法。这里展示的 Filter_Retina.cpp 算法演示了视网膜模型图像的使用,可用于执行具有增强信噪比和增强细节的纹理分析,这些细节对输入图像亮度范围具有鲁棒性。人类视网膜模型的主要特性如下:
-
频谱白化(中频细节增强)
-
高频时空噪声降低(时间噪声和高频空间噪声最小化)
-
低频亮度降低(亮度范围压缩):高亮度区域不再隐藏较暗区域的细节
-
局部对数亮度压缩允许在低光照条件下增强细节
注意
更多信息,请参阅 *《利用人类视觉系统建模进行生物启发式低级图像处理》,作者:Benoit A.,Caplier A.,Durette B.,Herault J.,Elsevier,计算机视觉与图像理解 114 (2010),第 758-773 页。DOI:dx.doi.org/10.1016/j.cviu.2010.01.011。
以下为示例代码:
#include "opencv2/opencv.hpp"
using namespace cv;
using namespace std;
int main(int argc, char* argv[])
{
//Declare input image and retina output buffers
Mat src, retinaOutput_parvo, retinaOutput_magno;
src = imread("starry_night.jpg", 1); // load image in RGB
//Create a retina instance with default parameters setup
Ptr< Retina> myRetina;
//Allocate "classical" retina :
myRetina = new Retina(src.size());
//Save default retina parameters file
myRetina->write("RetinaDefaultParameters.xml");
//The retina parameters may be reload using method "setup"
//Uncomment to load parameters if file exists
//myRetina->setup("RetinaSpecificParameters.xml");
myRetina->clearBuffers();
//Several iteration of the filter may be done
for( int iter = 1; iter < 6; iter++ ){
// run retina filter
myRetina->run(src);
// Retrieve and display retina output
myRetina->getParvo(retinaOutput_parvo);
myRetina->getMagno(retinaOutput_magno);
//Create windows and display results
namedWindow("Source Image", 0 );
namedWindow("Retina Parvo", 0 );
namedWindow("Retina Magno", 0 );
imshow("Source Image", src);
imshow("Retina Parvo", retinaOutput_parvo);
imshow("Retina Magno", retinaOutput_magno);
}
cout<<"Retina demo end"<< endl; // Program end message
waitKey();
return 0;
}
代码解释如下:示例首先读取输入图像,并使用模型的经典参数获取图像的视网膜模型。视网膜可以用各种参数设置;默认情况下,视网膜取消平均亮度并强制所有视觉场景的细节。然后,过滤器运行五次,并显示 parvo 和 magno 图像及其细节。以下截图显示了五次迭代后视网膜模型滤波器的输出:

五次迭代后视网膜滤波器的输出
算术和几何变换
算术变换会改变图像像素的值,并且是点对点应用,而几何变换会改变图像像素的位置。因此,图像中的点在输出图像中获得新的位置,而不会改变它们的强度值。算术变换的例子可能包括图像之间的加法、减法和除法。几何变换的例子包括图像的缩放、平移和旋转。更复杂的变换是解决由光学镜头产生的图像的桶形和垫形变形。
在 OpenCV 中,有几个函数可以执行算术和几何变换。这里我们展示了两个示例,通过 addWeighted 和 warpPerspective 函数分别实现图像加法和透视变换。
算术变换
函数 addWeighted 执行两个图像的线性组合,即通过将两个加权图像相加来实现线性混合。函数 void addWeighted(InputArray src1, double alpha, InputArray src2, double beta, double gamma, OutputArray dst, int dtype=-1) 有两个输入图像作为第一和第三个参数,它们具有相应的权重(第二个和第四个参数)。然后,输出图像是第六个参数。第五个参数 gamma 是加到每个和上的标量。最后一个参数 dtype 是可选的,表示输出图像的深度;当两个输入图像具有相同的深度时,它可以设置为 -1。
下面的 LinearBlend 示例展示了如何执行两个图像之间的线性混合:
#include "opencv2/highgui/highgui.hpp"
using namespace cv;
using namespace std;
int main()
{
double alpha = 0.5, beta, input;
Mat src1, src2, dst;
//Read images (same size and type )
src1 = imread("baboon.jpg");
src2 = imread("lena.jpg");
//Create windows
namedWindow("Final Linear Blend", CV_WINDOW_AUTOSIZE );
//Perform a loop with 101 iteration for linear blending
for(int k = 0; k <= 100; ++k ){
alpha = (double)k/100;
beta = 1 - alpha;
addWeighted( src2, alpha, src1, beta, 0.0, dst );
imshow( "Final Linear Blend", dst );
cvWaitKey(50);
}
namedWindow("Original Image 1", CV_WINDOW_AUTOSIZE );
namedWindow("Original Image 2", CV_WINDOW_AUTOSIZE );
imshow( "Original Image 1", src1 );
imshow( "Original Image 2", src2 );
cvWaitKey(); // Wait for key press
return 0; // End
}
代码解释如下:示例首先读取两个图像,src1= baboon.jpg 和 src2= lena.jpg,然后使用不同的权重 alpha 和 beta 值进行总共 101 次线性组合。第一次线性组合或混合时 alpha 等于零,因此是 src1 图像。在循环中 alpha 的值增加,而 beta 的值减少。因此,src2 图像被组合并叠加到 src1 图像上。这产生了一种变形效果,baboon.jpg 图像逐渐变成另一幅图像,即 lena.jpg。以下截图显示了迭代 1、10、20、30、40、50、70、85 和 100 时几个线性混合步骤的输出:

两种图像之间不同线性混合的输出
几何变换
函数 warpPerspective, void ocl::warpPerspective(const oclMat& src, oclMat& dst, const Mat& M, Size dsize, int flags=INTER_LINEAR) 对图像执行透视变换。它将输入或源图像 src 作为第一个参数,将输出或目标图像 dst 作为第二个参数。然后,第三个参数是从 getPerspectiveTransform 函数获得的 2 x 3 变换矩阵,该函数从两个图像中四对对应点的位置计算透视变换。warpPerspective 的第四个参数是输出图像的大小,最后一个参数是插值方法。默认情况下,插值方法是线性插值,INTER_LINEAR;支持的其他方法包括最近邻插值 INTER_NEAREST 和三次插值 INTER_CUBIC。
下面的 Geometrical_Transform 示例对输入图像 img.jpg 执行透视变换。
注意
有关示例的完整细节,请参阅 N. Amin 的 四边形对象的自动透视校正,链接为 opencv-code.com/tutorials/automatic-perspective-correction-for-quadrilateral-objects/。
#include "opencv2/highgui/highgui.hpp"
#include "opencv2/imgproc/imgproc.hpp"
#include <iostream>
#include <stdio.h>
using namespace cv;
using namespace std;
Point2f centerpoint(0,0);
Point2f computeIntersect(Vec4i a,Vec4i b){
int x1 = a[0], y1 = a[1], x2 = a[2], y2 = a[3], x3 = b[0], y3 = b[1], x4 = b[2], y4 = b[3];
if (float d = ((float)(x1 - x2) * (y3 - y4)) - ((y1 - y2) * (x3 - x4)))
{
Point2f pnt;
pnt.x = ((x1 * y2 - y1 * x2) * (x3 - x4) - (x1 - x2) * (x3 * y4 - y3 * x4)) / d;
pnt.y = ((x1 * y2 - y1 * x2) * (y3 - y4) - (y1 - y2) * (x3 * y4 - y3 * x4)) / d;
return pnt;
}
else
return Point2f(-1, -1);
}
void sortCorners(vector<Point2f>& corner_points, Point2f centerpoint)
{
vector<Point2f> top, bot;
for (int i = 0; i < corner_points.size(); i++)
{
if (corner_points[i].y < centerpoint.y)
top.push_back(corner_points[i]);
else
bot.push_back(corner_points[i]);
}
Point2f tl = top[0].x > top[1].x ? top[1] : top[0];
Point2f tr = top[0].x > top[1].x ? top[0] : top[1];
Point2f bl = bot[0].x > bot[1].x ? bot[1] : bot[0];
Point2f br = bot[0].x > bot[1].x ? bot[0] : bot[1];
corner_points.clear();
corner_points.push_back(tl);
corner_points.push_back(tr);
corner_points.push_back(br);
corner_points.push_back(bl);
}
int main(){
Mat src = imread("img.jpg");
if (src.empty())
return -1;
Mat dst = src.clone();
Mat bw;
cvtColor(src, bw, CV_BGR2GRAY);
Canny(bw, bw, 100, 100, 3);
vector<Vec4i> lines;
HoughLinesP(bw, lines, 1, CV_PI/180, 70, 30, 10);
vector<Point2f> corner_points;
for (int i = 0; i < lines.size(); i++)
{
for (int j = i+1; j < lines.size(); j++)
{
Point2f pnt = computeIntersect(lines[i], lines[j]);
if (pnt.x >= 0 && pnt.y >= 0)
corner_points.push_back(pnt);
}
}
vector<Point2f> approx;
approxPolyDP(Mat(corner_points), approx, arcLength(Mat(corner_points), true) * 0.02, true);
if (approx.size() != 4)
{
cout << "The object is not quadrilateral!" << endl;
return -1;
}
//Get center point
for (int i = 0; i < corner_points.size(); i++)
centerpoint += corner_points[i];
centerpoint *= (1\. / corner_points.size());
sortCorners(corner_points, centerpoint);
//Draw lines
for (int i = 0; i < lines.size(); i++)
{
Vec4i v = lines[i];
line(dst, Point(v[0], v[1]), Point(v[2], v[3]), CV_RGB(0,255,0));
}
//Draw corner points
circle(dst, corner_points[0], 3, CV_RGB(255,0,0), 2);
circle(dst, corner_points[1], 3, CV_RGB(0,255,0), 2);
circle(dst, corner_points[2], 3, CV_RGB(0,0,255), 2);
circle(dst, corner_points[3], 3, CV_RGB(255,255,255), 2);
//Draw mass center points
circle(dst, centerpoint, 3, CV_RGB(255,255,0), 2);
//Calculate corresponding points for corner points
Mat quad = Mat::zeros(src.rows, src.cols/2, CV_8UC3);
vector<Point2f> quad_pnts;
quad_pnts.push_back(Point2f(0, 0));
quad_pnts.push_back(Point2f(quad.cols, 0));
quad_pnts.push_back(Point2f(quad.cols, quad.rows));
quad_pnts.push_back(Point2f(0, quad.rows));
// Draw corresponding points
circle(dst, quad_pnts[0], 3, CV_RGB(255,0,0), 2);
circle(dst, quad_pnts[1], 3, CV_RGB(0,255,0), 2);
circle(dst, quad_pnts[2], 3, CV_RGB(0,0,255), 2);
circle(dst, quad_pnts[3], 3, CV_RGB(255,255,255), 2);
Mat transmtx = getPerspectiveTransform(corner_points, quad_pnts);
warpPerspective(src, quad, transmtx, quad.size());
//Create windows and display results
namedWindow("Original Image", CV_WINDOW_AUTOSIZE );
namedWindow("Selected Points", CV_WINDOW_AUTOSIZE );
namedWindow("Corrected Perspertive", CV_WINDOW_AUTOSIZE );
imshow("Original Image", src);
imshow("Selected Points", dst);
imshow("Corrected Perspertive", quad);
waitKey(); //Wait for key press
return 0; //End
}
代码解释如下:示例首先读取输入图像(img.jpg)并计算感兴趣区域或对象的特征点以执行透视变换。特征点是对象的角点。该算法仅适用于四边形对象。计算角点的方法(Canny 算子和霍夫变换)在第四章图像中的内容,分割中进行了说明。对应于对象角点的点是输出图像的角点。这些点在原始图像上用圆圈表示。输出图像的尺寸设置为与输入图像相同的高度和宽度的一半。最后,可视化校正后的对象图像。透视校正使用线性变换,INTER_LINEAR。以下截图显示了算法的输出:

对透视进行校正所执行的几何变换的输出
摘要
本章涵盖了计算机视觉中最常用的图像处理方法。图像处理通常是进行进一步计算机视觉应用之前的步骤。它有许多方法,通常用于图像校正和增强,例如图像直方图、图像均衡化、亮度与对比度建模、通过直方图匹配和颜色空间转换进行图像颜色转换、使用人眼视网膜模型进行滤波,以及算术和几何变换。
下一章将介绍计算机视觉系统中的下一个阶段,即分割过程。我们将看到如何从图像中提取感兴趣区域。
还有什么?
OpenCV 中与图像处理相关的其他重要函数与滤波有关。由于它们很简单,这些函数在章节中已被省略。OpenCV 包括一个示例,展示了如何使用主要滤波器(([opencv_source_code]/samples/cpp/filter2D_demo.cpp)。主要的滤波器函数包括:
-
GaussianBlur用于高斯滤波 -
medianBlur用于中值滤波 -
bilateralFilter用于各向异性滤波 -
blur用于均匀模糊
第四章 图像中的内容?分割
分割是将图像分割成多个区域或片段的任何过程。这些通常对应于有意义的区域或对象,例如人脸、汽车、道路、天空、草地等。分割是计算机视觉系统中最重要阶段之一。在 OpenCV 中,没有专门的分割模块,尽管在其他模块(大多数在 imgproc 中)中提供了许多现成的方法。在本章中,我们将介绍库中最重要的和最常用的方法。在某些情况下,可能需要添加额外的处理来提高结果或获取种子(这指的是允许算法执行完整分割的粗略片段)。在本章中,我们将探讨以下主要的分割方法:阈值化、轮廓和连通组件、区域填充、分水岭分割和 GrabCut 算法。
阈值化
阈值化是一种简单但非常有用的分割操作。我们可以安全地说,你几乎会在任何图像处理应用中使用某种形式的阈值化。我们将其视为分割操作,因为它将图像分割成两个区域,通常是对象及其背景。在 OpenCV 中,阈值化是通过函数 double threshold(InputArray src, OutputArray dst, double thresh, double maxval, int type) 来实现的。
前两个参数分别是输入和输出图像。第三个输入参数是选择的阈值。maxval 的含义取决于我们想要执行的阈值化类型。以下表格显示了每种类型执行的操作:
| 类型 | dst(x,y) |
|---|---|
THRESH_BINARY |
如果 src(x,y) 大于 thresh,则返回 maxval,否则返回 0 |
THRESH_BINARY_INV |
如果 src(x,y) 大于 thresh,则返回 0,否则返回 maxval |
THRESH_TRUNC |
如果 src(x,y) 大于 thresh,则返回 thresh,否则返回 src(x,y) |
THRESH_TOZERO |
如果 src(x,y) 大于 thresh,则返回 src(x,y),否则返回 0 |
THRESH_TOZERO_INV |
如果 src(x,y) 大于 thresh,则返回 0,否则返回 src(x,y) |
在之前的 OpenCV 书籍(以及可用的参考手册)中,每种类型的阈值化都是通过 1D 信号图来展示的,但我们的经验表明,数字和灰度级别能让你更快地掌握概念。以下表格展示了使用单行图像作为示例输入时,不同阈值类型的效果:

特殊值 THRESH_OTSU 可以与前面的值(使用 OR 运算符)组合。在这种情况下,阈值值将由函数自动估计(使用 Otsu 算法)。此函数返回估计的阈值值。
注意
大津法(Otsu's method)获得一个最佳阈值,该阈值能够最好地将背景与前景像素分开(从类间/类内方差比的角度来看)。有关完整解释和演示,请参阅www.labbookpages.co.uk/software/imgProc/otsuThreshold.html。
虽然描述的函数使用单个阈值处理整个图像,但自适应阈值处理为每个像素估计不同的阈值。当输入图像不太均匀(例如,具有不均匀照亮的区域)时,这会产生更好的结果。执行自适应阈值处理的函数如下:
adaptiveThreshold(InputArray src, OutputArray dst, double maxValue, int adaptiveMethod, int thresholdType, int blockSize, double C)
此函数与上一个函数类似。参数thresholdType必须是THRESH_BINARY或THRESH_BINARY_INV。此函数通过计算邻域内像素的加权平均值减去一个常数(C)来为每个像素计算一个阈值。当thresholdType是ADAPTIVE_THRESH_MEAN_C时,计算出的阈值是邻域的平均值(即所有元素都被同等加权)。当thresholdType是ADAPTIVE_THRESH_GAUSSIAN_C时,邻域内的像素根据高斯函数进行加权。
以下阈值示例展示了如何在图像上执行阈值操作:
#include "opencv2/opencv.hpp"
#include <iostream>
using namespace std;
using namespace cv;
Mat src, dst, adaptDst;
int threshold_value, block_size, C;
void thresholding( int, void* )
{
threshold( src, dst, threshold_value, 255, THRESH_BINARY );
imshow( "Thresholding", dst );
}
void adaptThreshAndShow()
{
adaptiveThreshold( src, adaptDst, 255, CV_ADAPTIVE_THRESH_MEAN_C, THRESH_BINARY, block_size, C);
imshow( "Adaptive Thresholding", adaptDst );
}
void adaptiveThresholding1( int, void* )
{
static int prev_block_size=block_size;
if ((block_size%2)==0) // make sure that block_size is odd
{
if (block_size>prev_block_size) block_size++;
if (block_size<prev_block_size) block_size--;
}
if (block_size<=1) block_size=3; // check block_size min value
adaptThreshAndShow();
}
void adaptiveThresholding2( int, void* )
{
adaptThreshAndShow();
}
int main(int argc, char *argv[])
{
//Read original image and clone it to contain results
src = imread("left12.jpg", CV_LOAD_IMAGE_GRAYSCALE );
dst=src.clone();
adaptDst=src.clone();
//Create 3 windows
namedWindow("Source", WINDOW_AUTOSIZE);
namedWindow("Thresholding", WINDOW_AUTOSIZE);
namedWindow("Adaptive Thresholding", WINDOW_AUTOSIZE);
imshow("Source", src);
//Create trackbars
threshold_value=127;
block_size=7;
C=10;
createTrackbar( "threshold", "Thresholding", &threshold_value, 255, thresholding );
createTrackbar( "block_size", "Adaptive Thresholding", &block_size, 25, adaptiveThresholding1 );
createTrackbar( "C", "Adaptive Thresholding", &C, 255, adaptiveThresholding2 );
//Perform operations a first time
thresholding(threshold_value,0);
adaptiveThresholding1(block_size, 0);
adaptiveThresholding2(C, 0);
// Position windows on screen
moveWindow("Source", 0,0);
moveWindow("Thresholding", src.cols,0);
moveWindow("Adaptive Thresholding", 2*src.cols,0);
cout << "Press any key to exit...\n";
waitKey(); // Wait for key press
return 0;
}
上述代码中的示例创建了一个包含源图像、灰度加载的源图像、阈值处理结果和自适应阈值处理结果的三个窗口。然后,它创建了三个滑块:一个与阈值处理结果窗口相关联(用于处理阈值值),另外两个与自适应阈值处理结果窗口相关联(用于处理块的尺寸和常数C的值)。请注意,由于在这种情况下需要两个回调函数,并且我们不希望重复代码,因此adaptiveThreshold的调用被嵌入到adaptThreshAndShow函数中。
接下来,调用执行操作的函数,使用默认参数值。最后,使用highgui中的moveWindow函数重新定位屏幕上的窗口(否则它们将重叠显示,并且只能看到第三个窗口)。此外,请注意,函数adaptiveThresholding1中的前六行是必要的,以保持block_size参数的奇数值。以下截图显示了示例的输出:

阈值示例的输出
注意
函数inRange(InputArray src, InputArray lowerb, InputArray upperb, OutputArray dst)在阈值处理中也很有用,因为它检查像素是否位于下限和上限阈值之间。lowerb和upperb都必须使用 Scalar 提供,如inRange(src, Scalar(bl,gl,rl), Scalar(bh,gh,rh), tgt);。
边缘和连通组件
轮廓提取操作可以被认为是特征提取和分割之间的中间步骤,因为它产生了一个二值图像,其中图像轮廓与其他均匀区域分离。轮廓通常对应于物体边界。
虽然许多简单的方法可以检测图像中的边缘(例如,Sobel 和 Laplace 滤波器),但Canny方法是一种稳健的算法来完成这项任务。
注意
此方法使用两个阈值来决定一个像素是否是边缘。在所谓的阈值过程(hysteresis procedure)中,使用一个下限和一个上限阈值(参见docs.opencv.org/doc/tutorials/imgproc/imgtrans/canny_detector/canny_detector.html)。由于 OpenCV 已经包含了一个好的 Canny 边缘检测器示例(在[opencv_source_code]/samples/cpp/edge.cpp中),我们这里没有包含一个(但请参见下面的floodFill示例)。相反,我们将继续描述基于检测到的边缘的其他非常有用的函数。
要检测直线,霍夫变换是一种经典方法。虽然霍夫变换方法在 OpenCV 中可用(例如,HoughLines和HoughLinesP函数,参见[opencv_source_code]/samples/cpp/houghlines.cpp),但较新的线段检测器(LSD)方法通常更稳健。LSD 通过寻找高梯度幅度像素的对齐来实现,这得益于其对齐容差特性。这种方法已被证明比最好的基于霍夫的前期检测器(渐进概率霍夫变换)更稳健且更快。
LSD 方法在 OpenCV 的 2.4.9 版本中不可用;尽管如此,在撰写本文时,它已经在 GitHub 代码源仓库中可用。该方法将在版本 3.0 中提供。库中的一个简短示例([opencv_source_code]/samples/cpp/lsd_lines.cpp)涵盖了这一功能。然而,我们将提供一个额外的示例,展示不同的特性。
注意
要测试 GitHub 上可用的最新源代码,请访问github.com/itseez/opencv并下载库代码作为 ZIP 文件。然后,将其解压缩到本地文件夹,并按照第一章中描述的相同步骤,入门,来编译和安装库。
LSD 检测器是一个 C++类。函数cv::Ptr<LineSegmentDetector> cv::createLineSegmentDetector (int _refine=LSD_REFINE_STD, double _scale=0.8, double_sigma_scale=0.6, double _quant=2.0, double _ang_th=22.5, double _log_eps=0, double _density_th=0.7, int _n_bins=1024)创建了一个类的对象,并返回指向它的指针。请注意,几个参数定义了创建的检测器。这些参数的含义需要你了解底层算法,而这超出了本书的范围。幸运的是,默认值对于大多数用途已经足够,因此我们建议读者查阅参考手册(关于库的 3.0 版本)以了解特殊情况。在此说明之后,第一个参数 scale 大致控制返回的线条数量。输入图像会自动按此因子缩放。在较低分辨率下,检测到的线条较少。
注意
cv::Ptr<>类型是一个用于包装指针的模板类。这个模板在 2.x API 中可用,以方便使用引用计数进行自动释放。cv::Ptr<>类型类似于std::unique_ptr。
检测本身是通过LineSegmentDetector::detect(const InputArray _image, OutputArray _lines, OutputArray width=noArray(), OutputArray prec=noArray(), OutputArraynfa=noArray())方法完成的。第一个参数是输入图像,而_lines数组将被填充为一个(STL)Vec4i对象的向量,该对象表示线条一端的(x, y)位置,然后是另一端的位置。可选参数width、prec和noArray返回有关检测到的线条的附加信息。第一个参数width包含估计的线条宽度。可以使用方便的(尽管简单)方法LineSegmentDetector::drawSegments(InputOutputArray _image, InputArray lines)绘制线条。线条将绘制在输入图像上,即_image。
以下lineSegmentDetector示例显示了检测器的实际应用:
#include "opencv2/opencv.hpp"
#include <iostream>
using namespace std;
using namespace cv;
vector<Vec4i> lines;
vector<float> widths;
Mat input_image, output;
inline float line_length(const Point &a, const Point &b)
{
return (sqrt((b.x-a.x)*(b.x-a.x) + (b.y-a.y)*(b.y-a.y)));
}
void MyDrawSegments(Mat &image, const vector<Vec4i>&lines, const vector<float>&widths,
const Scalar& color, const float length_threshold)
{
Mat gray;
if (image.channels() == 1)
{
gray = image;
}
else if (image.channels() == 3)
{
cvtColor(image, gray, COLOR_BGR2GRAY);
}
// Create a 3 channel image in order to draw colored lines
std::vector<Mat> planes;
planes.push_back(gray);
planes.push_back(gray);
planes.push_back(gray);
merge(planes, image);
// Draw segments if length exceeds threshold given
for(int i = 0; i < lines.size(); ++i)
{
const Vec4i& v = lines[i];
Point a(v[0], v[1]);
Point b(v[2], v[3]);
if (line_length(a,b) > length_threshold) line(image, a, b, color, widths[i]);
}
}
void thresholding(int threshold, void*)
{
input_image.copyTo(output);
MyDrawSegments(output, lines, widths, Scalar(0, 255, 0), threshold);
imshow("Detected lines", output);
}
int main(int argc, char** argv)
{
input_image = imread("building.jpg", IMREAD_GRAYSCALE);
// Create an LSD detector object
Ptr<LineSegmentDetector> ls = createLineSegmentDetector();
// Detect the lines
ls->detect(input_image, lines, widths);
// Create window to show found lines
output=input_image.clone();
namedWindow("Detected lines", WINDOW_AUTOSIZE);
// Create trackbar for line length threshold
int threshold_value=50;
createTrackbar( "Line length threshold", "Detected lines", &threshold_value, 1000, thresholding );
thresholding(threshold_value, 0);
waitKey();
return 0;
}
上述示例创建了一个包含源图像的窗口,该图像以灰度加载,并显示了drawSegments方法。然而,它允许你施加段长度阈值并指定线条颜色(drawSegments将以红色绘制所有线条)。此外,线条将以检测器估计的宽度绘制。主窗口关联了一个滑块来控制阈值的长度。以下截图显示了示例的输出:

线段检测器示例输出
可以使用函数 HoughCircles(InputArray image, OutputArray circles, int method, double dp, double minDist, double param1=100, double param2=100, int minRadius=0, int maxRadius=0) 来检测圆。第一个参数是一个灰度输入图像。输出参数 circles 将填充一个 Vec3f 对象的向量。每个对象代表一个圆的 (center_x, center_y, radius) 组件。最后两个参数代表最小和最大搜索半径,因此它们会影响检测到的圆的数量。OpenCV 已经包含了这个函数的直接示例,[opencv_source_code]/samples/cpp/houghcircles.cpp。该示例检测半径在 1 到 30 之间的圆,并将它们显示在输入图像上方。
分割算法通常形成连通组件,即在二值图像中的连接像素区域。在下一节中,我们将展示如何从二值图像中获取连通组件及其轮廓。轮廓可以通过现在经典的功能 findContours 获取。该函数的示例可以在参考手册中找到(也请参阅 [opencv_source_code]/samples/cpp/contours2.cpp 和 [opencv_source_code]/samples/cpp/segment_objects.cpp 示例)。此外,请注意,在 OpenCV 3.0 版本(以及在 GitHub 存储库中可用的代码)中,ShapeDistanceExtractor 类允许您将轮廓与形状上下文描述符(一个示例在 [opencv_source_code]/samples/cpp/shape_example.cpp)和 Hausdorff 距离进行比较。这个类在库的新模块 shape 中。形状变换也可以通过 ShapeTransformer 类(示例,[opencv_source_code]/samples/cpp/shape_transformation.cpp)进行。
新的函数 connectedComponents 和 connectedComponentsWithStats 获取连通组件。这些函数将是 3.0 版本的一部分,并且已经在 GitHub 存储库中可用。OpenCV 包含了一个示例,展示了如何使用第一个函数,[opencv_source_code]/samples/cpp/connected_components.cpp。
注意
实际上,标记功能性的连通组件在之前的 OpenCV 2.4.x 版本中被移除,现在又重新添加。
我们还提供了一个示例 (connectedComponents),展示了如何使用第二个函数,int connectedComponentsWithStats(InputArray image, OutputArray labels, OutputArray stats, OutputArray centroids, int connectivity=8, intltype=CV_32S),该函数提供了有关每个连通组件的有用统计信息。这些统计信息可以通过 stats(label, column) 访问,其中列可以是以下表格:
CC_STAT_LEFT |
在水平方向上,边界框的起始(x)坐标,即最左边的坐标 |
|---|---|
CC_STAT_TOP |
在垂直方向上,边界框的起始(y)坐标,即最顶部的坐标 |
CC_STAT_WIDTH |
矩形的水平尺寸 |
CC_STAT_HEIGHT |
矩形的垂直尺寸 |
CC_STAT_AREA |
连通组件的总面积(以像素为单位) |
以下为示例代码:
#include <opencv2/core/utility.hpp>
#include "opencv2/imgproc.hpp"
#include "opencv2/highgui.hpp"
#include <iostream>
using namespace cv;
using namespace std;
Mat img;
int threshval = 227;
static void on_trackbar(int, void*)
{
Mat bw = threshval < 128 ? (img < threshval) : (img > threshval);
Mat labelImage(img.size(), CV_32S);
Mat stats, centroids;
int nLabels = connectedComponentsWithStats(bw, labelImage, stats, centroids);
// Show connected components with random colors
std::vector<Vec3b> colors(nLabels);
colors[0] = Vec3b(0, 0, 0);//background
for(int label = 1; label < nLabels; ++label){
colors[label] = Vec3b( (rand()&200), (rand()&200), (rand()&200) );
}
Mat dst(img.size(), CV_8UC3);
for(int r = 0; r < dst.rows; ++r){
for(int c = 0; c < dst.cols; ++c){
int label = labelImage.at<int>(r, c);
Vec3b &pixel = dst.at<Vec3b>(r, c);
pixel = colors[label];
}
}
// Text labels with area of each cc (except background)
for (int i=1; i< nLabels;i++)
{
float a=stats.at<int>(i,CC_STAT_AREA);
Point org(centroids.at<double>(i,0), centroids.at<double>(i,1));
String txtarea;
std::ostringstream buff;
buff << a;
txtarea=buff.str();
putText( dst, txtarea, org,FONT_HERSHEY_COMPLEX_SMALL, 1, Scalar(255,255,255), 1);
}
imshow( "Connected Components", dst );
}
int main( int argc, const char** argv )
{
img = imread("stuff.jpg", 0);
namedWindow( "Connected Components", 1 );
createTrackbar( "Threshold", "Connected Components", &threshval, 255, on_trackbar );
on_trackbar(threshval, 0);
waitKey(0);
return 0;
}
前面的示例创建了一个带有相关滑块的窗口。滑块控制应用于源图像的阈值。在 on_trackbar 函数内部,使用阈值的结果调用 connectedComponentsWithStats。这之后是代码的两个部分。第一部分用随机颜色填充对应于每个连通组件的像素。属于每个组件的像素在 labelImage 中(labelImage 输出也由 connectedComponents 函数提供)。第二部分显示每个组件的面积文本。此文本位于每个组件的重心上。以下截图显示了示例的输出:

连通组件示例的输出
洪水填充
洪水填充操作用给定的颜色填充连通组件。从种子点开始,相邻像素用统一颜色着色。相邻像素可以位于当前像素指定范围内。洪水填充函数为 int floodFill(InputOutputArray image, Point seedPoint, Scalar newVal, Rect* rect=0, Scalar loDiff=Scalar(), Scalar upDiff=Scalar(),int flags=4)。参数 loDiff 和 upDiff 表示检查每个相邻像素的范围(注意可以指定 3 通道的差异阈值)。参数 newVal 是应用于范围内的像素的颜色。参数 flags 的较低部分包含要使用的像素连通性值(4 或 8)。较高部分定义了操作模式。
根据此模式,洪水填充函数将在输入图像中着色相邻像素,如果它位于当前像素或原始种子值的指定范围内(由 loDiff 和 upDiff 给出),或者如果相邻像素位于原始种子值的指定范围内。该函数还可以用掩码图像作为第二个参数调用。如果指定,则填充操作不会跨越掩码中的非零像素。请注意,掩码应是一个比输入图像宽 2 像素、高 2 像素的单通道 8 位图像。
flags 的最高位可以是 0 或以下组合之一:
-
FLOODFILL_FIXED_RANGE: 如果设置,则考虑当前像素和种子像素之间的差异。否则,考虑相邻像素之间的差异。 -
FLOODFILL_MASK_ONLY: 如果设置,则函数不更改图像(忽略newVal),但填充掩码。
在 OpenCV 的洪水填充示例([opencv_source_code]/samples/cpp/ffilldemo.cpp)中,掩码仅用作输出参数。在我们的floodFill示例中,如下所示,我们将将其用作输入参数以限制填充。想法是使用边缘检测器的输出作为掩码。这应该在边缘处停止填充过程:
#include "opencv2/opencv.hpp"
#include <iostream>
using namespace std;
using namespace cv;
Mat image, image1, image_orig;
int loDiff = 20, upDiff = 30;
int loCanny=10, upCanny=150;
void onMouse( int event, int x, int y, int, void* )
{
if( event != CV_EVENT_LBUTTONDOWN ) return;
Point seed = Point(x,y);
int flags = 4 + CV_FLOODFILL_FIXED_RANGE;
int b = (unsigned)theRNG() & 255;
int g = (unsigned)theRNG() & 255;
int r = (unsigned)theRNG() & 255;
Rect ccomp;
Scalar newVal = Scalar(b, g, r);
Mat dst = image;
// flood fill
floodFill(dst, seed, newVal, &ccomp, Scalar(loDiff, loDiff, loDiff), Scalar(upDiff, upDiff, upDiff), flags);
imshow("image", dst);
// Using Canny edges as mask
Mat mask;
Canny(image_orig, mask, loCanny, upCanny);
imshow("Canny edges", mask);
copyMakeBorder(mask, mask, 1, 1, 1, 1, cv::BORDER_REPLICATE);
Mat dst1 = image1;
floodFill(dst1, mask, seed, newVal, &ccomp, Scalar(loDiff, loDiff, loDiff), Scalar(upDiff, upDiff, upDiff), flags);
imshow("FF with Canny", dst1);
moveWindow("Canny edges", image.cols,0);
moveWindow("FF with Canny", 2*image.cols,0);
}
int main(int argc, char *argv[])
{
// Read original image and clone it to contain results
image = imread("lena.jpg", CV_LOAD_IMAGE_COLOR );
image_orig=image.clone();
image1=image.clone();
namedWindow( "image", WINDOW_AUTOSIZE );
imshow("image", image);
createTrackbar( "lo_diff", "image", &loDiff, 255, 0 );
createTrackbar( "up_diff", "image", &upDiff, 255, 0 );
createTrackbar( "lo_Canny", "image", &loCanny, 255, 0 );
createTrackbar( "up_Canny", "image", &upCanny, 255, 0 );
setMouseCallback( "image", onMouse, 0 );
moveWindow("image", 0,0);
cout << "Press any key to exit...\n";
waitKey(); // Wait for key press
return 0;
}
上述示例读取并显示一个彩色图像,然后创建四个滑块。前两个滑块控制floodFill函数的loDiff和upDiff值。其他两个滑块控制 Canny 边缘检测器的上下阈值参数。在这个示例中,用户可以在输入图像的任何地方点击。点击位置将被用作种子点以执行洪水填充操作。实际上,每次点击都会调用两次floodFill函数。第一个函数简单地使用随机颜色填充一个区域。第二个函数使用由 Canny 边缘检测器输出创建的掩码。请注意,copyMakeBorder函数是必要的,以便在掩码周围形成 1 像素宽的边界。以下截图显示了此示例的输出:

洪水填充示例的输出
注意,使用 Canny 边缘(右侧)的输出填充的像素比标准操作(左侧)少。
流域分割
流域分割是一种以其效率而闻名的分割方法。该方法本质上从用户指定的起始(种子)点开始,区域从这些点生长。假设可以提供良好的起始种子,则生成的分割对于许多用途都是有用的。
注意
关于图像分割中流域变换的更多细节和示例,请参阅cmm.ensmp.fr/~beucher/wtshed.html。
函数watershed(InputArray image, InputOutputArray markers)接受一个 3 通道输入图像和一个带有种子的markers图像。后者必须是一个 32 位单通道图像。种子可以指定为markers中的正值连接组件(0 不能用作种子的值)。作为输出参数,markers中的每个像素将被设置为种子组件的值或区域之间的边界处的-1。OpenCV 包含一个流域示例([opencv_source_code]/samples/cpp/watershed.cpp),其中用户需要绘制种子的区域。
显然,种子区域的选取很重要。理想情况下,种子应该自动选择,无需用户干预。水系分割的一个典型应用是首先对图像进行阈值处理,以将对象与背景分开,然后应用距离变换,最后使用距离变换图像的局部最大值作为分割的种子点。然而,第一个阈值步骤是关键的,因为对象的一部分可能被视为背景。在这种情况下,对象种子区域将太小,分割效果会较差。另一方面,为了执行水系分割,我们还需要背景的种子。虽然我们可以使用图像角落的点作为种子,但这将不足以满足需求。在这种情况下,背景种子区域太小。如果我们使用这些种子,分割得到的对象区域通常会比实际对象大得多。在我们接下来的 watershed 示例中,采用了一种不同的方法,可以得到更好的结果:
#include <opencv2/core/utility.hpp>
#include "opencv2/imgproc.hpp"
#include "opencv2/highgui.hpp"
#include "opencv2/core.hpp"
#include <iostream>
using namespace std;
using namespace cv;
void Watershed(const Mat &src)
{
Mat dst=src.clone();
// Flood fill outer part of the image
Point seed(0,0); // top-left corner
int loDiff=20;
int upDiff=20;
int flags=4 + FLOODFILL_FIXED_RANGE + FLOODFILL_MASK_ONLY + (255<<8);
Mat mask(src.size(), CV_8UC1);
mask.setTo(0);
copyMakeBorder(mask, mask, 1, 1, 1, 1, cv::BORDER_REPLICATE);
Scalar newVal;
Rect ccomp;
floodFill(dst, mask, seed, newVal, &ccomp,
Scalar(loDiff, loDiff, loDiff), Scalar(upDiff, upDiff, upDiff), flags);
// Flood fill inner part of the image
seed.x=(float)src.cols/2; // image center x
seed.y=(float)src.rows/2; // image center y
Mat mask1=mask.clone();
mask1.setTo(0);
floodFill(dst, mask1, seed, newVal, &ccomp,
Scalar(loDiff, loDiff, loDiff), Scalar(upDiff, upDiff, upDiff), flags);
// Form image with the two seed regions
Mat Mask = mask.clone();
mask=mask/2;
Mask = mask | mask1;
imshow("Seed regions", Mask);
moveWindow("Seed regions", src.cols, 0);
// Perform watershed
Mat labelImage(src.size(), CV_32SC1);
labelImage=Mask(Rect(1,1, src.cols, src.rows));
labelImage.convertTo(labelImage, CV_32SC1);
watershed(src, labelImage);
labelImage.convertTo(labelImage, CV_8U);
imshow("Watershed", labelImage);
moveWindow("Watershed", 2*src.cols, 0);
}
int main(int argc, char *argv[])
{
// Read original image and clone it to contain results
Mat src = imread("hand_sample2.jpg", IMREAD_COLOR );
// Create 3 windows
namedWindow("Source", WINDOW_AUTOSIZE);
imshow("Source", src);
Watershed(src);
// Position windows on screen
moveWindow("Source", 0,0);
cout << "Press any key to exit...\n";
waitKey(); // Wait for key press
return 0;
}
上述代码中的 Watershed 函数执行了三个步骤。首先,通过执行淹没填充来获得背景种子区域。淹没填充的种子是图像的左上角,即像素 (0, 0)。接下来,执行另一个淹没填充来获得对象的(在示例图像中的手)种子区域。这个淹没填充的种子取为图像的中心。然后,通过执行前两个淹没填充结果的 OR 操作来形成一个种子区域图像。这个结果图像被用作水系操作的种子图像。以下截图显示了种子图像位于图中心的输出示例:

水系分割示例的输出
GrabCut
GrabCut 是自 OpenCV 2.1 版本以来可用的一种优秀的迭代背景/前景分割算法。GrabCut 特别适用于在最少额外信息(大多数情况下,一个边界矩形就足够了)的情况下将对象从背景中分离出来。然而,它计算量很大,因此仅适用于分割静态图像。
注意
GrabCut 是 Microsoft Office 2010 中背景去除工具的底层算法。该算法最初由微软研究院剑桥的研究人员提出。该算法从用户提供的要分割的对象的边界框开始,估计目标对象和背景的颜色分布。这个估计通过最小化一个能量函数来进一步细化,其中具有相同标签的连接区域获得更多的权重。
主要功能是 grabCut(InputArray img, InputOutputArray mask, Rect rect, InputOutputArray bgdModel, InputOutputArray fgdModel, int iterCount, int mode=GC_EVAL)。参数 bgdModel 和 fgdModel 仅由函数内部使用(尽管它们必须被声明)。iterCount 变量是要执行的迭代次数。根据我们的经验,算法的迭代次数很少就能产生良好的分割效果。算法由一个边界矩形、一个掩码图像或两者共同辅助。所选择的选项由 mode 参数指示,可以是 GC_INIT_WITH_RECT、GC_INIT_WITH_MASK 或两者的 OR 组合。在前一种情况下,rect 定义了矩形。矩形外的像素被视为明显的背景。在后一种情况下,掩码是一个 8 位图像,其中像素可能具有以下值:
-
GC_BGD:这定义了一个明显的背景像素 -
GC_FGD:这定义了一个明显的背景(对象)像素 -
GC_PR_BGD:这定义了一个可能的背景像素 -
GC_PR_FGD:这定义了一个可能的背景像素
图像掩码也是输出图像,其中包含使用那些相同的前值得到的分割结果。OpenCV 包含了一个 GrabCut 的示例([opencv_source_code]/samples/cpp/grabcut.cpp),用户可以在其中绘制边界矩形以及前景和背景像素。
下面的 grabcut 示例使用初始边界矩形算法,然后将结果前景复制到同一图像的另一个位置:
#include "opencv2/opencv.hpp"
#include <iostream>
using namespace std;
using namespace cv;
int main(int argc, char *argv[])
{
// Read original image and clone it
Mat src = imread("stuff.jpg" );
Mat tgt = src.clone();
// Create source window
namedWindow("Source", WINDOW_AUTOSIZE);
imshow("Source", src);
moveWindow("Source", 0,0);
// GrabCut segmentation
Rect rectangle(180,279,60,60); // coin position
Mat result; // segmentation result
Mat bgModel,fgModel; // used internally
grabCut(src, result, rectangle, bgModel,fgModel, 1, GC_INIT_WITH_RECT);
result=(result & GC_FGD); // leave only obvious foreground
// Translation operation
Mat aff=Mat::eye(2,3,CV_32FC1);
aff.at<float>(0,2)=50;
warpAffine(tgt, src, aff, result.size());
warpAffine(result, result, aff, result.size());
src.copyTo(tgt, result);
// Show target window
imshow("Target", tgt);
moveWindow("Target", src.cols, 0);
cout << "Press any key to exit...\n";
waitKey(); // Wait for key press
return 0;
}
上述示例简单地使用源图像中硬币周围的固定矩形(参见本章第五个截图)进行分割。result 图像将包含介于 0 (GC_BGD) 和 3 (GC_PR_FGD) 之间的值。接下来的 AND 操作将非 GC_FGD 值转换为零,从而得到一个二值前景掩码。然后,源图像和掩码在水平方向上各移动 50 像素。使用一个仅改变 x 平移组件的单位矩阵进行仿射变换操作。
最后,将转换后的图像复制到目标图像上,使用(也转换过的)掩码。以下截图显示了源图像和目标图像。在这个特定示例中,增加迭代次数没有产生任何显著的影响:

GrabCut 示例中的源图像和目标图像
摘要
本章涵盖了计算机视觉中最重要的话题之一。分割通常是第一步,也是通常最棘手的一步。在本章中,我们向读者提供了洞察力和示例,以使用 OpenCV 中最有用的分割方法,例如阈值、轮廓和连通组件、区域填充、分水岭分割方法和 GrabCut 方法。
还有什么?
均值漂移分割(函数pyrMeanShiftFiltering)已被省略。OpenCV 包含一个示例,展示了如何使用此函数([opencv_source_code]/samples/cpp/meanshift_segmentation.cpp)。然而,这种方法相对较慢,并且倾向于产生过度分割的结果。
背景前景分割也可以通过视频实现,这将在第七章(part0055_split_000.html#page "Chapter 7. What Is He Doing? Motion")他在做什么?运动中介绍。
第五章. 关注有趣的 2D 特征
在大多数图像中,最有用的信息通常位于某些区域,这些区域通常对应于显著点和区域。在大多数应用中,只要这些点稳定且独特,对这些显著点周围的局部处理就足够了。在本章中,我们将介绍 OpenCV 提供的 2D 显著点和特征的基本概念。需要注意的是,检测器和描述符之间的区别。检测器仅从图像中提取兴趣点(局部特征),而描述符则获取这些点的邻域的相关信息。描述符,正如其名称所示,通过适当的特征来描述图像。它们以一种对光照变化和小型透视变形不变的方式描述兴趣点。这可以用来将它们与其他描述符(通常从其他图像中提取)匹配。为此,使用匹配器。这反过来可以用来检测对象并推断两张图像之间的相机变换。首先,我们展示兴趣点的内部结构,并解释 2D 特征和描述符提取。最后,本章处理匹配问题,即把不同图像的 2D 特征对应起来。
兴趣点
局部特征,也称为兴趣点,其特点是区域强度突然变化。这些局部特征通常分为边缘、角点和块。OpenCV 在KeyPoint类中封装了有趣点信息,该类包含以下数据:
-
兴趣点的坐标(
Point2f类型) -
有意义的关键点邻域的直径
-
关键点的方向
-
关键点的强度,这取决于所选的关键点检测器
-
提取关键点的金字塔层(八度);八度在
SIFT、SURF、FREAK或BRISK等一些描述符中使用 -
用于执行聚类的对象 ID
特征检测器
OpenCV 通过FeatureDetector抽象类及其Ptr<FeatureDetector> FeatureDetector::create(const string& detectorType)方法或直接通过算法类处理多个局部特征检测器的实现。在第一种情况下,指定了检测器的类型(以下图中用红色标出了本章中使用的检测器)。检测器和它们检测的局部特征类型如下:
-
FAST(FastFeatureDetector): 这种特征检测器用于检测角点和块 -
STAR(StarFeatureDetector): 这种特征检测器用于检测边缘、角点和块 -
SIFT(SiftFeatureDetector): 这种特征检测器用于检测角点和块(nonfree模块的一部分) -
SURF(SurfFeatureDetector): 这种特征检测器用于检测角点和块(nonfree模块的一部分) -
ORB(OrbFeatureDetector): 这种特征检测器用于检测角点和块 -
BRISK(BRISK): 这个特征检测器能够检测角点和块状物 -
MSER(MserFeatureDetector): 这个特征检测器能够检测块状物 -
GFTT(GoodFeaturesToTrackDetector): 这个特征检测器能够检测边缘和角点 -
HARRIS(GoodFeaturesToTrackDetector): 这个特征检测器能够检测边缘和角点(启用 Harris 检测器) -
Dense(DenseFeatureDetector): 这个特征检测器能够检测图像上分布密集且规则的特性 -
SimpleBlob(SimpleBlobDetector): 这个特征检测器能够检测块状物

OpenCV 中的 2D 特征检测器
我们应该注意,其中一些检测器,如 SIFT、SURF、ORB 和 BRISK,也是描述符。
通过 void FeatureDetector::detect(const Mat& image, vector<KeyPoint>& keypoints, const Mat& mask) 函数执行关键点检测,这是 FeatureDetector 类的另一种方法。第一个参数是输入图像,其中将检测关键点。第二个参数对应于存储关键点的向量。最后一个参数是可选的,它代表一个输入掩码图像,我们可以指定在其中查找关键点。
注意
Matthieu Labbé 实现了一个基于 Qt 的开源应用程序,你可以在这个应用程序中测试 OpenCV 的角点检测器、特征提取器和匹配算法,界面友好。它可在 code.google.com/p/find-object/ 查找。
历史上,第一个兴趣点是角点。1977 年,Moravec 将角点定义为在多个方向(45 度)上存在较大强度变化的兴趣点。Moravec 使用这些兴趣点在连续图像帧中找到匹配区域。后来,在 1988 年,Harris 使用泰勒展开来近似平移强度变化,从而改进了 Moravec 的算法。之后,出现了其他检测器,如基于高斯差分(DoG)和 Hessian 矩阵行列式(DoH)(例如,SIFT 或 SURF 分别)的检测器,或者基于 Moravec 算法的检测器,但考虑像素邻域中的连续强度值,如 FAST 或 BRISK(尺度空间 FAST)。
注意
卢在她的个人博客 LittleCheeseCake 中详细解释了一些最受欢迎的检测器和描述符。博客可在 littlecheesecake.me/blog/13804625/feature-detectors-and-descriptors 查阅。
The FAST detector
角点检测器基于加速段测试(FAST)算法。它被设计得非常高效,针对实时应用。该方法基于考虑候选角点 p 周围的 16 像素(邻域)圆。如果邻域中存在一组连续像素,这些像素都比 p+T 亮或比 p-T 暗,则 FAST 检测器将考虑 p 为角点,其中 T 是一个阈值值。这个阈值必须适当选择。
OpenCV 在 FastFeatureDetector() 类中实现了 FAST 检测器,这是一个 FAST() 方法的包装类。要使用此类,我们必须在我们的代码中包含 features2d.hpp 头文件。
接下来,我们展示一个代码示例,其中使用不同阈值的 FAST 方法检测角点。以下展示了 FASTDetector 代码示例:
#include "opencv2/core/core.hpp"
#include "opencv2/highgui/highgui.hpp"
#include "opencv2/imgproc/imgproc.hpp"
#include "opencv2/features2d/features2d.hpp"
#include <iostream>
using namespace std;
using namespace cv;
int main(int argc, char *argv[])
{
//Load original image and convert to gray scale
Mat in_img = imread("book.png");
cvtColor( in_img, in_img, COLOR_BGR2GRAY );
//Create a keypoint vectors
vector<KeyPoint> keypoints1,keypoints2;
//FAST detector with threshold value of 80 and 100
FastFeatureDetector detector1(80);
FastFeatureDetector detector2(100);
//Compute keypoints in in_img with detector1 and detector2
detector1.detect(in_img, keypoints1);
detector2.detect(in_img, keypoints2);
Mat out_img1, out_img2;
//Draw keypoints1 and keypoints2
drawKeypoints(in_img,keypoints1,out_img1,Scalar::all(-1),0);
drawKeypoints(in_img,keypoints2,out_img2,Scalar::all(-1),0);
//Show keypoints detected by detector1 and detector2
imshow( "out_img1", out_img1 );
imshow( "out_img2", out_img2 );
waitKey(0);
return 0;
}
代码的解释如下。在本例及以下示例中,我们通常执行以下三个步骤:
-
创建 2D 特征检测器。
-
在图像中检测关键点。
-
绘制获得的关键点。
在我们的示例中,FastFeatureDetector(int threshold=1, bool nonmaxSuppression= true, type=FastFeatureDetector::TYPE_9_16) 是定义检测器参数(如阈值值、非最大值抑制和邻域)的函数。
可以选择以下三种类型的邻域:
-
FastFeatureDetector::TYPE_9_16 -
FastFeatureDetector::TYPE_7_12 -
FastFeatureDetector::TYPE_5_8
这些邻域定义了考虑角点(关键点)有效所需的邻居数量(16、12 或 8)和连续像素的总数(9、7 或 5)。以下截图展示了 TYPE_9_16 的示例。
在我们的代码中,已选择阈值值 80 和 100,而其余参数使用默认值,nonmaxSuppression=true 和 type=FastFeatureDetector::TYPE_9_16,如下所示:
FastFeatureDetector detector1(80);
FastFeatureDetector detector2(100);
使用 void detect(const Mat& image, vector<KeyPoint>& keypoints, const Mat& mask=Mat()) 函数检测并保存关键点。在我们的情况下,我们创建了以下两个 FAST 特征检测器:
-
detector1将其关键点保存到keypoints1向量中 -
detector2将其关键点保存到keypoints2
void drawKeypoints(const Mat& image, const vector<KeyPoint>& keypoints, Mat& outImage, const Scalar& color=Scalar::all(-1), int flags=DrawMatchesFlags::DEFAULT) 函数用于在图像中绘制关键点。color 参数允许我们定义关键点的颜色,使用 Scalar:: all(-1) 选项,每个关键点将以不同的颜色绘制。
使用图像上的两个阈值值绘制关键点。我们会注意到检测到的关键点数量存在细微差异。这是由于每个情况下的阈值值不同。以下截图显示了在阈值为 80 的样本中检测到的角点,而在阈值为 100 时则未检测到:

使用阈值为 80(在左侧)检测到的关键点。相同的角点在阈值为 100(在右侧)时没有被检测到。
差异是由于 FAST 特征检测器是以默认类型创建的,即 TYPE_9_16。在示例中,p 像素取值为 228,因此至少需要九个连续像素比 p+T 更亮或比 p-T 更暗。以下截图显示了此特定关键点的邻域像素值。如果使用阈值为 80,则满足九个连续像素的条件。然而,使用阈值为 100 时,条件不满足:

关键点像素值和所有连续像素都比 p-T (228-80=148) 深的像素值,阈值为 80
SURF 检测器
加速鲁棒特征(SURF)检测器基于 Hessian 矩阵来寻找兴趣点。为此,SURF 使用二阶高斯核将图像划分为不同的尺度(级别和八度),并使用简单的盒式滤波器来近似这些核。这个滤波器盒在尺度和空间上主要进行插值,以便为检测器提供尺度不变性属性。SURF 是经典 尺度不变特征变换(SIFT)检测器的快速近似。SURF 和 SIFT 检测器都是受专利保护的,因此 OpenCV 在其 nonfree/nonfree.hpp 头文件中分别包含它们。
下面的 SURFDetector 代码示例展示了使用不同数量的高斯金字塔八度来检测关键点的例子:
//… (omitted for simplicity)
#include "opencv2/nonfree/nonfree.hpp"
int main(int argc, char *argv[])
{
//Load image and convert to gray scale (omitted for
//simplicity)
//Create a keypoint vectors
vector<KeyPoint> keypoints1,keypoints2;
//SURF detector1 and detector2 with 2 and 5 Gaussian pyramid
//octaves respectively
SurfFeatureDetector detector1(3500, 2, 2, false, false);
SurfFeatureDetector detector2(3500, 5, 2, false, false);
//Compute keypoints in in_img with detector1 and detector2
detector1.detect(in_img, keypoints1);
detector2.detect(in_img, keypoints2);
Mat out_img1, out_img2;
//Draw keypoints1 and keypoints2
drawKeypoints(in_img,keypoints1,out_img1,Scalar::all(-1), DrawMatchesFlags::DRAW_RICH_KEYPOINTS);
drawKeypoints(in_img,keypoints2,out_img2,Scalar::all(-1), DrawMatchesFlags::DRAW_RICH_KEYPOINTS);
//Show the 2 final images (omitted for simplicity)
return 0;
}
注意
在前面的示例(以及随后的示例)中,为了简单起见,没有重复某些代码部分,因为它们与前面的示例相同。
代码的解释如下。SURFFeatureDetector(double hessianThreshold, int nOctaves, int nOctaveLayers, bool extended, bool upright) 是创建 SURF 检测器的主要函数,其中我们可以定义检测器的参数值,例如 Hessian 阈值、高斯金字塔八度的数量、高斯金字塔中每个八度的图像数量、描述符中的元素数量以及每个特征的朝向。
高阈值值提取的关键点较少,但精度更高。低阈值值提取的关键点较多,但精度较低。在这种情况下,我们使用了一个较大的 Hessian 阈值(3500),以在图像中显示较少的关键点。此外,每个图像的八度数也会变化(分别为 2 和 5)。更多的八度也会选择更大尺寸的关键点。以下截图显示了结果:

左侧为具有两个高斯金字塔八度的 SURF 检测器,右侧为具有五个高斯金字塔八度的 SURF 检测器
再次提醒,我们使用drawKeypoints函数来绘制检测到的关键点,但在此情况下,由于 SURF 检测器具有方向属性,DrawMatchesFlags参数被定义为DRAW_RICH_KEYPOINTS。然后,drawKeypoints函数会绘制每个关键点及其大小和方向。
ORB 检测器
二进制鲁棒独立基本特征(BRIEF)是一种基于二进制字符串的描述符;它不寻找兴趣点。方向快速和旋转 BRIEF(ORB)检测器是 FAST 检测器和 BRIEF 描述符的联合,被认为是专利 SIFT 和 SURF 检测器的替代品。ORB 检测器使用带有金字塔的 FAST 检测器来检测兴趣点,然后使用 HARRIS 算法来排序特征并保留最佳特征。OpenCV 还允许我们使用 FAST 算法来排序特征,但通常这会产生更不稳定的特征点。下面的ORBDetector代码展示了这种差异的一个简单且清晰的例子:
int main(int argc, char *argv[])
{
//Load image and convert to gray scale (omitted for
//simplicity)
//Create a keypoint vectors
vector<KeyPoint> keypoints1,keypoints2;
//ORB detector with FAST (detector1) and HARRIS (detector2)
//score to rank the features
OrbFeatureDetector detector1(300, 1.1f, 2, 31,0, 2, ORB::FAST_SCORE, 31);
OrbFeatureDetector detector2(300, 1.1f, 2, 31,0, 2, ORB::HARRIS_SCORE, 31);
//Compute keypoints in in_img with detector1 and detector2
detector1.detect(in_img, keypoints1);
detector2.detect(in_img, keypoints2);
Mat out_img1, out_img2;
//Draw keypoints1 and keypoints2
drawKeypoints(in_img,keypoints1,out_img1,Scalar::all(-1), DrawMatchesFlags::DEFAULT);
drawKeypoints(in_img,keypoints2,out_img2,Scalar::all(-1), DrawMatchesFlags::DEFAULT);
//Show the 2 final images (omitted for simplicity)
return 0;
}

使用 FAST 算法选择 300 个最佳特征(在左侧)和使用 HARRIS 检测器选择 300 个最佳特征(在右侧)的 ORB 检测器
以下是代码的解释。OrbFeatureDetector(int nfeatures=500, float scaleFactor=1.2f, int nlevels=8, int edgeThreshold=31, int firstLevel=0, int WTA_K=2, int scoreType=ORB:: HARRIS_SCORE, int patchSize=31)函数是类构造函数,其中我们可以指定要保留的最大特征数、缩放比例、级别数以及用于排序特征的检测器类型(HARRIS_SCORE或FAST_SCORE)。
以下提出的代码示例显示了 HARRIS 和 FAST 算法在排序特征方面的差异;结果如前截图所示:
OrbFeatureDetector detector1(300, 1.1f, 2, 31,0, 2, ORB::FAST_SCORE, 31);
OrbFeatureDetector detector2(300, 1.1f, 2, 31,0, 2, ORB::HARRIS_SCORE, 31);
HARRIS 角点检测器在特征排序方面比 FAST 检测器使用得更频繁,因为它能够拒绝边缘并提供一个合理的分数。其余的功能与之前的检测器示例相同,包括关键点检测和绘制。
KAZE 和 AKAZE 检测器
KAZE 和 AKAZE 检测器将被包含在即将发布的 OpenCV 3.0 版本中。
小贴士
OpenCV 3.0 版本尚未可用。再次提醒,如果您想测试此代码并使用 KAZE 和 AKAZE 特征,您可以使用 OpenCV git 仓库中最新可用的版本,网址为code.opencv.org/projects/opencv/repository。
KAZE 检测器是一种可以在非线性尺度空间中检测 2D 特征的方法。这种方法允许我们保留重要的图像细节并去除噪声。加性算子分裂(AOS)方案用于非线性尺度空间。AOS 方案是高效的、稳定的且可并行化。该算法在多个尺度级别计算 Hessian 矩阵的响应以检测关键点。另一方面,加速-KAZE(AKAZE)特征检测器使用快速显式扩散来构建非线性尺度空间。
接下来,在KAZEDetector代码中,我们看到 KAZE 和 AKAZE 特征检测器的新示例:
int main(int argc, char *argv[])
{
//Load image and convert to gray scale (omitted for
//simplicity)
//Create a keypoint vectors
vector<KeyPoint> keypoints1,keypoints2;
//Create KAZE and AKAZE detectors
KAZE detector1(true,true);
AKAZE detector2(cv::AKAZE::DESCRIPTOR_KAZE_UPRIGHT,0,3);
//Compute keypoints in in_img with detector1 and detector2
detector1.detect(in_img, keypoints1);
detector2.detect(in_img, keypoints2,cv::Mat());
Mat out_img1, out_img2;
//Draw keypoints1 and keypoints2
drawKeypoints(in_img,keypoints1,out_img1,Scalar::all(-1), DrawMatchesFlags::DRAW_RICH_KEYPOINTS);
drawKeypoints(in_img,keypoints2,out_img2,Scalar::all(-1), DrawMatchesFlags::DRAW_RICH_KEYPOINTS);
//Show the 2 final images (omitted for simplicity)
return 0;
}
KAZE::KAZE(bool extended, bool upright)函数是 KAZE 类的构造函数,其中可以选择两个参数:extended和upright。extended参数增加了选择 64 或 128 个描述符的选项,而upright参数允许我们选择旋转或无不变性。在这种情况下,我们使用两个参数的true值。
另一方面,AKAZE::AKAZE(DESCRIPTOR_TYPE descriptor_type, int descriptor_size=0, int descriptor_channels=3)函数是 AKAZE 类的构造函数。此函数获取描述符类型、描述符大小和通道作为输入参数。对于描述符类型,应用以下枚举:
enum DESCRIPTOR_TYPE {DESCRIPTOR_KAZE_UPRIGHT = 2, DESCRIPTOR_KAZE = 3, DESCRIPTOR_MLDB_UPRIGHT = 4, DESCRIPTOR_MLDB = 5 };
以下截图显示了使用此示例获得的结果:

KAZE 检测器(在左侧)和 AKAZE 检测器(在右侧)
注意
Eugene Khvedchenya 的计算机视觉讲座博客包含有用的报告,比较了不同关键点的鲁棒性和效率。请参阅以下帖子:computer-vision-talks.com/articles/2012-08-18-a-battle-of-three-descriptors-surf-freak-and-brisk/ 和 computer-vision-talks.com/articles/2011-07-13-comparison-of-the-opencv-feature-detection-algorithms/。
特征描述符提取器
描述符描述局部图像区域,对图像变换(如旋转、缩放或平移)保持不变。它们为兴趣点周围的小区域提供测量和距离函数。因此,每当需要估计两个图像区域之间的相似性时,我们计算它们的描述符并测量它们的距离。在 OpenCV 中,基本 Mat 类型用于表示描述符集合,其中每一行是一个关键点描述符。
使用特征描述符提取器有以下两种可能性:
-
DescriptorExtractor通用接口 -
算法类直接
(请参见以下图表,其中本章使用的描述符用红色标出。)
常见的接口使我们能够轻松地在不同的算法之间切换。在选择算法来解决问题时,这非常有用,因为可以毫不费力地比较每种算法的结果。另一方面,根据算法的不同,有一些参数只能通过其类进行调整。

OpenCV 中的 2D 特征描述符
Ptr<DescriptorExtractor> DescriptorExtractor::create(const String& descriptorExtractorType) 函数创建了一个所选类型的新的描述符提取器。描述符可以分为两类:浮点和二进制。浮点描述符在向量中存储浮点值,这可能导致高内存使用。另一方面,二进制描述符存储二进制字符串,从而实现更快的处理时间和减少内存占用。当前实现支持以下类型:
-
SIFT:此实现支持浮点描述符
-
SURF:此实现支持浮点描述符
-
BRIEF:此实现支持二进制描述符
-
BRISK:此实现支持二进制描述符
-
ORB:此实现支持二进制描述符
-
FREAK:此实现支持二进制描述符
-
KAZE:此实现支持二进制描述符(OpenCV 3.0 新增)
-
AKAZE:此实现支持二进制描述符(OpenCV 3.0 新增)
DescriptorExtractor 的另一个重要功能是 void DescriptorExtractor::compute(InputArray image, vector<KeyPoint>& keypoints, OutputArray descriptors),该功能用于计算在先前步骤中检测到的图像中关键点集的描述符。该函数有一个接受图像集的变体。
提示
注意,可以混合来自不同算法的特征检测器和描述符提取器。然而,建议您使用来自同一算法的两种方法,因为它们应该更适合一起使用。
描述符匹配器
DescriptorMatcher 是一个抽象基类,用于匹配关键点描述符,正如 DescriptorExtractor 所发生的那样,这使得程序比直接使用匹配器更加灵活。使用 Ptr<DescriptorMatcher> DescriptorMatcher::create(const string& descriptorMatcherType) 函数,我们可以创建所需类型的描述符匹配器。以下支持的类型有:
-
BruteForce-L1:这是用于浮点描述符的。它使用 L1 距离,既高效又快速。
-
BruteForce:这是用于浮点描述符的。它使用 L2 距离,可能比 L1 更好,但需要更多的 CPU 使用量。
-
BruteForce-SL2:这是用于浮点描述符的,并避免了从 L2 计算平方根,这需要高 CPU 使用量。
-
BruteForce-Hamming:这是用于二进制描述符的,并计算比较描述符之间的汉明距离。
-
BruteForce-Hamming(2):这是用于二进制描述符(2 位版本)。
-
FlannBased:这用于浮点描述符,并且比暴力搜索更快,因为它通过预先计算加速结构(如数据库引擎)来减少内存使用。
void DescriptorMatcher::match(InputArray queryDescriptors, InputArray trainDescriptors, vector<DMatch>& matches, InputArray mask=noArray()) 和 void DescriptorMatcher::knnMatch(InputArray queryDescriptors, InputArray trainDescriptors, vector<vector<DMatch>>& matches, int k, InputArray mask=noArray(), bool compactResult=false) 函数为每个描述符提供最佳 k 个匹配,对于第一个函数,k 为 1。
void DescriptorMatcher::radiusMatch(InputArray queryDescriptors, InputArray trainDescriptors, vector<vector<DMatch>>& matches, float maxDistance, InputArray mask=noArray(), bool compactResult=false) 函数也找到每个查询描述符的匹配,但不超过指定的距离。这种方法的主要缺点是此距离的幅度未归一化,并且取决于特征提取器和描述符。
小贴士
为了获得最佳结果,我们建议您使用与描述符类型相同的匹配器。尽管可以将二进制描述符与浮点匹配器混合,反之亦然,但结果可能不准确。
匹配 SURF 描述符
SURF 描述符属于方向梯度描述符家族。它们通过方向梯度直方图/Haar-like 特征编码关于补丁中存在的几何形状的统计知识。它们被认为是 SIFT 的更有效替代品。它们是最知名的多尺度特征描述方法,其准确性已经得到广泛测试。尽管如此,它们有两个主要缺点:
-
它们是受专利保护的
-
它们比二进制描述符慢
每个描述符匹配应用程序都有一个通用管道,它使用本章前面解释的组件。它执行以下步骤:
-
在两幅图像中计算兴趣点。
-
从两个生成的兴趣点集中提取描述符。
-
使用匹配器来查找描述符之间的关联。
-
过滤结果以移除不良匹配。
以下是根据此管道的matchingSURF示例:
#include <iostream>
#include "opencv2/core/core.hpp"
#include "opencv2/highgui/highgui.hpp"
#include "opencv2/nonfree/nonfree.hpp"
using namespace std;
using namespace cv;
int main( int argc, char** argv )
{
Mat img_orig = imread( argv[1],IMREAD_GRAYSCALE);
Mat img_fragment = imread( argv[2], IMREAD_GRAYSCALE);
if(img_orig.empty() || img_fragment.empty())
{
cerr << " Failed to load images." << endl;
return -1;
}
//Step 1: Detect keypoints using SURF Detector
vector<KeyPoint> keypoints1, keypoints2;
Ptr<FeatureDetector> detector = FeatureDetector::create("SURF");
detector->detect(img_orig, keypoints1);
detector->detect(img_fragment, keypoints2);
//Step 2: Compute descriptors using SURF Extractor
Ptr<DescriptorExtractor> extractor = DescriptorExtractor::create("SURF");
Mat descriptors1, descriptors2;
extractor->compute(img_orig, keypoints1, descriptors1);
extractor->compute(img_fragment, keypoints2, descriptors2);
//Step 3: Match descriptors using a FlannBased Matcher
Ptr<DescriptorMatcher> matcher = DescriptorMatcher::create("FlannBased");
vector<DMatch> matches12;
vector<DMatch> matches21;
vector<DMatch> good_matches;
matcher->match(descriptors1, descriptors2, matches12);
matcher->match(descriptors2, descriptors1, matches21);
//Step 4: Filter results using cross-checking
for( size_t i = 0; i < matches12.size(); i++ )
{
DMatch forward = matches12[i];
DMatch backward = matches21[forward.trainIdx];
if( backward.trainIdx == forward.queryIdx )
good_matches.push_back( forward );
}
//Draw the results
Mat img_result_matches;
drawMatches(img_orig, keypoints1, img_fragment, keypoints2, good_matches, img_result_matches);
imshow("Matching SURF Descriptors", img_result_matches);
waitKey(0);
return 0;
}
以下是对代码的解释。正如我们之前所描述的,遵循应用程序管道意味着执行以下步骤:
-
首步是在输入图像中检测兴趣点。在这个例子中,使用通用接口通过以下行创建了一个 SURF 检测器:
Ptr<FeatureDetector> detector = FeatureDetector::create("SURF")。 -
之后,检测兴趣点,并使用通用接口创建描述符提取器:
Ptr<DescriptorExtractor> extractor = DescriptorExtractor::create( "SURF")。SURF 算法也用于计算描述符。 -
下一步是匹配两张图像的描述符,为此,也使用通用接口创建了一个描述符匹配器。以下行,
Ptr<DescriptorMatcher> matcher = DescriptorMatcher::create("FlannBased"),创建了一个基于 Flann 算法的新匹配器,该算法用于以下方式匹配描述符:matcher->match(descriptors1, descriptors2, matches12) -
最后,结果被过滤。请注意,计算了两个匹配集,因为之后执行了交叉检查过滤器。这种过滤只存储当使用输入图像作为查询图像和训练图像时出现在两个集合中的匹配:
![使用过滤器丢弃匹配的 SURF 描述符]()
使用和未使用过滤器匹配 SURF 描述符的结果
匹配 AKAZE 描述符
KAZE 和 AKAZE 是即将包含在 OpenCV 3.0 中的新描述符。根据发布的测试,两者在提高重复性和独特性方面都优于库中包含的先前检测器,适用于常见的二维图像匹配应用。AKAZE 比 KAZE 快得多,同时获得相似的结果,因此如果速度在应用中至关重要,应使用 AKAZE。
以下 matchingAKAZE 示例匹配了这种新算法的描述符:
#include <iostream>
#include "opencv2/core/core.hpp"
#include "opencv2/features2d/features2d.hpp"
#include "opencv2/highgui/highgui.hpp"
using namespace cv;
using namespace std;
int main( int argc, char** argv )
{
Mat img_orig = imread( argv[1], IMREAD_GRAYSCALE );
Mat img_cam = imread( argv[2], IMREAD_GRAYSCALE );
if( !img_orig.data || !img_cam.data )
{
cerr << " Failed to load images." << endl;
return -1;
}
//Step 1: Detect the keypoints using AKAZE Detector
Ptr<FeatureDetector> detector = FeatureDetector::create("AKAZE");
std::vector<KeyPoint> keypoints1, keypoints2;
detector->detect( img_orig, keypoints1 );
detector->detect( img_cam, keypoints2 );
//Step 2: Compute descriptors using AKAZE Extractor
Ptr<DescriptorExtractor> extractor = DescriptorExtractor::create("AKAZE");
Mat descriptors1, descriptors2;
extractor->compute( img_orig, keypoints1, descriptors1 );
extractor->compute( img_cam, keypoints2, descriptors2 );
//Step 3: Match descriptors using a BruteForce-Hamming Matcher
Ptr<DescriptorMatcher> matcher = DescriptorMatcher::create("BruteForce-Hamming");
vector<vector<DMatch> > matches;
vector<DMatch> good_matches;
matcher.knnMatch(descriptors1, descriptors2, matches, 2);
//Step 4: Filter results using ratio-test
float ratioT = 0.6;
for(int i = 0; i < (int) matches.size(); i++)
{
if((matches[i][0].distance < ratioT*(matches[i][1].distance)) && ((int) matches[i].size()<=2 && (int) matches[i].size()>0))
{
good_matches.push_back(matches[i][0]);
}
}
//Draw the results
Mat img_result_matches;
drawMatches(img_orig, keypoints1, img_cam, keypoints2, good_matches, img_result_matches);
imshow("Matching AKAZE Descriptors", img_result_matches);
waitKey(0);
return 0;
}
代码的解释如下。前两步与前面的例子非常相似;特征检测器和描述符提取器是通过它们的通用接口创建的。我们只更改传递给构造函数的字符串参数,因为这次使用的是 AKAZE 算法。
注意
这次使用了一个使用汉明距离的 BruteForce 匹配器,因为 AKAZE 是一个二进制描述符。
它是通过执行 Ptr<DescriptorMatcher> matcher = DescriptorMatcher::create("BruteForce-Hamming") 创建的。matcher.knnMatch(descriptors1, descriptors2, matches, 2) 函数计算图像描述符之间的匹配。值得注意的是最后一个整数参数,因为它对于之后执行的过滤器处理是必要的。这种过滤称为比率测试,它计算最佳匹配与次佳匹配之间的良好程度。要被认为是良好的匹配,此值必须高于一定比率,该比率可以在 0 到 1 之间的值范围内设置。如果比率接近 0,描述符之间的对应关系更强。
在以下屏幕截图中,我们可以看到当图像中的书出现旋转时匹配书封面的输出:

在旋转图像中匹配 AKAZE 描述符
以下屏幕截图显示了当第二张图像中没有书出现时的结果:

当训练图像不出现时匹配 AKAZE 描述符
摘要
在本章中,我们介绍了一个广泛使用的 OpenCV 组件。局部特征是相关计算机视觉算法(如物体识别、物体跟踪、图像拼接和相机标定)的关键部分。我们提供了一些介绍和示例,从而涵盖了使用不同算法进行兴趣点检测、从兴趣点提取描述符、匹配描述符以及过滤结果。
还有什么?
强大的词袋模型物体分类框架尚未包括在内。这实际上是我们在本章中介绍内容的一个附加步骤,因为提取的描述符被聚类并用于执行分类。一个完整的示例可以在 [opencv_source_code]/samples/cpp/bagofwords_classification.cpp 找到。
第六章。沃尔在哪里?目标检测
本章解释了如何使用 OpenCV 目标检测模块中包含的不同选项。通过包含的示例代码,可以使用级联和潜在 SVM 检测器,以及为特定目标检测应用创建自定义级联检测器。此外,本章还解释了 OpenCV 3 中包含的新场景文本检测器。
目标检测
目标检测处理的是在图像或视频中定位现实世界对象某一类实例的过程,例如人脸、汽车、行人和建筑物。检测算法通常首先从两组图像中提取特征。其中一组包含目标图像,另一组包含背景图像,其中搜索的对象不存在。然后,基于这些特征对这些检测器进行训练,以识别对象类未来的实例。
注意
指纹识别,现在被集成在一些笔记本电脑和智能手机中,或者大多数数码相机中常见的面部检测,都是目标检测应用的日常例子。
使用 OpenCV 进行对象检测
OpenCV 在其objdetect模块中实现了多种目标检测算法。在这个模块中,实现了级联和潜在 SVM 检测器,以及 OpenCV 3 中新增的场景文本检测器。所有这些算法都相对高效,并能获得准确的结果。
级联很美
大多数目标检测问题,如人脸/人体检测或医学中的病变检测,都需要在许多图像块中搜索目标。然而,检查所有图像区域并为每个区域计算特征集都是耗时的任务。级联检测器因其在此方面的效率高而得到广泛应用。
级联检测器由各种提升阶段组成。提升算法选择最佳特征集来创建和组合多个弱树分类器。因此,提升不仅是一个检测器,也是一种特征选择方法。每个阶段通常训练以正确检测近 100%的对象并丢弃至少 50%的背景图像。因此,背景图像,代表更多的图像,在级联的早期阶段被丢弃时需要更少的时间处理。此外,最终的级联阶段使用比早期阶段更多的特征,即使如此,只有目标和难以识别的背景图像需要更多的时间来评估。
离散 AdaBoost(自适应提升)、真实 AdaBoost、温和 AdaBoost 和 LogitBoost 都在 OpenCV 中作为提升阶段实现。另一方面,可以使用 Haar-like、局部二值模式(LBP)和方向梯度直方图(HOG)特征与不同的提升算法一起使用。
所有这些优点和可用技术使级联在构建实用检测应用方面非常有用。
使用级联进行目标检测
OpenCV 附带了一些预训练的级联检测器,用于最常见的检测问题。它们位于OPENCV_SOURCE\data目录下。以下是一些及其对应的子目录列表:
-
子目录
haarcascades:-
haarcascade_frontalface_default.xml -
haarcascade_eye.xml -
haarcascade_mcs_nose.xml -
haarcascade_mcs_mouth.xml -
haarcascade_upperbody.xml -
haarcascade_lowerbody.xml -
haarcascade_fullbody.xml
-
-
子目录
lbpcascades:-
lbpcascade_frontalface.xml -
lbpcascade_profileface.xml -
lbpcascade_silverware.xml
-
-
子目录
hogcascades:hogcascade_pedestrians.xml
以下pedestrianDetection示例用于说明如何使用级联检测器并在 OpenCV 的视频文件中定位行人:
#include "opencv2/core/core.hpp"
#include "opencv2/objdetect/objdetect.hpp"
#include "opencv2/highgui/highgui.hpp"
#include "opencv2/imgproc/imgproc.hpp"
#include <iostream>
using namespace std;
using namespace cv;
int main(int argc, char *argv[]){
CascadeClassifier cascade(argv[1]);
if (cascade.empty())
return -1;
VideoCapture vid(argv[2]);
if (!vid.isOpened()){
cout<<"Error. The video cannot be opened."<<endl;
return -1;
}
namedWindow("Pedestrian Detection");
Mat frame;
while(1) {
if (!vid.read(frame))
break;
Mat frame_gray;
if(frame.channels()>1){
cvtColor( frame, frame_gray, CV_BGR2GRAY );
equalizeHist( frame_gray, frame_gray );
}else{
frame_gray = frame;
}
vector<Rect> pedestrians;
cascade.detectMultiScale( frame_gray, pedestrians, 1.1, 2, 0, Size(30, 30), Size(150, 150) );
for( size_t i = 0; i < pedestrians.size(); i++ ) {
Point center( pedestrians[i].x +
pedestrians[i].width*0.5,
pedestrians[i].y +
pedestrians[i].height*0.5 );
ellipse( frame, center, Size( pedestrians[i].width*0.5,
pedestrians[i].height*0.5), 0, 0, 360,
Scalar( 255, 0, 255 ), 4, 8, 0 );
}
imshow("Pedestrian Detection", frame);
if(waitKey(100) >= 0)
break;
}
return 0;
}
代码说明如下:
-
CascadeClassifier: 此类提供在处理级联时所需的所有方法。此类的一个对象代表一个训练好的级联检测器。 -
constructor CascadeClassifier:: CascadeClassifier(const string& filename): 此类初始化对象实例并加载存储在系统文件中由变量filename指示的级联检测器的信息。注意
注意,方法
bool CascadeClassifier::load(const string& filename)实际上在构造函数之后隐式调用。 -
bool CascadeClassifier:: empty(): 此方法检查级联检测器是否已加载。 -
cvtColor和equalizeHist:这些方法用于图像灰度转换和直方图均衡。由于级联检测器是用灰度图像训练的,而输入图像可以有不同的格式,因此需要将它们转换为正确的颜色空间并均衡它们的直方图,以获得更好的结果。这是通过以下使用cvtColor和equalizeHist函数的代码完成的:Mat frame_gray; if(frame.channels()>1){ cvtColor( frame, frame_gray, CV_BGR2GRAY ); equalizeHist( frame_gray, frame_gray ); }else{ frame_gray = frame; } -
void CascadeClassifier::detectMultiScale(const Mat& image, vector<Rect>& objects, double scaleFactor=1.1, int minNeighbors=3, int flags=0, Size minSize=Size(), Size maxSize=Size()): 此方法检查image变量中的图像,应用加载的级联,并将所有检测到的对象插入到objects中。检测结果存储在类型为Rect的矩形向量中。参数scaleFactor和minNeighbors表示在考虑的每个图像缩放级别中图像大小减少的程度以及指示正检测的最小邻居数。检测受minSize和maxSize指示的最小和最大尺寸限制。最后,当使用用opencv_traincascade创建的级联时,参数flags不使用。小贴士
在获得存储检测对象的向量后,通过读取每个矩形(由
Rect类的对象表示)的坐标,很容易在原始图像上显示它们,并在指定的区域绘制多边形。
下面的截图显示了将基于 HOG 的预训练行人检测器hogcascade_pedestrians.xml应用于768x576.avi视频帧的结果,该视频存储在OPENCV_SCR/samples文件夹中。

使用 OpenCV 训练的 HOG 级联检测器进行行人检测
有几个项目和贡献解决了 OpenCV 社区中的其他检测相关的问题,这些问题不仅涉及检测对象,还包括区分其状态。这类检测器的一个例子是自版本 2.4.4 以来包含在 OpenCV 中的微笑检测器。代码可以在文件OPENCV_SCR/samples/c/smiledetect.cpp中找到,存储级联检测器的 XML 文件haarcascade_smile.xml可以在OPENCV_SCR/data/haarcascades中找到。此代码首先使用存储在haarcascade_frontalface_alt.xml中的预训练级联检测正面人脸,然后检测图像底部部分的微笑嘴部模式。最后,根据检测到的邻居数量计算微笑的强度。
训练自己的级联
尽管 OpenCV 提供了预训练的级联,但在某些情况下,有必要训练一个级联检测器来寻找特定的对象。对于这些情况,OpenCV 附带了一些工具来帮助训练级联,生成训练过程中所需的所有数据以及包含检测器信息的最终文件。这些文件通常存储在OPENCV_BUILD\install\x64\mingw\bin目录中。以下是一些应用程序的列表:
-
opencv_haartraining:这个应用程序在历史上是创建级联的第一个版本。 -
opencv_traincascade:这个应用程序是创建级联应用程序的最新版本。 -
opencv_createsamples:这个应用程序用于创建包含对象实例的图像的.vec文件。生成的文件被前面的训练可执行文件接受。 -
opencv_performance:这个应用程序可以用来评估使用opencv_haartraining工具训练的级联。它使用一组标记的图像来获取评估信息,例如误报或检测率。
由于opencv_haartraining是较老版本的程序,并且它比opencv_traincascade具有更少的功能,因此这里只描述后者。
在这里,使用 MIT CBCL 人脸数据库解释级联训练过程。该数据库包含 19 x 19 像素的人脸和背景图像,排列方式如以下截图所示:

图片文件组织
注意
本节解释了在 Windows 上的训练过程。对于 Linux 和 Mac OS X,过程类似,但会考虑操作系统的特定方面。有关在 Linux 和 Mac OS X 上训练级联检测器的更多信息,请参阅 opencvuser.blogspot.co.uk/2011/08/creating-haar-cascade-classifier-aka.html 和 kaflurbaleen.blogspot.co.uk/2012/11/how-to-train-your-classifier-on-mac.html。
训练过程包括以下步骤:
-
设置当前目录:在 命令提示符 窗口中,将当前目录设置为存储训练图像的目录。例如,如果目录是
C:\chapter6\images,则使用以下命令:>cd C:\chapter6\images -
创建背景图像信息文本文件:如果背景图像存储在
C:\chapter6\images\train\non-face且其格式为.pgm,则可以使用以下命令创建 OpenCV 所需的文本文件:>for %i in (C:\chapter6\images\train\non-face\*.pgm) do @echo %i >> train_non-face.txt以下截图显示了背景图像信息文件的内容。此文件包含背景图像的路径:
![训练自己的级联]()
背景图像信息文件
-
创建目标图像文件:这涉及以下两个步骤:
-
创建包含目标坐标的
.dat文件。在此特定数据库中,目标图像仅包含一个目标实例,且位于图像中心,并缩放以占据整个图像。因此,每张图像中的目标数量为 1,目标坐标为0 0 19 19,这是包含目标的矩形的初始点以及宽度和高度。如果目标图像存储在
C:\chapter6\images\train\face,则可以使用以下命令生成文件:>for %i in (C:\chapter6\images\train\face\*.pgm) do @echo %i 1 0 0 19 19 >> train_face.dat.dat文件的内容可以在以下截图中看到:![训练自己的级联]()
目标图像文件
-
在创建包含目标坐标的
.dat文件后,需要创建 OpenCV 所需的.vec文件。此步骤可以使用opencv_createsamples程序执行,并使用以下参数:–info(.dat文件);-vec(.vec输出文件名);-num(图像数量);-w和–h(输出图像宽度和高度);以及–maxxangle、-maxyangle和-maxzangle(图像旋转角度)。要查看更多选项,请不带参数执行opencv_createsamples。在这种情况下,使用的命令如下:>opencv_createsamples -info train_face.dat -vec train_face.vec -num 2429 -w 19 -h 19 -maxxangle 0 -maxyangle 0 -maxzangle 0小贴士
OpenCV 包含一个大小为 24 x 24 像素的样本
.vec文件,包含面部图像。
-
-
训练级联:最后,使用
opencv_traincascade可执行文件并训练级联检测器。在这种情况下使用的命令如下:>opencv_traincascade -data C:\chapter6\trainedCascade -vec train_face.vec -bg train_non-face.txt -numPos 242 -numNeg 454 -numStages 10 -w 19 -h 19参数表示输出目录(
-data)、.vec文件(-vec)、背景信息文件(-bg)、每个阶段训练的正负图像数量(-numPos和–numNeg)、最大阶段数(-numStages)以及图像的宽度和高度(-w和–h)。训练过程的输出如下:
PARAMETERS: cascadeDirName: C:\chapter6\trainedCascade vecFileName: train_face.vec bgFileName: train_non-face.txt numPos: 242 numNeg: 454 numStages: 10 precalcValBufSize[Mb] : 256 precalcIdxBufSize[Mb] : 256 stageType: BOOST featureType: HAAR sampleWidth: 19 sampleHeight: 19 boostType: GAB minHitRate: 0.995 maxFalseAlarmRate: 0.5 weightTrimRate: 0.95 maxDepth: 1 maxWeakCount: 100 mode: BASIC ===== TRAINING 0-stage ===== <BEGIN POS count : consumed 242 : 242 NEG count : acceptanceRatio 454 : 1 Precalculation time: 4.524 +----+---------+---------+ | N | HR | FA | +----+---------+---------+ | 1| 1| 1| +----+---------+---------+ | 2| 1| 1| +----+---------+---------+ | 3| 0.995868| 0.314978| +----+---------+---------+ END> Training until now has taken 0 days 0 hours 0 minutes 9 seconds. . . . Stages 1, 2, 3, and 4 . . . ===== TRAINING 5-stage ===== <BEGIN POS count : consumed 242 : 247 NEG count : acceptanceRatio 454 : 0.000220059 Required leaf false alarm rate achieved. Branch training terminated.
最后,级联的 XML 文件存储在输出目录中。这些文件是 cascade.xml、params.xml 和一系列 stageX.xml 文件,其中 X 是阶段号。
Latent SVM
Latent SVM 是一种使用 HOG 特征和由根滤波器以及一系列部分滤波器组成的星形结构、基于部分模型的检测器,用于表示一个对象类别。HOGs 是通过计算图像局部区域中梯度方向出现的频率而获得的特征描述符。另一方面,在这个检测器中使用了 支持向量机(SVM)分类器的变体,用于使用部分标记的数据训练模型。SVM 的基本思想是在高维空间中构建一个或多个超平面。这些超平面被用来具有最大的距离到最近的训练数据点(功能间隔以实现低泛化误差)。像级联检测器一样,Latent SVM 使用一个滑动窗口,具有不同的初始位置和尺度,算法应用于检测窗口内是否存在对象。
OpenCV Latent SVM 实现的一个优点是它允许通过在同一多对象检测器实例中结合几个简单的预训练检测器来检测多个对象类别。
以下 latentDetection 示例说明了如何使用 Latent SVM 检测器从图像中定位某一类别的对象:
#include "opencv2/core/core.hpp"
#include "opencv2/objdetect/objdetect.hpp"
#include "opencv2/highgui/highgui.hpp"
#include <iostream>
using namespace std;
using namespace cv;
int main(int argc, char* argv[]){
String model = argv[1];
vector<String> models;
models.push_back( model );
vector<String> names;
names.push_back( "category" );
LatentSvmDetector detector( models , names);
if( detector.empty() ) {
cout << "Model cannot be loaded" << endl;
return -1;
}
String img = argv[2];
Mat image = imread( img );
if( image.empty() ){
cout << "Image cannot be loaded" << endl;
return -1;
}
vector<LatentSvmDetector::ObjectDetection> detections;
detector.detect( image, detections, 0.1, 1);
for( size_t i = 0; i < detections.size(); i++ ) {
Point center( detections[i].rect.x +
detections[i].rect.width*0.5,
detections[i].rect.y +
detections[i].rect.height*0.5 );
ellipse( image, center, Size( detections[i].rect.width*0.5,
detections[i].rect.height*0.5), 0, 0, 360,
Scalar( 255, 0, 255 ), 4, 8, 0 );
}
imshow( "result", image );
waitKey(0);
return 0;
}
代码解释如下:
-
LatentSvmDetector: 此类有一个对象,代表由一个或多个预训练检测器组成的 Latent SVM 检测器。 -
constructor LatentSvmDetector::LatentSvmDetector(const vector<String>& filenames, const vector<string>& classNames=vector<String>()): 此类初始化对象实例并加载存储在系统路径中由向量filenames指示的检测器信息。第二个参数,向量classNames包含类别名称。构造函数之后隐式调用方法bool LatentSvmDetector::load(const vector<string>& filenames, const vector<string>& classNames=vector<string>())。 -
void LatentSvmDetector::detect(const Mat& image, vector<ObjectDetection>& objectDetections, float overlapThreshold = 0.5f, int numThreads = -1): 此方法通过在变量image上应用简单或组合检测器来检查图像,并将所有检测到的对象放入objectDetections。所有检测都存储在ObjectDetection结构体的向量中。此结构体有以下三个变量:-
检测到的边界框(
rect) -
置信度水平(
score) -
类别 ID(
classID)
参数
overlapThreshold是非最大值抑制算法用于消除重叠检测的阈值。最后,numThreads是算法并行版本中使用的线程数。 -
以下截图显示了使用之前的代码和文件cat.xml和cat.png检测到的猫,以及使用car.xml和cars.png检测到的汽车。这些文件包含在 OpenCV 额外数据中,可以在官方仓库中找到。因此,可以使用以下命令运行程序:
>latentDetection.exe xmlfile imagefile
在之前的命令中,xmlfile是潜在 SVM 检测器,imagefile是要检查的图像。
注意
OpenCV 额外数据提供了更多样本和测试文件,用户可以使用这些文件创建和测试自己的项目,同时节省时间。可以在github.com/Itseez/opencv_extra找到。
除了汽车和猫检测器之外,OpenCV 还为2007 年 PASCAL 视觉对象类别挑战赛中定义的其他类别提供了预训练的检测器(pascallin.ecs.soton.ac.uk/challenges/VOC/voc2007)。以下是一些检测器:
-
aeroplane.xml -
bicycle.xml -
bird.xml -
boat.xml -
bottle.xml -
bus.xml -
car.xml -
cat.xml -
chair.xml -
cow.xml -
diningtable.xml -
dog.xml -
horse.xml -
motorbike.xml -
person.xml -
pottedplant.xml -
sheep.xml -
sofa.xml -
train.xml -
tvmonitor.xml

使用潜在 SVM 检测到的猫和一些汽车
小贴士
可以通过更改overlapThreshold参数的值来调整误报率。
场景文本检测
场景文本检测算法通过逐步从 0 到 255 对图像进行阈值处理来构建图像的组件树。为了增强结果,这个过程对每个颜色通道、强度和梯度幅度图像都进行了处理。之后,从连续级别获得的连通组件根据它们的包含关系进行分层组织,如下所示图所示。这种树状组织可能包含大量的区域:

树状组织示例
因此,算法在两个阶段之后选择了一些区域。首先,为每个区域计算面积、周长、边界框和欧拉数描述符,并按顺序使用它们来估计类条件概率。如果外部区域的局部最大概率值高于全局限制,并且它们的局部最大值和最小值之间的差异也高于指定的限制,则选择具有局部最大概率的外部区域。
第二阶段包括将第一阶段选定的外部区域根据整个区域比率、凸包比率和外边界拐点数量作为特征分类为字符和非字符类别。
最后,选定的外部区域被分组以获得单词、行或段落。算法的这一部分使用基于感知组织的聚类分析。
以下 textDetection 示例说明了如何使用场景文本检测算法并在图像中定位文本:
#include "opencv2/opencv.hpp"
#include "opencv2/objdetect.hpp"
#include "opencv2/highgui.hpp"
#include "opencv2/imgproc.hpp"
#include <vector>
#include <iostream>
#include <iomanip>
using namespace std;
using namespace cv;
int main(int argc, const char * argv[]){
Mat src = imread(argv[1]);
vector<Mat> channels;
computeNMChannels(src, channels);
//Negative images from RGB channels
channels.push_back(255-channels[0]);
channels.push_back(255-channels[1]);
channels.push_back(255-channels[2]);
channels.push_back(255-channels[3]);
for (int c = 0; c < channels.size(); c++){
stringstream ss;
ss << "Channel: " << c;
imshow(ss.str(),channels.at(c));
}
Ptr<ERFilter> er_filter1 = createERFilterNM1(
loadClassifierNM1(argv[2]),
16, 0.00015f, 0.13f, 0.2f,
true, 0.1f );
Ptr<ERFilter> er_filter2 = createERFilterNM2(
loadClassifierNM2(argv[3]), 0.5 );
vector<vector<ERStat> > regions(channels.size());
// Apply filters to each channel
for (int c=0; c<(int)channels.size(); c++){
er_filter1->run(channels[c], regions[c]);
er_filter2->run(channels[c], regions[c]);
}
for (int c=0; c<(int)channels.size(); c++){
Mat dst = Mat::zeros( channels[0].rows +
2, channels[0].cols + 2, CV_8UC1 );
// Show ERs
for (int r=0; r<(int)regions[c].size(); r++)
{
ERStat er = regions[c][r];
if (er.parent != NULL){
int newMaskVal = 255;
int flags = 4 + (newMaskVal << 8) +
FLOODFILL_FIXED_RANGE +
FLOODFILL_MASK_ONLY;
floodFill( channels[c], dst, Point(er.pixel %
channels[c].cols,er.pixel /
channels[c].cols), Scalar(255), 0,
Scalar(er.level), Scalar(0), flags);
}
}
stringstream ss;
ss << "Regions/Channel: " << c;
imshow(ss.str(), dst);
}
vector<Rect> groups;
erGrouping( channels, regions, argv[4], 0.5, groups );
for (int i=(int)groups.size()-1; i>=0; i--)
{
if (src.type() == CV_8UC3)
rectangle( src,groups.at(i).tl(), groups.at(i).br(),
Scalar( 0, 255, 255 ), 3, 8 );
else
rectangle( src,groups.at(i).tl(), groups.at(i).br(),
Scalar( 255 ), 3, 8 );
}
imshow("grouping",src);
waitKey(-1);
er_filter1.release();
er_filter2.release();
regions.clear();
groups.clear();
}
代码说明如下:
-
void computeNMChannels(InputArray _src, OutputArrayOfArrays _channels, int _mode=ERFILTER_NM_RGBLGrad): 这个函数从_src图像中计算不同的通道,以便独立处理以获得高定位召回率。默认情况下(_mode=ERFILTER_NM_RGBLGrad),这些通道是红色(R)、绿色(G)、蓝色(B)、亮度(L)和梯度幅度(∇),如果_mode=ERFILTER_NM_IHSGrad,则是强度(I)、色调(H)、饱和度(S)和梯度幅度(∇)。最后,计算出的通道保存在_channels参数中。 -
Ptr<ERFilter> createERFilterNM1(const Ptr<ERFilter::Callback>& cb, int thresholdDelta = 1, float minArea = 0.00025, float maxArea = 0.13, float minProbability = 0.4, bool nonMaxSuppression = true, float minProbabilityDiff = 0.1): 这个函数创建了一个用于算法定义的第一阶段分类器的极值区域过滤器。第一个参数通过函数loadClassifierNM1(const std::string& filename)加载分类器。thresholdDelta变量指示在组件树获取过程中的阈值步长。参数minArea和maxArea建立了外部区域检索的图像大小百分比。当在分支概率上应用非最大值抑制时,布尔参数nonMaxSuppression的值为true,否则为false。最后,minProbability和minProbabilityDiff参数控制检索外部区域允许的最小概率值和最大值与最小值之间的最小概率差异。 -
Ptr<ERFilter> createERFilterNM2(const Ptr<ERFilter::Callback>& cb, float minProbability = 0.3): 这个函数创建了一个用于算法定义的第二阶段分类器的外部区域过滤器。第一个参数通过函数loadClassifierNM2(const std::string& filename)加载分类器。其他参数minProbability是允许检索外部区域的最小概率。 -
void ERFilter::run( InputArray image, std::vector<ERStat>& regions): 这个方法将过滤器加载的级联分类器应用于获取第一级或第二级的外部区域。image参数是需要检查的通道,regions是第一阶段的输出以及第二阶段的输入/输出。 -
void erGrouping(InputArrayOfArrays src, std::vector<std::vector<ERStat>>& regions, const std::string& filename, float minProbability, std::vector<Rect>& groups): 此函数将获取的外部区域分组。它使用提取的通道 (src)、每个通道获取的外部区域 (regions)、分组分类器的路径以及接受一个组的最低概率 (minProbability)。最终组,即来自Rect的矩形,存储在groups向量中。
以下一系列截图显示了获取的图像通道。这些是红色 (R)、绿色 (G)、蓝色 (B)、强度 (I)、梯度幅度 (∇)、反转红色 (iR)、反转绿色 (iG)、反转蓝色 (iB) 和反转强度 (iI)。在第一行中,显示了 R、G 和 B 通道。第二行显示了 I、∇ 和 iR 通道。最后,在第三行中,显示了 iG、iB 和 iI 通道:

提取的图像通道
以下一系列截图显示了可以查看从每个通道提取的外部区域。通道 R、G、B、L 和 ∇ 产生更准确的结果。在第一行中,显示了 R、G 和 B 通道的外部区域。第二行显示了从 I、∇ 和 iR 通道提取的外部区域。最后,在第三行中,显示了 iG、iB 和 iI 通道:

从每个通道获取的外部区域
最后,以下截图显示了将文本区域分组为行和段落的输入图像:

获取的组
注意
要重现这些结果或使用 OpenCV 场景文本检测器,可以使用库提供的示例文件运行此代码。输入图像和分类器可以在 OPENCV_SCR/samples/cpp 目录中找到。这里使用的图像是 cenetext01.jpg。第一级和第二级分类器是 trained_classifierNM1.xml 和 trained_classifierNM2.xml。最后,OpenCV 提供的分组分类器是 trained_classifier_erGrouping.xml。
摘要
本章介绍了 OpenCV 的 objdetect 模块。它解释了如何使用和训练级联检测器以及如何使用潜在 SVM 检测器。此外,本章还解释了 OpenCV 3 中包含的新场景文本检测器。
下一章将解释检测和跟踪运动对象的方法。
还有什么?
级联检测器已在多个应用中得到了广泛应用,例如人脸识别和行人检测,因为它们速度快且提供良好的结果。软级联是经典级联检测器的一种变体。这种新型级联在 OpenCV 3 的softcascade模块中实现。软级联使用 AdaBoost 进行训练,但生成的检测器仅由一个阶段组成。这个阶段有几个弱分类器,它们按顺序进行评估。在评估每个弱分类器后,结果将与相应的阈值进行比较。与多阶段级联中进行的评估过程类似,一旦评估出负的非目标实例,就会尽快将其丢弃。
第七章. 他正在做什么?运动
在本章中,我们将向您展示从视频帧中估计出的不同运动技术。在简短介绍和定义之后,我们将向您展示如何读取从摄像头捕获的视频帧。然后,我们将探讨至关重要的光流技术。在第三部分,我们将向您展示可用于跟踪的不同函数。运动历史和背景减法技术在第四和第五部分中分别解释。最后,我们将解释使用 ECC 方法进行图像对齐。每个示例都已针对 GitHub 上 OpenCV 的最新版本开发和测试。大多数函数在先前版本中也可以同样工作,导致了一些将被讨论的变化。本章中介绍的大多数函数都在video模块中。
注意
要测试 GitHub 上可用的最新源代码,请访问github.com/itseez/opencv并下载库代码作为 ZIP 文件。然后将其解压到本地文件夹,并按照第一章中描述的相同步骤,开始,来编译和安装库。
运动历史
运动是计算机视觉中的一个非常重要的话题。一旦我们检测和隔离了感兴趣的对象或人,我们就可以提取有价值的数据,如位置、速度、加速度等。这些信息可用于动作识别、行为模式研究、视频稳定、增强现实等。
光流技术是一种对象的明显运动模式。视觉场景中的表面和边缘是由观察者与场景或摄像头与场景之间的相对运动引起的。光流技术的概念在计算机视觉中至关重要,并与运动检测、对象分割、时间控制信息、扩展焦点计算、亮度、运动补偿编码和立体视差测量等技术/任务相关联。
视频跟踪是指使用从摄像头或文件捕获的视频在一段时间内定位移动对象(或多个对象)。视频跟踪的目的是将连续视频帧中的目标对象关联起来。它有多种用途,其中一些包括视频编辑、医学成像、交通控制、增强现实、视频通信和压缩、安全和监控以及人机交互。
运动模板是在 1996 年由 Bobick 和 David 在麻省理工学院媒体实验室发明的。使用运动模板是一种简单而稳健的技术,可以跟踪一般运动。OpenCV 运动模板函数仅适用于单通道图像。需要一个物体的轮廓(或部分轮廓)。这些轮廓可以通过不同的方式获得。例如,可以使用分割技术来检测感兴趣的对象,然后使用运动模板进行跟踪。另一种选择是使用背景减法技术来检测前景对象,然后跟踪它们。还有其他技术,尽管在本章中,我们将看到两个使用背景减法技术的示例。
背景减法是一种技术,通过它可以从图像中提取前景或感兴趣区域以进行进一步处理,例如人、汽车、文本等。背景减法技术是检测从静态摄像头捕获的视频中移动对象的一种广泛使用的方法。背景减法技术的本质是从当前帧和没有目标对象存在的参考图像之间的差异中检测移动对象,这通常被称为背景图像。
图像配准可以看作是来自不同视角的两个或多个图像的坐标系之间的映射。因此,第一步是选择一个合适的几何变换,足以模拟这种映射。该算法可以应用于广泛的领域,例如图像配准、目标跟踪、超分辨率和移动摄像头的视觉监控。
读取视频序列
要处理视频序列,我们应该能够读取每一帧。OpenCV 开发了一个易于使用的框架,可以处理视频文件和摄像头输入。
以下代码是一个 videoCamera 示例,它使用从视频摄像头捕获的视频。这个示例是对 第一章 中的示例的修改,入门指南,我们将用它作为本章其他示例的基本结构:
#include "opencv2/opencv.hpp"
using namespace std;
using namespace cv;
int videoCamera()
{
//1-Open the video camera
VideoCapture capture(0);
//Check if video camera is opened
if(!capture.isOpened()) return 1;
bool finish = false;
Mat frame;
Mat prev_frame;
namedWindow("Video Camera");
if(!capture.read(prev_frame)) return 1;
//Convert to gray image
cvtColor(prev_frame,prev_frame,COLOR_BGR2GRAY);
while(!finish)
{
//2-Read each frame, if possible
if(!capture.read(frame)) return 1;
//Convert to gray image
cvtColor(frame ,frame, COLOR_BGR2GRAY);
//Here, we will put other functions
imshow("Video Camera", prev_frame);
//Press Esc to finish
if(waitKey(1)==27) finish = true;
prev_frame = frame;
}
//Release the video camera
capture.release();
return 0;
}
int main( )
{
videoCamera();
}
之前的代码示例创建了一个窗口,显示灰度视频的摄像头捕获。为了启动捕获,已经创建了一个 VideoCapture 类的实例,其摄像头索引为零。然后,我们检查视频捕获是否可以成功启动。然后使用 read 方法从视频序列中读取每一帧。该视频序列使用 cvtColor 方法与 COLOR_BGR2GRAY 参数转换为灰度,并在用户按下 Esc 键之前显示在屏幕上。然后,最终释放视频序列。上一帧被存储,因为它将被用于后续的一些示例。
注意
COLOR_BGR2GRAY 参数可以在 OpenCV 3.0 中使用。在之前的版本中,我们也有 CV_BGR2GRAY。
在总结中,我们向您展示了一个使用视频相机处理视频序列的简单方法。最重要的是,我们已经学会了如何访问每个视频帧,现在可以处理任何类型的帧。
注意
关于 OpenCV 支持的音视频格式,更多详细信息可以在ffmpeg.org网站上找到,该网站提供了一个完整的开源跨平台解决方案,用于音频和视频的读取、录制、转换和流式传输。与视频文件一起工作的 OpenCV 类都是基于这个库构建的。在Xvid.org网站上,你可以找到一个基于 MPEG-4 标准的开源视频编解码库。这个编解码库有一个竞争对手叫做 DivX,它提供专有但免费的编解码器和软件工具。
Lucas-Kanade 光流
Lucas-Kanade(LK)算法最初于 1981 年提出,并已成为计算机视觉中最成功的方法之一。目前,这种方法通常应用于输入图像的关键点子集。该方法假设光流是考虑像素局部邻域中的一个必要常数,并为该邻域中的每个像素(x, y)解决基本光流技术方程(见方程(1))。该方法还假设连续帧之间的位移很小,并且是获得考虑点过度约束系统的一种方法:
*I(x, y, t) = I(x + ∆x, y + ∆y, t + ∆t) (1)*
我们现在将关注金字塔 Lucas-Kanade方法,该方法使用calcOpticalFlowPyrLK()函数在金字塔中估计光流。这种方法首先在金字塔的顶部估计光流,从而避免了由违反我们对小而一致运动假设引起的问题。然后,从这个第一级的光流估计被用作下一个级别的运动估计的起点,如下图中金字塔所示:

金字塔 Lucas-Kanade
以下示例使用maxMovementLK函数实现运动检测器:
void maxMovementLK(Mat& prev_frame, Mat& frame)
{
// 1-Detect right features to apply the Optical Flow technique
vector<Point2f> initial_features;
goodFeaturesToTrack(prev_frame, initial_features,MAX_FEATURES, 0.1, 0.2 );
// 2-Set the parameters
vector<Point2f>new_features;
vector<uchar>status;
vector<float> err;
TermCriteria criteria(TermCriteria::COUNT | TermCriteria::EPS, 20, 0.03);
Size window(10,10);
int max_level = 3;
int flags = 0;
double min_eigT = 0.004;
// 3-Lucas-Kanade method for the Optical Flow technique
calcOpticalFlowPyrLK(prev_frame, frame, initial_features, new_features, status, err, window, max_level, criteria, flags, min_eigT );
// 4-Show the results
double max_move = 0;
double movement = 0;
for(int i=0; i<initial_features.size(); i++)
{
Point pointA (initial_features[i].x, initial_features[i].y);
Point pointB(new_features[i].x, new_features[i].y);
line(prev_frame, pointA, pointB, Scalar(255,0,0), 2);
movement = norm(pointA-pointB);
if(movement > max_move)
max_move = movement;
}
if(max_move >MAX_MOVEMENT)
{
putText(prev_frame,"INTRUDER",Point(100,100),FONT_ITALIC,3,Scalar(255,0,0),5);
imshow("Video Camera", prev_frame);
cout << "Press a key to continue..." << endl;
waitKey();
}
}
上述示例展示了每次移动时窗口的显示情况。如果存在较大移动,屏幕上会显示一条消息。首先,我们需要在图像中获取一组适当的关键点,以便我们可以估计光流。goodFeaturesToTrack()函数使用 Shi 和 Tomasi 最初提出的方法以可靠的方式解决这个问题,尽管你也可以使用其他函数来检测重要且易于跟踪的特征(参见第五章,专注于有趣的 2D 特征)。MAX_FEATURES被设置为500以限制关键点的数量。然后设置 Lucas-Kanade 方法参数并调用calcOpticalFlowPyrLK()。当函数返回时,检查状态(status)数组以查看哪些点被成功跟踪,并且使用估计位置的新点集(new_features)。绘制线条以表示运动,如果位移大于MAX_MOVEMENT——例如——100,屏幕上会显示一条消息。我们可以看到以下两个屏幕截图:

maxMovementLK示例的输出
使用修改后的videoCamera示例,我们已将maxMovementLK()函数应用于检测大移动:
...
while(!finish)
{
capture.read(frame);
cvtColor(frame,frame,COLOR_BGR2GRAY);
// Detect Maximum Movement with Lucas-Kanade Method
maxMovementLK(prev_frame, frame);
...
这种方法计算效率高,因为跟踪仅在重要或有趣点上执行。
Gunnar-Farneback 光流
Gunnar-Farneback算法被开发出来以产生密集的光流技术结果(即在密集的点网格上)。第一步是使用二次多项式近似两帧的每个邻域。之后,考虑到这些二次多项式,通过全局位移构建一个新的信号。最后,通过等于二次多项式产出的系数来计算这个全局位移。
现在让我们看看这个方法的实现,它使用了calcOpticalFlowFarneback()函数。以下是一个示例(maxMovementFarneback),它使用此函数检测前一个示例中显示的最大移动:
void maxMovementFarneback(Mat& prev_frame, Mat& frame)
{
// 1-Set the Parameters
Mat optical_flow = Mat(prev_frame.size(), COLOR_BGR2GRAY);
double pyr_scale = 0.5;
int levels = 3;
int win_size = 5;
int iterations = 5;
int poly_n = 5;
double poly_sigma = 1.1;
int flags = 0;
// 2-Farneback method for the Optical Flow technique
calcOpticalFlowFarneback(prev_frame, frame, optical_flow, pyr_scale, levels, win_size, iterations, poly_n, poly_sigma, flags);
// 3-Show the movements
int max_move = 0;
for (int i = 1; i <optical_flow.rows ; i++)
{
for (int j = 1; j <optical_flow.cols ; j++)
{
Point2f &p = optical_flow.at<Point2f>(i, j);
Point pA = Point(round(i + p.x),round(j + p.y));
Point pB = Point(i, j);
int move = sqrt(p.x*p.x + p.y*p.y);
if( move >MIN_MOVEMENT )
{
line(prev_frame, pA, pB, Scalar(255,0,0),2);
if ( move > max_move )
max_move = move;
}
}
}
if(max_move >MAX_MOVEMENT)
{
putText(prev_frame,"INTRUDER",Point(100,100),FONT_ITALIC,3,Scalar(255,0,0),5);
imshow("Video Camera", prev_frame);
cout << "Press a key to continue..." << endl;
waitKey();
}
}
此函数接收两个连续帧,使用不同的参数估计光流,并返回一个与输入帧大小相同的数组,其中每个像素实际上是一个点(Point2f),代表该像素的位移。首先,为此函数设置不同的参数。当然,你也可以使用自己的标准来配置性能。然后,使用这些参数,在每两个连续帧之间执行光流技术。因此,我们获得一个包含每个像素估计的数组,即optical_flow。最后,显示屏幕上大于MIN_MOVEMENT的移动。如果最大移动大于MAX_MOVEMENT,则显示INTRUDER消息。
可以理解,这种方法相当慢,因为光流技术在每一帧的每个像素上计算。此算法的输出与先前的方法类似,尽管它要慢得多。
Mean-Shift 跟踪器
Mean-Shift方法允许你定位从该函数离散采样得到的密度函数的最大值。因此,它对于检测这种密度的模式非常有用。Mean-Shift 是一种迭代方法,需要初始估计。
该算法可用于视觉跟踪。在这种情况下,跟踪对象的颜色直方图用于计算置信图。此类算法中最简单的一种会在新图像中基于从上一图像中获取的对象直方图创建置信图,并使用 Mean-Shift 在对象之前位置附近找到置信图的峰值。置信图是新图像上的概率密度函数,为每个新图像的像素分配一个概率,即该像素颜色在上一图像中的对象中出现的概率。接下来,我们通过此函数(trackingMeanShift)展示一个示例:
void trackingMeanShift(Mat& img, Rect search_window)
{
// 1-Criteria to MeanShift function
TermCriteria criteria(TermCriteria::COUNT | TermCriteria::EPS, 10, 1);
// 2-Tracking using MeanShift
meanShift(img, search_window, criteria);
// 3-Show the result
rectangle(img, search_window, Scalar(0,255,0), 3);
}
此示例显示了一个初始居中的矩形窗口,其中执行跟踪。首先,设置准则参数。实现此方法的函数需要三个参数:主图像、我们想要搜索的兴趣区域以及不同跟踪模式的术语准则。最后,从meanShift()获得一个矩形,并在主图像上绘制search_window。
使用修改后的videoCamera示例,我们将此方法应用于跟踪。使用屏幕的静态窗口进行搜索。当然,你可以手动调整另一个窗口或使用其他功能来检测感兴趣的对象,然后对它们进行跟踪:
...
while(!finish)
{
capture.read(frame);
cvtColor(frame,frame,COLOR_BGR2GRAY);
// Tracking using MeanShift with an initial search window
Rect search_window(200,150,100,100);
trackingMeanShift(prev_frame, search_window);
...
这里,我们可以看到以下两个屏幕截图:

trackingMeanShift 示例的输出
CamShift 跟踪器
CamShift(连续自适应 Mean Shift)算法是由 OpenCV 的 Gary Bradski 在 1998 年提出的一种图像分割方法。它与MeanShift的不同之处在于搜索窗口会调整其大小。如果我们有一个很好地分割的分布(例如,保持紧凑的面部特征),当人靠近或远离相机时,此方法会自动调整到人脸大小。
注意
我们可以在docs.opencv.org/trunk/doc/py_tutorials/py_video/py_meanshift/py_meanshift.html找到CamShift的参考。
我们现在将使用此方法查看以下示例(trackingCamShift):
void trackingCamShift(Mat& img, Rect search_window)
{
//1-Criteria to CamShift function
TermCriteria criteria(TermCriteria::COUNT | TermCriteria::EPS, 10, 1);
//2-Tracking using CamShift
RotatedRect found_object = CamShift(img, search_window, criteria);
//3-Bounding rectangle and show the result
Rect found_rect = found_object.boundingRect();
rectangle(img, found_rect, Scalar(0,255,0),3);
}
这个函数结构与前一个部分中的非常相似;唯一的区别是CamShift()返回一个边界矩形。
运动模板
动作模板是图像处理中用于找到与模板图像匹配的图像或轮廓的小部分的技术。此模板匹配器用于进行相似性比较和检查相似或差异。模板可能需要大量点的采样。然而,通过降低搜索分辨率可以减少这些点的数量;另一种改进这些模板的技术是使用金字塔图像。
在 OpenCV 的示例([opencv_source_code]/samples/c/motempl.c)中可以找到一个相关的程序。
动作历史模板
现在我们假设我们有一个好的轮廓或模板。然后,使用当前时间戳作为权重捕获并叠加新的轮廓。这些依次淡出的轮廓记录了之前的运动历史,因此被称为动作历史模板。时间戳比当前时间戳早于指定DURATION的轮廓被设置为零。我们使用updateMotionHistory()OpenCV 函数在两个帧上创建了一个简单的示例(motionHistory)如下:
void updateMotionHistoryTemplate(Mat& prev_frame, Mat& frame, Mat& history)
{
//1-Calculate the silhouette of difference between the two
//frames
absdiff(frame, prev_frame, prev_frame);
//2-Applying a threshold on the difference image
double threshold_val = 100; threshold(prev_frame,prev_frame,threshold_val,255,THRESH_BINARY);
//3-Calculate the current time
clock_t aux_time = clock();
double current_time = (aux_time-INITIAL_TIME)/CLOCKS_PER_SEC;
//4-Performing the Update Motion history template
updateMotionHistory(prev_frame, history, current_time, DURATION);
}
注意
在 OpenCV 3.0 中可以使用THRESH_BINARY参数。在之前的版本中,我们也有CV_THRESH_BINARY。
此示例向您展示了一个绘制动作历史的窗口。第一步是获取轮廓;为此使用了背景减法技术。从两个输入帧中获取绝对值的差异。第二步,应用二值阈值以从轮廓中去除噪声。然后,获取当前时间。最后一步是使用 OpenCV 的函数更新动作历史模板。
我们还将DURATION设置为5。请注意,初始化INITIAL_TIME和history是必要的。此外,我们可以使用修改后的videoCamera示例中的此函数调用如下:
...
// Calculate the initial time
INITIAL_TIME = clock()/CLOCKS_PER_SEC;
//Create a Mat to save the Motion history template
Mat history(prev_frame.rows, prev_frame.cols, CV_32FC1);
while(!finish)
{
capture.read(frame);
cvtColor(frame,frame,COLOR_BGR2GRAY);
// Using Update Motion history template
updateMotionHistoryTemplate(prev_frame, frame, history);
imshow("Video Camera", history);
...
要使用获取当前时间的clock()函数,我们需要包含<ctime>。以下是一些屏幕截图,显示有人在摄像机前行走。

动作历史示例的输出
动作梯度
一旦动作模板在时间上叠加了一系列物体轮廓,我们可以通过计算历史图像的梯度来获得运动方向。以下示例(motionGradient)计算了梯度:
void motionGradientMethod(Mat& history, Mat& orientations)
{
//1-Set the parameters
double max_gradient = 3.0;
double min_gradient = 1.0;
//The default 3x3 Sobel filter
int apertura_size = 3;
//Distance to show the results
int dist = 20;
Mat mask = Mat::ones(history.rows, history.cols, CV_8UC1);
//2-Calcule motion gradients
calcMotionGradient(history, mask, orientations, max_gradient, min_gradient, apertura_size);
//3-Show the results
Mat result = Mat::zeros(orientations.rows, orientations.cols, CV_32FC1);
for (int i=0;i<orientations.rows; i++)
{
for (int j=0;j<orientations.cols; j++)
{
double angle = 360-orientations.at<float>(i,j);
if (angle!=360)
{
Point point_a(j, i);
Point point_b(round(j+ cos(angle)*dist), round(i+ sin(angle)*dist));
line(result, point_a, point_b, Scalar(255,0,0), 1);
}
}
}
imshow("Result", result);
}
屏幕截图显示有人在摄像机前移动头部(见下述截图)。每一行代表每个像素的梯度。不同的帧在t时间上也有重叠:

动作梯度示例的输出(有人在摄像机前移动头部)。
上述示例展示了显示运动方向的窗口。作为第一步,设置参数(要检测的最大和最小梯度值)。第二步使用calcMotionGradient()函数获取梯度方向角度的矩阵。最后,为了显示结果,使用默认距离dist将这些角度绘制在屏幕上。同样,我们可以从以下修改后的videoCamera示例中使用此功能:
...
//Create a Mat to save the Motion history template
Mat history(prev_frame.rows, prev_frame.cols, CV_32FC1);
while(!finish)
{
capture.read(frame);
cvtColor(frame,frame,COLOR_BGR2GRAY);
//Using Update Motion history template
updateMotionHistoryTemplate(prev_frame, frame, history);
//Calculate motion gradients
Mat orientations = Mat::ones(history.rows, history.cols, CV_32FC1);
motionGradientMethod(history, orientations);
...
背景减除技术
背景减除技术包括在背景上获取重要对象。
现在,让我们看看 OpenCV 中可用于背景减除技术的可用方法。目前,以下四种重要技术对于这项任务来说是必需的:
-
MOG(混合高斯)
-
MOG2
-
GMG(Geometric MultiGrip)
-
KNN(K-Nearest Neighbors)
接下来,我们将看到一个使用 KNN 技术(backgroundSubKNN)的示例:
#include<opencv2/opencv.hpp>
using namespace cv;
using namespace std;
int backGroundSubKNN()
{
//1-Set the parameters and initializations
Mat frame;
Mat background;
Mat foreground;
bool finish = false;
int history = 500;
double dist2Threshold = 400.0;
bool detectShadows = false;
vector< vector<Point>> contours;
namedWindow("Frame");
namedWindow("Background");
VideoCapture capture(0);
//Check if the video camera is opened
if(!capture.isOpened()) return 1;
//2-Create the background subtractor KNN
Ptr <BackgroundSubtractorKNN> bgKNN = createBackgroundSubtractorKNN (history, dist2Threshold, detectShadows);
while(!finish)
{
//3-Read every frame if possible
if(!capture.read(frame)) return 1;
//4-Using apply and getBackgroundImage method to get
//foreground and background from this frame
bgKNN->apply(frame, foreground);
bgKNN->getBackgroundImage(background);
//5-Reduce the foreground noise
erode(foreground, foreground, Mat());
dilate(foreground, foreground, Mat());
//6-Find the foreground contours
findContours(foreground,contours,RETR_EXTERNAL,CHAIN_APPROX_NONE);
drawContours(frame,contours,-1,Scalar(0,0,255),2);
//7-Show the results
imshow("Frame", frame);
imshow("Background", background);
moveWindow("Frame", 0, 100);
moveWindow("Background",800, 100);
//Press Esc to finish
if(waitKey(1) == 27) finish = true;
}
capture.release();
return 0;
}
int main()
{
backGroundSubKNN();
}
注意
createBackgroundSubtractorKNN方法仅包含在 OpenCV 的 3.0 版本中。
在以下屏幕截图(其中有人在摄像机前行走)中显示了背景减除帧和屏幕截图:

backgroundSubKNN 示例的输出
上述示例展示了两个带有减去背景图像的窗口,并绘制了找到的人的轮廓。首先,设置参数作为背景与每一帧之间的距离阈值以检测对象(dist2Threshol)以及禁用阴影检测(detectShadows)。在第二步中,使用createBackgroundSubtractorKNN()函数创建一个背景减除器,并使用智能指针构造(Ptr<>)以确保我们不需要释放它。第三步是读取每一帧,如果可能的话。使用apply()和getBackgroundImage()方法,获取前景和背景图像。第五步是通过应用形态学闭合操作(在腐蚀—erode()—和膨胀—dilate()—顺序)来减少前景噪声。然后,在前景图像上检测轮廓,并绘制它们。最后,显示背景和当前帧图像。
图像配准
OpenCV 现在实现了 ECC 算法,该算法仅从版本 3.0 开始可用。此方法估计输入帧和模板帧之间的几何变换(扭曲),并返回扭曲后的输入帧,该帧必须接近第一个模板。估计的变换是最大化模板和扭曲后的输入帧之间相关系数的变换。在 OpenCV 示例([opencv_source_code]/samples/cpp/image_alignment.cpp)中可以找到一个相关的程序。
注意
ECC 算法基于论文 使用增强相关系数最大化的参数图像对齐 的 ECC 标准。您可以在 xanthippi.ceid.upatras.gr/people/evangelidis/george_files/PAMI_2008.pdf 找到它。
现在我们将看到一个示例 (findCameraMovement),它使用 findTransformECC() 函数来应用这种 ECC 技术:
#include <opencv2/opencv.hpp>
using namespace cv;
using namespace std;
int findCameraMovement()
{
//1-Set the parameters and initializations
bool finish = false;
Mat frame;
Mat initial_frame;
Mat warp_matrix;
Mat warped_frame;
int warp_mode = MOTION_HOMOGRAPHY;
TermCriteria criteria(TermCriteria::COUNT | TermCriteria::EPS, 50, 0.001);
VideoCapture capture(0);
Rect rec(100,50,350,350); //Initial rectangle
Mat aux_initial_frame;
bool follow = false;
//Check if video camera is opened
if(!capture.isOpened()) return 1;
//2-Initial capture
cout << "\n Press 'c' key to continue..." << endl;
while(!follow)
{
if(!capture.read(initial_frame)) return 1;
cvtColor(initial_frame ,initial_frame, COLOR_BGR2GRAY);
aux_initial_frame = initial_frame.clone();
rectangle(aux_initial_frame, rec, Scalar(255,255,255),3);
imshow("Initial frame", aux_initial_frame);
if (waitKey(1) == 99) follow = true;
}
Mat template_frame(rec.width,rec.height,CV_32F);
template_frame = initial_frame.colRange(rec.x, rec.x + rec.width).rowRange(rec.y, rec.y + rec.height);
imshow("Template image", template_frame);
while(!finish)
{
cout << "\n Press a key to continue..." << endl;
waitKey();
warp_matrix = Mat::eye(3, 3, CV_32F);
//3-Read each frame, if possible
if(!capture.read(frame)) return 1;
//Convert to gray image
cvtColor(frame ,frame, COLOR_BGR2GRAY);
try
{
//4-Use findTransformECC function
findTransformECC(template_frame, frame, warp_matrix, warp_mode, criteria);
//5-Obtain the new perspective
warped_frame = Mat(template_frame.rows, template_frame.cols, CV_32F);
warpPerspective (frame, warped_frame, warp_matrix, warped_frame.size(), WARP_INVERSE_MAP + WARP_FILL_OUTLIERS);
}
catch(Exception e) { cout << "Exception: " << e.err << endl;}
imshow ("Frame", frame);
imshow ("Warped frame", warped_frame);
//Press Esc to finish
if(waitKey(1) == 27) finish = true;
}
capture.release();
return 0;
}
main()
{
findCameraMovement();
}
下面的截图显示了部分屏幕截图。左侧的框架表示初始和模板框架。右上角是当前帧,右下角是变形帧。

findCameraMovement 示例的输出。
代码示例展示了四个窗口:初始模板、初始帧、当前帧和变形帧。第一步是将初始参数设置为 warp_mode (MOTION_HOMOGRAPHY)。第二步是检查视频摄像头是否已打开,并获取一个模板,该模板将使用中心矩形计算。当按下 C 键时,此区域将被捕获作为模板。第三步是读取下一帧并将其转换为灰度帧。使用 findTransformECC() 函数应用此矩阵计算 warp_matrix,然后使用 warpPerspective(),可以通过 warped_frame 来校正相机运动。
摘要
本章涵盖了计算机视觉中的一个重要主题。运动检测是一个基本任务,在本章中,我们为读者提供了所需的知识和示例,以便了解 OpenCV 中最实用的方法:处理视频序列(参见 videoCamera 示例)、光流技术(参见 maxMovementLK 和 maxMovementFarneback 示例)、跟踪(参见 trackingMeanShift 和 trackingCamShift 示例)、运动模板(参见 motionHistory 和 motionGradient 示例)、背景减法技术(参见 backgroundSubKNN 示例)和图像对齐(参见 findCameraMovement 示例)。
还有什么?
在 OpenCV 库中,还有其他处理运动的功能。实现了其他光流技术方法,例如 Horn 和 Schunk (cvCalcOpticalFlowHS)、块机器 (cvCalcOpticalFlowBM) 和简单流 (calcOpticalFlowSF) 方法。还有一个用于估计全局运动的方法 (calcGlobalOrientation)。最后,还有其他获取背景的方法,如 MOG (createBackgroundSubtractorMOG)、MOG2 (createBackgroundSubtractorMOG2) 和 GMG (createBackgroundSubtractorGMG) 方法。
第八章. 高级主题
本章涵盖了较少使用的主题,例如多类机器学习和基于 GPU 的优化。这两个主题都看到了兴趣和实际应用的增长,因此它们值得一个完整的章节。我们认为它们是高级的,只要需要关于机器学习/统计分类和并行化的额外知识。我们将从解释一些最著名的分类器开始,如 KNN、SVM 和随机森林,它们都可在 ml 模块中找到,并展示它们如何与不同的数据库格式和多类工作。最后,将描述一组用于利用基于 GPU 的计算资源的类和函数。
机器学习
机器学习处理允许计算机通过自身学习并做出决策的技术。机器学习的一个核心概念是分类器。分类器从数据集中的示例中学习,其中每个样本的标签是已知的。通常,我们手头有两个数据集:训练集和测试集。分类器使用训练集构建模型。这个训练好的分类器预计可以预测未见过的样本的标签,因此我们最终使用测试集来验证它并评估标签识别率。
在本节中,我们解释了 OpenCV 提供的不同类和函数,用于分类,以及它们使用的简单示例。机器学习类和函数包括用于数据统计分类、回归和聚类的 ml 模块。
KNN 分类器
K 近邻算法(KNN)是最简单的分类器之一。它是一种监督分类方法,通过学习现有案例并通过最小距离对新的案例进行分类。K 是决策中要分析的邻居数量。要分类的新数据点(查询)被投影到与学习点相同的空间,其类别由训练集中其 KNN 中最频繁的类别给出。
以下 KNNClassifier 代码是使用 KNN 算法对每个图像像素进行分类的示例,分类到最近的颜色:黑色(0, 0, 0)、白色(255, 255, 255)、蓝色(255, 0, 0)、绿色(0, 255, 0)或红色(0, 0, 255):
#include <iostream>
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/ml/ml.hpp>
using namespace std;
using namespace cv;
int main(int argc, char *argv[]){
//Create Mat for the training set and classes
Mat classes(5, 1, CV_32FC1);
Mat colors(5, 3, CV_32FC1);
//Training set (primary colors)
colors.at<float>(0,0)=0, colors.at<float>(0,1)=0, colors.at<float>(0,2)=0;
colors.at<float>(1,0)=255, colors.at<float>(1,1)=255, colors.at<float>(1,2)=255;
colors.at<float>(2,0)=255, colors.at<float>(2,1)=0, colors.at<float>(2,2)=0;
colors.at<float>(3,0)=0, colors.at<float>(3,1)=255, colors.at<float>(3,2)=0;
colors.at<float>(4,0)=0, colors.at<float>(4,1)=0, colors.at<float>(4,2)=255;
//Set classes to each training sample
classes.at<float>(0,0)=1;
classes.at<float>(1,0)=2;
classes.at<float>(2,0)=3;
classes.at<float>(3,0)=4;
classes.at<float>(4,0)=5;
//KNN classifier (k=1)
CvKNearest classifier;
classifier.train(colors,classes,Mat(),false,1,false);
//Load original image
Mat src=imread("baboon.jpg",1);
imshow("baboon",src);
//Create result image
Mat dst(src.rows , src.cols, CV_8UC3);
Mat results;
Mat newPoint(1,3,CV_32FC1);
//Response for each pixel and store the result in the result image
float prediction=0;
for(int y = 0; y < src.rows; ++y){
for(int x = 0; x < src.cols; ++x){
newPoint.at<float>(0,0)= src.at<Vec3b>(y, x)[0];
newPoint.at<float>(0,1) = src.at<Vec3b>(y, x)[1];
newPoint.at<float>(0,2) = src.at<Vec3b>(y, x)[2];
prediction=classifier.find_nearest(newPoint,1,&results, 0, 0);
dst.at<Vec3b>(y, x)[0]= colors.at<float>(prediction-1,0);
dst.at<Vec3b>(y, x)[1]= colors.at<float>(prediction-1,1);
dst.at<Vec3b>(y, x)[2]= colors.at<float>(prediction-1,2);
}
}
//Show result image
cv::imshow("result KNN",dst);
cv::waitKey(0);
return 0;
}
注意
记住,OpenCV 使用 BGR 颜色方案。
OpenCV 通过 CvKNearest 类提供 KNN 算法。通过 bool CvKNearest::train(const Mat& trainData, const Mat& responses, const Mat& sampleIdx, bool isRegression, int maxK, bool updateBase) 函数将训练信息添加到 KNN 分类器中。示例创建了一个包含五个样本的训练集(Mat colors(5, 3, CV_32FC1)),代表每个类别(颜色)(Mat classes(5, 1, CV_32FC1));这些是前两个输入参数。isRegression 参数是一个布尔值,定义了是否想要执行分类或回归。maxK 值表示测试阶段将使用的最大邻居数。
最后,updateBaseparameter 允许我们指示是否想要使用数据训练新的分类器或使用它来更新之前的训练数据。然后,代码示例使用 float CvKNearest::find_nearest(const Mat& samples, int k, Mat* results=0, const float** neighbors=0, Mat* neighborResponses=0, Mat* dist=0) 函数对原始图像的每个像素执行测试阶段。该函数测试输入样本,选择 KNN,并最终预测此样本的类别值。
在以下屏幕截图中,我们可以看到代码输出以及此 KNN 分类后原始图像和结果图像之间的差异:

使用原色作为类别的 KNN 分类(左:原始图像,右:结果图像)
随机森林分类器
随机森林是一类使用决策树作为基分类器的集成构建方法。随机森林分类器是 Bagging 分类器(Bootstrap Aggregating)的一种变体。Bagging 算法是一种分类方法,使用自助法生成弱个体分类器。每个分类器都在训练集的随机重新分配上训练,以便许多原始示例可能在每个分类中重复。
Bagging 和随机森林的主要区别在于,Bagging 使用每个树节点中的所有特征,而随机森林则选择特征的一个随机子集。合适的随机特征数量对应于特征总数的平方根。对于预测,一个新的样本被推送到树中,并分配给树中终端(或叶)节点的类别。这种方法在所有树上迭代,最后,将所有树的预测的平均投票作为预测结果。以下图表显示了随机森林算法:

RF 分类器
随机森林目前在识别能力和效率方面都是最好的分类器之一。在我们的示例RFClassifier中,我们使用了 OpenCV 随机森林分类器和 OpenCV 的CvMLData类。在机器学习问题中通常处理大量信息,因此使用.cvs文件很方便。CvMLData类用于以下方式从这样的文件中加载训练集信息:
//… (omitted for simplicity)
int main(int argc, char *argv[]){
CvMLData mlData;
mlData.read_csv("iris.csv");
mlData.set_response_idx(4);
//Select 75% samples as training set and 25% as test set
CvTrainTestSplit cvtts(0.75f, true);
//Split the iris dataset
mlData.set_train_test_split(&cvtts);
//Get training set
Mat trainsindex= mlData.get_train_sample_idx();
cout<<"Number of samples in the training set:"<<trainsindex.cols<<endl;
//Get test set
Mat testindex=mlData.get_test_sample_idx();
cout<<"Number of samples in the test set:"<<testindex.cols<<endl;
cout<<endl;
//Random Forest parameters
CvRTParams params = CvRTParams(3, 1, 0, false, 2, 0, false, 0, 100, 0, CV_TERMCRIT_ITER | CV_TERMCRIT_EPS);
CvRTrees classifierRF;
//Taining phase
classifierRF.train(&mlData,params);
std::vector<float> train_responses, test_responses;
//Calculate train error
cout<<"Error on train samples:"<<endl;
cout<<(float)classifierRF.calc_error( &mlData, CV_TRAIN_ERROR,&train_responses)<<endl;
//Print train responses
cout<<"Train responses:"<<endl;
for(int i=0;i<(int)train_responses.size();i++)
cout<<i+1<<":"<<(float)train_responses.at(i)<<" ";
cout<<endl<<endl;
//Calculate test error
cout<<"Error on test samples:"<<endl;
cout<<(float)classifierRF.calc_error( &mlData, CV_TEST_ERROR,&test_responses)<<endl;
//Print test responses
cout<<"Test responses:"<<endl;
for(int i=0;i<(int)test_responses.size();i++)
cout<<i+1<<":"<<(float)test_responses.at(i)<<" ";
cout<<endl<<endl;
return 0;
}
小贴士
数据集由 UC Irvine 机器学习仓库提供,可在archive.ics.uci.edu/ml/找到。对于此代码示例,使用了 Iris 数据集。
如我们之前提到的,CvMLData类允许您使用read_csv函数从.csv文件加载数据集,并通过set_response_idx函数指示类列。在这种情况下,我们使用这个数据集进行训练和测试阶段。可以将数据集分成两个不相交的集合用于训练和测试。为此,我们使用CvTrainTestSplit结构和void CvMLData::set_train_test_split(const CvTrainTestSplit* spl)函数。在CvTrainTestSplit结构中,我们指示用作训练集的样本百分比(在我们的情况下为 0.75%),以及我们是否希望从数据集中混合训练和测试样本的索引。set_train_test_split函数执行分割。然后,我们可以使用get_train_sample_idx()和get_test_sample_idx()函数将每个集合存储在Mat中。
随机森林分类器是通过CvRTrees类创建的,其参数由CvRTParams::CvRTParams(int max_depth, int min_sample_count, float regression_accuracy, bool use_surrogates, int max_categories, const float* priors, bool calc_var_importance, int nactive_vars, int max_num_of_trees_in_the_forest, float forest_accuracy, int termcrit_type)构造函数定义。其中一些最重要的输入参数指的是树的最大深度(max_depth)——在我们的示例中,它的值为 3——每个节点中随机特征的数量(nactive_vars),以及森林中树的最大数量(max_num_of_trees_in_the_forest)。如果我们将nactive_vars参数设置为 0,随机特征的数量将是特征总数的平方根。
最后,一旦使用train函数训练了分类器,我们可以使用float CvRTrees::calc_error(CvMLData* data, int type, std::vector<float>* resp=0 )方法获得误分类样本的百分比。参数类型允许您选择错误的来源:CV_TRAIN_ERROR(训练样本中的错误)或CV_TEST_ERROR(测试样本中的错误)。
以下截图显示了训练和测试错误以及两个集合中的分类器响应:

随机森林分类器示例结果
用于分类的支持向量机(SVM)
支持向量机(SVM)分类器通过最大化类别之间的几何间隔来找到一个判别函数。因此,空间被映射得尽可能使类别之间尽可能分离。SVM 最小化训练误差和几何间隔。如今,这种分类器是可用的最佳分类器之一,并被应用于许多实际问题。下面的 SVMClassifier 示例代码使用 SVM 分类器和包含 66 个图像对象的数据库进行分类。该数据库分为四个类别:一双训练鞋(类别 1)、一个毛绒玩具(类别 2)、一个塑料杯子(类别 3)和一个蝴蝶结(类别 4)。以下截图显示了四个类别的示例。总共使用了 56 张图像用于训练集,10 张图像用于测试集。训练集中的图像采用以下名称结构:[1-14].png 对应于类别 1,[15-28].png 对应于类别 2,[29-42].png 对应于类别 3,[43-56].png 对应于类别 4。另一方面,测试集中的图像以“unknown”一词开头,后跟一个数字,例如,unknown1.png。
小贴士
四个类别的图像已从位于 aloi.science.uva.nl/ 的阿姆斯特丹物体图像库(ALOI)中提取。

为 SVM 分类示例选择的类别
SVMClassifier 示例代码如下:
//… (omitted for simplicity)
#include <opencv2/features2d/features2d.hpp>
#include <opencv2/nonfree/features2d.hpp>
using namespace std;
using namespace cv;
int main(int argc, char *argv[]){
Mat groups;
Mat samples;
vector<KeyPoint> keypoints1;
//ORB feature detector with 15 interest points
OrbFeatureDetector detector(15, 1.2f, 2, 31,0, 2, ORB::HARRIS_SCORE, 31);
Mat descriptors, descriptors2;
//SURF feature descriptor
SurfDescriptorExtractor extractor;
//Training samples
for(int i=1; i<=56; i++){
stringstream nn;
nn <<i<<".png";
//Read the image to be trained
Mat img=imread(nn.str());
cvtColor(img, img, COLOR_BGR2GRAY);
//Detect interest points
detector.detect(img, keypoints1);
//Compute SURF descriptors
extractor.compute(img, keypoints1, descriptors);
//Organize and save information in one row
samples.push_back(descriptors.reshape(1,1));
keypoints1.clear();
}
//Set the labels of each sample
for(int j=1; j<=56; j++){
if(j<=14) groups.push_back(1);
else if(j>14 && j<=28) groups.push_back(2);
else if(j>28 && j<=42) groups.push_back(3);
else groups.push_back(4);
}
//Indicate SVM parameters
CvSVMParams params=CvSVMParams(CvSVM::C_SVC, CvSVM::LINEAR, 0, 1, 0, 1, 0, 0, 0, cvTermCriteria(CV_TERMCRIT_ITER+CV_TERMCRIT_EPS, 100, FLT_EPSILON));
//Create SVM classifier
CvSVM classifierSVM;
//Train classifier
classifierSVM.train(samples, groups, Mat(), Mat(), params );
//Test samples
for(int i=1; i<=10; i++){
stringstream nn;
nn <<"unknown"<<i<<".png";
//Read the image to be tested
Mat unknown=imread(nn.str());
cvtColor(unknown, unknown, COLOR_BGR2GRAY);
//Detect interest points
detector.detect(unknown, keypoints1);
//Compute descriptors
extractor.compute(unknown, keypoints1, descriptors2);
//Test sample
float result=classifierSVM.predict(descriptors2.reshape(1,1));
//Print result
cout<<nn.str()<<": class "<<result<<endl;
}
return 0;
}
代码的解释如下。在这个例子中,图像通过其描述符来表示(参见第五章,专注于有趣的 2D 特征)。对于训练集中的每个图像,使用Oriented FAST and Rotated BRIEF(ORB)检测器(OrbFeatureDetector)检测其兴趣点,并使用Speeded Up Robust Features(SURF)描述符(SurfDescriptorExtractor)计算其描述符。
使用 CvSVM 类创建 SVM 分类器,并使用 CvSVMParams::CvSVMParams(int svm_type, int kernel_type, double degree, double gamma, double coef0, double Cvalue, double nu, double p, CvMat* class_weights, CvTermCriteria term_crit) 构造函数设置其参数。在这个构造函数中,有趣的参数是 SVM 的类型(svm_type)和核的类型(kernel_type)。第一个指定的参数在我们的情况下采用 CvSVM::C_SVC 值,因为我们需要一个 n 类分类(n
2)且类别之间不完全分离。它还使用 C 惩罚值来处理异常值。因此,C 起到正则化的作用。kernel_type 参数指示 SVM 核的类型。核代表分离案例所需的基础函数。对于 SVM 分类器,OpenCV 包含以下核:
-
CvSVM::LINEAR:线性核 -
CvSVM::POLY:多项式核 -
CvSVM::RBF:径向基函数 -
CvSVM::SIGMOID:Sigmoid 核
然后,分类器使用训练集(通过train函数)构建一个最优的线性判别函数。现在,它已准备好对新未标记样本进行分类。测试集用于此目的。请注意,我们还需要为测试集中的每张图像计算 ORB 检测器和 SURF 描述符。结果如下所示,其中所有类别都已正确分类:

使用 SVM 进行分类的结果
那么,GPU 怎么样?
CPU 似乎已经达到了它们的速度和热功率极限。构建具有多个处理器的计算机变得复杂且昂贵。这就是 GPU 发挥作用的地方。通用计算图形处理单元(GPGPU)是一种新的编程范式,它使用 GPU 进行计算,并使程序执行更快,功耗降低。它们包括数百个通用计算处理器,可以完成比渲染图形更多的工作,特别是如果它们用于可以并行化的任务,这在计算机视觉算法中是常见的情况。
OpenCV 包括对 OpenCL 和 CUDA 架构的支持,后者实现了更多算法,并且优化更好。这就是为什么我们在本章中引入 CUDA GPU 模块的原因。
配置 OpenCV 与 CUDA
在第一章中介绍的安装指南,入门,为了包含 GPU 模块,需要一些额外的步骤。我们假设 OpenCV 将要安装的计算机已经安装了该指南中详细说明的软件。
在 Windows 上编译 OpenCV 与 CUDA 结合时,需要满足以下新要求:
-
CUDA 兼容的 GPU:这是主要要求。请注意,CUDA 由 NVIDIA 开发,因此它仅与 NVIDIA 显卡兼容。此外,显卡的型号必须列在
developer.nvidia.com/cuda-gpus上。所谓的计算能力(CC)也可以在这个网站上检查,因为它将在以后需要。 -
Microsoft Visual Studio:CUDA 仅与此 Microsoft 编译器兼容。可以安装免费的 Visual Studio Express 版本。请注意,截至写作时,Visual Studio 2013 仍然与 CUDA 不兼容,所以我们在这本书中使用 Visual Studio 2012。
-
NVIDIA CUDA Toolkit:这包括 GPU 编译器、库、工具和文档。此工具包可在
developer.nvidia.com/cuda-downloads获取。 -
Qt 库针对 Visual C++编译器:在第一章,入门中,安装了 Qt 库的 MinGW 二进制文件,但它们与 Visual C++编译器不兼容。可以通过位于
C:\Qt中的MaintenanceTool应用程序使用软件包管理器下载兼容版本。一个好的选择是下面的截图所示的msvc201232 位组件。还需要更新Path环境变量以包含新位置(例如,在我们的本地系统中,它是C:\Qt\5.2.1\msvc2012\bin)。Qt 库包含在编译中,以便利用其用户界面功能。![设置带有 CUDA 的 OpenCV]()
下载 Qt 库的新版本
配置 OpenCV 的构建
使用 CMake 的构建配置与第一章中解释的典型配置在某些方面有所不同。以下是对这些差异的解释:
-
当选择项目的生成器时,你必须选择与机器中安装的环境相对应的 Visual Studio 编译器版本。在我们的例子中,Visual Studio 11 是正确的编译器,因为它对应于 Visual Studio 2012 中包含的编译器版本。以下截图显示了此选择。
-
在选择构建选项时,我们必须关注与 CUDA 相关的选项。如果 CUDA 工具包的安装正确,CMake 应自动检测其位置并激活
WITH_CUDA选项。此外,工具包的安装路径通过CUDA_TOOLKIT_ROOT_DIR显示。另一个有趣的选项是CUDA_ARCH_BIN,因为如果我们只选择我们 GPU 的相应版本,编译时间可以显著减少;否则,它将为所有架构编译代码。如前所述,版本可以在developer.nvidia.com/cuda-gpus上检查。以下截图显示了我们的构建配置中设置的选项:![配置 OpenCV 的构建]()
CMake 构建配置
构建和安装库
CMake 在目标目录中生成多个 Visual Studio 项目,ALL_BUILD是基本的项目。一旦在 Visual Studio 中打开,我们可以选择构建配置(调试或发布)以及架构(Win32 或 Win64)。通过按F7或点击构建解决方案开始编译。编译完成后,建议打开并构建INSTALL项目,因为它会生成包含所有必要文件的安装目录。
最后,需要使用新生成的二进制文件的位置更新Path系统。重要的是要从Path变量中删除旧位置,并只在其中包含一个版本的二进制文件。
注意
Qt Creator 应该现在可以找到两个编译器和两个 Qt 版本:一个用于 Visual C++,一个用于 MingGW。在创建新项目时,我们必须根据开发的应用程序选择正确的工具包。也可以更改现有项目的配置,因为工具包是可以管理的。
设置 OpenCV 与 CUDA 的一步快速方法
安装过程可以总结如下步骤:
-
安装 Microsoft Visual Studio Express 2012。
-
下载并安装 NVIDIA CUDA Toolkit(可在
developer.nvidia.com/cuda-downloads获取)。 -
将 Visual C++ 编译器的二进制文件添加到 Qt 安装目录,并使用新位置更新
Path系统环境变量(例如,C:\Qt\5.2.1\msvc2012\bin)。 -
使用 CMake 配置 OpenCV 构建。设置
WITH_CUDA、CUDA_ARCH_BIN、WITH_QT和BUILD_EXAMPLES选项。 -
打开
ALL_BUILDVisual Studio 项目并构建它。对INSTALL项目执行相同的操作。 -
修改
Path环境变量以更新 OpenCV 的 bin 目录(例如,C:\opencv-buildCudaQt\install\x86\vc11\bin)。
我们的第一个基于 GPU 的程序
在本节中,我们展示了同一程序的两种版本:一个版本使用 CPU 进行计算,另一个版本使用 GPU。这两个示例分别称为 edgesCPU 和 edgesGPU,使我们能够指出在使用 OpenCV 的 GPU 模块时的差异。
首先介绍 edgesCPU 示例:
#include <iostream>
#include "opencv2/core/core.hpp"
#include "opencv2/highgui/highgui.hpp"
#include "opencv2/imgproc/imgproc.hpp"
using namespace cv;
int main(int argc, char** argv){
if ( argc < 2 ){
std::cout << "Usage: ./edgesGPU <image>" << std::endl;
return -1;
}
Mat orig = imread(argv[1]);
Mat gray, dst;
bilateralFilter(orig,dst,-1,50,7);
cvtColor(dst,gray,COLOR_BGR2GRAY);
Canny(gray,gray,7,20);
imshow("Canny Filter", gray);
waitKey(0);
return 0;
}
现在 edgesGPU 示例如下所示:
#include <iostream>
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/gpu/gpu.hpp>
using namespace cv;
int main( int argc, char** argv){
if ( argc < 2 ){
std::cout << "Usage: ./edgesGPU <image>" << std::endl;
return -1;
}
Mat orig = imread(argv[1]);
gpu::GpuMat g_orig, g_gray, g_dst;
//Transfer the image data to the GPU
g_orig.upload(orig);
gpu::bilateralFilter(g_orig,g_dst,-1,50,7);
gpu::cvtColor(g_dst,g_gray,COLOR_BGR2GRAY);
gpu::Canny(g_gray,g_gray,7,20);
Mat dst;
//Copy the image back to the CPU memory
g_gray.download(dst);
imshow("Canny Filter", dst);
waitKey(0);
return 0;
}
以下是代码的解释。尽管它们最终获得相同的结果,如以下截图所示,但前几个示例中存在一些差异。添加了一个新的头文件作为新的数据类型,并使用了不同的算法实现。#include <opencv2/gpu/gpu.hpp> 包含了 GpuMat 数据类型,这是在 GPU 内存中存储图像的基本容器。它还包括第二示例中使用的过滤器算法的特定 GPU 版本。
一个重要的考虑因素是我们需要在 CPU 和 GPU 之间传输图像。这是通过 g_orig.upload(orig) 和 g_gray.download(dst) 方法实现的。一旦图像上传到 GPU,我们就可以对其应用不同的操作,这些操作在 GPU 上执行。为了区分需要运行的算法版本,使用 gpu 命名空间,如 gpu::bilateralFilter、gpu::cvtColor 和 gpu::Canny。在应用过滤器之后,图像再次复制到 CPU 内存中并显示。
关于性能,CPU 版本运行时间为 297 毫秒,而 GPU 版本只需 18 毫秒。换句话说,GPU 版本运行速度快了 16.5 倍。

edgesCPU 和 edgesGPU 示例的输出
实时处理
使用 GPU 在图像中进行计算的主要优点之一是它们要快得多。这种速度的提升允许你在实时应用中运行重量级的计算算法,例如立体视觉、行人检测或密集光流。在下一个matchTemplateGPU示例中,我们展示了一个在视频序列中匹配模板的应用:
#include <iostream>
#include "opencv2/core/core.hpp"
#include "opencv2/highgui/highgui.hpp"
#include "opencv2/features2d/features2d.hpp"
#include "opencv2/gpu/gpu.hpp"
#include "opencv2/nonfree/gpu.hpp"
using namespace std;
using namespace cv;
int main( int argc, char** argv )
{
Mat img_template_cpu = imread( argv[1],IMREAD_GRAYSCALE);
gpu::GpuMat img_template;
img_template.upload(img_template_cpu);
//Detect keypoints and compute descriptors of the template
gpu::SURF_GPU surf;
gpu::GpuMat keypoints_template, descriptors_template;
surf(img_template,gpu::GpuMat(),keypoints_template, descriptors_template);
//Matcher variables
gpu::BFMatcher_GPU matcher(NORM_L2);
//VideoCapture from the webcam
gpu::GpuMat img_frame;
gpu::GpuMat img_frame_gray;
Mat img_frame_aux;
VideoCapture cap;
cap.open(0);
if (!cap.isOpened()){
cerr << "cannot open camera" << endl;
return -1;
}
int nFrames = 0;
uint64 totalTime = 0;
//main loop
for(;;){
int64 start = getTickCount();
cap >> img_frame_aux;
if (img_frame_aux.empty())
break;
img_frame.upload(img_frame_aux);
cvtColor(img_frame,img_frame_gray, CV_BGR2GRAY);
//Step 1: Detect keypoints and compute descriptors
gpu::GpuMat keypoints_frame, descriptors_frame;
surf(img_frame_gray,gpu::GpuMat(),keypoints_frame, descriptors_frame);
//Step 2: Match descriptors
vector<vector<DMatch>>matches; matcher.knnMatch(descriptors_template,descriptors_frame,matches,2);
//Step 3: Filter results
vector<DMatch> good_matches;
float ratioT = 0.7;
for(int i = 0; i < (int) matches.size(); i++)
{
if((matches[i][0].distance < ratioT*(matches[i][1].distance)) && ((int) matches[i].size()<=2 && (int) matches[i].size()>0))
{
good_matches.push_back(matches[i][0]);
}
}
// Step 4: Download results
vector<KeyPoint> keypoints1, keypoints2;
vector<float> descriptors1, descriptors2;
surf.downloadKeypoints(keypoints_template, keypoints1);
surf.downloadKeypoints(keypoints_frame, keypoints2);
surf.downloadDescriptors(descriptors_template, descriptors1);
surf.downloadDescriptors(descriptors_frame, descriptors2);
//Draw the results
Mat img_result_matches;
drawMatches(img_template_cpu, keypoints1, img_frame_aux, keypoints2, good_matches, img_result_matches);
imshow("Matching a template", img_result_matches);
int64 time_elapsed = getTickCount() - start;
double fps = getTickFrequency() / time_elapsed;
totalTime += time_elapsed;
nFrames++;
cout << "FPS : " << fps <<endl;
int key = waitKey(30);
if (key == 27)
break;;
}
double meanFps = getTickFrequency() / (totalTime / nFrames);
cout << "Mean FPS: " << meanFps << endl;
return 0;
}
代码的解释如下。正如第五章中详细描述的,专注于有趣的 2D 特征,可以使用特征来找到两张图像之间的对应关系。随后在每一帧中搜索的模板图像首先使用 GPU 版本的 SURF(gpu::SURF_GPU surf;)来检测兴趣点和提取描述符。这是通过运行surf(img_template,gpu::GpuMat(),keypoints_template, descriptors_template);来实现的。对于视频序列中取出的每一帧,都执行相同的处理过程。为了匹配两张图像的描述符,还创建了一个 GPU 版本的 BruteForce 匹配器gpu::BFMatcher_GPU matcher(NORM_L2);。由于兴趣点和描述符存储在 GPU 内存中,并且在我们能够显示它们之前需要下载,因此需要额外的步骤。这就是为什么执行了surf.downloadKeypoints(keypoints, keypoints);和surf.downloadDescriptors(descriptors, descriptors);。下面的截图显示了示例运行的情况:

使用摄像头进行模板匹配
性能
选择 GPU 编程的主要动机是性能。因此,这个例子包括了时间测量,以比较相对于 CPU 版本获得的速度提升。具体来说,通过getTickCount()方法在程序主循环的开始处节省了时间。在这个循环的末尾,同样使用了getTickFrequency,这有助于计算当前帧的 FPS。每一帧所花费的时间被累积,在程序结束时计算平均值。前一个例子平均延迟为 15 FPS,而使用 CPU 数据类型和算法的相同例子仅达到 0.5 FPS。这两个例子都在相同的硬件上进行了测试:一台配备 i5-4570 处理器的 PC 和 NVIDIA GeForce GTX 750 显卡。显然,速度提升 30 倍是显著的,尤其是在我们只需要更改几行代码的情况下。
摘要
在本章中,我们介绍了 OpenCV 的两个高级模块:机器学习和 GPU。机器学习具有让计算机做出决策的能力。为此,需要训练和验证一个分类器。本章提供了三个分类示例:KNN 分类器、使用 .cvs 数据库的随机森林,以及使用图像数据库的 SVM。本章还讨论了与 CUDA 一起使用 OpenCV 的方法。GPU 在密集型任务中发挥着越来越重要的作用,因为它们可以卸载 CPU 并运行并行任务,例如在计算机视觉算法中遇到的任务。已经提供了几个 GPU 示例:GPU 模块安装、一个基本的第一个 GPU 程序,以及实时模板匹配。
还有什么吗?
现在的 GPU 模块涵盖了 OpenCV 的大多数功能;因此,建议您探索这个库并检查哪些算法可用。此外,performance_gpu 程序可以在 [opencv_build]/install/x86/vc11/samples/gpu 找到,它展示了使用 GPU 版本时许多 OpenCV 算法的加速效果。










浙公网安备 33010602011771号