OpenCV C++记录(二):Mat数据类型
Mat数据类型
Mat是OpenCV特有的结构,其可以存储图像和矩阵,其中Mat支持不同的数据类型,表示不同的数据长度和通道数量。命名规则是CV_nUCm,其中n是数据长度,m是通道数量,从数据长度来看一般有CV_8U
、CV_8S
、CV_16U
、CV_16S
、CV_32S
、CV_32F
、CV_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
11Mat 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
5Mat 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
14Mat 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和type:depth只和数据和数据长度有关,和通道数无关,对应关系是:
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[]索引:
size()输出尺寸是矩阵的宽度和高度,即列数×行数,列数在前!
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 | Mat m(4, 4, CV_64FC3,Scalar(123,124,128)); |
Mat像素赋值和Vec
使用operator()时能够自动初始化为0,如果使用create构造,则需要使用setTo统一赋值;
1
2
3Mat m;
m.create(3,3,CV_64FC1);
m.setTo(255);
矩阵初始化需要使用Mat_模板如下: 1
2
3
4Mat_<double>rawMat = (Mat_<double>(3,3)<<
135.752395091636, 51.0103552955011, 102.6712174223711,
208.4825800582035, 111.9060426536122, 63.69016545467102,
197.9148945049116, 195.0959945478937, 78.79538704479675);
对于单通道数据而言,使用at1
2Mat m(4, 4, CV_64FC1,Scalar(123));
m.at<double>(0,0) = 255; //修改第一位数据,注意一定要对应数据类型
对于多通道数据,定位了行列,得到的是一个多通道类型,因此需要了解Vec
类型:
1
2
3
4
5
6Vec3b: 三通道、8位无符号整型,0——255;
Vec3w: 三通道、16位无符号整型,0——65535;
Vec3s: 三通道、16位Short类型;
Vec3i: 三通道、32位int类型;
Vec3f: 三通道、32位float类型;
Vec3d: 三通道、64位double类型;1
2Mat m(4,4,CV_8UC3,Scalar(255,255,0));
m.at<Vec3b>(0,0)[2] = 255; //0行0列的第二通道(G通道)修改成255
细节问题
Scalar最大支持四通道赋值,超过就需要使用遍历的方法了;
Mat的默认维度是2维,尽管创建一维矩阵,仍认为是n*1的;
Mat的内存分配策略
Mat的管理分成两大块,一个指向矩阵头信息,包含矩阵的存储地址、存储方式、矩阵大小等,另一个就是矩阵本身,存储矩阵数据;几个重要的问题如下:
拷贝策略:构造/赋值拷贝不会复制矩阵,仅复制其矩阵头并增加引用数(浅拷贝),而深拷贝需要
clone
、copyTo
支持,会真正开辟空间存储矩阵数据。1
2
3
4
5
6
7
8
9
10
11
12
13Mat 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;
分配策略:注意Mat的分配不一定是完全连续的,体现在存在内存对齐、行间地址跳跃等情况;首先对于一个矩阵,尤其是多通道矩阵,为了提升内存访问效率,每行的数据末尾可能存在填充,导致行间的地址不是连续的,也即step(见上,步长,代表行实际占据空间)可能大于该行数据的宽度。因此,通过data+step每行的首地址仍然是可知的,再计算行内偏移,能够实现数据访问;此外,Mat提供接口
m.isContinuous()
用于判断矩阵分布是否连续的。
Mat指针
二维数据使用at能够定位像素,但是高维数据就很难办了,Mat建议使用data+step任意访问Mat的数据,但是要手动计算偏移量。
Mat.data和多维step索引
Mat.data会返回一个uchar*指针,指向的是矩阵第一位数据,
1
2Mat 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
7int 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
13int 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
4uchar* 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 | Mat m = Mat::zeros(2, 2, CV_8UC1); //零矩阵,等同于初始化0 |
类型变换convertTo
只适用于相同通道数的类型变换: 1
2
3
4
5
6
7void 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))
参考链接: