二、OpenCV 基础知识简介
在第 1 章、OpenCV入门中介绍了 OpenCV 在不同操作系统上的安装之后,我们将在本章介绍 OpenCV 开发的基础知识。 它首先展示如何使用 CMake 创建我们的项目。 我们将介绍最基本的图像数据结构和矩阵,以及在我们的项目中工作所需的其他结构。 我们将介绍如何使用 XML/YAML 持久性 OpenCV 函数将变量和数据保存到文件中。
在本章中,我们将介绍以下主题:
- 使用 CMake 配置项目
- 从磁盘读取图像/向磁盘写入图像
- 阅读视频和访问摄像设备
- 主要图像结构(例如,矩阵)
- 其他重要和基本的结构(例如,矢量和标量)
- 基本矩阵运算入门
- 使用 XML/YAML 持久性 OpenCV API 进行文件存储操作
技术要求
本章要求熟悉基本的 C++ 编程语言。 本章中使用的所有代码都可以从以下 gihub 链接下载:https://github.com/PacktPublishing/Building-Computer-Vision-Projects-with-OpenCV4-and-CPlusPlus/tree/master/Chapter02。这些代码可以在任何操作系统上执行,尽管它只在 Ubuntu 上测试过。 查看以下视频了解实际操作的代码: http://bit.ly/2QxhNBa
基本 CMake 配置文件
要配置和检查项目的所有必需依赖项,我们将使用 CMake,但这不是唯一的方法;我们可以在任何其他工具或 IDE 中配置我们的项目,例如Makefiles或Visual Studio,但 CMake 是配置多平台C++项目的一种更可移植的方式。
CMake 使用名为CMakeLists.txt
的配置文件,其中定义了编译和依赖过程。 对于基于从单个源代码文件构建的可执行文件的基本项目,只需包含三行代码的CMakeLists.txt
文件即可。 该文件如下所示:
cmake_minimum_required (VERSION 3.0)
project (CMakeTest)
add_executable(${PROJECT_NAME} main.cpp)
第一行定义了所需的 CMake 的最低版本。 此行在我们的CMakeLists.txt
文件中是必需的,并允许我们使用特定版本中定义的 CMake 的功能;在我们的示例中,我们至少需要 CMake 3.0。 第二行定义项目名称。 此名称保存在名为PROJECT_NAME
的变量中。
最后一行从main.cpp
文件创建一个可执行命令(add_executable()
),并为其提供与我们的项目(${PROJECT_NAME}
)相同的名称,并将源代码编译成名为CMakeTest的可执行文件,这是我们设置为项目名称的名称。 ${}
表达式允许访问我们的环境中定义的任何变量。 然后,我们可以使用${PROJECT_NAME}
变量作为可执行的输出名称。
创建库
CMake 允许我们创建 OpenCV 构建系统使用的库。 分解多个应用之间的共享代码是软件开发中常见且有用的做法。 在大型应用中,或者在多个应用中共享的公共代码中,这种做法非常有用。 在本例中,我们不创建二进制可执行文件,而是创建一个包含所有函数、类等的编译文件。 然后,我们可以与其他应用共享此库文件,而无需共享源代码。
为此,CMake 包含add_library
函数:
# Create our hello library
add_library(Hello hello.cpp hello.h)
# Create our application that uses our new library
add_executable(executable main.cpp)
# Link our executable with the new library
target_link_libraries(executable Hello)
以#
开头的行添加注释,并被 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
库。
管理依赖项
CMake 能够搜索我们的依赖项和外部库,使我们能够根据项目中的外部组件构建复杂的项目,并添加一些需求。
在本书中,最重要的依赖项当然是 OpenCV,我们将把它添加到我们的所有项目中:
cmake_minimum_required (VERSION 3.0)
PROJECT(Chapter2)
# Requires OpenCV
FIND_PACKAGE( OpenCV 4.0.0 REQUIRED )
# Show a message with the opencv version detected
MESSAGE("OpenCV version : ${OpenCV_VERSION}")
# Add the paths to the include directories/to the header files
include_directories(${OpenCV_INCLUDE_DIRS})
# Add the paths to the compiled libraries/objects
link_directories(${OpenCV_LIB_DIR})
# Create a variable called SRC
SET(SRC main.cpp)
# Create our executable
ADD_EXECUTABLE(${PROJECT_NAME} ${SRC})
# Link our library
TARGET_LINK_LIBRARIES(${PROJECT_NAME} ${OpenCV_LIBS})
现在,让我们从以下几个方面来了解脚本的工作原理:
cmake_minimum_required (VERSION 3.0)
cmake_policy(SET CMP0012 NEW)
PROJECT(Chapter2)
第一行定义了 CMake 的最低版本,第二行告诉 CMake 使用 CMake 的新行为来帮助识别正确的数字和布尔常量,而无需取消引用具有此类名称的变量;该策略是在 CMake 2.8.0 中引入的,当该策略未从 3.0.2 版开始设置时,CMake 会发出警告。 最后,最后一行定义了项目标题。 定义项目名称后,我们必须定义需求、库和依赖项:
# Requires OpenCV
FIND_PACKAGE( OpenCV 4.0.0 REQUIRED )
# Show a message with the opencv version detected
MESSAGE("OpenCV version : ${OpenCV_VERSION}")
include_directories(${OpenCV_INCLUDE_DIRS})
link_directories(${OpenCV_LIB_DIR})
这里是我们搜索 OpenCV 依赖项的地方。 FIND_PACKAGE
是一个函数,它允许我们查找依赖项、所需的最低版本以及该依赖项是必需的还是可选的。 在此示例脚本中,我们查找版本 4.0.0 或更高版本的 OpenCV,并声明它是必需的软件包。
The FIND_PACKAGE
command includes all OpenCV submodules, but you can specify the submodules that you want to include in the project by executing your application smaller and faster. For example, if we are only going to work with the basic OpenCV types and core functionality, we can use the following command: FIND_PACKAGE(OpenCV 4.0.0 REQUIRED core)
.
如果 CMake 没有找到它,它会返回一个错误,并且不会阻止我们编译我们的应用。 MESSAGE
函数在终端或 CMake GUI 中显示消息。 在我们的示例中,我们显示的 OpenCV 版本如下:
OpenCV version : 4.0.0
${OpenCV_VERSION}
是 CMake 存储 OpenCV 包版本的变量。include_directories()
和link_directories()
将指定库的头和目录添加到我们的环境中。 OpenCV CMake 的模块将此数据保存在${OpenCV_INCLUDE_DIRS}
和${OpenCV_LIB_DIR}
变量中。 并非所有平台(如 Linux)都需要这些行,因为这些路径通常位于环境中,但建议使用多个 OpenCV 版本来选择正确的链接并包含目录。 现在是将我们开发的资源包括在内的时候了:
# Create a variable called SRC
SET(SRC main.cpp)
# Create our executable
ADD_EXECUTABLE(${PROJECT_NAME} ${SRC})
# Link our library
TARGET_LINK_LIBRARIES(${PROJECT_NAME} ${OpenCV_LIBS})
最后一行创建可执行文件,并将可执行文件与 OpenCV 库链接,正如我们在上一节创建库中所看到的那样。*这段代码中有一个新函数SET
;该函数创建一个新变量,并向其添加我们需要的任何值。 在我们的示例中,我们在SRC
变量中合并了main.cpp
参数的值。 我们可以向同一变量添加越来越多的值,如以下脚本所示:
SET(SRC main.cpp
utils.cpp
color.cpp
)
使脚本更加复杂
在本节中,我们将展示一个更复杂的脚本,它包含子文件夹、库和可执行文件;总共只有两个文件和几行代码,如此脚本所示。 创建多个CMakeLists.txt
文件不是强制性的,因为我们可以在主CMakeLists.txt
文件中指定所有内容。 但是,对每个项目子文件夹使用不同的CMakeLists.txt
文件更为常见,从而使其更加灵活和可移植。
此示例有一个代码结构文件夹,其中包含一个用于utils
库的文件夹和一个根文件夹,根文件夹包含主可执行文件:
CMakeLists.txt
main.cpp
utils/
CMakeLists.txt
computeTime.cpp
computeTime.h
logger.cpp
logger.h
plotting.cpp
plotting.h
然后,我们必须定义两个CMakeLists.txt
文件,一个在根文件夹中,另一个在根文件夹中。 CMakeLists.txt
根文件夹文件包含以下内容:
cmake_minimum_required (VERSION 3.0)
project (Chapter2)
# Opencv Package required
FIND_PACKAGE( OpenCV 4.0.0 REQUIRED )
#Add opencv header files to project
include_directories(${OpenCV_INCLUDE_DIR})
link_directories(${OpenCV_LIB_DIR})
# Add a subdirectory to the build.
add_subdirectory(utils)
# Add optional log with a precompiler definition
option(WITH_LOG "Build with output logs and images in tmp" OFF)
if(WITH_LOG)
add_definitions(-DLOG)
endif(WITH_LOG)
# generate our new executable
add_executable(${PROJECT_NAME} main.cpp)
# link the project with his dependencies
target_link_libraries(${PROJECT_NAME} ${OpenCV_LIBS} Utils)
除了我们将解释的一些函数外,几乎所有的代码行都在前面几节中进行了描述。add_subdirectory()
告诉 CMake 分析所需的子文件夹的CMakeLists.txt
。 在继续主要的CMakeLists.txt
文件解释之前,我们先解释一下utils
中的第二个CMakeLists.txt
文件。
在utils
个文件夹的CMakeLists.txt
个文件中,我们将编写一个新的库以包括在我们的主项目文件夹中:
# Add new variable for src utils lib
SET(UTILS_LIB_SRC
computeTime.cpp
logger.cpp
plotting.cpp
)
# create our new utils lib
add_library(Utils ${UTILS_LIB_SRC})
# make sure the compiler can find include files for our library
target_include_directories(Utils PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
这个 CMake 脚本文件定义了一个变量UTILS_LIB_SRC
,我们在其中添加库中包含的所有源文件,使用add_library
函数生成库,并使用target_include_directories
函数来允许我们的主项目检测所有头文件。 离开utils
子文件夹并继续使用根 CMake 脚本,Option 函数将创建一个新变量(在我们的示例中为WITH_LOG
),并附上一小段描述。 这个变量可以通过ccmake
命令行或 CMake GUI 界面(其中显示说明)以及允许用户启用或禁用此选项的复选框来更改。*此函数非常有用,可让用户决定是否启用或禁用日志、使用 Java 或 Python 支持进行编译(就像 OpenCV 所做的那样)。
在我们的示例中,我们使用此选项在应用中启用记录器。 要启用记录器,我们在代码中使用预编译器定义,如下所示:
#ifdef LOG
logi("Number of iteration %d", i);
#endif
此对数宏可以通过调用add_definitions
函数(-DLOG
)在我们的CMakeLists.txt
中定义,该函数本身可以由 CMake 变量WITH_LOG
运行或隐藏,条件很简单:
if(WITH_LOG)
add_definitions(-DLOG)
endif(WITH_LOG)
现在我们已经准备好创建我们的 CMake 脚本文件,以便在任何操作系统上编译我们的计算机视觉项目。 然后,在开始示例项目之前,我们将继续学习 OpenCV 基础知识。
图像和矩阵
毫无疑问,计算机视觉中最重要的结构是图像。 计算机视觉中的图像是用数字设备捕获的物理世界的表示。 这张图片只是一个以矩阵格式存储的数字序列(参见下图)。 每个数字都是对所考虑的波长(例如,彩色图像中的红、绿或蓝)或波长范围(对于全色设备)的光强度的测量。 图像中的每个点都称为非像素(对于图片元素),每个像素可以存储一个或多个值,具体取决于它是仅存储一个值的黑白图像(也称为二进制图像),如0
或1
,存储两个值的灰度图像,还是存储三个值的彩色图像。 这些值通常在整数0
和255
之间,但您可以使用其他范围,例如浮点数的0
到1
,如h高动态范围成像(HDRI)或热像:
图像以矩阵格式存储,其中每个像素都有一个位置,并且可以通过列号和行号来引用。 OpenCV 使用Mat
类来实现此目的。 对于灰度图像,使用单个矩阵,如下图所示:
在彩色图像的情况下,如下图所示,我们使用宽度 x 高度 x 颜色通道数的矩阵:
但是Mat
类不仅用于存储图像;它还允许您存储任何类型和不同大小的矩阵。 您可以将其用作代数矩阵并对其执行运算。 在接下来的几节中,我们将介绍最重要的矩阵运算,例如加法、乘法、对角化。 但是,在此之前,了解矩阵在计算机内存中的内部存储方式很重要,因为访问内存插槽总是比使用 OpenCV 函数访问每个像素效率更高。
在内存中,矩阵保存为按列和行排序的数组或值序列。 下表显示了BGR图像格式的像素序列:
| 第 0 行 | 第 1 行 | 第 2 行 | | 使用 0 | 使用 1 个 | 使用 2 个 | 使用 0 | 使用 1 个 | 使用 2 个 | 使用 0 | 使用 1 个 | 使用 2 个 | | 像素ρ1 | 像素 2 | 像素 3 | 像素 4 | 像素 5 | 像素 6 | 像素 7 | 像素 8 | 像素 9 | | 英语字母表中第二个字母 / B 音 / 乙等 | 英语字母表中第七个字母 / 第七列 | 英语字母中的第十八个字母 / R 类 | 英语字母表中第二个字母 / B 音 / 乙等 | 英语字母表中第七个字母 / 第七列 | 英语字母中的第十八个字母 / R 类 | 英语字母表中第二个字母 / B 音 / 乙等 | 英语字母表中第七个字母 / 第七列 | 英语字母中的第十八个字母 / R 类 | 英语字母表中第二个字母 / B 音 / 乙等 | 英语字母表中第七个字母 / 第七列 | 英语字母中的第十八个字母 / R 类 | 英语字母表中第二个字母 / B 音 / 乙等 | 英语字母表中第七个字母 / 第七列 | 英语字母中的第十八个字母 / R 类 | 英语字母表中第二个字母 / B 音 / 乙等 | 英语字母表中第七个字母 / 第七列 | 英语字母中的第十八个字母 / R 类 | 英语字母表中第二个字母 / B 音 / 乙等 | 英语字母表中第七个字母 / 第七列 | 英语字母中的第十八个字母 / R 类 | 英语字母表中第二个字母 / B 音 / 乙等 | 英语字母表中第七个字母 / 第七列 | 英语字母中的第十八个字母 / R 类 | 英语字母表中第二个字母 / B 音 / 乙等 | 英语字母表中第七个字母 / 第七列 | 英语字母中的第十八个字母 / R 类 |
按照此顺序,我们可以通过遵循以下公式访问任何像素:
Value= Row_i*num_cols*num_channels + Col_i + channel_i
OpenCV functions are quite optimized for random access, but sometimes, direct access to the memory (work with pointer arithmetic) is more efficient, for example, when we have to access all pixels in a loop.
读/写图像
在介绍了矩阵之后,我们将从 OpenCV 代码基础开始。 我们首先要学习的是如何读写图像:
#include <iostream>
#include <string>
#include <sstream>
using namespace std;
// OpenCV includes
#include "opencv2/core.hpp"
#include "opencv2/highgui.hpp"
using namespace cv;
int main(int argc, const char** argv)
{
// Read images
Mat color= imread("../lena.jpg");
Mat gray= imread("../lena.jpg",CV_LOAD_IMAGE_GRAYSCALE);
if(! color.data ) // Check for invalid input
{
cout << "Could not open or find the image" << std::endl ;
return -1;
}
// Write images
imwrite("lenaGray.jpg", gray);
// Get same pixel with opencv function
int myRow=color.cols-1;
int myCol=color.rows-1;
Vec3b pixel= color.at<Vec3b>(myRow, myCol);
cout << "Pixel value (B,G,R): (" << (int)pixel[0] << "," << (int)pixel[1] << "," << (int)pixel[2] << ")" << endl;
// show images
imshow("Lena BGR", color);
imshow("Lena Gray", gray);
// wait for any key press
waitKey(0);
return 0;
}
现在让我们继续理解代码:
// OpenCV includes
#include "opencv2/core.hpp"
#include "opencv2/highgui.hpp"
using namespace cv;
首先,我们必须在示例中包含所需函数的声明。 这些函数来自core
(基本图像数据处理)和highgui
(OpenCV 提供的跨平台 I/O 函数包括core
和highui;
;第一个函数包括矩阵等基础类,第二个函数包括图形界面读、写和显示图像的函数)。 现在是阅读图片的时候了:
// Read images
Mat color= imread("../lena.jpg");
Mat gray= imread("../lena.jpg",CV_LOAD_IMAGE_GRAYSCALE);
imread
是读取图像的主要功能。 此函数用于打开图像并以矩阵格式存储。 imread
接受两个参数。 第一个参数是包含图像路径的字符串,第二个参数是可选的,默认情况下将图像加载为彩色图像。 第二个参数允许以下选项:
cv::IMREAD_UNCHANGED
:如果设置,则在输入具有相应深度时返回 16 位/32 位图像,否则将其转换为 8 位cv::IMREAD_COLOR
:如果设置,则始终将图像转换为彩色图像(BGR,8 位无符号)cv::IMREAD_GRAYSCALE
:如果设置,则始终将图像转换为灰度图像(8 位无符号)
要保存图像,我们可以使用imwrite
函数,该函数将矩阵图像存储在我们的计算机中:
// Write images
imwrite("lenaGray.jpg", gray);
第一个参数是我们要用所需的扩展格式保存图像的路径。 第二个参数是我们要保存的矩阵图像。 在我们的代码示例中,我们创建并存储图像的灰色版本,然后将其另存为.jpg
文件。 我们加载的灰度图像将存储在第二个灰度变量中:
// Get same pixel with opencv function
int myRow=color.cols-1;
int myCol=color.rows-1;
使用矩阵的.cols
和.rows
属性,我们可以获取图像中的列数和行数,也就是宽度和高度:
Vec3b pixel= color.at<Vec3b>(myRow, myCol);
cout << "Pixel value (B,G,R): (" << (int)pixel[0] << "," << (int)pixel[1] << "," << (int)pixel[2] << ")" << endl;
要访问图像的一个像素,我们使用Mat
OpenCV 类中的模板函数cv::Mat::at<typename t>(row,col)
。 模板参数是所需的返回类型。 8 位彩色图像中的一个类型名称是存储三个无符号字符数据(Vec=向量,3=分量数,b=1 字节)的Vec3b
类。 对于灰色图像,我们可以直接使用无符号字符,或图像中使用的任何其他数字格式,如uchar pixel= color.at<uchar>(myRow, myCol)
。最后,为了显示图像,我们可以使用imshow
函数,该函数创建一个窗口,第一个参数是标题,第二个参数是图像矩阵:
// show images
imshow("Lena BGR", color);
imshow("Lena Gray", gray);
// wait for any key press
waitKey(0);
If we want to stop the application from waiting, we can use the OpenCV function waitKey
, with a parameter of the number of milliseconds we want to wait for a key press. If we set up the parameter to 0
, then the function will wait until a key is pressed.
上述代码的结果如下图所示。 左边的图像是彩色图像,右边的图像是灰度图像:
最后,作为以下示例的示例,我们将创建CMakeLists.txt
代码文件,并了解如何使用该文件编译代码。
下面的代码描述了CMakeLists.txt
文件:
cmake_minimum_required (VERSION 3.0)
cmake_policy(SET CMP0012 NEW)
PROJECT(project)
# Requires OpenCV
FIND_PACKAGE( OpenCV 4.0.0 REQUIRED )
MESSAGE("OpenCV version : ${OpenCV_VERSION}")
include_directories(${OpenCV_INCLUDE_DIRS})
link_directories(${OpenCV_LIB_DIR})
ADD_EXECUTABLE(sample main.cpp)
TARGET_LINK_LIBRARIES(sample ${OpenCV_LIBS})
要使用此CMakeLists.txt
文件编译代码,我们必须执行以下步骤:
- 创建一个
build
文件夹。 - 在
build
文件夹内,执行 CMake 或在 Windows 中打开 CMake GUI 应用,选择source
和build
文件夹,然后按配置和生成按钮。 - 如果您使用的是 Linux 或 MacOS,请像往常一样生成一个 Makefile,然后使用*
make
命令编译该项目。 如果您使用的是 Windows,请使用在步骤 2 中选择的编辑器打开项目,然后编译。
最后,在编译我们的应用之后,我们将在 Build 文件夹中拥有一个名为app
的可执行文件,我们可以执行该文件。
阅读视频和摄像机
本节使用这个简单的示例向您介绍视频和相机阅读。 在解释如何读取视频或摄像机输入之前,我们想介绍一个新的、非常有用的类,它可以帮助我们管理输入命令行参数。 此新类是在 OpenCV 3.0 版中引入的,是CommandLineParser
类:
// OpenCV command line parser functions
// Keys accepted by command line parser
const char* keys =
{
"{help h usage ? | | print this message}"
"{@video | | Video file, if not defined try to use webcamera}"
};
对于CommandLineParser
,我们必须做的第一件事是定义在常量char
向量中需要或允许哪些参数;每行都有以下模式:
"{name_param | default_value | description}"
name_param
前面可以有@
,它将此参数定义为默认输入。 我们可以使用多个name_param
:
CommandLineParser parser(argc, argv, keys);
构造函数将获得 Main 函数的输入和先前定义的键常量:
//If requires help show
if (parser.has("help"))
{
parser.printMessage();
return 0;
}
.has
类方法检查参数是否存在。 在示例中,我们检查用户是否添加了参数help
或?
,然后使用类函数printMessage
显示所有描述参数:
String videoFile= parser.get<String>(0);
使用.get<typename>(parameterName)
函数,我们可以访问和读取任何输入参数:
// Check if params are correctly parsed in his variables
if (!parser.check())
{
parser.printErrors();
return 0;
}
获取所有必需的参数后,我们可以检查这些参数是否正确解析,如果其中一个参数没有解析,则会显示错误消息,例如,添加字符串而不是数字:
VideoCapture cap; // open the default camera
if(videoFile != "")
cap.open(videoFile);
else
cap.open(0);
if(!cap.isOpened()) // check if we succeeded
return -1;
视频读取和摄像头读取的类是相同的:与先前版本的 OpenCV 一样,属于videoio
子模块的是VideoCapture
类,而不是属于highgui
子模块的VideoCapture
类。 创建对象后,我们检查输入命令行参数videoFile
是否有路径文件名。 如果它是空的,则我们尝试打开网络摄像机;如果它有文件名,则打开视频文件。 为此,我们使用open
函数,给出我们想要打开的视频文件名或索引摄像机作为参数。 如果我们只有一台摄像机,我们可以使用0
作为参数。
为了检查是否可以读取视频文件名或摄像头,我们使用isOpened
函数:
namedWindow("Video",1);
for(;;)
{
Mat frame;
cap >> frame; // get a new frame from camera
if(frame)
imshow("Video", frame);
if(waitKey(30) >= 0) break;
}
// Release the camera or video cap
cap.release();
最后,我们使用namedWindow
函数创建一个窗口来显示帧,并且使用无限循环,使用>>
操作抓取每个帧,并且如果我们正确地检索到帧,则使用imshow
函数显示该帧。 在本例中,我们不想停止应用,但将等待 30 毫秒,以检查是否有用户想要使用waitKey(30)
使用任何键来停止应用执行。
The time required to wait for the next frame using camera access is calculated from the camera speed and our spent algorithm time. For example, if a camera works at 20 fps, and our algorithm spent 10 milliseconds, a great waiting value is 30 = (1000/20) - 10 milliseconds. This value is calculated considering a wait of a sufficient amount of time to ensure that the next frame is in the buffer. If our camera takes 40 milliseconds to take each image, and we use 10 milliseconds in our algorithm, then we only need to stop with waitKey 30 milliseconds, because 30 milliseconds of wait time, plus 10 milliseconds of our algorithm, is the same amount of time for which each frame of the camera is accessible.
当用户想要完成应用时,他们所要做的就是按任意键,然后我们必须使用释放功能释放所有的视频资源。
It is very important to release all resources that we use in a computer vision application. If we do not, we can consume all RAM memory. We can release the matrices using the release
function.
前面代码的结果是一个新窗口,显示 BGR 格式的视频或网络摄像机。
其他基本对象类型
我们已经了解了Mat
和Vec3b
类,但我们还需要学习更多的类。
在本节中,我们将了解大多数项目所需的最基本的对象类型:
Vec
Scalar
Point
Size
Rect
RotatedRect
VEC 对象类型
Vec
是一个主要用于数值向量的模板类。 我们可以定义任何类型的向量和分量数量:
Vec<double,19> myVector;
我们还可以使用任何预定义类型:
typedef Vec<uchar, 2> Vec2b;
typedef Vec<uchar, 3> Vec3b;
typedef Vec<uchar, 4> Vec4b;
typedef Vec<short, 2> Vec2s;
typedef Vec<short, 3> Vec3s;
typedef Vec<short, 4> Vec4s;
typedef Vec<int, 2> Vec2i;
typedef Vec<int, 3> Vec3i;
typedef Vec<int, 4> Vec4i;
typedef Vec<float, 2> Vec2f;
typedef Vec<float, 3> Vec3f;
typedef Vec<float, 4> Vec4f;
typedef Vec<float, 6> Vec6f;
typedef Vec<double, 2> Vec2d;
typedef Vec<double, 3> Vec3d;
typedef Vec<double, 4> Vec4d;
typedef Vec<double, 6> Vec6d;
All the following vector operations are also implemented:
v1 = v2 + v3
v1 = v2 - v3
v1 = v2 * scale
v1 = scale * v2
v1 = -v2
v1 += v2
实现的其他扩充操作如下:
v1 == v2, v1 != v2
norm(v1) (euclidean norm)
。
标量对象类型
Scalar
对象类型是从Vec
派生的具有四个元素的模板类。 Scalar
类型在 OpenCV 中广泛用于传递和读取像素值。
要访问Vec
和Scalar
值,我们使用[]
操作符,它可以从另一个标量、向量或逐值初始化,如以下示例所示:
Scalar s0(0);
Scalar s1(0.0, 1.0, 2.0, 3.0);
Scalar s2(s1);
点对象类型
另一个非常常见的类模板是Point
。 此类定义由其坐标x
和y
指定的二维点。
Like Point
, there is a Point3
template class for 3D point support.
与Vec
类一样,为方便起见,OpenCV 定义了以下Point
别名:
typedef Point_<int> Point2i;
typedef Point2i Point;
typedef Point_<float> Point2f;
typedef Point_<double> Point2d;
The following operators are defined for points:
pt1 = pt2 + pt3;
pt1 = pt2 - pt3;
pt1 = pt2 * a;
pt1 = a * pt2;
pt1 = pt2 / a;
pt1 += pt2;
pt1 -= pt2;
pt1 *= a;
pt1 /= a;
double value = norm(pt); // L2 norm
pt1 == pt2;
pt1 != pt2;
大小对象类型
OpenCV 中另一个非常重要且广泛使用的模板类是用于指定图像或矩形大小的模板类Size
。 该类添加了两个成员:width 和 Height,以及有用的函数 Sizearea()
。*在下面的示例中,我们可以看到使用 SIZE 的多种方法:
Size s(100,100);
Mat img=Mat::zeros(s, CV_8UC1); // 100 by 100 single channel matrix
s.width= 200;
int area= s.area(); returns 100x200
矩形对象类型
Rect
是定义由以下参数定义的 2D 矩形的另一个重要模板类:
-
左上角的坐标
-
矩形的宽度和高度
Rect
模板类可用于定义图像的感兴趣区域和(ROI),如下所示:
Mat img=imread("lena.jpg");
Rect rect_roi(0,0,100,100);
Mat img_roi=img(r);
RotatedRect 对象类型
最后一个有用的类是一个名为RotatedRect
的特定矩形。 此类表示由中心点、矩形的宽度和高度以及旋转角度(以度为单位)指定的旋转矩形:
RotatedRect(const Point2f& center, const Size2f& size, float angle);
这个类的一个有趣的函数是boundingBox
。 此函数返回Rect
,其中包含旋转的矩形:
基本矩阵运算
在本节中,我们将学习一些可以应用于图像或任何矩阵数据的基本且重要的矩阵运算。 我们学习了如何加载图像并将其存储在Mat
变量中,但我们可以手动创建Mat
。 最常见的构造函数是为矩阵指定大小和类型,如下所示:
Mat a= Mat(Size(5,5), CV_32F);
You can create a new matrix linking with a stored buffer from third-party libraries without copying data using this constructor: Mat(size, type, pointer_to_buffer)
.
支持的类型取决于您要存储的号码类型和频道数量。 最常见的类型如下:
CV_8UC1
CV_8UC3
CV_8UC4
CV_32FC1
CV_32FC3
CV_32FC4
You can create any type of matrix using CV_number_typeC(n)
, where the number_type
is 8 bits unsigned (8U) to 64 float (64F), and where (n)
is the number of channels; the number of channels permitted ranges from 1
to CV_CN_MAX
.
初始化不会设置数据值,因此您可能会得到不需要的值。 为避免不需要的值,您可以使用0
或1
值以及它们各自的函数来初始化矩阵:
Mat mz= Mat::zeros(5,5, CV_32F);
Mat mo= Mat::ones(5,5, CV_32F);
上述矩阵的结果如下:
特殊矩阵初始化是 EYE 函数,它创建具有指定类型和大小的单位矩阵:
Mat m= Mat::eye(5,5, CV_32F);
输出如下:
OpenCV 的Mat
类中允许所有矩阵运算。 我们可以使用+
和-
运算符将两个大小相同的矩阵相加或相减,如以下代码块所示:
Mat a= Mat::eye(Size(3,2), CV_32F);
Mat b= Mat::ones(Size(3,2), CV_32F);
Mat c= a+b;
Mat d= a-b;
上述操作的结果如下:
我们可以使用*
运算符乘以标量,或者使用mul
函数计算每个元素的矩阵,并且可以使用**
运算符执行矩阵乘法:
Mat m1= Mat::eye(2,3, CV_32F);
Mat m2= Mat::ones(3,2, CV_32F);
// Scalar by matrix
cout << "nm1.*2n" << m1*2 << endl;
// matrix per element multiplication
cout << "n(m1+2).*(m1+3)n" << (m1+1).mul(m1+3) << endl;
// Matrix multiplication
cout << "nm1*m2n" << m1*m2 << endl;
上述操作的结果如下:
其他常见的数学矩阵运算是转置和矩阵求逆,分别由函数t()
和inv()
定义。 OpenCV 提供的其他有趣功能是矩阵中的数组操作,例如,对非零元素进行计数。 这对于计算对象的像素或面积很有用:
int countNonZero(src);
OpenCV 提供了一些统计功能。 通道的平均值和标准偏差可以使用以下函数meanStdDev
计算:
meanStdDev(src, mean, stddev);
另一个有用的统计函数是minMaxLoc
。 此函数用于查找矩阵或数组的最小值和最大值,并返回位置和值:
minMaxLoc(src, minVal, maxVal, minLoc, maxLoc);
这里,src
是输入矩阵,minVal
和maxVal
是检测到的双值,minLoc
和maxLoc
是检测到的Point
值。
Other core and useful functions are described in detail at: http://docs.opencv.org/modules/core/doc/core.html.
基本数据持久化和存储
在完成本章之前,我们将探索用于存储和读取数据的 OpenCV 函数。 在许多应用中,例如校准或机器学习,当我们完成一些计算时,我们需要保存这些结果,以便在后续操作中检索它们。 为此,OpenCV 提供了一个 XML/YAML 持久层。
写入文件存储
要使用某些 OpenCV 或其他数字数据写入文件,我们可以使用FileStorage
类,并使用 STL 流等流<<
运算符:
#include "opencv2/opencv.hpp"
using namespace cv;
int main(int, char** argv)
{
// create our writer
FileStorage fs("test.yml", FileStorage::WRITE);
// Save an int
int fps= 5;
fs << "fps" << fps;
// Create some mat sample
Mat m1= Mat::eye(2,3, CV_32F);
Mat m2= Mat::ones(3,2, CV_32F);
Mat result= (m1+1).mul(m1+3);
// write the result
fs << "Result" << result;
// release the file
fs.release();
FileStorage fs2("test.yml", FileStorage::READ);
Mat r;
fs2["Result"] >> r;
std::cout << r << std::endl;
fs2.release();
return 0;
}
要创建保存数据的文件存储,我们只需调用构造函数,给出所需扩展格式(XML 或 YAML)的路径文件名,并将第二个参数设置为 WRITE:
FileStorage fs("test.yml", FileStorage::WRITE);
如果我们想要保存数据,我们只需要使用流运算符,在第一阶段给出一个标识符,然后给出我们想要保存的矩阵或值。 例如,要保存int
变量,我们只需编写以下代码行:
int fps= 5;
fs << "fps" << fps;
否则,我们可以写入/保存mat
,如下所示:
Mat m1= Mat::eye(2,3, CV_32F);
Mat m2= Mat::ones(3,2, CV_32F);
Mat result= (m1+1).mul(m1+3);
// write the result
fs << "Result" << result;
上述代码的结果是 YAML 格式:
%YAML:1.0
fps: 5
Result: !!opencv-matrix
rows: 2
cols: 3
dt: f
data: [ 8., 3., 3., 3., 8., 3\. ]
从文件存储中读取以读取先前保存的文件与save
功能非常相似:
#include "opencv2/opencv.hpp"
using namespace cv;
int main(int, char** argv)
{
FileStorage fs2("test.yml", FileStorage::READ);
Mat r;
fs2["Result"] >> r;
std::cout << r << std::endl;
fs2.release();
return 0;
}
第一步是使用适当的参数、路径和FileStorage::READ
使用FileStorage
构造函数打开保存的文件:
FileStorage fs2("test.yml", FileStorage::READ);
要读取任何存储的变量,我们只需要使用公共流运算符>>
(使用我们的FileStorage
对象)和标识符(带有[]
运算符):
Mat r;
fs2["Result"] >> r;
简略的 / 概括的 / 简易判罪的 / 简易的
在本章中,我们学习了 OpenCV 的基础知识和最重要的类型和操作,图像和视频的访问,以及它们如何存储在矩阵中。 我们学习了存储像素、向量等的基本矩阵运算和其他基本 OpenCV 类。 最后,我们学习了如何将数据保存在文件中,以便在其他应用或其他执行中可以直接读取。
在下一章中,我们将学习如何创建我们的第一个应用,学习 OpenCV 提供的图形用户界面的基础知识。 我们将创建按钮和滑块,并介绍一些图像处理基础知识。