OpenCV学习笔记:Mat类

1,695 阅读6分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第4天,点击查看活动详情

本文是我之前在微信公众号上的一篇文章记录。原链接为:# OpenCV学习笔记:Mat类

什么是Mat

在OpenCV中,类Mat是用来存放图像基本类,它是用来保存图像矩阵类型的数据信息,包括向量、矩阵、灰度或彩色图像等数据。主要包含有两部分数据:一部分是矩阵头(matrix header),这部分的大小是固定的,包含矩阵的大小,存储的方式,矩阵存储的地址和引用次数等;另一个部分是一个指向矩阵包含像素值的指针(data)。在绝大多数情况下矩阵头大小远小于矩阵中数据量的大小,因此图像复制和传递过程中主要的开销是存放矩阵数据。为了解决这个问题,在OpenCV中复制和传递图像时,只是复制了矩阵头和指向存储数据的指针,因此在创建Mat类时可以先创建矩阵头后赋值数据。

Mat类的关键部分定义如下:

class CV_EXPORTS Mat
{
    public:
    // 一系列函数
    ...
    /* flag 参数中包含许多关于矩阵的信息,如:
    -Mat 的标识
    -数据是否连续
    -深度
    -通道数目
    */
    int flags;
    // 矩阵的维数,取值应该大于或等于 2
    int dims;
    // 矩阵的行数和列数,如果矩阵超过 2 维,这两个变量的值都为-1
    int rows, cols;
    // 指向数据的指针
    uchar* data;
    // 指向引用计数的指针
    // 如果数据是由用户分配的,则为 NULL
    int* refcount;
    // 其他成员变量和成员函数
    ...
}

下面我们通过读取一张本地图片返回一个Mat结构:

cv::Mat a; //仅创建了头部
a = cv::imread("1.jpg");
cv::Mat b = a;

上面这段代码首先创建了一个名为a的矩阵头,之后读入一张图像并将a中的矩阵指针指向该图像的像素数据,最后将a矩阵头中的内容复制到b矩阵头中。虽然a、b有各自的矩阵头,但是其矩阵指针指向的是同一个矩阵数据,通过任意一个矩阵头修改矩阵中的数据,另一个矩阵头指向的数据也会跟着发生改变。但是当删除a变量时,b变量并不会指向一个空数据,只有当两个变量都删除后,才会释放矩阵数据。因为在OpenCV的设计中,矩阵头中引用次数标记了引用某个矩阵数据的次数,只有当矩阵数据引用次数为0的时候才会真正释放矩阵数据。

采用引用次数来释放存储内容是C++中常见的方式,用这种方式可以避免仍有某个变量引用数据时将这个数据删除造成程序崩溃的问题,同时极大的缩减了程序运行时所占用的内存。

Mat存储类型

Mat的存储是逐行存储的,矩阵中的数据类型包括:Mat_<uchar>对应的是CV_8U,Mat_<uchar>对应的是CV_8U,Mat_<char>对应的是CV_8S,Mat_<int>对应的是CV_32S,Mat_<float>对应的是CV_32F,Mat_<double>对应的是CV_64F,对应的数据深度如下:

• CV_8U - 8-bit unsigned integers ( 0..255 )
• CV_8S - 8-bit signed integers ( -128..127 )
• CV_16U - 16-bit unsigned integers ( 0..65535 )
• CV_16S - 16-bit signed integers ( -32768..32767 )
• CV_32S - 32-bit signed integers ( -2147483648..2147483647 )
• CV_32F - 32-bit floating-point numbers ( -FLT_MAX..FLT_MAX, INF, NAN )
• CV_64F - 64-bit floating-point numbers ( -DBL_MAX..DBL_MAX, INF, NAN )

创建一个3*3的矩阵,矩阵数据类型是int类型:

cv::Mat matri_int = cv::Mat_<int>(3, 3);//创建一个3*3的矩阵,矩阵数据类型是int类型

通过编译,我们直接用cout<<matri_int<<endl;输出看到上面matri_int确实是一个int型3*3的随机矩阵:

image-20210603064801725

另外,我们都知道图像不单是有像素决定的,还需要根据存储类型以及图像通道数,例如灰色图是单通道的,彩色图一般是三通道的。有时候还增加了alpha(A)透明度变成四通道。

我们下面来看看三通道的图像内存分布:

cv::Mat a(2, 2, CV_8UC3);
cout<<"a="<<endl<<a<<endl;
cv::Mat b(2, 2, CV_8UC3, cv::Scalar(0,0,255));
cout<<"b="<<endl<<b<<endl;

输出:

image-20210608071058550

