大數據

带你读《OpenCV 4计算机视觉项目实战 (原书第2版)》之二:OpenCV基础知识导论

点击查看第一章
点击查看第三章

第2章 OpenCV基础知识导论

在第1章介绍了在不同操作系统上安装OpenCV之后,我们将在本章介绍OpenCV开发的基础知识。首先介绍如何使用CMake创建项目。我们将介绍基本的图像数据结构和矩阵,以及在项目中工作所需的其他结构。我们还会介绍如何通过OpenCV的XML/YAML存储函数将变量和数据保存到文件中。
本章介绍以下主题:

  • 使用CMake配置项目
  • 从/向磁盘读取/写入图像
  • 读取视频和访问相机设备
  • 主要图像结构(例如,矩阵)
  • 其他重要和基本的结构(例如,向量和标量)
  • 基本矩阵运算简介
  • 使用XML/YAML存储OpenCV API进行文件存储操作

2.1 技术要求

本章需要读者熟悉基本的C++编程语言,所使用的所有代码都可以从以下GitHub链接下载:https://github.com/PacktPublishing/Learn-OpenCV-4-By-Building-Projects-Second-Edition/tree/master/Chapter_02 。代码可以在任何操作系统上执行,尽管只在Ubuntu上测试过。

2.2 基本CMake配置文件

为配置和检查项目的所有必要依赖项,我们会用到CMake,但这不是唯一可以完成此操作的方法。我们可以在任何其他工具或IDE中配置我们的项目,例如Makefiles或Visual Studio,但CMake是一种用于配置多平台C++项目的更便携的方式。
CMake使用名为CMakeLists.txt的配置文件,可以在其中定义编译和依赖关系过程。对于从单个源代码文件构建可执行文件的基本项目,只需要一个包含三行代码的CMakeLists.txt文件。
该文件的内容类似于:


image.png

第一行定义所需的CMake最低版本,该行在CMakeLists.txt文件中是必需的,它使我们能够使用在特定版本中定义的CMake功能。在我们的例子中,要求最低版本为CMake 3.0。第二行定义项目的名称。这个名称保存在名为PROJECT_NAME的变量中。
最后一行从main.cpp文件创建一个可执行命令(add_executable()),并将其命名为与项目(${PROJECT_NAME})相同的名称,然后将源代码编译成一个名为CMakeTest的可执行文件,这是我们设置的项目名称。${}表达式能够访问环境中定义的任何变量。之后,我们就可以用${PROJECT_NAME}变量作为输出的可执行文件的名称。

2.3 创建一个库

CMake可用于创建由OpenCV构建系统使用的库。在多个应用程序之间分解共享代码是软件开发中常见且有用的做法。在大型应用程序中,或者在多个应用程序共享的公共代码中,这种做法非常有用。在这种情况下,我们不创建二进制可执行文件,而是创建一个包含所有函数、类等的编译文件。这样就可以和其他应用程序分享此库文件,而无须共享我们的源代码。
CMake为此提供了add_library函数:

image.png

以#开头的行是添加的注释,会被CMake忽略。add_library(Hello hello.cpp hello.h)命令定义库的源文件及其名称,其中Hello是库名,hello.cpp和hello.h是源文件。我们还添加了头文件,使得诸如Visual Studio这样的IDE能够链接到头文件。该行将会生成一个共享(.so适用于Mac OS X和Unix,.dll适用于Windows)或静态库(.a适用于Mac OS X和Unix,.lib适用于Windows)文件,具体取决于我们是否在库名和源文件之间添加SHARED或STATIC字。target_link_libraries(executable Hello)是将可执行文件链接到所需库的函数,在我们的例子中,需要的库是Hello库。

2.4 管理依赖项

CMake具备搜索依赖项和外部库的能力,这使我们能够根据项目中的外部组件构建复杂的项目,并添加一些要求。
在本书中,最重要的依赖项自然是OpenCV,我们将把它添加到我们的所有项目中:

image.png

现在,我们通过以下代码了解脚本的工作原理:

image.png

