:cv::Mat
数据机构,它是程序库中的关键部件,用来操作图像和矩阵(从计算机和数学的角度看,图像其实就是矩阵),它采用了很巧妙的内存管理机制。在开发程序时,会经常用到这个数据结构,因此有必要熟悉它。
图片:puppy.png
可以用下面的程序来测试:cv::Mat
数据机构的不同属性
#include
#include
#include
//测试图像,它创建一幅图像
cv::Mat function() {
//创建图像
cv::Mat ima(500, 500, CV_8U, 50);
//返回图像
return ima;
}
int main(int argc, char* argv[]) {
//创建一个240行x320列的新图像
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("Imgae", 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("Imgae", image2); //显示图像
cv::waitKey(0); //等待按键
//读入一幅图像
cv::Mat image3 = cv::imread("puppy.png");
//所有这些图像都指向同一个数据块
cv::Mat image4(image3);
image1 = image3; //浅复制
//这些图像是源图像的副本图像
image3.copyTo(image2); //copyto做深复制,目标图像会调用create方法
cv::Mat image5 = image1.clone(); //clone创建一个完全相同的新图像
//转换图像进行测试
cv::flip(image3, image3, -1);
//检查哪些图像在处理过程中受到了影响
cv::imshow("Imgae3", image3); //显示图像
cv::imshow("Imgae1", image1); //显示图像
cv::imshow("Imgae2", image2); //显示图像
cv::imshow("Imgae4", image4); //显示图像
cv::imshow("Imgae5", image5); //显示图像
cv::waitKey(0); //等待按键
//从函数中读取一个灰度图像
cv::Mat gray = function();
//当局部变量ima超出作用范围后,ima会被释放。但从相关引用计数器可以看到,另一个实例(即变量gray)引用了ima内部的图像数据,因此ima的内存块不会被释放
cv::imshow("Imgae", gray); //显示图像
cv::waitKey(0); //等待按键
//作为灰度图像输入
image1 = cv::imread("HappyFish.jpg", CV_LOAD_IMAGE_GRAYSCALE);
//如果你需要把一幅图像复制到另一幅图像中,且两者的数据类型不一定相同,那就要使用convertTo方法了(这两幅图像的通道数量必须相同)
image1.convertTo(image2, CV_32F, 1/255.0, 0.0);
cv::imshow("Imgae", image2); //显示图像
cv::waitKey(0); //等待按键
return 0;
}
:cv::Mat
有两个必不可少的组成部分:一个:头部
和一个:数据块
。头部包含了矩阵的所有相关信息(大小、通道数量、数据类型等),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 属性也会被“偷偷地”修改,这会影响这个类的后续行为(反之亦然)。这违反了面向对象编程中重要的封装性原理。为了避免这种类型的错误,你需要将其改成返回属性的一个副本。