第一我们创建一个2*2的三通道图像a,我们没有设置初始化值,所以看到输出默认都是0。图像b我们设置了初始值,每个像素值是(0, 0, 255)。

创建Mat

我们上面已经介绍过几个Mat类创建的方法了,但是Mat类的初始化函数还有很多其他的:

// 默认形式
cv::Mat mat;
// 拷贝构造形式
cv::Mat mat(const cv::Mat& mat);
// 指定行列范围的拷贝构造
cv::Mat mat(const cv::Mat& mat, const cv::Range& rows, const cv::Range& cols);
// 指定ROI(感兴趣的区域-矩形区域)的拷贝构造
cv::Mat mat(const cv::Mat& mat, const cv::Rect& roi);
// 使用多维数组中指定范围内的数据的拷贝构造
cv::Mat mat(const cv::Mat& mat, const cv::Range* ranges);
// 指定类型和大小(行列)的二维数组(注意:是行在前,列在后)
cv::Mat mat(int rows, int cols, int type);
// 有初始化值的置顶类型和大小(行列)的二维数据
cv::Mat mat(int rows, int cols, int type, const Scalar& s);
// 使用预先存在数据定义的指定类型和大小(行列)的二维数组
cv::Mat mat(int rows, int cols, int type, void *data, size_t step = AUTO_STEP);
// 指定大小和类型的二维数组
cv::Mat mat(cv::Size sz, int type, const Scalar& s);
// 使用预先存在的数据定义的指定大小和类型的二维数组
cv::Mat mat(cv::Size sz, int type, void *data, size_t step = AUTO_STEP);
// 指定类型的多维数据
cv::Mat mat(int ndims, const int *sizes, int type);
// 使用预先存在的数据定义的指定类型的多维数组
cv::Mat mat(int ndims, const int* sizes, int type, void* data, size_t step = AUTO_STEP);
// 使用cv::Vec定义相同类型、
cv::Mat mat(const cv::Vec<T, n>& vec, bool = copyData = true);
// 使用cv::Matx定义相同类型、大小为mxn的二维数组
cv::Mat mat(const cv::Matx<T, m, n>& vec, bool copyData = true);
// 使用STL vector定义相同类型的一维数组
cv::Mat mat(const std::vector<T>& vec, bool copyData = true);
// 使用zeros()函数定义指定大小和类型的cv::Mat(全为0)
cv::Mat mat = cv::Mat::zeros(int rows, int cols, int type);
// 使用ones()函数定义指定大小和类型的cv::Mat(全为0)
cv::Mat mat = cv::Mat::ones(int rows, int cols, int type);
// 使用eye()函数定义指定大小和类型的cv::Mat(恒等矩阵)
cv::Mat mat = cv::Mat::eye(int rows, int cols, int type);

特殊矩阵创建

用Mat类中快速赋值的方法创建矩阵, 可以生成单位矩阵,对角矩阵和全0矩阵:

cv::Mat mat_zeros = cv::Mat::zeros(3,4,CV_8UC1);
cout<<"mat_zeros="<<endl<<mat_zeros<<endl;
cv::Mat mat_ones = cv::Mat::ones(3,4,CV_8UC1);
cout<<"mat_ones="<<endl<<mat_ones<<endl;
//unit matrix
cv::Mat mat_eye = cv::Mat::eye(3,4,CV_8UC1);
cout<<"mat_eye="<<endl<<mat_eye<<endl;

输出结果如下:

image-20210612193239793

区域拷贝创建

cv::Mat mat_src(4, 5, CV_8UC3, cv::Scalar(0,0,255));
cout<<"mat_src="<<endl<<mat_src<<endl;
cv::Mat mat_range = cv::Mat(mat_src, cv::Range(1, 3), cv::Range(1, 4));
cout<<"mat_range="<<endl<<mat_range<<endl;

输出:

image-20210612213611908

上面我们先创建了一个4*5的三通道图像,初始值每个像素都是(0,0,255),然后我们使用range类对选定区域拷贝,也就是拷贝第一和第二行和第一第二第三列.

特定大小创建

cv::Mat mat_size(cv::Size(4,5), CV_8UC3, cv::Scalar(0,0,255));
cout<<"mat_size="<<endl<<mat_size<<endl;  

输出:

image-20210612224353693

特别要注意cv::Size(4,5)表示的是5*4,就是五行四列的图像. cv::Size的原型是:输入参数是图像的宽度和高度.

Size_ (_Tp _width, _Tp _height)

好了,创建Mat类就讲这么多了,其他的对着上面的列出来的创建也比较容易的.

复制Mat

复制Mat类我们上面有提到过,Mat类的复制可以分为两种:

  • 一种是浅拷贝, 不复制数据只创建矩阵头,数据共享, 更改其中的任意一个都会对另外的产生同样的作用

  • 另一种是深拷贝,是一个完全独立的副本.