第一行定义CMake的最低版本,第二行告诉CMake使用CMake的新行为,以便识别正确的数字和布尔常量,而无须使用这些名称间接引用变量。该策略是在CMake 2.8.0中引入的,当3.0.2版本中未设置此策略时,CMake会发出警告。最后一行定义项目的标题。定义项目名称后,我们必须定义需求、库和依赖项:

image.png

这段代码搜索OpenCV依赖项。FIND_PACKAGE能够查找依赖项、所需的最低版本以及该依赖是必需的还是可选的。在这个示例脚本中,我们查找4.0.0或更高版本的OpenCV,并声明它是必需包。

image.png

如果CMake没有找到它,就会返回错误,并且不会阻止我们编译应用程序。MESSAGE函数在终端或CMake GUI中显示一条消息。在这个例子中,我们将这样显示OpenCV版本:

image.png

${OpenCV_VERSION}是CMake用来存储OpenCV包版本的变量。include_directories()和link_directories()向环境中添加指定库的头文件和路径。OpenCV CMake的模块将这些数据保存在${OpenCV_INCLUDE_DIRS}和${OpenCV_LIB_DIR}变量中。并非所有平台(例如Linux)都需要这些命令行,因为这些路径通常位于环境中,但是建议使用多个OpenCV版本来选择正确的链接并包含路径。现在包含我们开发的源文件:

image.png

最后一行创建可执行文件,并将可执行文件与OpenCV库链接,如上一节中所述。这段代码中有一个新的函数SET,该函数创建一个新变量,并向其添加我们需要的任何值。在这个例子中,我们将main.cpp值合并到SRC变量中。我们还可以在同一个变量中添加更多的值,如下面的脚本所示:

image.png

2.5 让脚本更复杂

在本节中,我们将要展示一个更复杂的脚本,它包括子文件夹、库和可执行文件。但实际上,该脚本只有两个文件和几行代码,如下例所示。没有必要创建多个CMakeLists.txt文件,因为我们可以在主CMakeLists.txt文件中指定所有内容。但是,为每个项目子文件夹使用不同的CMakeLists.txt文件更为常见,可以使其更加灵活和便携。
这个例子有一个代码结构文件夹,其中包含一个utils库文件夹和一个根文件夹,后者包含主可执行文件:

image.png

然后,我们必须定义两个CMakeLists.txt文件,一个在根文件夹中,另一个在utils文件夹中。CMakeLists.txt根文件夹文件具有以下内容:

image.png

除了我们将要解释的一些函数之外,几乎所有的代码行都在前面中有过描述。add_subdirectory()告诉CMake分析所需子文件夹的CMakeLists.txt。在继续说明主CMakeLists.txt文件之前,我们先解释utils中的CMakeLists.txt文件。
在utils文件夹的CMakeLists.txt文件中,我们将编写一个将包含在主项目文件夹中的新库:

image.png

此CMake脚本文件定义一个变量UTILS_LIB_SRC,我们在其中添加库中包含的所有源文件,并使用add_library函数生成库,并且使用target_include_directories函数以便允许主项目检测所有头文件。离开utils子文件夹,继续准备根CMake脚本,其中,Option函数创建一个新的变量,在这个例子中为WITH_LOG,并附带一小段描述。可以通过ccmake命令行或显示描述内容的CMake GUI界面更改这个变量,用户还可以通过一个复选框启用或禁用此选项。这个函数非常有用,它使用户能够决定编译时功能,例如,我们是否要启用或禁用日志,是否像OpenCV一样使用Java或Python进行编译,等等。
在这个例子中,我们使用此选项在应用程序中启用记录器。为启用记录器,我们在代码中使用了一个预编译器定义,如下所示:

image.png

可以通过调用add_definitions函数(-DLOG)在CMakeLists.txt中定义这个LOG宏,该函数本身可以使用简单条件根据CMake变量WITH_LOG运行或隐藏:

image.png

至此,我们就完成了创建CMake脚本文件的准备工作,可以在任何操作系统中编译我们的计算机视觉项目。然后,在开始示例工程之前,我们会继续介绍OpenCV的基础知识。

2.6 图像和矩阵

