第4章 OpenCV数据结构与基本绘图
文章目录
前言
笔记系列
参考书籍:OpenCV3编程入门
作者:毛星云
版权方:电子工业出版社
出版日期:2015-02
笔记仅供本人参考使用,不具备共通性
笔记中代码均是OpenCV+Qt的代码,并非用vs开发,请勿混淆
本章核心函数/类清单
函数/类名称 | 说明 | 对应讲解章节 |
---|---|---|
Mat::mat() | Mat类的构造函数 | 4.1.4 |
Mat::Creat() | Mat类的成员函数,可用于Mat类的初始化操作 | 4.1.1 |
Point类 | 用于表示点的数据结构 | 4.2.1 |
Scalar类 | 用于表示颜色的数据结构 | 4.2.2 |
Size类 | 用于表示尺寸的数据结构 | 4.2.3 |
Rect类 | 用于表示矩形的数据结构 | 4.2.4 |
cvtColor | 用于颜色空间转换 | 4.2.5 |
4.1 基础图像容器Mat
4.1.1 数字图像存储概述
-
图像在转化到数字设备中时,设备记录的是图像中每个点的数值
-
如上图所示,矩阵就是图像在数码设备中的表现形式
4.1.2 Mat结构的使用
-
Mat类的好处
- 1.不必手动为其开辟空间
- 2.不必在不需要时立即将其空间释放
- 上述两点好处对比OpenCV1.0时代的代码来说,方便了太多
- 注意:此处说的是手动开辟空间并非必须,但这种方式是依旧存在的----大多是OpenCV函数仍然会手动地为输入数据开辟空间
- 当传递一个已经存在的Mat对象时,开辟好的矩阵空间会被重用.也就是说,我们每次都是使用大小正好的内存来完成任务
-
Mat是一个类,由两个数据部分组成
- 矩阵头(包括矩阵尺寸,存储方法,存储地址等信息)
- 指向存储所有像素值的矩阵的指针(根据所选存储方法的不同,该矩阵可以是不同的维数)
-
矩阵头的尺寸是常数值,但存储像素值的矩阵本身会以图象的大小不同而不同(通常都比矩阵头大上数个数量级),因此,在程序中传递图像并创建副本时,大的内存开销是由矩阵造成的
-
OpenCV是一个图像处理库,囊括了大量的图像处理函数,为了解决问题通常要使用库中的多个函数,因此传递图像(并创建副本)是常有的事,但基于上一条的原因,所以不到万不得已,尽量不要进行大图像的复制,因为这会降低程序的运行速度
-
当然OpenCV也设法解决了上面的问题-----引用计数机制
- 其思路为:让每个Mat对象都有自己的信息头,但是共享一个矩阵
- 这个方法通过让矩阵指针指向同一地址而实现
- 拷贝构造函数时,程序只复制信息头和矩阵指针,而不复制矩阵
-
来看下面这段代码
-
Mat A,C;//仅创建信息头部分 A = imread("1.jpg",CV_LOAD_IMAGE_COLOR);//为矩阵开辟内存,并将图片存入 Mat B(A);//使用拷贝构造函数 C = A;//使用赋值运算符
-
上面代码中的A、B、C三个对象最终都指向同一个数据矩阵
-
虽然A、B、C三个对象的信息头不一样,但是通过任何一个对象所作的改变也会影响到其他对象
-
-
矩阵指针指向同一个数据矩阵的不同Mat对象只是访问相同数据的不同途径而已
-
OpenCV还可以创建只引用一部分数据的信息头,如下
-
//创建一个感兴趣区域(ROI),它是只包含边界信息的信息头 Mat D(A,Rect(10,10,100,100));//使用矩形界定 Mat E = A(Range::all(),Range(1,3));//使用行和列来界定
-
-
当矩阵属于多个Mat对象时,由最后使用它的对象来清理
- 通过上面的说的计数机制
- 无论什么时候复制一个Mat对象的信息头,都会增加矩阵的引用次数
- 反之,当一个信息头被释放后,引用次数就会减一
- 当引用次数归零时,就说明没有对象引用该矩阵了,矩阵就会被执行清理程序
- 通过上面的说的计数机制
-
复制矩阵本身的方法
-
可以用clone()函数或者copyTo()函数
-
Mat F = A.clone(); Mat G; A.copyTo(G);
-
通过上面的操作,现在对F和G做出改变就不会影响到A,B,C三个对象所指向的矩阵了
-
-
-
总结:
- OpenCV函数中输出图像的内存分配是自动完成的(如果不特别指定的话)
- 使用OpenCV的C++接口时不需要考虑内存释放的问题
- 赋值运算符和拷贝构造函数只复制信息头
- 函数clone()或者函数copyTo()会把图像的矩阵也复制一份
4.1.3 像素值的存储方式
- 存储像素值需要制定颜色空间和数据类型
颜色空间
- 是指针对一个给定的颜色,如何组合颜色原色以对其编码
- 最简单的颜色空间是灰度级空间,它只处理白色和黑色,对他们进行组合便可以产生不同程度的灰色
- 对于彩色方式有更多种类的颜色空间,无论哪种方式都是把颜色分成三个或者四个基本元素,通过组合基本元素可以产生所有的颜色
- RGB颜色空间是最常用的彩色空间
- 它的基色是红(Red)绿(Green)蓝(Blue)
- 有时为了控制透明度还会加入第四个元素Alpha(A)
- 颜色系统有很多
RGB
是最常见的,因为其基色是人眼内部构成颜色的方式HSV
和HLS
把颜色分解成色调、饱和度和亮度/透明的- 这是描述颜色更自然的方式,比如可以通过抛弃最后一个元素,是算法对输入图像的光照条件不敏感
YCrCB
在JPEG图像格式中广泛使用CIE L*a*b*
是一种在感知上均匀的颜色空间,它适合用来度量两个颜色之间的距离
数据类型
- 每个组成元素(即元素空间的每一个颜色)都有自己的定义域,而定义域取决于其数据类型,如何存储一个元素决定了我们在其定义域上能够控制的精度
- 最小的数据类型是char
- 占一个字节
- 可以是有符号型(0255之间),也可以是无符号型(-127+128之间)
- 使用三个char型已经可以表示1600万种可能的颜色(RGB的颜色空间)
- 如果使用float型(4字节)或double型(8字节)可以给出更加精细的颜色分辨能力
- 增加元素的尺寸的同时也会增加所占内存空间的大小
4.1.4 显示创建Mat对象的七种方法
-
可以通过Mat的运算符"<<"来查看图像矩阵的实际值,但是该方法只对二维矩阵有效果
-
Mat不单是一个非常有用的图像类容器,同时也是一个通用的矩阵类,我们可以用它来创建和操作多为矩阵
-
创建Mat对象的方法有:
【方法一】使用Mat()构造函数
-
最简单的方法
-
Mat M(2,2,CV_8UC3,Scalar(0,0,255)); cout<<"M = "<<endl<<""<<M<<endl<<endl; //Qt Creator中如果要使用cout,需要像VS里面那样,#includ <iostream>,同时using namespace std;
-
上述代码运行效果(Qt Creator中)
-
对于二维通道图像
-
首先要定义其尺寸,即行数和列数
-
然后需要指定存储元素的数据类型和每个矩阵点的通道数(CV_8UC3)
-
对此,可以依据下面的规则来定义
-
CV_[The number of bits per item][Signed or Unsigned][Type Prefix]C[The channel number]
-
即:
-
CV_[位数][带符号与否][类型前缀]C[通道数]
-
比如
- CV_8UC3表示使用8位的unsignerd char型,每个像素由三个元素组成三通道
- CV_32FC2表示使用32位的单精度浮点型,每个像素由两个元素组成双通道
-
-
-
【方法二】在C/C++中通过构造函数进行初始化
-
int sz[3] = { 2,2,2}; Mat M(3,sz,CV_8UC(2),Scalar::all(0)); //cout<<"M = "<<endl<<""<<M<<endl<<endl;
-
上面的例子演示了如何创建一个超过两维的矩阵
-
首先指定位数3(Mat构造函数的第一个参数)
-
然后,传递一个指向一个数组的指针sz(Mat构造函数的第二个参数)
- 这个数组里面包含了每个维度的尺寸
- 这个参数不能写成
&sz
,因为在C语言中,数组名就是指向该数组的第一个数组元素的地址
-
后面两个参数与方法一中的相同,不再赘叙
-
cout<<"M = "<<endl<<""<<M<<endl<<endl;
-
这句代码是不能写在此处的,因为用Mat的"<<"字符来查看矩阵元素只对二维矩阵有效果
-
如果写了这句代码,则系统会报错
-
OpenCV: terminate handler is called! The last OpenCV error is: OpenCV(3.4.13) Error: Assertion failed (m.dims <= 2) in FormattedImpl, file D:\opencv\opencv-3.4.13\modules\core\src\out.cpp, line 86
-
上述错误信息中,有这么一句:
(m.dims <= 2)
就是在提醒开发者,只有维度小于等于2的矩阵才可以cout出来
-
-
【方法三】为已存在的IplImage指针创建信息头
-
(1) IplImage 转 Mat: IplImage* image = cvLoadImage( "1.jpg"); Mat mat=cvarrToMat(image); (2)Mat转IplImage: Mat img = imread("test.jpg"); IplImage* ipl_img = cvCreateImage(cvSize(img.cols, img.rows),IPL_DEPTH_8U,img.channels()); memcpy(ipl_img->imageData, img.data, ipl_img->height * ipl_img->widthStep);
-
书上写的方法如下,但因为我装的OpenCV版本过高(3.4.13),这些方法都失效了
-
IplImage* image = cvLoadImage( "1.jpg"); Mat mat(image);
【方法四】利用Create()函数
-
Mat M(2,2,CV_32FC3,Scalar(0,0,255)); M.create(4,4,CV_8UC(2)); cout<<"M = "<<endl<<""<<M<<endl<<endl;
-
需要注意的是,此创建方法不能为矩阵设初值,只是在改变尺寸的时候重新为矩阵数据开辟内存而已
【方法五】采用Matlab式的初始化方式
-
Mat E = Mat::eye(4,4,CV_64F); cout<<"E = "<<endl<<""<<E<<endl<<endl; Mat O = Mat::ones(2,2,CV_32F); cout<<"O = "<<endl<<""<<O<<endl<<endl; Mat Z = Mat::zeros(3,3,CV_8UC1); cout<<"Z = "<<endl<<""<<Z<<endl<<endl;
-
结果如下:
E =
[1, 0, 0, 0;
0, 1, 0, 0;
0, 0, 1, 0;
0, 0, 0, 1]
O =
[1, 1;
1, 1]
Z =
[ 0, 0, 0;
0, 0, 0;
0, 0, 0]
【方法六】对小矩阵使用逗号分隔式初始化函数
-
Mat C = (Mat_<double>(3,3)<<0,-1,0,-1,5,-1,0,-1,0); cout<<"C = "<<endl<<""<<C<<endl<<endl;
-
结果如下:
C =
[0, -1, 0;
-1, 5, -1;
0, -1, 0]
如过输入的元素不够矩阵的总元素,则会如下
C =
[0, -1, 0;
-1, 5, -1;
2.143248128504563e-312, 1.958064995486337e-306, 0]
如上:未输入元素会变成不可预知的值
【方法七】为已存在的对象创建信息头
-
使用成员函数clone()或者copyTo()为一个已经存在的Mat对象创建一个新的信息头
-
Mat C = (Mat_<double>(3,3)<<0,-1,0,-1,5,-1,0,0,0); cout<<"C = "<<endl<<""<<C<<endl<<endl; Mat RowClone = C.row(1).clone(); cout<<"RowClone = "<<endl<<""<<RowClone<<endl<<endl;
-
结果如下:
C =
[0, -1, 0;
-1, 5, -1;
0, 0, 0]
RowClone =
[-1, 5, -1]
4.1.5 OpenCV中的格式化输出方法
-
首先先定义一个R矩阵,使用Randu()函数产生的随机数来填充矩阵,当然需要给定一个上限和下限来确保随机值在期望的范围内
-
Mat r = Mat(10,3,CV_8UC3); randu(r,Scalar::all(0),Scalar::all(255));
【风格一】OpenCV默认风格
-
cout<<"r (OpenCV default style)="<<endl<<r<<";"<<endl<<endl;
-
效果如下
-
r (OpenCV default style)=
[ 91, 2, 79, 179, 52, 205, 236, 8, 181;
239, 26, 248, 207, 218, 45, 183, 158, 101;
102, 18, 118, 68, 210, 139, 198, 207, 211;
181, 162, 197, 191, 196, 40, 7, 243, 230;
45, 6, 48, 173, 242, 125, 175, 90, 63;
90, 22, 112, 221, 167, 224, 113, 208, 123;
214, 35, 229, 6, 143, 138, 98, 81, 118;
187, 167, 140, 218, 178, 23, 43, 133, 154;
150, 76, 101, 8, 38, 238, 84, 47, 7;
117, 246, 163, 237, 69, 129, 60, 101, 41];
【风格二】python风格
-
cout<<"r (python style) ="<<endl<<format(r,Formatter::FMT_PYTHON)<<";"<<endl<<endl;
-
效果如下
-
r (python style) =
[[[ 91, 2, 79], [179, 52, 205], [236, 8, 181]],
[[239, 26, 248], [207, 218, 45], [183, 158, 101]],
[[102, 18, 118], [ 68, 210, 139], [198, 207, 211]],
[[181, 162, 197], [191, 196, 40], [ 7, 243, 230]],
[[ 45, 6, 48], [173, 242, 125], [175, 90, 63]],
[[ 90, 22, 112], [221, 167, 224], [113, 208, 123]],
[[214, 35, 229], [ 6, 143, 138], [ 98, 81, 118]],
[[187, 167, 140], [218, 178, 23], [ 43, 133, 154]],
[[150, 76, 101], [ 8, 38, 238], [ 84, 47, 7]],
[[117, 246, 163], [237, 69, 129], [ 60, 101, 41]]];
【风格三】逗号分隔风格(Comma separated values,CSV)
-
cout<<"r (CSV) ="<<endl<<format(r,Formatter::FMT_CSV