Mat数据类型

Mat是OpenCV特有的结构,其可以存储图像和矩阵,其中Mat支持不同的数据类型,表示不同的数据长度和通道数量。命名规则是CV_nUCm,其中n是数据长度m是通道数量,从数据长度来看一般有CV_8UCV_8SCV_16UCV_16SCV_32SCV_32FCV_64F七种,U代表Unsigned、S代表Signed,数字取8、16、32代表位数,决定了整型数据的表示范围,此外还有32F、64F代表32位、64位精度浮点数;从通道数来看分别有C1——C4,表示了1——4个通道,二者结合就是基本的数据类型。

所谓不同通道,实际上是列数增加,例如RGB图像就是三个通道,即将三个数字看成是一个统一数据。

1
2
3
4
5
6
7
8
9
10
11
Mat m1(4, 3, CV_8SC1,256);cout<<m1; //单通道
// [127, 127, 127;
// 127, 127, 127;
// 127, 127, 127;
// 127, 127, 127]

Mat m2(4, 3, CV_8SC2,256);cout<<m2; //双通道,第一个数据是[127,0];
// [127, 0, 127, 0, 127, 0;
// 127, 0, 127, 0, 127, 0;
// 127, 0, 127, 0, 127, 0;
// 127, 0, 127, 0, 127, 0]

从例子也看见,当数值超出定义范围时,数值自动向下取整;而多通道初始化值,默认只对数据的第一个通道初始化,如果需要初始化多个通道,应该使用Scalar函数:

1
2
3
4
5
Mat m3(4, 3, CV_8SC3,Scalar(123,124,128)); cout<<m3; //三通道
// [123, 124, 127, 123, 124, 127, 123, 124, 127;
// 123, 124, 127, 123, 124, 127, 123, 124, 127;
// 123, 124, 127, 123, 124, 127, 123, 124, 127;
// 123, 124, 127, 123, 124, 127, 123, 124, 127]

Mat性质相关接口

相关摘录如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Mat m(4, 3, CV_8UC2,Scalar(123,124,128));
cout<<m.type()<<endl; //8
cout<<m.depth()<<endl; //0

cout<<m.dims<<endl; //2维,因为只有行列
cout<<m.channels()<<endl; //2通道

cout<<m.size()<<endl; //[3 x 4]
cout<<m.size; //[4 x 3]
cout<<m.size[0]; //4
cout<<m.size[1]; //3

cout<<m.rows<<endl; //4行
cout<<m.cols<<endl; //3列

depth()、type()

  • depth和typedepth只和数据和数据长度有关,和通道数无关,对应关系是:enum{CV_8U=0,CV_8S=1,CV_16U=2,CV_16S=3,CV_32S=4,CV_32F=5,CV_64F=6} ,而type输出数字和具体类型映射一一对应,如下表:
类型 C1 C2 C3 C4
CV_8U 0 8 16 24
CV_8S 1 9 17 25
CV_16U 2 10 18 26
CV_16S 3 11 19 27
CV_32S 4 12 20 28
CV_32F 5 13 21 29
CV_64F 6 14 22 30

dims和channels()

  • dims和channels:注意不要混淆,dims指的是维度,上例四行三列,无高,因此dims是二维;而通道数是C2,因此channel=2;

size()/size[]、rows、cols

  • 注意大小、行列无括号

  • 这里不要混淆几个size接口,一个是重载括号的size(),以及size和size[]索引:

  1. size()输出尺寸是矩阵的宽度和高度,即列数×行数,列数在前

  2. size和size[]可输出各维度的大小高维度也可适用

  • 维度是三维及以上rows、cols均返回-1,size()仅返回两个维度,均失效

elemSize()、elemSize1()

  • elemSize()返回矩阵数据类型的字节数,8U、8S为1、16U、16S为2、32S、32F为4,64F为8,此外需要考虑通道数,例如8UC4就是4了。

  • elemSize1()单通道的字节数elemSize1()不考虑通道数,8UC4仍然是1;

step、step1()

  • step每行占的字节数,对于高维度的偏移量索引非常重要,详见后文指针访问部分。

  • step1()每行的元素数目

1
2
3
4
5
6
7
Mat m(4, 4, CV_64FC3,Scalar(123,124,128));

cout<<m.step<<endl;
//96:每个数据占8字节,其中三通道、每行4列,因此8*3*4=96

