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

显示图像如下所示:

Blog_OpenCV_Learnl_11.png


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

显示图像如下所示:

Blog_OpenCV_Learnl_12.png


六、数据访问与修改

元素访问(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);
}

运行后如下所示:

Blog_OpenCV_Learnl_1.png

十、综合示例

可以用下面的程序来测试 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;
}

运行这个程序,如下图所示:

Blog_OpenCV_Learnl_5.png


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

Blog_OpenCV_Learnl_3.png


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

Blog_OpenCV_Learnl_4.png

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

Blog_OpenCV_Learnl_2.png


程序讲解

(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)图像的尺寸信息通常也需要传递给调用函数。我们可以用属性 colsrows 来获得 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 Mat类详解 - 一杯清酒邀明月 - 博客园

Opencv学习笔记(1)cv::Mat_cvmat有size吗-CSDN博客


posted @ 2025-07-21 10:42  fengMisaka  阅读(234)  评论(0)    收藏  举报