OpenCV实战(2)——OpenCV核心数据结构

发布于:2022-11-16 ⋅ 阅读:(11) ⋅ 点赞:(0) ⋅ 评论:(0)

0. 前言

cv::Mat 类是用于保存图像(以及其他矩阵数据)的数据结构,该数据结构是所有 OpenCV 类和函数的核心,这是 OpenCV 库的一个关键元素,用于处理图像和矩阵(从计算和数学的角度来看,图像本质上是一个矩阵),同时 cv::Mat 数据结构结合了优雅的内存管理机制,因此,使用起来十分高效。此数据结构在应用程序开发中广泛使用,因此再进一步学习前我们必须熟悉 cv::Mat 数据结构。

1. cv::Mat 数据结构

1.1 cv::Mat 简介

cv::Mat 数据结构基本上由两部分组成:标头 (header) 和数据块 (data block)。标头包含与矩阵相关的所有信息(尺寸大小、通道数、数据类型等)。我们已经介绍了如何访问包含在 cv::Mat 标头中的一些属性,例如,通过使用 colsrowschannels 访问矩阵的列数、行数或通道数;数据块包含图像的所有像素值,标头中包含一个指向这个数据块的指针变量,即 data 属性。cv::Mat 数据结构的一个重要特性是内存块仅在明确请求时才被复制,大多数操作只是简单地复制 cv::Mat 标头,这样多个对象本质上同时指向同一个数据块,这种内存管理模型使应用程序更高效,同时可以避免内存泄漏。

1.2 cv::Mat 属性

接下来,我们通过编写测试程序 (mat.cpp) 来测试 cv::Mat 数据结构的不同属性。

1. 首先,声明 opencv 头文件和 c++ i/o 流文件:

#include <iostream>
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>

2. 创建函数来生成新的灰度图像,其所有像素具有相同的默认值:

cv::Mat function() {
   // 创建新图像并返回
   cv::Mat ima(500, 500, CV_8U, 50);
   return ima;
}

默认情况下, cv::Mat 对象在创建时的大小为零,但也可以指定初始大小:

cv::Mat image1(240,320,CV_8U,100);

如果指定图像初始大小,就需要指定每个矩阵元素的类型,以上代码使用 CV_8U 类型,对应创建的图像像素为 1 字节 (8 位),字母 U 表示它是无符号的;还可以使用字母 S 声明有符号整数类型。对于彩色图像,需要指定三个通道 (CV_8UC3),也可以声明大小为 1632 的整数(同样可以为有符号或无符号)图像,例如,CV_16SC3 表示 16 位有符号整数类型。除了整数类型,还可以使用 32 位或 64 位浮点数,例如,CV_32F 表示 32 位浮点数。

3. 在主函数中,创建六个窗口来显示图像处理结果:

cv::namedWindow("Image 1");
cv::namedWindow("Image 2");
cv::namedWindow("Image 3");
cv::namedWindow("Image 4");
cv::namedWindow("Image 5");
cv::namedWindow("Image");

4. 创建多个不同的图像矩阵(具有不同的尺寸、通道和默认值),并等待按键被按下:

// 创建一个尺寸为 240x320 的图像,创建时直接定义像素值
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); 
// 创建一个由红色填充的图像,OpenCV 中色彩通道顺序为 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); 

图像(或矩阵)的每个元素可以由多个值组成(例如,彩色图像具有三个通道,因此每个坐标点都有三个像素值),因此,OpenCV 引入了一种简单的数据结构,用于将像素值传递给函数,即 cv::Scalar 结构体,一般用来保存一个值或者三个值。例如,要创建一个用红色像素初始化的彩色图像:

cv::Mat image2(240,320,CV_8UC3,cv::Scalar(0,0,255));

