OpenCV入门(3):初识之基础图像容器Mat详解
cv::Mat 类是用于保存图像(以及其他矩阵数据)的数据结构,该数据结构是所有 OpenCV 类和函数的核心,这是 OpenCV 库的一个关键元素,用于处理图像和矩阵(从计算和数学的角度来看,图像本质上是一个矩阵),同时 cv::Mat 数据结构结合了优雅的内存管理机制,因此,使用起来十分高效。此数据结构在应用程序开发中广泛使用,因此再进一步学习 OpenCV 前我们必须熟悉 cv::Mat 数据结构。
一、Mat 类的基本概念
- 核心作用:存储多维、多通道的数值型数据(例如图像、矩阵、点云等)。
- 核心优势:
- 内存自动管理:通过引用计数(Reference Counting)实现,避免内存泄漏。
- 高效数据共享:浅拷贝(Shallow Copy)默认共享数据,深拷贝(Deep Copy)需显式调用。
- 灵活的数据类型:支持
uchar,float,double等数据类型和多种通道数(如 RGB 图像为 3 通道)。
二、Mat 类的数据结构
下面给出 OpenCV 中 Mat 类的一个粗略定义如下:
class CV_EXPORTS Mat
{
public:
// ... a lot of methods ...
int type() const;
int depth() const;
int channels() const;
...
/*! includes several bit-fields:
- the magic signature
- continuity flag
- depth
- number of channels
*/
int flags;
//! the array dimensionality, >= 2
int dims;
//! the number of rows and columns or (-1, -1) when the array has more than 2 dimensions
int rows, cols;
//! pointer to the data
uchar* data;
//! pointer to the reference counter;
// when array points to user-allocated data, the pointer is NULL
int* refcount;
// other members
...
};
关键属性
| 属性 | 描述 |
|---|---|
dims |
矩阵的维度(如 2D 图像为 2 维) |
rows, cols |
2D 矩阵的行数和列数(仅当 dims=2 时有效) |
data |
指向实际数据的指针(uchar* 类型) |
type() |
数据类型和通道数(如 CV_8UC3 表示 8 位无符号 3 通道) |
depth() |
位数(8 位,32位等等) |
channels() |
通道数(如灰度图=1,RGB=3) |
数据类型与通道表示
OpenCV 使用宏 CV_<位数><符号><类型>C<通道数> 定义数据类型,例如:
CV_8UC1: 8 位无符号单通道(灰度图)CV_32FC3: 32 位浮点型 3 通道(RGB 浮点数据)
三、构造函数
(1)默认构造函数
// 默认构造(空矩阵)
cv::Mat mat;
默认构造函数:生成一个矩阵并由 OpenCV 提供的函数(一般是 Mat::create() 和 cv::imread())来分配储存空间。
-
Mat 类可以分为两个部分:矩阵头和指向像素数据的矩阵指针。
-
矩阵头:包括数字图像的矩阵尺寸、存储方法、存储地址和引用次数等,矩阵头的大小是一个常数,不会随着图像的大小而改变。
-
但是保存图像像素数据的矩阵则会随着图像的大小而改变,通常数据量会很大,比矩阵头大几个数量级。
-
这样,在图像复制和传递过程中,主要的开销是由存放图像像素的矩阵而引起的。因此,OpenCV使用了引用次数,当进行图像复制和传递时,不再复制整个Mat数据,而只是复制矩阵头和指向像素矩阵的指针,例如:
cv::Mat a ; // 默认构造函数,创建矩阵头
a = cv::imread("test.jpg"); // 读入图像,矩阵指针指向该像素数据
cv::Mat b = a ; // 复制
上面的 a,b 有各自的矩阵头,但是其矩阵指针指向同一个矩阵(共享内存),也就是其中任何一个改变了矩阵数据都会影响另外一个。那么,多个 Mat 共用一个矩阵数据,最后谁来释放矩阵数据呢?
这就是引用计数的作用,当 Mat 对象每被复制一次时,就会将引用计数加 1,而每销毁一个 Mat 对象(共用同一个矩阵数据)时引用计数会被减 1,当引用计数为 0 时,矩阵数据会被清理。
(2)常用构造函数——1
cv::Mat::Mat(int rows,int cols,int type)
重载的构造函数,这也是常用构造函数之一,在创建对象同时,提供矩阵的大小(rows,行数;cols ,列数),以及存储类型(type),该类型表示矩阵中每一个元素在计算机内存的存储类型,如CV_8UC3,具体含义为 “3 通道 8 位无符号数”。
示例:
Mat src(10,10,CV_32FC3); // 表示src是一个10*10的矩阵,且矩阵元素以32位float型存储
类似,OpenCV 还提供了一种 Size() 数据结构来构造 Mat 对象。
(3)常用构造函数——2
cv::Mat::Mat(Size size,int type )
Size 类等效于一个成对数据,size::Size(cols,rows),特别注意 cols 和 rows 的位置
示例:
Mat src1(3, 4, CV_32FC3);
Mat src2(Size(3, 4), CV_32FC3);
cout << "src1.rows=" << src1.rows << " src1.cols=" << src1.cols <<endl;
cout << "src2.rows=" << src2.rows << " src2.cols=" << src2.cols << endl;
cout << "src1.size="<<src1.size() << endl <<"src2.size=" << src2.size() <<endl;
不得不说,这个 Size 类的数据结构有点 “反人类”,但这样做的好处是方便了计算机内部的运算(比如 OpenCV 很多函数计算 Size 相关的数据也是按这个顺序来的,具体为什么这样,可能是行业标准);
还有,我们平时所说分辨率,也是 Size 的类型,比如屏幕分别率 1440*900,其中 cols=1440,rows=900;
(4)常用构造函数——3
cv::Mat::Mat(int ndims,const int * sizes,int type,const Scalar& s)
该构造函数与使用了Scalar参数,作用是能够通过Scalar数据类来初始化元素值,例如,我们要生成一张白色背景的图片:
Mat src1(300, 400, CV_8UC3, Scalar(0,0,255));
imshow("test", src1);
其中,(0,0,255)对应以 8 位无符号数存储,BGR 色域的红色值。
(5)常用构造函数——4
cv::Mat::Mat(const Mat & m)
引用 m 矩阵,注意,这里是引用值。
四、成员函数
4.1 at函数
at 函数的功能是访问矩阵元素,根据不同的使用场景,有多个重载函数可供选择。 如,访问一个二维的矩阵,可用 at 函数原型为:
_Tp& cv::Mat::at(int i0,int i1)
示例:
Mat src = imread("test.jpg");
int elem = src.at<int>(0,0);
访问 test.jpg 图像的(0 , 0)元素
4.2 channels函数
int cv::Mat::channels() const
返回图像的通道数
4.3 clone函数
Mat cv::Mat::clone() const // 深拷贝(独立内存)
矩阵复制
4.4 convertTo函数
void cv::Mat::convertTo(OutputArray m,int rtype,double alpha = 1,double beta = 0) const
转换矩阵存储类型,具体计算公式如下:
m(x,y)=saturate_cast<rType>(α(∗this)(x,y)+β)
m 是输入矩阵,rtype 是目标类型,alpha 是放缩系数,beta 是增减标量
4.5 copyTo函数
void cv::Mat::copyTo(OutputArray m) const
从 m 矩阵复制 data 数据单元,与 clone 函数的作用类似
4.6 create函数
void cv::Mat::create(int rows,int cols,int type)
分配矩阵的存储单元,一般和默认构造函数配合使用
4.7 depth函数
int cv::Mat::depth() const
返回图像深度,即矩阵元素的存储方式
4.8 diag函数
Mat cv::Mat::diag(int d = 0) const
提取矩阵的对角元素
4.9 mul函数
MatExpr cv::Mat::mul(InputArray m,double scale = 1) const
矩阵的乘法
4.10 inv函数
MatExpr cv::Mat::inv(int method = DECOMP_LU) const
求逆矩阵
4.11 t函数
MatExpr cv::Mat::t() const
求转置矩阵
4.12 total函数
size_t cv::Mat::total() const
返回矩阵的元素总个数,如 30*40 的图像,存在 1200 个像素点
4.13 pop_back函数
object.pop_back(); // object是一个矩阵,该函数功能是弹出最后一行元素
4.14 release函数
在必要的情况下,递减引用计数并释放该矩阵。
void Mat::release()
该方法递减与矩阵的数据关联的引用计数。当引用计数减为 0 时,矩阵的数据将被释放,数据和引用计数器指针设置为 NULL。如果矩阵头指向外部数据集 (见 Mat::Mat()), 引用计数为 NULL,并且该方法在这种情况下无效。
可以手动调用此方法强制矩阵数据释放。但由于这种方法在析构函数中是自动调用的,或以更改数据指针的其他方法,因此通常不需要调用这个函数。在支持它的平台上,引用计数器递减并检查是否为 0 是一个原子操作。因此,在不同的线程异步调用相同的矩阵是安全的操作。
4.15 Mat::ones()函数
Mat m = Mat::ones(2, 2, CV_8UC3); // 相当于:Mat m = Mat(2, 2, CV_8UC3, 1);
相当于每个像素的第一个通道为 1,其余两个通道为 0;
4.16 Mat::zeros()函数
Mat m = Mat::zeros(2, 2, CV_8UC3);
相当于创建一张黑色的图,每个像素的每个通道都为 0,Scalar(0,0,0);
五、使用 Mat 创建与初始化图像
可以通过 Mat 类创建和初始化图像。
5.1 创建图像
创建图像:可以创建一个指定大小和类型的图像。
cv::Mat newImage(480, 640, CV_8UC3, cv::Scalar(0, 0, 255)); // 创建一个640x480的红色图像
示例:
#include <QApplication>
#include <QDebug>
// 添加相关头文件和包
#include <opencv2/highgui/highgui.hpp>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
// 创建一个640x480的红色图像
cv::Mat newImage(480, 640, CV_8UC3, cv::Scalar(0, 0, 255));
// 显示图像
cv::imshow("newImage", newImage);
return a.exec();
}
显示图像如下所示:

5.2 初始化图像
初始化图像:可以使用 setTo 方法初始化图像。
image.setTo(cv::Scalar(0, 255, 255)); // 将图像初始化为黄色
示例:
#include <QApplication>
#include <QDebug>
// 添加相关头文件和包
#include <opencv2/highgui/highgui.hpp>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
// 创建一个640x480的红色图像
cv::Mat newImage(480, 640, CV_8UC3, cv::Scalar(0, 0, 255));
// 将图像初始化为黄色
newImage.setTo(cv::Scalar(0, 255, 255));
// 显示图像
cv::imshow("newImage", newImage);
return a.exec();
}
显示图像如下所示:

六、数据访问与修改
元素访问(2D 矩阵)
// 方法 1: ptr<T>(行号) + 列号(高效连续访问)
for (int r = 0; r < mat.rows; r++) {
uchar* row_ptr = mat.ptr<uchar>(r);
for (int c = 0; c < mat.cols; c++) {
row_ptr[c] = 255; // 设置为 255(例如灰度图)
}
}
// 方法 2: at<T>(行, 列)(适合随机访问)
cv::Mat float_mat(100, 100, CV_32FC3);
float_mat.at<cv::Vec3f>(10, 20) = cv::Vec3f(1.0, 2.0, 3.0); // 3 通道浮点数据
多维矩阵访问
// 3D 矩阵(尺寸为 dim1×dim2×dim3)
int dim1 = 3, dim2 = 4, dim3 = 5;
cv::Mat mat(3, sizes, CV_8UC1); // sizes = {3,4,5}
uchar* data_ptr = mat.data;
for (int i = 0; i < dim1; i++) {
for (int j = 0; j < dim2; j++) {
for (int k = 0; k < dim3; k++) {
// 计算偏移量(需根据 step 调整)
long offset = i * mat.step[0] + j * mat.step[1] + k;
data_ptr[offset] = 100;
}
}
}
七、内存管理与拷贝机制
引用计数(浅拷贝)
- 默认行为:多个
Mat对象共享同一数据块,通过引用计数跟踪使用者数量。 - 写时复制(Copy-on-Write):当修改数据时,若引用计数 >1,则自动深拷贝数据。
显式深拷贝
cv::Mat mat1 = cv::Mat::ones(100, 100, CV_8UC1);
cv::Mat mat2 = mat1.clone(); // 深拷贝(独立内存)
cv::Mat mat3;
mat1.copyTo(mat3); // 另一种深拷贝方式
八、实战技巧与注意事项
- 避免直接操作 data 指针:除非明确数据布局(如
isContinuous()为 true)。 - 多通道数据访问:使用 cv::Vec3b(3 通道 uchar)或 cv::Vec3f(3 通道 float)。
- 性能优化:尽量使用 OpenCV 内置函数(如
cv::add()比遍历快得多)。 - 错误排查:检查
mat.empty()避免操作空矩阵。
九、代码示例:遍历 RGB 图像
Widget::Widget(QWidget *parent)
: QWidget(parent)
{
cv::Mat image = cv::imread("E:\\Learn\\Qt\\OpenCV_Qt\\OpenCV_Test\\demo1.jpg"); // 读取为 CV_8UC3 类型
// 遍历图像并反转
for (int r = 0; r < image.rows; r++) {
cv::Vec3b* row_ptr = image.ptr<cv::Vec3b>(r);
for (int c = 0; c < image.cols; c++) {
cv::Vec3b& pixel = row_ptr[c];
pixel[0] = 255 - pixel[0]; // 反转 B 通道
pixel[1] = 255 - pixel[1]; // 反转 G 通道
pixel[2] = 255 - pixel[2]; // 反转 R 通道
}
}
// 显示
cv::imshow("readImage", image);
}
运行后如下所示:

十、综合示例
可以用下面的程序来测试 cv::Mat 数据机构的不同属性:
#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(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("./dog.jpg");
// 所有这些图像都指向同一个数据块
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); // 等待按键
return 0;
}
运行这个程序,如下图所示:

按下任意按键,会额外显示一个 200*200 的新图像界面,如下图所示:

再按下任意按键,上面的新图像界面会变成 "红色图像",如下图所示:

再按下任意按键,你将得到下面 5 张图像:

程序讲解
(1)新创建的 cv::Mat 对象默认大小为 0,但也可以指定一个初始大小,例如:
// 创建一个 240 行×320 列的新图像
cv::Mat image1(240,320,CV_8U,100);
我们需要指定每个矩阵元素的类型,这里用CV_8U 表示每个像素对应 1字节(灰度图像),用字母 U 表示无符号;你也可用字母 S 表示有符号。对于彩色图像,你应该用三通道类型(CV_8UC3)。
(2)图像(或矩阵)的每个元素都可以包含多个值(例如彩色图像中的三个通道),因此 OpenCV 引入了一个简单的数据结构 cv::Scalar ,用于在调用函数时传递像素值。该结构通常包含一个或三个值。如果要创建一个彩色图像并用红色像素初始化,可用如下代码:
// 创建一个红色的图像
// 通道次序为BGR
cv::Mat image2(240, 320, CV_8UC3, cv::Scalar(0, 0, 255));
与之类似,初始化灰度图像可这样使用这个数据结构:cv::Scalar(100) 。
(3)图像的尺寸信息通常也需要传递给调用函数。我们可以用属性 cols 和 rows 来获得 cv::Mat 实例的大小。 cv::Size 结构包含了矩阵高度和宽度,同样可以提供图像的尺寸信息。另外,可以用 size() 方法得到当前矩阵的大小。当需要指明矩阵的大小时,很多方法都使用这种格式。例如,可以这样创建一幅图像:
// 创建一个未初始化的彩色图像
cv::Mat image2(cv::Size(320,240),CV_8UC3);
(4)可以随时用 create 方法分配或重新分配图像的数据块。如果图像已被分配,其原来的内容会先被释放。出于对性能的考虑,如果新的尺寸和类型与原来的相同,就不会重新分配内存
// 重新分配一个新图像
//(仅在大小或类型不同时)
image1.create(200,200,CV_8U);
(5)一旦没有了指向 cv::Mat 对象的引用,分配的内存就会被自动释放。这一点非常方便,因为它避免了 C++ 动态内存分配中经常发生的内存泄漏问题。这是 OpenCV(从第 2版开始引入)中的一个关键机制,它的实现方法是:通过 cv::Mat 实现计数引用和浅复制。因此,当在两幅图像之间赋值时,图像数据(即像素)并不会被复制,此时两幅图像都指向同一个内存块。这同样适用于图像间的值传递或值返回。由于维护了一个引用计数器,因此:只有当图像的所有引用都将释放或赋值给另一幅图像时,内存才会被释放:
// 所有图像都指向同一个数据块
cv::Mat image4(image3);
image1= image3;
(6)对上面图像中的任何一个进行转换都会影响到其他图像。如果要对图像内容做一个深复制,你可以使用 copyTo 方法,目标图像将会调用 create 方法。另一个生成图像副本的方法是 clone,即创建一个完全相同的新图像:
// 这些图像是原始图像的新副本
image3.copyTo(image2);
cv::Mat image5= image3.clone();
(7)在例子中,我们对 image3 做了修改。其他图像也包含了这幅图像,有的图像共用了同一个图像数据,有的图像则有图像数据的独立副本。查看显示的图像,找出哪些图像因修改 image3 而产生了变化。如果你需要把一幅图像复制到另一幅图像中,且两者的数据类型不一定相同,那就要使用 convertTo 方法了:
// 转换成浮点型图像[0,1]
image1.convertTo(image2,CV_32F,1/255.0,0.0);
(8)本例中的原始图像被复制进了一幅浮点型图像。这一方法包含两个可选参数:缩放比例和偏移量。需要注意的是,这两幅图像的通道数量必须相同。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 的内存块不会被释放。
参考:
【OpenCV】Mat类详解_cvarrtomat-CSDN博客
Opencv学习笔记(1)cv::Mat_cvmat有size吗-CSDN博客

浙公网安备 33010602011771号