opencv计算机视觉编程攻略: 第1章 图像编程入门

        本章我们开始学习OpenCV库。你将学习:

         安装OpenCV库;

         装载、显示和保存图像;

         深入理解cv::Mat数据结构;

         定义ROI区域(感兴趣区域)。

1.1   简介

       本章介绍OpenCV的基本要素,并演示如何完成最基本的图像处理任务:读取、显示和保存图像。开始之前,首先需要安装OpenCV库。安装过程非常简单,1.2节会详细介绍。

       所有的计算机视觉应用程序都包含对图像的处理。因此,OpenCV提供的最基础的工具即为一个操作图像和矩阵的数据结构。此数据结构功能非常强大,具有多种实用属性和方法。此外,它还包含先进的内存管理模型,十分有助于应用程序的开发。本章最后两节介绍如何使用这个重要的OpenCV数据结构。

1.2  安装opencv库

       OpenCV是一个开源的程序库,用于开发在Windows、Linux、Android和Mac OS系统中运行的计算机视觉应用程序。在BSD许可协议下,它可以用来开发学术应用和商业应用,可随意使用、发布和修改。本节介绍如何安装OpenCV程序库。

1.2.1 准备工作

      访问OpenCV官方网站http://opencv.org/,你可以看到最新版程序库、在线文档以及诸多其他有用的资源。

1.2.2 安装

       在OpenCV网站上选择你使用的平台(Unix、Windows或Android),并转到相应的DOWNLOADS(下载)页面,从那里下载OpenCV包,然后将其解压;解压的目录名通常与程序库版本一致(例如,在Windows下可解压到C:\OpenCV2.4.9)。解压之后,你会看到很多构成程序库的文件和目录。注意有一个sources目录,它包含所有的源代码文件。(是的,它是开源的!)但是,要完成程序库安装并使之能使用,你还需执行一步操作:针对所选环境生成程序库的二进制文件。这时,你必须选定创建OpenCV程序所用的目标平台。使用哪种操作系统?Windows还是Linux?使用什么编译器?Microsoft VS2013还是MinGW?32位还是64位?你在开发项目时将要使用的集成开发环境(IDE)也会引导你做这些选择。

       注意,如果你在装有Visual Studio的Windows环境下操作,可执行安装包很可能不仅仅安装源文件,还可能安装构建应用程序所需的已编译的二进制文件。检查一下build目录,它应该包含子目录x64和x86(分别对应64位和32位版本)。这些子目录下有vc10、vc11、vc12等目录,这些目录包含了用于不同版本MS Visual Studio的二进制文件。在此情况下,除非你想使用特殊的选项进行个性化构建,否则可以跳过本节讲解的编译过程直接使用OpenCV。

       为了完成安装过程并构建OpenCV二进制文件,你需要使用CMake工具,该工具可从http://cmake.org下载。CMake是另一个开源软件工具,用于控制使用了跨平台配置文件的软件系统的编译过程。它可以生成在特定环境下编译软件库所需的makefile和workspace文件。因此你需要事先下载并安装CMake,之后可以使用命令行工具来运行CMake,但更容易的方法是使用GUI工具(cmake-gui)。使用GUI,你只需要指定OpenCV库源文件和二进制文件所在的目录,点击Configure按钮来选择适合的编译器,然后再一次点击Configure按钮。

enter image description here

       现在可以点击Generate按钮生成项目文件了,这些项目文件用来编译程序库。这是安装过程的最后一个步骤,会生成能在指定的开发环境下使用的程序库。例如,如果你选用Visual Studio,那么只需要打开由CMake创建的位于顶层的解决方案文件(通常是OpenCV.sln文件),然后在Visual Studio中输入Build Solution命令。如果要得到Release和Debug两个版本,你就需要编译两次——每个相应的配置一次。在已创建的目录bin下包含动态库文件,可执行文件在运行时需要调用这些动态库文件。别忘了在控制面板中设置环境变量PATH,以确保运行程序时操作系统能找到这些dll文件。

enter image description here

      在Linux环境中,你可用make实用命令运行前面生成的makefile文件。为了完成所有目录的安装,你需要运行Build INSTALL或者sudo make INSTALL命令。

      在构建程序库之前,一定要检查一下OpenCV安装程序产生的内容;安装程序可能已经产生了程序库,那样就可免去编译的步骤。如果要使用Qt作为IDE,1.2.4节描述了编译OpenCV项目的另一种方法。

1.2.3 实现原理

      从2.2版开始,OpenCV库就分成了几个模块。这些模块是内置的库文件,位于lib目录下。其中常用的模块有:

       opencv_core模块,包含了程序库的核心功能,特别是基本的数据结构和算法函数;

       opencv_imgproc模块,包含了主要的图像处理函数;

       opencv_highgui模块,包含图像、视频读写函数和部分用户界面函数;

       opencv_features2d模块,包含特征点检测器、描述子以及特征点匹配框架;

       opencv_calib3d模块,包含相机标定、双视角几何估计以及立体函数;

       opencv_video模块,包含运动估计、特征跟踪以及前景提取函数和类;

       opencv_objdetect模块,包含目标检测函数,例如面部和人体探测器。

      OpenCV库还包含了其他实用模块:机器学习函数(opencv_ml)、计算几何算法(opencv_flann)、共享代码(opencv_contrib)、过时的代码(opencv_legacy)以及GPU加速代码(opencv_gpu)。此外还有一些专门用来实现较高层次函数的库,例如用于计算摄影的opencv_photo和实现图像拼接算法的opencv_stitching。另外还有一个opencv_nonfree模块,它包含在使用过程中可能有限制的函数。如果程序用到了某些OpenCV函数,你就必须在编译时将程序与包含这些函数的库链接。一般来说,使用刚才列出的前三个模块,然后根据具体程序的作用域选择其他模块。

      所有这些模块都有一个对应的头文件(位于include目录中)。因此,典型的OpenCV C++代码会首先包含必需的模块。例如(这是推荐的声明样式):

#include <opencv2/core/core.hpp>

#include <opencv2/imgproc/imgproc.hpp>

#include <opencv2/highgui/highgui.hpp>

      下载示例代码

      若用Packt账号购买了本书英文版,你可以从http://www.packtpub.com下载示例代码文件 。如果你是从其他地方购买的本书英文版,那么可以访问http://www.packtpub.com/support并注册,然后会通过邮件接收到文件。

      你可能会看到用以下命令开头的OpenCV代码:

#include "cv.h"

       这是因为在程序库被重构成多个模块之前,应用使用了老式的风格。最后要注意的是,以后OpenCV还会被重构;因此,如果你下载2.4以后的版本,模块的划分可能会不同。

1.2.4 扩展阅读

        OpenCV网站http://opencv.org/上有详细的安装说明,还有完整的在线文档,包括几个针对程序库中不同组件的教程。

       1 . 使用Qt进行OpenCV开发

       Qt是开发C++应用程序的跨平台IDE,是作为开源项目发展起来的。你可以在LPGL开源协议下使用Qt,也可以在商业(付费)协议下用Qt开发专有项目。它由两个独立的部分组成:一个称为Qt creator的跨平台IDE、一系列Qt类库和开发工具。使用Qt来开发C++应用程序有以下好处:

        它是由Qt社区发起的开源项目,提供各种Qt组件的源代码;

        它是一个跨平台IDE,这意味着开发的应用程序能在不同操作系统上运行,如Windows、Linux、Mac OS X等;

        它包含了一个完整并且跨平台的GUI库,遵循高效的面向对象和事件驱动的模型;

        Qt还包含几个跨平台库,有助于开发多媒体、图形、数据库、多线程、Web应用以及很多用于设计高级应用程序的有趣的构建模块。

       你可以从http://qt-project.org/下载Qt。在安装Qt时需要选择不同的编译器。在Windows下,MinGW是一款可以用来取代Visual Studio的非常不错的编译器。

       因为Qt可以读取CMake文件,用它来编译OpenCV特别容易。在安装OpenCV和CMake后,你只需要在Qt菜单中选择Open File或者Project...,打开OpenCV中sources目录下的CMakeLists.txt文件,这样就会生成一个用Build Project Qt命令构建的OpenCV项目:

       可能会有几个警告信息,但你可以不管它们。

2 . OpenCV开发者站点

     OpenCV是一个开源项目,非常欢迎用户做出贡献。你可以访问开发者网站http://code. opencv.org。除此之外,你也可以获得当前已经开发完毕的OpenCV版本。这个社区使用Git作为版本控制系统,因此必须使用Git来获得最新版本的OpenCV。作为一个免费、开源的软件系统,Git可能是管理源代码的最好工具,它可在http://git-scm.com/下载。

1.2.5 参阅

         作者的网站(www.laganiere.name)上有安装最新版本OpenCV库的详细步骤;

         1.3.4节详解如何用Qt创建OpenCV项目。

1.3 装载、显示和存储图像

       现在我们开始运行第一个OpenCV应用程序。既然OpenCV是处理图像的,这里我们来演示几个图像程序开发中最基本的操作,即从文件中装载一个输入的图像、在窗口中显示图像、应用一个处理函数,然后把输出图像存储到磁盘。

1.3.1准备工作

      使用你喜欢的IDE(例如MS Visual Studio或者Qt)新建一个控制台应用程序,使用待填充内容的main函数。

1.3.2 如何实现      

       首先要引入头文件,这些头文件定义了所需的类和函数。这里我们只是简单地显示一个图像,因此需要定义了图像数据结构的核心库和包含了所有图形接口函数的highgui头文件:

#include <opencv2/core/core.hpp>

#include <opencv2/highgui/highgui.hpp>

main函数中,首先定义一个表示图像的变量。在OpenCV 2中,定义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。

1.3.3 实现原理

在OpenCV的C++ API中,所有类和函数都在命名空间cv内定义。我们有两种方法可以访问它们,第一种方法是在定义main函数前使用如下声明:

using namespace cv;

第二种方法是使用命名空间规范给所有OpenCV的类和函数加上前缀cv::,本书即采用这种方法。使用前缀可让OpenCV的类和函数更容易识别。

在highgui模块中有一批函数可用来方便地显示图像和对图像进行操作。在使用imread函数装载图像时,你可以通过设置选项把它转换为灰度图像。这个选项非常实用,因为有些计算机视觉算法是必须使用灰度图像的。在读入图像的同时进行色彩转换,这可以提高运行速度并减少内存使用。做法如下:

// 读入一个图像文件并转换为灰度图像

image= cv::imread("puppy.bmp", CV_LOAD_IMAGE_GRAYSCALE);

这样生成的图像由无符号字节(C++中为unsigned char)构成,OpenCV中用定义的常量CV_8U表示。另外,即使图像是作为灰度图像保存的,有时仍需要在读入时把它转换成三通道彩色图像。要实现这个功能,可把imread函数的第二个参数设置为正数:

// 读取图像,并转换为三通道彩色图像

image= cv::imread("puppy.bmp", CV_LOAD_IMAGE_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); // 就地处理
1.3.4 扩展阅读

highgui模块中有大量可用来处理图像的函数,运用这些函数可以使程序对鼠标或键盘事件做出响应,也可以在图像上绘制轮廓或写入文本。

1 . 在图像上点击

你可以通过编程实现鼠标置于图像窗口上时运行特定的指令。要实现这个功能,需定义一个适当的回调函数。回调函数不会被显式地调用,但是会在响应特定事件(这里是指有关鼠标与图像窗口交互的事件)的时候被程序调用。为了能被程序识别,回调函数需要具有特定的签名,并且必须注册。对于这种鼠标事件处理函数,回调函数必须具有这种签名:

void onMouse( int event, int x, int y, int flags, void* param);

第一个参数是整数,表示触发回调函数的鼠标事件的类型。后面两个参数是事件发生时鼠标的位置,用像素坐标表示。参数flags表示事件发生时按下了鼠标的哪个键。最后一个参数是执行任意对象的指针,作为附加的参数发送给函数。你可用下面的方法在程序中注册回调函数:

cv::setMouseCallback("Original Image", onMouse,

                 reinterpret_cast<void*>(&image));

本例中,函数onMouse与名为Original Image(原始图像)的图像窗口建立关联,同时把所显示图像的地址作为附加参数传给函数。现在,只要用下面的代码定义回调函数onMouse,每当遇到鼠标点击事件时,控制台中就会显示对应像素的值(这里我们假定它是灰度图像)。

void onMouse( int event, int x, int y, int flags, void* param) {

  cv::Mat *im= reinterpret_cast<cv::Mat*>(param);


  switch (event) { // 调度事件

case CV_EVENT_LBUTTONDOWN: // 鼠标左键按下事件

  // 显示像素值(x,y)

  std::cout << "at (" << x << "," << y << ") value is: "

    << static_cast<int>(

              im->at<uchar>(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。

2 . 在图像上绘图

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);                         // 文本厚度

在测试图像上调用上述两个函数后,得到的结果如下图所示:


3 . 用Qt运行示例程序

要使用Qt运行OpenCV应用程序,你需要创建项目文件。针对本节中的例子,项目文件(loadDisplaySave.pro)显示如下:

QT       += core

QT       -= gui



TARGET = loadDisplaySave

CONFIG   += console

CONFIG   -= app_bundle



TEMPLATE = app



SOURCES += loadDisplaySave.cpp

INCLUDEPATH += C:\OpenCV2.4.9\build\include

LIBS += -LC:\OpenCV2.4.9\build\x86\MinGWqt32\lib \

-lopencv_core249 \

-lopencv_imgproc249 \

-lopencv_highgui249

这个项目文件表明了头文件和库文件所在的路径,还列出了示例程序所用的库模块。请确保选用的库文件与Qt编译器兼容。网上下载的本书示例程序的源代码中包含了CMakeLists文件,可以用Qt(或CMake)打开,用来创建有关项目。

1.3.5 参阅

 cv::Mat类是用来存放图像(以及其他矩阵数据)的数据结构。在所有OpenCV类和函数中,这个数据结构具有核心地位,1.4节我们将对它做详细介绍。

 你可以从这里下载本书示例程序的源代码:https://github.com/laganiere/。

1.4 深入了解cv::Mat

1.3节提到了cv::Mat数据结构。正如前面所说,它是程序库中的关键元素,用来操作图像和矩阵(从计算机和数学的角度看,图像其实就是矩阵)。在开发程序时你会经常用到这个数据结构,因此有必要熟悉它。通过本节的学习你将了解到它采用了很巧妙的内存管理机制,因此支持高效的内存使用。

1.4.1 如何实现

下面的程序可用来测试cv::Mat数据结构的不同属性:

#include <iostream>

#include <opencv2/core/core.hpp>

#include <opencv2/highgui/highgui.hpp>



// 测试函数,它创建一个图像

cv::Mat function() {

// 创建图像

cv::Mat ima(500,500,CV_8U,50);

// 返回图像

return ima;

}



int main() {

// 定义图像窗口

cv::namedWindow("Image 1");

cv::namedWindow("Image 2");

cv::namedWindow("Image 3");

cv::namedWindow("Image 4");

cv::namedWindow("Image 5");

cv::namedWindow("Image");



// 创建一个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;

}

运行这个程序,你将得到下面这些图像:


1.4.2 实现原理

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();

如果你需要把一个图像复制到另一个图像中,而两者的数据类型不一定相同,那就要使用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实例到图像gray,实际上只是进行了一次浅复制。当局部变量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属性也会被“偷偷地”修改,这会影响这个类的后续行为(反过来也一样)。为了避免这种类型的错误,你需要将其改成回送属性的一个副本。

1.4.3 扩展阅读

OpenCV中还有几个与cv::Mat相关的类,熟练掌握这些类也很重要。

1 . 输入和输出数组

在OpenCV的文档中,有很多方法和函数使用cv::InputArray类型作为输入参数。cv::InputArray类型是一个简单的代理类,用来概括OpenCV中数组的概念,避免同一个方法或函数因为使用了不同类型的输入参数而出现多个不同的版本。也就是说,你可以在参数中使用cv::Mat对象或者其他的兼容类型。cv::InputArray只是一个接口,因此你不能在代码中显式地定义它。比较有趣的是,cv::InputArray也能使用常见的std::vector类来构造,这意味着std::vector的对象可作为内容对象输入OpenCV的算法和函数(只要这么做是有意义的)。其他兼容的类型有cv::Scalar和cv::Vec,其中cv::Vec将在下一章介绍。此外还有一个代理类cv::OutputArray,用来指定某些方法或函数的返回数组。

2 . 老式的IplImage结构

在OpenCV第2版中引入了一个新的C++接口。早期版本使用C语言风格的函数和结构(现在仍能使用)。特别是用IplImage结构来操作图像,该结构是从IPL库继承的(即Intel Image Processing库),现在已经与IPP库(即Intel Integrated Performance Primitive库)合并。如果使用老式的C语言接口创建的代码和库,你就需要操作这些IplImage结构。幸运的是,把IplImage结构转换成cv::Mat对象非常容易,如下面的代码所示:

IplImage* iplImage = cvLoadImage("puppy.bmp");

cv::Mat image(iplImage,false);

cvLoadImage是用C语言接口装载图像的函数。cv::Mat对象的构造函数中,第二个参数表示不复制数据(如需要复制,可把它设为true;默认值是false,因此可以省略),这意味着IplImage和image会共用同一块图像数据。 这里你需要特别小心,以避免产生悬挂指针,因此更安全的做法是把IplImage指针封装进OpenCV 2的引用计数指针类中:

cv::Ptr<IplImage> iplImage = cvLoadImage("puppy.bmp");

否则,当释放IplImage结构指向的内存时需要显式执行:

cvReleaseImage(&iplImage);

记住,请不要使用这个过时的数据结构,要改用cv::Mat数据结构。

1.4.4 参阅

 要查看完整的OpenCV文档,请访问http://docs.opencv.org/。

 第2章将介绍如何高效地访问和修改cv::Mat表示的图像的像素值。

 1.5节将解释如何定义图像内的兴趣区域。

1.5 定义兴趣区域

有时我们需要让一个处理函数只在图像的某个部分起作用。OpenCV内嵌了一个精致又简洁的机制,可以定义图像的子区域,并把这个子区域当作普通图像进行操作。本节介绍如何定义图像内部的兴趣区域。

1.5.1 准备工作

假设我们要把一个小图像复制到一个大图像上。例如,我们要把下面的小标志插入到测试图像中。

enter image description here

为了实现这个功能,我们可以定义一个兴趣区域(Region Of Interest,ROI),在它上面进行复制操作,这个ROI的位置将决定标志的插入位置。

1.5.2 如何实现

第一步是定义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是标志图像(相对较小)。运行上述代码后,你将得到下面的图像:

enter image description here

1.5.3 实现原理

定义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);
1.5.4 扩展阅读

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);

执行这段代码后你将得到下面这个图像:

enter image description here

标志的背景是黑色的(因此值为0),所以我们很容易把它作为被复制图像和掩码。当然,我们可以在程序中自己决定如何定义掩码。OpenCV中大多数基于像素的操作都可以使用掩码。

1.5.5 参阅

 2.6节将用到row和col方法。它们是rowRange和colRange方法的特例,即开始和结束的索引是相同的,以定义一个单行或单列的ROI。





  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值