同样,灰度图像的初始化也可以通过使用此结构完成 (cv::Scalar(100))。
图像尺寸大小通常也需要作为参数传递给函数,我们已经知道 colsrows 属性可用于获取 cv::Mat 实例的维度。尺寸大小信息也可以通过 cv::Size 结构提供,它只包含矩阵的高度和宽度,size() 方法可以获取当前矩阵大小。cv::Size 结构是许多必须指定矩阵大小的方法中常用的格式。例如,使用以下方式创建图像:

cv::Mat image2(cv::Size(320,240),CV_8UC3);

5. 使用 imread 函数读取图像并将其复制到另一个图像矩阵:

// 读取图像
cv::Mat image3 =  cv::imread("1.png"); 
// 令 image4 和 image1 指向同一个数据块 image3
cv::Mat image4(image3);
image1 = image3;
// image2 和 image5 是 image3 的副本
image3.copyTo(image2);
cv::Mat image5 = image3.clone();

6. 对复制后的图像应用图像转换函数 (cv::flip()),显示创建的所有图像,然后等待按键:

// 水平翻转图像
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); 

7. 使用创建的函数来生成一个新的灰色图像:

// 使用 function 函数创建灰度图像
cv::Mat gray = function();
cv::imshow("Image", gray); // 显示图像
cv::waitKey(0); 

8. 加载一个彩色图像,在加载过程中将其转换为灰度图像,然后,将其值转换为浮点矩阵:

// 将图像读取为灰度图像
image1=  cv::imread("1.png", cv::IMREAD_GRAYSCALE); 
// 将图像转换为值在 [0, 1] 区间内的浮点图像
image1.convertTo(image2,CV_32F,1/255.0,0.0);
cv::imshow("Image", image2); // 显示图像

1.3 完整代码示例

完整代码 (mat.cpp) 如下所示:

#include <iostream>
#include <opencv2/core.hpp>
#include <opencv2/highgui.hpp>

cv::Mat function() {
   // 创建新图像并返回
   cv::Mat ima(500,500,CV_8U,50);
   return ima;
}

int main() {
	// 创建一个尺寸为 240x320 的图像,创建时直接定义像素值
	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); 
	// 创建一个由红色填充的图像,OpenCV 中色彩通道顺序为 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("1.png"); 
	// 令 image4 和 image1 指向同一个数据块 image3
	cv::Mat image4(image3);
	image1 = image3;
	// image2 和 image5 是 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); 
	// 使用 function 函数创建灰度图像
	cv::Mat gray = function();
	cv::imshow("Image", gray); // 显示图像
	cv::waitKey(0); 
	// 将图像读取为灰度图像
	image1=  cv::imread("1.png", cv::IMREAD_GRAYSCALE); 
	// 将图像转换为值在 [0, 1] 区间内的浮点图像
	image1.convertTo(image2,CV_32F,1/255.0,0.0);
	cv::imshow("Image", image2); // 显示图像
	// 使用 cv::Matx 创建一个 3x3 的双精度 (double-precision) 矩阵
	cv::Matx33d matrix(3.0, 2.0, 1.0,
		               2.0, 1.0, 3.0,
		               1.0, 2.0, 3.0);
	// 使用 cv::Matx 创建一个 3x1 的双精度矩阵 (向量)
	cv::Matx31d vector(5.0, 1.0, 3.0);
	// 矩阵相乘
	cv::Matx31d result = matrix*vector;
	std::cout << result;
	cv::waitKey(0); 
	return 0;
}

编译后,运行此程序查看生成图像(生成的纯色图像未列出):

$ g++ mat.cpp -o mat `pkg-config --cflags --libs opencv4`
$ ./mat

cv::Mat 数据结构探索

2. 探索 cv::Mat 数据结构

2.1 cv::Mat 对象的创建

图像的数据块可以使用 create() 方法分配或重新分配。当图像先前已被分配时,它的旧内容首先被释放,出于效率考虑,如果新分配的尺寸大小和类型与现有的尺寸大小和类型匹配,则不会执行新的内存分配:

image1.create(200,200,CV_8U);

