【OpenCV C++20 学习笔记】基本图像容器——Mat

【OpenCV C++20 学习笔记】基本图像容器------Mat

概述

电子设备中储存的图像本质是图像在每个像素点上的数值,这些数据形成一个矩阵,除此之外还包括一些描述这个数值矩阵的信息。OpenCV作为一个计算机视觉库,同样也是要处理这样的信息。所以,要学习OpenCV,首要的事情是了解在OpenCV中是如何储存图像的数值矩阵以及描述信息的。

(本文较长,详细介绍了Mat对象的原理、创建方式和输出方式,读者可根据目录跳转至相关章节)

Mat

2001年OpenCV诞生的时候,是在C语言的接口上创建的,并将图像储存在一个称作IplImage的C语言数据结构中。这种方式最大的一个缺点就是需要用户自己管理内存。如果是小型的项目尚可,如果数据量变大,管理内存就会使人很头疼。

OpenCV2.0 引入了C++接口,实现了内存的自动管理。Mat成为了OpenCV储存图片信息的数据结构。
Mat不需要手动分配或释放内存,大部分的OpenCV方法都会自动为输出的Mat对象分配内存。如果你已经为一个Mat对象分配了它需要的内存,那么你在传输它的时候,这个内存会被重复利用。也就是说,在执行任务的时候,不会使用多余的内存。

内部结构

Mat实质上是一个包含了两个部分的类:

  • 矩阵头(matrix header):它包含了矩阵的大小、存储方式、存储地址等信息
  • 指向矩阵的指针:Mat对象只是储存了矩阵的指针,并没有储存矩阵本身,而矩阵中包含了像素值(像素值矩阵的维度由存储方式决定)
    Mat对象的大小是固定的,但是矩阵本身的大小是跟随图像变化的。
    在函数间传递图像是OpenCV中非常常见的操作,而且某些图像处理算法很复杂。为了提高程序的运行速度,OpenCV使用了"引用计数机制"。每个Mat对象都有自己独立的矩阵头,但是同一个矩阵可能会被多个Mat对象共享,即多个Mat对象的指针可能会指向内存中的同一个矩阵。而复制操作只会复制Mat对象的矩阵头以及指向矩阵的指针,并不会直接复制矩阵的数值!

下面的代码详细展示了Mat对象在实际应用中的内存分配问题:

cpp 复制代码
Mat A, C;	//创建Mat对象的时候只是创建了矩阵头的部分
A = imread(argv[1], IMREAD_COLOR);	//读取图片,分配内存存储图片的数值矩阵,并将A的指针指向这个矩阵的内存地址

Mat B(A);	//调用复制构造函数创建B,但仅仅是将A中的指向图片矩阵的指针复制到B中,并没有复制图片的数值矩阵

C = A;	//赋值操作也只是将A中的指针复制到C中

上面的代码最终使A、B、C3个Mat对象中的指针都指向同一个图片的数值矩阵,虽然进行了复制和赋值操作,但内存中始终只有一个数值矩阵,如下图:

因为3个Mat对象的指针都是指向同一个数据矩阵,所以在任何一个Mat对象中对数据矩阵进行修改都会影响到其他Mat对象。实际上,不同的Mat对象只是为处理同一个数据矩阵提供了不同的使用方法。但是这些Mat对象的矩阵头部分是不同的,你甚至可以创建一个只指向数据矩阵的其中一部分的Mat对象。例如,要想在图像中创建一个感兴趣区域(region of interest,ROI),你可以新建一个Mat对象:

cpp 复制代码
Mat D(A, Rect(10, 10, 100, 100);	//使用矩形区域
Mat E = A(Range::all(), Range(1, 3));	//使用行和列

引用计数机制

如果像上面的例子一样,同一个数据矩阵属于不同的Mat对象,那到底谁来负责释放它的内存呢?答案是:最后一个使用它的Mat对象。这就是通过上面所说的"引用计数机制"来实现的。当有指向数据矩阵A的Mat对象被复制的时候,矩阵A的引用计数就会增加;当有指向矩阵A的Mat对象被销毁的时候,矩阵A的引用计数就会减少。当计数为0的时候,矩阵A就会被释放。

OpenCV还提供了深度复制数据矩阵的方法,当你不想只是复制指针,而是想复制矩阵的值的时候,可以使用cv::Mat::clone()cv::Mat::copyTo()方法。

cpp 复制代码
Mat F = A.clone();	//将A指向的数据矩阵复制给F
Mat G;
A.copyTo(G);	//将A指向的数据矩阵复制到G

这样,修改F和G的时候就不会影响A指向的数据矩阵了。

总结一下:

  • OpenCV中函数导出的图像数据是自动分配内存的(除非特别指定不自动分配)
  • 使用OpenCV的C++接口的时候不用考虑内存管理的问题
  • 赋值运算符和复制构造函数只是复制Mat对象的头部信息和指针
  • 可以用cv::Mat::clone()cv::Mat::copyTo()方法实现底层的图片数据矩阵的复制

颜色数据格式

对于如何储存像素的值,通常从两个方面考虑:颜色空间和数据类型。

颜色空间是指利用基本的颜色组合成特定的颜色的方式。有多种方式可以选择:

  • RGB:这是最常用的,因为它与人眼编码颜色的方式相似;由红、绿、蓝3中基本颜色的值,加上透明度alpha,来确定最终颜色;注意,OpenCV中的标准颜色显示系统为BGR,红色和蓝色的值调换了位置
  • HSV和HLS:将颜色分解为色调、饱和度和亮度;这种方式能更方便地处理图片的亮度
  • YCrCb:这是JPEG格式的图片常用的颜色编码方式
  • CIT Lab*:这种编码方式能够方便测量两种颜色之间的差距
  • 灰度:只有黑色和白色两种基本颜色

显式创建Mat对象

使用cv::Mat::Mat构造函数

cpp 复制代码
Mat M(2,2, CV_8U3, Scalar(0,0,255));
cout << "M = " << endl << " " << M << endl << endl;

这里使用了Mat类的其中一个构造函数。该构造函数一共包括4个参数:

  1. 行数:定义矩阵行数
  2. 列数:定义矩阵列数
  3. 数据类型:定义每个数据项的类型,下文详述
  4. Scalar常量:用来定义每个数据项的值的向量数组

矩阵的数据项

矩阵数据项的数据类型的定义遵循以下语法规则:
CV_[每个数据项的比特数][有符号或无符号][类型前缀]C[通道数量]

  • 比特数:确定每个数据项,即像素点,的数值的长度,如8比特;比特数越高,每个像素点的值域就越大,比如,32比特的浮点类型比8比特的char类型能够储存更多的颜色值
  • 有符号或无符号:确定每个数据项的值是否是有符号的(可省略,默认为无)
  • 类型前缀:如果是char类型,则为C,如果是float类型,则为F......
  • 通道数量:确定每个数据项中包含的颜色通道数量;比如,RGB颜色空间可以有4个通道,分别是红色值、绿色值、蓝色值和透明度值;通道数量可以加上括号,如CV_8UC(3) (可省略,默认为1)
    上面代码中的CV_8U3就代表每个数据项的是具有3个通道的8比特无符号的值,输出结果如下:

    可以看到矩阵中每个项有3个数值,代表3个颜色通道;共有2*2个项;每个项中的3个颜色通道的值都与Scalar中定义的相同。

使用数组进行初始化的构造函数

除了2维的矩阵,也可以创建3维矩阵的Mat对象

cpp 复制代码
int sz[3]{ 2,2,2 };
Mat L(3, sz, CV_8UC1, Scalar::all(0));

这个构造函数也使用4个参数:

  1. 维度:确定矩阵的维度
  2. 大小:一个数组,用来确定每个维度的大小
  3. 数据项的数据类型:同上一个构造函数
  4. Scalar常量:同上一个构造函数
    所以,这里创建了一个3维的矩阵,每个维度都只有2个数据项,即222;每个数据项使用的都是只有1个颜色通道的8比特无符号数值;每个数据项的值都为0。

cv::Mat::create函数

这个函数看起来像是在创建一个Mat对象,但其实它只能修改已有的Mat对象。

比如,对上面创建的M对象进行修改:

cpp 复制代码
M.create(4, 4, CV_8UC2);
cout << "M = "<< endl << " " << M << endl << endl;

cv::Mat::create函数使用了3个参数:

  1. 行数:修改后的行数
  2. 列数:修改后的列数
  3. 数据项类型:修改后的数据项类型
    输出结果为:

    可以看到原本22的3颜色通道的矩阵变成了4 4的2颜色通道矩阵。cv::Mat::create函数为M对象重新分配了内存,使其能储存修改之后的更大的矩阵。

MATLAB风格的初始化

cv::Mat::zeros, cv::Mat::ones, cv::Mat::eye等与MATLAB语言类似的函数也可以用来初始化OpenCV中的Mat对象
zeros函数用来创建全为0值的矩阵;ones函数用来创建全为1值的矩阵;eye函数用来创建对角线为1,其他值为0的矩阵

cpp 复制代码
 Mat E {Mat::eye(4, 4, CV_64F)};
 cout << "E = " << endl << " " << E << endl << endl;
 Mat O {Mat::ones(2, 2, CV_32F)};
 cout << "O = " << endl << " " << O << endl << endl;
 Mat Z {Mat::zeros(3,3, CV_8UC1)};
 cout << "Z = " << endl << " " << Z << endl << endl;

这些函数都使用相同的参数列表:

  1. 行数
  2. 列数
  3. 数据项类型
    输出结果如下:

小型矩阵

如果要构造小型矩阵,可以直接以逗号为间隔,用<<运算符将每个值一行一行依次输入;

在C++11之后,也可以使用{}风格的初始化列表

cpp 复制代码
//<<运算符
 Mat C = (Mat_<double>(3,3) << 0, -1, 0, -1, 5, -1, 0, -1, 0);
 cout << "C = " << endl << " " << C << endl << endl;
 //初始化列表
  C = (Mat_<double>({0, -1, 0, -1, 5, -1, 0, -1, 0})).reshape(3);	//reshape函数将矩阵中的数据项变成3通道类型
 cout << "C = " << endl << " " << C << endl << endl;

输出结果如下:

通过复制创建Mat对象

要复制Mat对象,需要第二节讲的使用cv::Mat::clonecv::Mat::copyTo 函数

Mat对象的输出

上面的例子中的输出使用的都是默认格式,但还有几种其他的输出格式

首先使用随机数创建一个3通道的3*2矩阵

cpp 复制代码
 Mat R {Mat(3, 2, CV_8UC3)};
 randu(R, Scalar::all(0), Scalar::all(255));

cv::randu()为随机数生成函数,使用3个参数:

  1. Mat对象:用来储存随机值的Mat对象
  2. 最低值:Scalar常量类型,确定随机数的最小值
  3. 最高值:Scalar常量类型,确定随机数的最大值
    接下来使用format函数定义输出格式,该函数使用两个参数:
  4. Mat对象:需要输出的Mat对象
  5. 格式定义:在Formatter中定义的枚举类型
    详见以下代码:
cpp 复制代码
cout << "R (default) = " << endl << " " << R << endl << endl;
cout << "R (Python) = " << endl << format(R, Formatter::FMT_PYTHON) << endl << endl;
cout << "R (csv) = " << endl << format(R, Formatter::FMT_CSV) << endl << endl;
cout << "R (numpy) = " << endl << format(R, Formatter::FMT_NUMPY) << endl << endl;
cout << "R (C) = " << endl << format(R, Formatter::FMT_C) << endl << endl;

输出结果如下:

其他普通数据项的输出

OpenCV中的大部分数据结构都支持<<运算符

以下代码展示了如何运用<<运算符输出点、向量类型的对象

cpp 复制代码
Point2f P(5, 1);
cout << "Point (2D)= " << P << endl << endl;

Point3f P3f(2, 6, 7);
cout << "Point (3D) = " << P3f << endl << endl;

vector<float> v;
v.push_back((float)CV_PI); v.push_back(2); v.push_back(3.01f);
cout << "Vector of floats via Mat = " << Mat(v) << endl << endl;

vector<Point2f> vPoints(20);
for (size_t i = 0; i < vPoints.size(); ++i)
	vPoints[i] = Point2f((float)(i * 5), (float)(i % 7));
cout << "A vector of 2D Points = " << vPoints << endl << endl;

输出结果如下:

相关推荐
西岸行者2 天前
学习笔记:SKILLS 能帮助更好的vibe coding
笔记·学习
悠哉悠哉愿意3 天前
【单片机学习笔记】串口、超声波、NE555的同时使用
笔记·单片机·学习
别催小唐敲代码3 天前
嵌入式学习路线
学习
毛小茛3 天前
计算机系统概论——校验码
学习
babe小鑫3 天前
大专经济信息管理专业学习数据分析的必要性
学习·数据挖掘·数据分析
winfreedoms3 天前
ROS2知识大白话
笔记·学习·ros2
在这habit之下3 天前
Linux Virtual Server(LVS)学习总结
linux·学习·lvs
我想我不够好。3 天前
2026.2.25监控学习
学习
im_AMBER3 天前
Leetcode 127 删除有序数组中的重复项 | 删除有序数组中的重复项 II
数据结构·学习·算法·leetcode
CodeJourney_J3 天前
从“Hello World“ 开始 C++
c语言·c++·学习