cout<<m.step1()<<endl;
//12:每行元素的数目:列数*通道数 = 4*3 = 12;

Mat像素赋值和Vec

使用operator()时能够自动初始化为0,如果使用create构造,则需要使用setTo统一赋值;

1
2
3
Mat m;
m.create(3,3,CV_64FC1);
m.setTo(255);

矩阵初始化需要使用Mat_模板如下:

1
2
3
4
Mat_<double>rawMat = (Mat_<double>(3,3)<<
135.752395091636, 51.0103552955011, 102.6712174223711,
208.4825800582035, 111.9060426536122, 63.69016545467102,
197.9148945049116, 195.0959945478937, 78.79538704479675);

对于单通道数据而言,使用at(row,col)可以定位到某个元素,例如:

1
2
Mat m(4, 4, CV_64FC1,Scalar(123));
m.at<double>(0,0) = 255; //修改第一位数据,注意一定要对应数据类型

对于多通道数据,定位了行列,得到的是一个多通道类型,因此需要了解Vec类型:

1
2
3
4
5
6
Vec3b: 三通道、8位无符号整型,0——255
Vec3w: 三通道、16位无符号整型,0——65535
Vec3s: 三通道、16位Short类型;
Vec3i: 三通道、32int类型;
Vec3f: 三通道、32float类型;
Vec3d: 三通道、64double类型;
除了3通道,Vec还支持2、4、6、8等通道类型;因此通过这种形式可以单独修改某行、某列、某个通道的像素信息:
1
2
Mat m(4,4,CV_8UC3,Scalar(255,255,0));
m.at<Vec3b>(0,0)[2] = 255; //0行0列的第二通道(G通道)修改成255

细节问题

  1. Scalar最大支持四通道赋值,超过就需要使用遍历的方法了;

  2. Mat的默认维度是2维,尽管创建一维矩阵,仍认为是n*1的;

Mat的内存分配策略

Mat的管理分成两大块,一个指向矩阵头信息,包含矩阵的存储地址、存储方式、矩阵大小等,另一个就是矩阵本身,存储矩阵数据;几个重要的问题如下:

  1. 拷贝策略构造/赋值拷贝不会复制矩阵仅复制其矩阵头并增加引用数(浅拷贝),而深拷贝需要clonecopyTo支持,会真正开辟空间存储矩阵数据。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    Mat m(4, 4, CV_64FC3,Scalar(123,124,128));
    Mat m1(m); //拷贝
    Mat m2 = m1; //赋值

    Mat m3 = m1.clone(); //深拷贝
    Mat m4;
    m1.copyTo(m4); //深拷贝

    cout << (void*)m.data<<endl; //转成void*才能被打印;
    cout << (void*)m1.data<<endl;
    cout << (void*)m2.data<<endl; //前三者输出一致
    cout << (void*)m3.data<<endl; //后二者均不同
    cout << (void*)m4.data<<endl;

    深拷贝除了开辟独立地址空间另一个作用就是将非连续Mat转换成连续分别的Mat

  2. 分配策略:注意Mat的分配不一定是完全连续的,体现在存在内存对齐、行间地址跳跃等情况;首先对于一个矩阵,尤其是多通道矩阵,为了提升内存访问效率,每行的数据末尾可能存在填充,导致行间的地址不是连续的,也即step(见上,步长,代表行实际占据空间)可能大于该行数据的宽度。因此,通过data+step每行的首地址仍然是可知的,再计算行内偏移,能够实现数据访问;此外,Mat提供接口m.isContinuous()用于判断矩阵分布是否连续的。

Mat指针

二维数据使用at能够定位像素,但是高维数据就很难办了,Mat建议使用data+step任意访问Mat的数据,但是要手动计算偏移量。

Mat.data和多维step索引

Mat.data会返回一个uchar*指针,指向的是矩阵第一位数据,

1
2
Mat m1(4, 4, CV_8UC3,Scalar(123,124,128));
cout<<static_cast<int>(*(m1.data)); //123

通过判断if(!Mat.data)if(Mat.empty())似乎均能判断是否返回了一个空矩阵(注意不是零矩阵,是尺寸为0的矩阵),data的原理是判断该数据空间是否分配,empty则是判断元素的数目是否为0,但因为空间是自动析构的,无元素的矩阵一般对应空间也回收了,这点差异也基本抹平了,尤其是矩阵只定义、不初始化时(Mat m1;),Mat.data也是nullptr;