毫无疑问,计算机视觉中最重要的结构是图像。计算机视觉中的图像是用数字设备捕获的物理世界的表示。这种图片只是以矩阵格式存储的一系列数字(参见图2-1)。每个数字是所考虑的波长(例如,彩色图像中的红色、绿色或蓝色)或波长范围(对于全色设备)的光强度的测量结果。图像中的每个点都称为像素(对于图像元素),并且每个像素可以存储一个或多个值,这取决于它是否是仅存储一个值的黑白图像(也称为二进制图像,比如只存储0或1),还是存储两个值的灰度图像,或者是存储三个值的彩色图像。这些值通常在整数0~255,但也可以使用其他范围,比如在高动态范围成像(high dynamic range imaging,简称HDRI)或热图像领域中的浮点数0~1。
图像是以矩阵格式存储的,其中的每个像素都有一个位置,并且可以通过列和行的编号来引用。OpenCV用Mat类来达到这个目的。在灰度图像中,使用单个矩阵,如图2-2所示。
在如图2-3所示的彩色图像中,使用了一个宽度×高度×颜色通道数的矩阵。


image.png
image.png

但Mat类不仅仅用于存储图像,它还能存储任何类型和不同大小的矩阵。你可以将其用作代数矩阵并用它执行运算。在接下来的内容中,我们将描述最重要的矩阵运算,例如加法、乘法、对角化。但是,在此之前,了解矩阵如何存储在计算机内存中是非常重要的,因为直接访问内存,总比用OpenCV函数访问每个像素更加高效。
在内存中,矩阵被保存为按列和行排序的数组或值序列。表2-1显示BGR图像格式的像素序列。

image.png

按照这个顺序,我们可以通过以下公式来访问任何像素:

image.png

2.7 读/写图像

在介绍矩阵之后,我们将首先讨论OpenCV代码的基础知识。我们要学习的第一件事是如何读/写图像:

image.png
image.png

现在我们来理解代码。

image.png

首先,必须包括例子中需要的函数的声明。这些函数来自core(基本图像数据处理)和highgui(OpenCV提供的跨平台I/O函数是core和highgui;第一个包括基本类,比如矩阵,而第二个包括读函数、写函数,以及用图形界面显示图像的函数)。现在读取图像:

image.png

imread是读取图像的主函数。该函数打开图像,并以矩阵格式存储它。imread接受两个参数,第一个参数是图像路径字符串,第二个参数是可选的,用于指定要加载的图像类型,默认情况下为彩色图像。第二个参数可以使用以下选项:

  • cv::IMREAD_UNCHANGED:如果设置,当输入具有相应的深度时,返回16位/ 32位图像,否则将其转换为8位
  • cv::IMREAD_COLOR:如果设置,它总是将图像转换为彩色图像(BGR,8位无符号)
  • cv::IMREAD_GRAYSCALE:如果设置,它总是将图像转换为灰度图像(8位无符号)

要保存图像,可以使用imwrite函数,它将矩阵图像存储在计算机中:

image.png

第一个参数是保存图像的路径,以及想要的扩展名格式,第二个参数是要保存的矩阵图像。在这个代码例子中,我们创建并存储图像的灰度版本,然后将其另存为.jpg文件。加载的灰度图像将存储在gray变量中:

image.png

通过使用矩阵的.cols和.rows属性,可以访问图像的列数和行数,换句话说,可以访问其宽度和高度:

image.png

要访问图像的一个像素,可以用Mat OpenCV类中的模板函数cv::Mat::at (row,col),模板参数是所需的返回类型。8位彩色图像中的类型名称是Vec3b类,它存储三个无符号字符数据(Vec =向量,3 =组件数,b = 一个字节)。在灰度图像中,可以直接使用无符号字符,或图像中使用的任何其他数字格式,例如uchar pixel = color.at (myRow,myCol)。最后,为了展示图像,可以使用imshow函数,它创建一个窗口,其标题作为第一个参数,图像矩阵作为第二个参数:

image.png

前面代码的结果如图2-4所示,左边的图像是彩色图像,右边的图像是灰度图像。

image.png

最后,我们按以下示例创建CMakeLists.txt文件,并使用该文件编译代码。
以下代码描述了CMakeLists.txt文件:

image.png

