本文将介绍 OpenCV 的基本要素,并演示如何完成最基本的图像处理任务:读取、显示和存 储图像。在开始之前,首先需要安装 OpenCV 库。安装过程非常简单,1.2 节会详细介绍。 所有的计算机视觉应用程序都涉及对图像的处理,因此 OpenCV 提供了一个操作图像和矩阵 的数据结构。此数据结构功能非常强大,具有多种实用属性和方法。此外,它还包含先进的内存 管理模型,对于应用程序的开发大有帮助。本章最后两节将介绍如何使用这个重要的 OpenCV 数据结构。
OpenCV 是一个开源的计算机视觉程序库,可在 Windows、Linux、Mac、Android、iOS 等多 种平台下运行。在 BSD 许可协议下,它可以用于学术应用和商业应用的开发,可随意使用、发 布和修改。本节将介绍如何安装 OpenCV 程序库。
你可以在 OpenCV 官方网站http://opencv.org/获取最新版程序库、在线 API 文档以及诸多其他 有用的资源。
在 OpenCV 网站上找到最新版本,选择你使用的平台(Windows、Linux/Mac 或 iOS),下载 OpenCV 包并解压。解压时会生成 opencv 目录。最好修改这个目录名称,以体现出当前版本号(例 如在 Windows 系统中可用 C:\opencv-3.2)。该目录下的文件和子目录构成了程序库。请注意,有 一个 sources 目录,它包含所有的源代码文件。(是的,它是开源的!)
要完成程序库的安装并投入使用,还有一个重要的步骤:针对所选环境生成程序库的二进制文件。这时必须选定创建 OpenCV 程序所用的目标平台:使用哪种操作系统?使用什么编译器? 使用哪个版本? 32 位还是 64 位?正因为选项众多,才必须根据实际需要生成二进制文件。
在集成开发环境(integrated development environment,IDE)中也可以设置这些选项。请注意,库文件也是预先编译好的,如果与环境匹配(可查看 build 目录),可以直接使用。如果二进制文件能满足需求,就可以开始使用了。
需要特别注意的是,OpenCV 已经分成了两个主要部分。第一部分是包含了成熟算法的 OpenCV 主源码库,也就是之前下载的内容。此外还有一个独立的代码库,它包含了 最近加入 OpenCV 的计算机视觉算法。如果只想使用 OpenCV 的核心函数,就不需要这个 contrib 包;但如果要使用最先进的算法,就很可能需要这个额外的模块。实际上,本书就介绍了其中的 几种高级算法,因此你需要准备好 contrib 模块——到https://github.com/opencv/opencv_contrib 即可下载额外的 OpenCV 模块(zip 文件)。模块解压后,可以放在任何目录下,但需要能够在 opencv_contrib-master/modules 中找到。方便起见,可以将文件夹改名为 contrib,并直接复制到主程序包的 sources 目录下。在这些额外的模块中,你可以只选取和保存需要使用的;不过为了避免麻烦,这里先全部保存下来。
现在就可以开始安装了!编译 OpenCV 时,强烈建议你使用 CMake 工具(可从 http://cmake. org 下载)。CMake 也是一个开源软件,采用平台无关的配置文件,可以控制软件的编译过程。它可以根据不同的环境,生成编译所需的 makefile 或 solution 文件,因此它是必须下载和安装的。 你还可以根据需要下载可视化工具包(Visualization Toolkit,VTK),详情请参见 1.2.4 节。
你可以在命令行中运行 cmake,但是采用图形界面(cmake-gui)的 CMake 会更加容易。 如果使用图形界面,只需要指定 OpenCV 源程序和二进制文件的路径,点击 Configure 并选择编译器即可。
除了基本设置,CMake 还有许多选项,例如是否安装文档、是否安装额外的库。如果不是非常熟悉,最好采用默认设置。但因为这次需要包含附加模块,所以需要指定这些模块的安装 目录。
指定附加模块后,再次点击 Configure。现在可以点击 Generate 按钮生成项目文件了,这些项目文件将用来编译程序库。这是安装过程的最后一个步骤,会生成能在指定开发环境下使用的程序库。如果你选用 MS Visual Studio,那么只需要打开由 CMake 创建的位于顶层的解决方案 文件(通常是 OpenCV.sln 文件),然后选择 INSTALL 项目(CMakeTargets 下)并执行 Build 指令(使用右键)。
如果要得到 Release和 Debug两个版本,你就需要编译两次,每个相应的配置各一次。如果一 切顺利,build 目录下将自动创建 install 目录,该目录下有关联到应用程序的 OpenCV 库的所有二 进制文件,以及程序需要调用的动态库文件。别忘了在控制面板中设置环境变量 PATH,以确保 运行程序时操作系统能找到这些 dll 文件(例如 C:\opencv-3.2\build\install\x64\vc14\bin
)。你还可 以定义环境变量 OPENCV_DIR
,让它指向 INSTALL 路径。这样,在配置其他工程时,CMake 就 能找到库文件了。
在 Linux 环境中,可以用 CMake 生成 Makefile 文件,然后运行 sudo make install 命令 完成安装过程;也可以使用打包工具 apt-get,自动完成安装过程。Mac 系统则可以使用 Homebrew 管理工具。安装这个工具后,只需要输入 brew install opencv3 --with-contrib 即可完 成整个 OpenCV 安装过程(输入 brew info opencv3 可查看选项)。
OpenCV 一直在升级,第 3 版增加了很多新功能,也提高了性能。从第 2 版起,API 就开始 迁移到 C++,现在已经基本迁移完毕,并实现了更一致的接口。新版本的一个主要变化是重构了库模块,使部署更加方便。它创建了一个包含最新算法的独立库(contrib 库),其中包含一些基于特定许可协议、需要付费才能使用的算法。这样做的好处是,开发者和研发人员可以在 OpenCV 上共享最新的功能,同时能确保核心 API 的稳定性和易维护性。你可以从 http://opencv.org/下载 主体模块,而附加模块则必须到 GitHub(https://github.com/opencv/)下载。需要注意的是,这些附加模块仍在开发中,它们的算法会经常修改。
OpenCV 库分为多个模块:opencv_core
模块包含库的核心功能,opencv_imgproc
模块 包含主要的图像处理函数,opencv_highgui
模块提供了读写图像和视频的函数以及一些用户交 互函数,等等。在使用某个模块之前,需要包含该模块对应的头文件。很多使用 OpenCV 的应用 程序会在文件的开头处声明:
#include
#include
#include
在学习 OpenCV 的过程中,你会逐步发现它的众多模块中包含的大量功能。
OpenCV 网站 http://opencv.org/上有详细的安装说明,还有完整的在线文档,包括几个针对程序库中不同组件的教程。
现在开始运行第一个 OpenCV 应用程序。既然 OpenCV 是用来处理图像的,那就先来演示几 个图像应用程序开发中最基本的操作:从文件中装载输入的图像、在窗口中显示图像、应用处理 函数再保存输出的图像。
使用你喜欢的 IDE(例如 MS Visual Studio 或者 Qt)新建一个控制台应用程序,使用待填充内容的 main 函数。
1.3.2 如何实现 首先要引入定义了所需的类和函数的头文件。这里我们只想显示一幅图像,因此需要定义了 图像数据结构的核心头文件和包含了所有图形接口函数的 highgui 头文件:
#include
#include
首先在 main 函数中定义一个表示图像的变量。在 OpenCV 中,就是定义 cv::Mat
类的对象:
cv::Mat image; // 创建一个空图像
这个定义创建了一个尺寸为 0×0 的图像,可以通过访问 cv::Mat
的 size 属性来验证这一点:
std::cout << "This image is " << image.rows << " x "
<< image.cols << std::endl;
接下来只需调用读函数,即会从文件读入一个图像,解码,然后分配内存
image= cv::imread("puppy.bmp"); // 读取输入图像
现在可以使用这幅图像了,但是要先检查一下图像的读取是否正确(如果找不到文件、文件 被破坏或者文件格式无法识别,就会发生错误)。用下面的代码来验证图像是否有效:
if (image.empty()) { // 错误处理
// 未创建图像……
// 可能显示一个错误消息
// 并退出程序
...
}
如果没有分配图像数据,empty 方法将返回 true。 对这幅图像的第一个操作就是显示它——你可以使用 highgui 模块的函数来实现。首先定 义用来显示图像的窗口,然后让图像在指定的窗口中显示出来:
// 定义窗口(可选)
cv::namedWindow("Original Image");
// 显示图像
cv::imshow("Original Image", image);
可以看到,这个窗口是以名称命名的。稍后可以用这个窗口来显示其他图像,也可以用不同 的名称创建多个窗口。运行这个应用程序,可看到如下的图像窗口。
这时,我们通常会对图像做一些处理。OpenCV 提供了大量处理函数,本书将对其中的一些 进行深入探讨。先来看一个水平翻转图像的简单函数。OpenCV 中的有些图像转换过程是就地进 行的,即转换过程直接在输入的图像上进行(不创建新的图像),比如翻转方法就是这样。不过, 我们总是可以创建新的矩阵来存放输出结果。下面就试试这种方法:
cv::Mat result; // 创建另一个空的图像
cv::flip(image,result,1); // 正数表示水平
// 0 表示垂直
// 负数表示水平和垂直
在另一个窗口显示结果:
cv::namedWindow("Output Image"); // 输出窗口
cv::imshow("Output Image", result);
因为它是控制台窗口,会在 main 函数结束时关闭,所以需要增加一个额外的 highgui 函 数,待用户键入数值后再结束程序:
cv::waitKey(0); // 0 表示永远地等待按键
// 键入的正数表示等待的毫秒数
我们可以在另一个窗口上看到输出的图像,如下所示。
最后,可以使用 highgui 函数把处理过的图像存储在磁盘里:
cv::imwrite("output.bmp", result); // 保存结果
保存图像时会根据文件名后缀决定使用哪种编码方式。其他常见的受支持图像格式是 JPG、 TIFF 和 PNG。
OpenCV 的 C++ API 中,所有类和函数都在命名空间 cv
内定义。访问它们的方法共有两 种,第一种是在定义 main 函数前使用如下声明:
using namespace cv;
第二种方法是根据命名空间规范给所有 OpenCV 的类和函数加上前缀 cv::
,本书采用的就 是这种方法。添加前缀后,代码中 OpenCV 的类和函数将更容易识别。
highgui 模块中有一批能帮助我们轻松显示图像并对图像进行操作的函数。在使用 imread 函数装载图像时,你可以通过设置选项把它转换为灰度图像。这个选项非常实用,因为有些计算 机视觉算法是必须使用灰度图像的。在读入图像的同时进行色彩转换,可以提高运行速度并减少 内存使用,做法如下所示:
// 读入一个图像文件并将其转换为灰度图像
image= cv::imread("puppy.bmp", CV::IMREAD_GRAYSCALE);
这样生成的图像由无符号字节(unsigned byte,C++中为 unsigned char)构成,在 OpenCV 中用常量 CV_8U 表示。另外,即使图像是作为灰度图像保存的,有时仍需要在读入时把它转换 成三通道彩色图像。要实现这个功能,可把 imread 函数的第二个参数设置为正数:
// 读取图像,并将其转换为三通道彩色图像
image= cv::imread("puppy.bmp", CV::IMREAD_COLOR);
在这样创建的图像中,每个像素有 3 字节,OpenCV 中用 CV_8UC3 表示。当然了,如果输 入的图像文件是灰度图像,这三个通道的值就是相同的。最后,如果要在读入图像时采用文件本 身的格式,只需把第二个参数设置为负数。可用 channels 方法检查图像的通道数:
std::cout << "This image has "
<< image.channels() << " channel(s)";
请注意,当用 imread 打开路径指定不完整的图像时(前面例子的做法),imread 会自动采 用默认目录。如果从控制台运行程序,默认目录显然就是当前控制台的目录;但是如果直接在 IDE 中运行程序,默认目录通常就是项目文件所在的目录。因此,要确保图像文件在正确的目录下。
当你用 imshow 显示由整数(CV_16U 表示 16 位无符号整数,CV_32S 表示 32 位有符号整 数)构成的图像时,图像每个像素的值会被除以 256,以便能够在 256 级灰度中显示。同样,在 显示由浮点数构成的图像时,值的范围会被假设为 0.0(显示黑色)~1.0(显示白色)。超出这个 范围的值会显示为白色(大于 1.0 的值)或黑色(小于 0.0 的值)。
highgui 模块非常适用于快速构建原型程序。在生成程序的最终版本前,你很可能会用到 IDE 提供的 GUI 模块,这样会让程序看起来更专业。
这个程序同时使用了输入图像和输出图像,作为练习,你可以对这个示例程序做一些改动, 比如改成就地处理的方式,也就是不定义输出图像而直接写入原图像:
cv::flip(image,image,1); // 就地处理
highgui 模块中有大量可用来处理图像的函数,它们可以使程序对鼠标或键盘事件做出响应, 也可以在图像上绘制形状或写入文本。
通过编程,你可以让鼠标在置于图像窗口上时运行特定的指令。要实现这个功能,需定义一 个合适的回调函数。回调函数不会被显式地调用,但是会在响应特定事件(这里是指有关鼠标与 图像窗口交互的事件)的时候被程序调用。为了能被程序识别,回调函数需要具有特定的签名, 并且必须注册。对于鼠标事件处理函数,回调函数必须具有这种签名:
void onMouse( int event, int x, int y, int flags, void* param);
第一个参数是整数,表示触发回调函数的鼠标事件的类型。后面两个参数是事件发生时鼠标 的位置,用像素坐标表示。参数 flags 表示事件发生时按下了鼠标的哪个键。最后一个参数是 指向任意对象的指针,作为附加的参数发送给函数。你可用下面的方法在程序中注册回调函数:
cv::setMouseCallback("Original Image", onMouse,
reinterpret_cast(&image));
在本例中,函数 onMouse
与名为 Original Image(原始图像)的图像窗口建立了关联,同 时把所显示图像的地址作为附加参数传给函数。现在,只要用下面的代码定义回调函数 onMouse
, 每当遇到鼠标点击事件时,控制台就会显示对应像素的值(这里假定它是灰度图像):
void onMouse( int event, int x, int y, int flags, void* param) {
cv::Mat *im= reinterpret_cast(param);
switch (event) { // 调度事件
case CV::EVENT_LBUTTONDOWN: // 鼠标左键按下事件
// 显示像素值(x,y)
std::cout << "at (" << x << "," << y << ") value is: "
<< static_cast(
im->at(cv::Point(x,y))) << std::endl;
break;
}
}
这里用 cv::Mat
对象的 at 方法来获取(x, y)的像素值,第 2 章会详细讨论这个方法。鼠 标事件的回调函数可能收到的事件还有 cv::EVENT_MOUSEMOVE
、cv::EVENT_LBUTTONUP
、 cv::EVENT_RBUTTONDOWN
和 cv::EVENT_RBUTTONUP
。
OpenCV 还提供了几个用于在图像上绘制形状和写入文本的函数。基本的形状绘制函数有 circle、ellipse、line 和 rectangle。这是一个使用 circle 函数的例子:
cv::circle(image, // 目标图像
cv::Point(155,110), // 中心点坐标
65, // 半径
0, // 颜色(这里用黑色)
3); // 厚度
在 OpenCV 的方法和函数中,我们经常用 cv::Point
结构来表示像素的坐标。这里假定是 在灰度图像上进行绘制的,因此用单个整数来表示颜色。1.4 节将讲述如何使用 cv::Scalar
结 构表示彩色图像颜色值。你也可以在图像上写入文本,方法如下所示:
cv::putText(image, // 目标图像
"This is a dog.", // 文本
cv::Point(40,200), // 文本位置
cv::FONT_HERSHEY_PLAIN, // 字体类型
2.0, // 字体大小
255, // 字体颜色(这里用白色)
2); // 文本厚度
在测试图像上调用上述两个函数后,得到的结果如下图所示。
请注意,只有在包含顶层模块头文件 opencv2/imgproc.hpp
的前提下,这些例子才能正常运行。
cv::Mat
类是用来存放图像(以及其他矩阵数据)的数据结构。在所有 OpenCV 类和函 数中,这个数据结构占据着核心地位,1.4 节将对它做详细介绍。
1.3 节提到了 cv::Mat
数据结构。正如前面所说,它是程序库中的关键部件,用来操作图像 和矩阵(从计算机和数学的角度看,图像其实就是矩阵)。在开发程序时,你会经常用到这个数 据结构,因此有必要熟悉它。通过本节的学习,你将了解到它采用了很巧妙的内存管理机制。
可以用下面的程序来测试 cv::Mat 数据结构的不同属性:
#include
#include
#include
// 测试函数,它创建一幅图像
cv::Mat function() {
// 创建图像
cv::Mat ima(500,500,CV_8U,50);
// 返回图像
return ima;
}
int main() {
// 创建一个 240 行×320 列的新图像
cv::Mat image1(240,320,CV_8U,100);
cv::imshow("Image", image1); // 显示图像
cv::waitKey(0); // 等待按键
// 重新分配一个新图像
image1.create(200,200,CV_8U);
image1= 200;
cv::imshow("Image", image1); // 显示图像
cv::waitKey(0); // 等待按键
// 创建一个红色的图像
// 通道次序为 BGR
cv::Mat image2(240,320,CV_8UC3,cv::Scalar(0,0,255));
// 或者
// cv::Mat image2(cv::Size(320,240),CV_8UC3);
// image2= cv::Scalar(0,0,255);
cv::imshow("Image", image2); // 显示图像
cv::waitKey(0); // 等待按键
// 读入一幅图像
cv::Mat image3= cv::imread("puppy.bmp");
// 所有这些图像都指向同一个数据块
cv::Mat image4(image3);
image1= image3;
// 这些图像是源图像的副本图像
image3.copyTo(image2);
cv::Mat image5= image3.clone();
// 转换图像进行测试
cv::flip(image3,image3,1);
// 检查哪些图像在处理过程中受到了影响
cv::imshow("Image 3", image3);
cv::imshow("Image 1", image1);
cv::imshow("Image 2", image2);
cv::imshow("Image 4", image4);
cv::imshow("Image 5", image5);
cv::waitKey(0); // 等待按键
// 从函数中获取一个灰度图像
cv::Mat gray= function();
cv::imshow("Image", gray); // 显示图像
cv::waitKey(0); // 等待按键
// 作为灰度图像读入
image1= cv::imread("puppy.bmp", CV_LOAD_IMAGE_GRAYSCALE);
image1.convertTo(image2,CV_32F,1/255.0,0.0);
cv::imshow("Image", image2); // 显示图像
cv::waitKey(0); // 等待按键
return 0;
}
运行这个程序,你将得到下面这些图像。
cv::Mat
有两个必不可少的组成部分:一个头部和一个数据块。头部包含了矩阵的所有相关 信息(大小、通道数量、数据类型等),1.3 节介绍了如何访问 cv::Mat
头部文件的某些属性(例如通过使用 cols
、rows
或 channels
)。数据块包含了图像中所有像素的值。头部有一个指向数据块的指针,即 data
属性。cv::Mat
有一个很重要的属性,即只有在明确要求时,内存块才会被复制。实际上,大多数操作仅仅复制了 cv::Mat
的头部,因此多个对象会指向同一个数据块。这种内存管理模式可以提高应用程序的运行效率,避免内存泄漏,但是我们必须了解它带来 的后果。本节的例子会对这点进行说明。
新创建的 cv::Mat 对象默认大小为 0,但也可以指定一个初始大小,例如:
// 创建一个 240 行×320 列的新图像
cv::Mat image1(240,320,CV_8U,100);
我们需要指定每个矩阵元素的类型,这里用 CV_8U 表示每个像素对应 1 字节(灰度图像), 用字母 U 表示无符号;你也可用字母 S 表示有符号。对于彩色图像,你应该用三通道类型 (CV_8UC3),也可以定义 16 位和 32 位的整数(有符号或无符号),例如 CV_16SC3。我们甚至 可以使用 32 位和 64 位的浮点数(例如 CV_32F)。
图像(或矩阵)的每个元素都可以包含多个值(例如彩色图像中的三个通道),因此 OpenCV 引入了一个简单的数据结构 cv::Scalar,用于在调用函数时传递像素值。该结构通常包含一个 或三个值。如果要创建一个彩色图像并用红色像素初始化,可用如下代码:
// 创建一个红色图像
// 通道次序是 BGR
cv::Mat image2(240,320,CV_8UC3,cv::Scalar(0,0,255));
与之类似,初始化灰度图像可这样使用这个数据结构:cv::Scalar(100)
。
图像的尺寸信息通常也需要传递给调用函数。前面讲过,我们可以用属性 cols
和 rows 来 获得 cv::Mat
实例的大小。cv::Size 结构包含了矩阵高度和宽度,同样可以提供图像的尺寸 信息。另外,可以用 size()
方法得到当前矩阵的大小。当需要指明矩阵的大小时,很多方法都 使用这种格式。
例如,可以这样创建一幅图像:
// 创建一个未初始化的彩色图像
cv::Mat image2(cv::Size(320,240),CV_8UC3);
可以随时用 create
方法分配或重新分配图像的数据块。如果图像已被分配,其原来的内容 会先被释放。出于对性能的考虑,如果新的尺寸和类型与原来的相同,就不会重新分配内存:
// 重新分配一个新图像
//(仅在大小或类型不同时)
image1.create(200,200,CV_8U);
一旦没有了指向 cv::Mat
对象的引用,分配的内存就会被自动释放。这一点非常方便,因 为它避免了 C++动态内存分配中经常发生的内存泄漏问题。这是 OpenCV(从第 2 版开始引入) 中的一个关键机制,它的实现方法是通过 cv::Mat
实现计数引用和浅复制。因此,当在两幅图 像之间赋值时,图像数据(即像素)并不会被复制,此时两幅图像都指向同一个内存块。这同样 适用于图像间的值传递或值返回。由于维护了一个引用计数器,因此只有当图像的所有引用都将 释放或赋值给另一幅图像时,内存才会被释放:
// 所有图像都指向同一个数据块
cv::Mat image4(image3);
image1= image3;
对上面图像中的任何一个进行转换都会影响到其他图像。如果要对图像内容做一个深复制, 你可以使用 copyTo 方法,目标图像将会调用 create 方法。另一个生成图像副本的方法是 clone, 即创建一个完全相同的新图像:
// 这些图像是原始图像的新副本
image3.copyTo(image2);
cv::Mat image5= image3.clone();
在本节的例子中,我们对 image3 做了修改。其他图像也包含了这幅图像,有的图像共用了 同一个图像数据,有的图像则有图像数据的独立副本。查看显示的图像,找出哪些图像因修改 image3 而产生了变化。
如果你需要把一幅图像复制到另一幅图像中,且两者的数据类型不一定相同,那就要使用 convertTo 方法了:
// 转换成浮点型图像[0,1]
image1.convertTo(image2,CV_32F,1/255.0,0.0);
本例中的原始图像被复制进了一幅浮点型图像。这一方法包含两个可选参数:缩放比例和偏 移量。需要注意的是,这两幅图像的通道数量必须相同。
cv::Mat 对象的分配模型还能让程序员安全地编写返回一幅图像的函数(或类方法):
cv::Mat function() {
// 创建图像
cv::Mat ima(240,320,CV_8U,cv::Scalar(100));
// 返回图像
return ima;
}
我们还可以从 main 函数中调用这个函数:
// 得到一个灰度图像
cv::Mat gray= function();
运行这条语句后,就可以用变量 gray
操作这个由 function
函数创建的图像,而不需要额 外分配内存了。正如前面解释的,从 cv::Mat
实例到灰度图像实际上只是进行了一次浅复制。 当局部变量 ima
超出作用范围后,ima
会被释放。但是从相关引用计数器可以看出,另一个实例 (即变量 gray
)引用了 ima
内部的图像数据,因此 ima 的内存块不会被释放。
请注意,在使用类的时候要特别小心,不要返回图像的类属性。下面的实现方法很容易 引发错误:
class Test {
// 图像属性
cv::Mat ima;
public:
// 在构造函数中创建一幅灰度图像
Test() : ima(240,320,CV_8U,cv::Scalar(100)) {}
// 用这种方法回送一个类属性,这是一种不好的做法
cv::Mat method() { return ima; }
};
如果某个函数调用了这个类的 method
,就会对图像属性进行一次浅复制。副本一旦被修改, class
属性也会被“偷偷地”修改,这会影响这个类的后续行为(反之亦然)。这违反了面向对 象编程中重要的封装性原理。为了避免这种类型的错误,你需要将其改成返回属性的一个副本。
OpenCV 中还有几个与 cv::Mat 相关的类,熟练掌握这些类也很重要。
在 OpenCV 的文档中,很多方法和函数都使用 cv::InputArray
类型作为输入参数。 cv::InputArray
类型是一个简单的代理类,用来概括 OpenCV 中数组的概念,避免同一个方法 或函数因为使用了不同类型的输入参数而有多个版本。也就是说,你可以在参数中使用 cv::Mat 对象或者其他的兼容类型。因为它是一个输入数组,所以你必须确保函数不会修改这个数据结构。 有趣的是,cv::InputArray
也能使用常见的 std::vector
类来构造;也就是说,用这种方式 构造的对象可以作为 OpenCV 方法和函数的输入参数(但千万不要在自定义类和函数中使用这个 类)。其他兼容的类型有 cv::Scalar 和 cv::Vec
,后者将在下一章介绍。此外还有一个代理类 cv::OutputArray
,用来指定某些方法或函数的返回数组。
开发应用程序时,你可能会遇到需要处理小矩阵的情况,这时就可以使用模板类 cv::Matx 和它的子类。举个例子,下面的代码定义了一个 3×3 的双精度型浮点数矩阵和一个 3 元素的向量, 然后使两者相乘:
// // 3×3 双精度型矩阵
cv::Matx33d matrix(3.0, 2.0, 1.0,
2.0, 1.0, 3.0,
1.0, 2.0, 3.0);
// 3×1 矩阵(即向量)
cv::Matx31d vector(5.0, 1.0, 3.0);
// 相乘
cv::Matx31d result = matrix*vector;
这些矩阵可以进行常见的数学运算。
要查看完整的 OpenCV 文档,请访问 http://docs.opencv.org/。
第 2 章将介绍如何高效地访问和修改 cv::Mat 表示的图像的像素值。
1.5 节将解释如何定义图像内的感兴趣区域。
有时需要让一个处理函数只在图像的某个部分起作用。OpenCV 内嵌了一个精致又简洁的机 制,可以定义图像的子区域,并把这个子区域当作普通图像进行操作。本节将介绍如何定义图像 内部的感兴趣区域。
假设我们要把一个小图像复制到一个大图像上。例如要把下面的标志插入到测试图像中。
为了实现这个功能,可以定义一个感兴趣区域(Region Of Interest,ROI),在此处进行复制 操作,这个 ROI 的位置将决定标志的插入位置。
第一步是定义 ROI。定义后,就可以把 ROI 当作一个普通的 cv::Mat 实例进行操作。关键 在于,ROI 实际上就是一个 cv::Mat 对象,它与它的父图像指向同一个数据缓冲区,并且在头 部指明了 ROI 的坐标。接着,可以用下面的方法插入标志:
// 在图像的右下角定义一个 ROI
cv::Mat imageROI(image,
cv::Rect(image.cols-logo.cols, // ROI 坐标
image.rows-logo.rows,
logo.cols,logo.rows)); // ROI 大小
// 插入标志
logo.copyTo(imageROI);
这里的 image 是目标图像,logo 是标志图像(相对较小)。运行上述代码后,你将得到下 面的图像。
定义 ROI 的一种方法是使用 cv::Rect
实例。正如其名,它通过指明左上角的位置(构造 函数的前两个参数)和矩形的尺寸(后两个参数表示宽度和高度),描述了一个矩形区域。在这 个例子中,我们利用图像和标志的尺寸来确定标志的位置,即图像的右下角。很明显,整个 ROI 肯定处于父图像的内部。
ROI 还可以用行和列的值域来描述。值域是一个从开始索引到结束索引的连续序列(不含开 始值和结束值),可以用 cv::Range
结构来表示这个概念。因此,一个 ROI 可以用两个值域来 定义。本例中的 ROI 也可以定义为:
imageROI= image(cv::Range(image.rows-logo.rows,image.rows),
cv::Range(image.cols-logo.cols,image.cols));
cv::Mat
的 operator()
函数返回另一个 cv::Mat
实例,可供后续使用。由于图像和 ROI 共享了同一块图像数据,因此 ROI 的任何转变都会影响原始图像的相关区域。在定义 ROI 时, 数据并没有被复制,因此它的执行时间是固定的,不受 ROI 尺寸的影响。
要定义由图像中的一些行组成的 ROI,可用下面的代码:
cv::Mat imageROI= image.rowRange(start,end);
与之类似,要定义由图像中一些列组成的 ROI,可用下面的代码:
cv::Mat imageROI= image.colRange(start,end);
OpenCV 的方法和函数包含了很多本书并未涉及的可选参数。第一次使用某个函数时,你需要花时间看一下文档,查清该函数支持哪些选项。一个十分常见的选项很可能被用来定义图像掩码。
OpenCV 中的有些操作可以用来定义掩码。函数或方法通常对图像中所有的像素进行操作, 通过定义掩码可以限制这些函数或方法的作用范围。掩码是一个 8 位图像,如果掩码中某个位置 的值不为 0,在这个位置上的操作就会起作用;如果掩码中某些像素位置的值为 0,那么对图像 中相应位置的操作将不起作用。例如,在调用 copyTo
方法时就可以使用掩码,我们可以利用掩 码只复制标志中白色的部分,如下所示:
// 在图像的右下角定义一个 ROI
imageROI= image(cv::Rect(image.cols-logo.cols,
image.rows-logo.rows,
logo.cols,logo.rows));
// 把标志作为掩码(必须是灰度图像)
cv::Mat mask(logo);
// 插入标志,只复制掩码不为 0 的位置
logo.copyTo(imageROI,mask);
执行这段代码后将得到下面这幅图像。
因为标志的背景是黑色的(因此值为 0),所以很容易同时作为被复制图像和掩码来使用。当然, 我们也可以在程序中自己决定如何定义掩码。OpenCV中大多数基于像素的操作都可以使用掩码。