一般的直接等于号赋值都是属于浅拷贝,只有使用了clone函数才是深拷贝,如下:

//注意:浅拷贝
Mat a;Mat b = a;Mat c(a); 
//注意:深拷贝
Mat a;Mat b = a.clone();Mat c;a.copyTo(c);

Clone图像没什么好介绍的了,就是一个单纯的复制。我们来说说copyTo,看如下代码:

std::string img_path = "/home/codemaxi/Downloads/11.png";
cv::Mat srcImage = cv::imread(img_path, cv::IMREAD_COLOR);
std::string img_path2 = "/home/codemaxi/Downloads/mn.jpeg";
cv::Mat logoImage = cv::imread(img_path2, cv::IMREAD_COLOR);
cv::Mat resize_img;
cv::resize(logoImage, resize_img, cv::Size(90, 160));
cv::Mat dstImage = srcImage(cv::Rect(10, 10, resize_img.cols, resize_img.rows));
//将logo图片拷贝到img的ROI上(注意copyTo函数要求两图像大小和类型都相同,否则无效)
resize_img.copyTo(dstImage);
cv::imshow("srcImage", srcImage);
cv::waitKey(0);

上面的代码是在一张大图像上加一个小图像logo,首先准备两张图像,11.png是车子,mn.jpeg是妹子,妹子图像太大了,需要用resize缩小一下,先在原图上取一个ROI用于放logo图(注意ROI区域必须要跟logo图一样大小88平平米),然后调用copyto将logo图像放到ROI区域上,然后输出结果如下:

image-20210614074209770

合并Mat

合并Mat,opencv提供有专门的接口:

 vconcat(B,C,A); // 等同于A=[B ;C]-纵向拼接
 hconcat(B,C,A); // 等同于A=[B  C]-横向拼接

专门的接口我们先不讲,我们讲直接用copyto函数实现图像拼接,我们来看下面的代码:

//读取两个图像
std::string img_path = "/home/codemaxi/Downloads/11.png";
cv::Mat srcImage = cv::imread(img_path, cv::IMREAD_COLOR);
std::string img_path2 = "/home/codemaxi/Downloads/mn.jpeg";
cv::Mat logoImage = cv::imread(img_path2, cv::IMREAD_COLOR);
//将图像2按照图像1的高度等比例缩小
int h = srcImage.rows;
int w = logoImage.cols * srcImage.rows / logoImage.rows;
cv::Mat resize_img;
cv::resize(logoImage, resize_img, cv::Size(w, h));
//创建一个能容下图像1和图像3的空图像
cv::Mat dstImage =
    cv::Mat(cv::Size(srcImage.cols + resize_img.cols, srcImage.rows), CV_8UC3);
//获取图像1拷贝区域,并进行拷贝
cv::Mat dst1 = dstImage(cv::Rect(0, 0, srcImage.cols, srcImage.rows));
srcImage.copyTo(dst1);
//获取图像2拷贝区域,并进行拷贝
cv::Mat dst2 =
    dstImage(cv::Rect(srcImage.cols, 0, resize_img.cols, resize_img.rows));
resize_img.copyTo(dst2);
//最后将拼接好的图像显示
cv::imshow("dstImage", dstImage);
cv::waitKey(0);

上面的代码已经注释很明白了,不做多解释了,直接看输出图像:

image-20210614130211169

Mat元素访问

Mat元素访问这里介绍三种,如下:

使用at访问

//三通道
cv::Mat img_c3(4, 5, CV_8UC3, cv::Scalar(0, 0, 0));
for (int h = 0; h < img_c3.rows; h++) {
  for (int w = 0; w < img_c3.cols; w++) {
    img_c3.at<cv::Vec3b>(h, w)[0] = 1;
    img_c3.at<cv::Vec3b>(h, w)[1] = 2;
    img_c3.at<cv::Vec3b>(h, w)[2] = 3;
  }
}
cout << "img_c3=" << endl << img_c3 << endl;
//单通道
cv::Mat img_c1(4, 5, CV_8UC1, cv::Scalar(0));
for (int h = 0; h < img_c1.rows; h++) {
  for (int w = 0; w < img_c1.cols; w++) {
    img_c1.at<uchar>(h, w) = 1;
  }
}
cout << "img_c1=" << endl << img_c1 << endl;

输出结果:

image-20210614160941673

使用迭代器访问:

//三通道
cv::Mat img_c3(4, 5, CV_8UC3, cv::Scalar(0, 0, 0));
// it is Mat_<cv::Vec3b>::iterator
for (auto it = img_c3.begin<cv::Vec3b>(); it != img_c3.end<cv::Vec3b>(); it++) {
  (*it)[0] = 1;
  (*it)[1] = 2;
  (*it)[2] = 3;
}
cout << "img_c3=" << endl << img_c3 << endl;
//单通道
cv::Mat img_c1(4, 5, CV_8UC1, cv::Scalar(0));
// it is Mat_<uchar>::iterator
for (auto it = img_c1.begin<uchar>(); it != img_c1.end<uchar>(); it++) {
  (*it) = 1;
}
cout << "img_c1=" << endl << img_c1 << endl;

这个输出跟上面一样的。

使用ptr访问

下面使用ptr访问可以做到多种通道的代码一致:

//三通道
cv::Mat img_c3(4, 5, CV_8UC3, cv::Scalar(0, 0, 0));
int nRows = img_c3.rows;
int nCols = img_c3.cols * img_c3.channels();
for (int h = 0; h < nRows; h++) {
  uchar *ptr = img_c3.ptr<uchar>(h);
  for (int w = 0; w < nCols; /*W++*/) {
    ptr[w++] = 1;
    ptr[w++] = 2;
    ptr[w++] = 3;
  }
}
cout << "img_c3=" << endl << img_c3 << endl;
//单通道
cv::Mat img_c1(4, 5, CV_8UC1, cv::Scalar(0));
int nRows2 = img_c1.rows;
int nCols2 = img_c1.cols * img_c1.channels();
for (int h = 0; h < nRows2; h++) {
  uchar *ptr = img_c1.ptr<uchar>(h);
  for (int w = 0; w < nCols2; /*w++*/) {
    ptr[w++] = 1;
  }
}
cout << "img_c1=" << endl << img_c1 << endl;

输出如下:

image-20210614200552755

Mat成员函数

大概的成员和函数如下:

//
cv::Mat img_c3(4, 5, CV_8UC3, cv::Scalar(0, 0, 0));
//行数
cout << "img_c3-"
     << "rows         : " << img_c3.rows << endl;
//列数
cout << "img_c3-"
     << "cols         : " << img_c3.cols << endl;
//数组的维数
cout << "img_c3-"
     << "dims         : " << img_c3.dims << endl;
cout << "img_c3-"
     << "type         : " << img_c3.type() << endl;
//返回矩阵通道的数目。
cout << "img_c3-"
     << "channels     : " << img_c3.channels() << endl;
//返回数组元素的总数。
cout << "img_c3-"
     << "total        : " << img_c3.total() << endl;
//返回一个矩阵大小。
cout << "img_c3-"
     << "size         : " << img_c3.size() << endl;
//返回矩阵元素大小(以字节为单位)。如果矩阵类型是CV_16SC3,该方法返回3*sizeof(short)或6
cout << "img_c3-"
     << "elemSize     : " << img_c3.elemSize() << endl;
//以字节为单位返回每个矩阵元素通道的大小。如果矩阵类型是
//CV_16SC3,该方法返回sizeof(short)或2。
cout << "img_c3-"
     << "elemSize1    : " << img_c3.elemSize1() << endl;
//返回一个矩阵元素的深度。
cout << "img_c3-"
     << "depth        : " << img_c3.depth() << endl;
//如果数组有没有 elemens,则返回 true。
cout << "img_c3-"
     << "empty        : " << img_c3.empty() << endl;
//返回矩阵是否连续。
cout << "img_c3-"
     << "isContinuous : " << img_c3.isContinuous() << endl;

输出结果:

image-20210614212249088

这里说一下isContinuous, 它是确定矩阵元素在内存中是否为连续存储。显然,只有一个元素的矩阵、只有一行的矩阵的所有元素在内存中都是连续存储的。通过create()创建的矩阵的元素也总是连续的。但是由矩阵部分元素得到的子矩阵,如某一列col()、diag()等肯定是不连续的。通常矩阵为连续时,可以将这个矩阵的数据存储区域看成一个具有大量元素的行向量(访问时可以变得简单)。

depth返回的是矩阵的数据类型,CV中有专门的宏定义,如下:

#define CV_8U 0
// uchar
#define CV_8S 1
// char
#define CV_16U 2
// ushort
#define CV_16S 3
// short
#define CV_32S 4
// int
#define CV_32F 5
// float
#define CV_64F 6
// double
#define CV_USRTYPE1 7

还有type返回的是矩阵的类型,上面代码的类型是CV_8UC3,他的值就是16。下面列出几种常用类型的值:

image-20210614213347691

好了,关于Mat类的介绍就到这里了,也是刚开始学习OpenCV,可能有些理解不正确的,望指教。