要使用此CMakeLists.txt文件编译代码,必须执行以下步骤:

  1. 创建一个build文件夹。
  2. 在build文件夹内,(在Windows中)执行CMake或打开CMake GUI应用程序,选择source文件夹和build文件夹,然后按下“Configure”(配置)和“Generate”(生成)按钮。
  3. 如果正在使用Linux或MacOSX,请照常生成Makefile,然后用make命令编译项目。如果正在使用Windows,请用在步骤2中选择的编辑器打开项目,然后进行编译。

在编译应用程序之后,将会在build文件夹中生成一个名为app的可执行文件。

2.8 读取视频和摄像头

本节将用这个简单示例向你介绍视频和摄像头的读取。在解释如何读取视频或摄像头的输入之前,我们想介绍一个非常有用的新类,它可以帮助我们管理输入命令行参数。这个新类是在OpenCV 3.0版中引入的,它就是CommandLineParser类:

image.png

我们必须为CommandLineParser做的第一件事是在常量char向量中定义我们需要或允许的参数,每一行都采用以下模式:

image.png

name_param可以以@开头,这会将此参数定义为默认输入。我们可以使用多个name_param:

image.png

构造函数将获取main函数的输入和先前定义的key常量:

image.png

.has类方法检查参数是否存在。在示例中,我们检查用户是否添加参数help或?,然后使用类函数printMessage显示所有描述参数:

image.png

使用.get(parameterName)函数可以访问和读取任何输入参数:

image.png

获取所有必需的参数以后,即可检查这些参数是否被正确解析,并在其中一个参数未被解析时显示错误消息,例如,添加的是一个字符串而不是一个数字:

image.png

用于视频读取和摄像头读取的类是相同的VideoCapture类,与之前版本的OpenCV中一样,它属于videoio子模块而不是highgui子模块。创建对象后,我们检查输入命令行参数videoFile是否有路径文件名。如果它是空的,那么尝试打开网络摄像头;如果它有文件名,则打开视频文件。为此,可以使用open函数,将视频文件名或我们要打开的索引摄像头作为参数。如果我们有一个摄像头,可以用0作为参数。
要检查是否可以读取视频文件名或摄像头,可以使用isOpened函数:

image.png

最后,创建一个窗口,使用namedWindow函数和无限循环来显示帧,用>>操作抓取每个帧,如果正确地检索到帧,则使用imshow函数显示该帧。在这种情况下,我们不想让应用程序停止,但是会调用waitKey(30)等待30毫秒,以此检查用户是否使用任何键停止应用程序的执行。

image.png

当用户想结束应用程序时,他们所要做的就是按下任意键,然后我们必须使用释放函数释放所有的视频资源。

image.png

前面代码的结果是用一个新窗口显示BGR格式的视频或网络摄像头。

2.9 其他基本对象类型

我们已经了解了Mat和Vec3b类,但还有很多类需要学习。
在本节中,我们将学习大多数项目中所需的最基本的对象类型:

  • Vec
  • Scalar
  • Point
  • Size
  • Rect
  • RotatedRect

2.9.1 Vec对象类型

Vec是一个主要用于数值向量的模板类。我们可以定义向量的类型和组件的数量:

image.png

我们还可以使用任何的预定义类型:

image.png
image.png
image.png


2.9.2 Scalar对象类型

Scalar对象类型是从Vec派生的模板类,有四个元素。Scalar类型在OpenCV中广泛用于传递和读取像素值。
要访问Vec和Scalar值,可以使用[]运算符,其初始化可以用传值的方式通过设置另一个标量、向量或值来完成,如下例所示:

image.png

2.9.3 Point对象类型

另一个非常常见的类模板是Point。该类定义一个由其坐标x和y指定的2D点。

image.png

与Vec类一样,OpenCV为方便起见定义了以下Point别名:

image.png

OpenCV为Point定义了以下运算符:

image.png

2.9.4 Size对象类型

Size是另一个非常重要并且在OpenCV中广泛使用的模板类,用于指定图像或矩形大小。这个类添加了两个成员width和height,以及有用的area()函数。在下面的示例中,我们可以看到许多使用Size的方法:

image.png

2.9.5 Rect对象类型

Rect是另一个重要的模板类,用于定义由以下参数定义的2D矩形:

  • 左上角的坐标
  • 矩形的宽度和高度