step在二维中表示一行元素占据的空间大小,称为步长,在三维及以上,step是一个多维数组,描述了某个维度确定时,增加另外维度的步长,例如三维数据,step[0]就是确定了x,此时对应步长对应的是一个yz平面,因此步长就是数据大小(包括通道数)*yz的总维度,立体几何原理,其余以此类推:

1
2
3
4
5
6
7
int size[3] = {2,4,10}; 
Mat m(3, size,CV_8UC3,Scalar(123,124,255));//三维构造:维度、各维度大小、类型、通道

uchar* head = m.data;
cout<<m.step[0]<<endl; //120:三通道、第一维度确定是yz平面,因此3*4*10 = 120;
cout<<m.step[1]<<endl; //30:三通道、第一、二维度确定是z维度,因此3*10 = 30;
cout<<m.step[2]<<endl; //3:x、y、z均确定,增加一步就是增加三个通道数据,为3字节;

data代表起始指针,step能获取每行的步长,即使Mat行间不连续的情况下,也可以通过指针+偏移量遍历每一个像素,这个方法是高维索引唯一适用的方法:

遍历三维单通道:

1
2
3
4
5
6
7
8
9
10
11
12
13
int size[3] = {2,3,4};
Mat m(3,size,CV_8UC1);

for(int i=0; i<2; i++){
for(int j=0; j<3; j++){
for(int k=0; k<4; k++){
uchar* elem = m.data + i*m.step[0] + j*m.step[1] + k*m.step[2];
cout<<(int)elem[0]<<'\t';
}
cout<<";";
}
cout<<endl;
}

遍历多通道,例如三通道,只需要索引时多跳过3即可,此时分别偏移0、1、2即对应的通道;

1
2
3
4
uchar* elem = m.data + i*m.step[0] + j*m.step[1] + k*m.step[2]; //step已经考虑通道数,无需乘3
cout<<(int)elem[0]<<'\t'; //R通道
cout<<(int)elem[1]<<'\t'; //G通道
cout<<(int)elem[2]<<'\t'; //B通道

Mat.ptr索引

ptr虽然不完全适用于高维(一说四维及以上失效),但是最常用的二维图像时,比at、迭代器的方法仍然更加高效

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//单通道
Mat m(4,4,CV_8UC1,255); //4*4二维图像、单通道
for(int i=0; i<m.rows; i++){
uchar* elem = m.ptr<uchar>(i); //取出某行指针
for(int j=0; j<m.cols; j++){
cout<<(int)elem[j]<<"\t";
}
cout<<endl;
}

//多通道
Mat m(4,4,CV_8UC3,Scalar(255,127,0)); //4*4二维图像、三通道
for(int i=0; i<m.rows; i++){
Vec3b* elem = m.ptr<Vec3b>(i); //取出某行指针
for(int j=0; j<m.cols; j++){
cout<<(int)elem[j][0]<<" "; //第j列第一通道
cout<<(int)elem[j][1]<<" ";
cout<<(int)elem[j][2]<<endl;
}
}

Mat零矩阵、1矩阵、单位矩阵

1
2
3
Mat m = Mat::zeros(2, 2, CV_8UC1); //零矩阵,等同于初始化0
Mat m1 = Mat::ones(2, 2, CV_8UC1); //第一通道全1,等同于初始化1
Mat m2 = Mat::eye(2, 2, CV_8UC1); //2*2的单位矩阵

类型变换convertTo

只适用于相同通道数的类型变换:

1
2
3
4
5
6
7
void convertTo(OutputArray m, int rtype, double alpha=1, double beta=0) const;
//参数:m是输出新矩阵,rtype是输出新类型,alpha、beta变换因子指定后输出元素会乘alpha、加上beta作为新元素;

//example:
Mat m(3,3,CV_64FC3,Scalar(1.0,2.2,3.2));
Mat m1;
m.convertTo(m1,CV_8UC3); //64F截断成8U

子矩阵

取0到2行、2到4列:结束为开区间.

1
Mat m1 = m(Range(0,3),Range(2,5))

参考链接:

  1. Opencv学习(三)简记Mat中的数据类型

  2. OpenCV中的Mat数据类型

  3. OpenCV3.4.0学习笔记(一)——cv::Mat的内存结构与访问

  4. OpenCV中Mat的建立,高维度Mat的建立,以及各维度下值的索引问题