当没有引用指向给定的 cv::Mat 对象时,分配的内存会自动释放,这样可以避免在 C++ 中经常与动态内存分配相关的常见内存泄漏问题,这是 OpenCV 中的一个关键机制,通过让 cv::Mat 类实现引用计数和浅复制来实现。因此,当一个图像分配给另一个图像时,图像数据(即像素)不会被复制,两个图像都将指向同一个内存块,按值传递或按值返回的图像同样也是如此。保留引用计数,以便仅当对图像的所有引用都被销毁或分配给另一个图像时才会释放内存:

cv::Mat image4(image3);
image1= image3;

应用于以上图像 (image3image4image1) 之一的任何变换也将影响其他图像;如果希望创建图像内容的深层副本,需要使用 copyTo() 方法,在这种情况下,将在目标图像上调用 create() 方法;另一种生成图像副本的方法是 clone() 方法,它创建一个相同的新图像:

image3.copyTo(image2);
cv::Mat image5= image3.clone();

如果需要将一个图像复制到另一个不一定具有相同数据类型的图像中,则必须使用 convertTo() 方法:

image1.convertTo(image2,CV_32F,1/255.0,0.0);

以上代码中,源图像 image3 被复制到浮点图像 image2 中。convertTo() 方法包括两个可选参数——比例因子和偏移量。需要注意的是,两个图像的通道数必须相同。
cv::Mat 对象的分配模型还允许我们安全地编写返回图像的函数(或类方法):

cv::Mat function() {
   // 创建新图像并返回
   cv::Mat ima(500,500,CV_8U,50);
   return ima;
}

可以从主函数 main() 中调用这个函数 function()

cv::Mat gray= function();

如果我们调用函数 function(),那么变量 gray 将保存由函数 function() 创建的图像,而无需分配额外的内存。事实上,从 funtion() 返回的 cv::Mat 实例只是 ima 图像的浅拷贝。当 ima 局部变量超出范围时,该变量被释放,但由于关联的引用计数器指示其内部图像数据正在被另一个实例(即变量 gray )引用,因此不会释放其内存块。
需要注意的是,在创建类实例时,不要返回图像的类属性。接下来,我们介绍一个容易出错的实现示例:

class ErrorExample {
	// 图像属性
	cv::Mat ima;
	public:
	ErrorExample(): ima(240, 320, CV_8U, cv::Scalar(100)){}
	cv::Mat method() {return ima;}
}

在以上代码中,如果一个函数调用这个类的方法,它会获得一个图像属性的浅拷贝。如果以后这个副本被修改,类属性也会被修改,这会影响类的后续行为(反之亦然),为了避免这类错误,我们应该返回属性的副本。

2.2 OpenCV 输入和输出数组

查看 OpenCV 文档,可以看到许多方法和函数都接受 cv::InputArray 类型的参数作为输入。该类型是一个简单的代理类,引入此类型用于概括 OpenCV 中数组的概念,从而避免重复实现具有不同输入参数类型的相同方法或函数的多个版本,这意味着可以通过提供 cv::Mat 对象或其他兼容类型作为参数,该类只是一个接口,所以不应该在代码中显式地声明它。
cv::InputArray 也可以使用 std::vector 类构造,这意味着这类对象可以用作 OpenCV 方法和函数的输入。其他兼容类型包括 cv::Scalarcv::Vec;相应的,还存在一个 cv::OutputArray 代理类,用于指定某些方法或函数返回的数组。

小结

cv::Mat 数据结构是 OpenCV 中最基础也是最核心的数据结构,通过此数据结构构成了 OpenCV 图像处理所用的复杂类和函数。本节中,我们介绍了 cv::Mat 结构的基本用法,包括如何创建空白/非空白图像、修改图像数据类型以及复制图像内容等操作,并且深入探索了创建 cv::Mat 数据结构时的内存分配方式,最后介绍了 OpenCV 输入和输出数组的两个代理类,包括 cv::InputArraycv::OutputArray

系列链接

OpenCV实战(1)——OpenCV与图像处理基础