Rect模板类可用于定义图像的感兴趣区域(Region of Interest,简称ROI),如下所示:

image.png

2.9.6 RotatedRect对象类型

最后一个有用的类是名为RotatedRect的特定矩形。该类表示一个旋转矩形,该矩形由中心点、矩形的宽度和高度以及单位为度的旋转角度指定:

image.png

这个类的一个有趣的函数是boundingBox,该函数返回一个包含旋转矩形的Rect,如图2-5所示。

image.png

2.10 基本矩阵运算

在本节中,我们将学习一些基本和重要的矩阵运算,这些运算可以应用于图像或任何矩阵数据。我们已经知道如何加载图像并将其存储在变量Mat中,此外还可以手动创建Mat。最常见的构造函数是为矩阵提供大小和类型,如下所示:

image.png

受支持的类型取决于要存储的数字类型和通道数,最常见的类型如下:

image.png

初始化不会设置数据的值,因此可能获得不需要的值。为了避免不需要的值,可以使用0或1值及其各自的函数来初始化矩阵:

image.png

前面矩阵的结果如图2-6所示。

image.png

一个特殊矩阵初始化是eye函数,它可以创建具有指定类型和大小的单位矩阵:

image.png

其输出如图2-7所示。

image.png

OpenCV的Mat类能够执行所有的矩阵运算。我们可以用+和-运算符来加上或减去两个相同大小的矩阵,如以下代码块所示:

image.png

上述操作的结果如图2-8所示。

image.png

我们可以用运算符乘以一个标量,或者用mul函数乘以矩阵的每个元素,也可以用运算符执行矩阵乘法:

image.png

上述操作的结果如图2-9所示。

image.png

其他常见的数学矩阵运算是转置(transposition)和矩阵求逆(matrix inversion),分别由t()和inv()函数定义。OpenCV提供的其他有趣的函数是矩阵中的数组运算,例如,计算非零元素。这对于计算对象的像素或区域很有用:

image.png

OpenCV提供了一些统计功能,可以使用meanStdDev函数计算通道的平均值和标准差:

image.png

另一个有用的统计函数是minMaxLoc,该函数可以查找矩阵或数组的最小值和最大值,并返回位置和值:

image.png

这里的src是输入矩阵,minVal和maxVal是检测到的最小值和最大值,minLoc和maxLoc是检测到的Point值。

image.png

2.11 基本数据存储

在结束本章之前,我们将探讨OpenCV用来存储和读取数据的函数。在许多应用程序中(例如校准或机器学习),当我们完成大量计算时,需要保存这些结果,以便在后续操作中检索它们。OpenCV为此提供了XML / YAML持久层。
写入FileStorage
要把一些OpenCV或其他数值数据写入文件,可以用FileStorage类,同时要使用流运算符<<操作STL流:


image.png
image.png

要创建保存数据的文件,只需调用构造函数,并提供包含所需扩展名格式的路径文件名(XML或YAML),以及第二个要写入的参数集:

image.png

如果要保存数据,只需在第一步给出一个标识符,然后提供想要保存的矩阵或值,通过这种方式来使用流操作符。例如,要保存int变量,只需要编写以下代码行:

image.png

否则,可以按如下所示写入/保存mat:

image.png

上述代码的结果是YAML格式:

image.png

从文件中读取先前保存的文件与save函数非常相似:

image.png

第一个阶段是通过调用FileStorage构造函数并使用适当的参数、路径和FileStorage::READ来打开一个保存的文件:

image.png

要读取任何存储的变量,只需使用公共的流运算符>>并使用FileStorage对象和带[]运算符的标识符:

image.png

2.12 总结

在本章中,我们学习了OpenCV的基础知识和最重要的类型和操作(访问图像和视频),以及它们如何存储在矩阵中。我们还学习了基本的矩阵运算和用于存储像素的其他基本OpenCV类、向量等。最后,我们学习了如何将数据保存在文件中,以便在其他应用程序或其他操作中读取它们。
在下一章中,我们将学习如何创建第一个应用程序,从而学习OpenCV提供的图形用户界面的基础知识。我们将创建按钮和滑块,并介绍一些图像处理的基础知识。

Leave a Reply

Your email address will not be published. Required fields are marked *