1.图像存储器
数字图像在计算机中以矩阵形式进行存储,矩阵中的每个元素都描述一定的图像信息,如亮度、颜色等,如下图所示。数字图像处理就是通过一系列操作从矩阵数据中提取更深层次信息的过程,因此学习图像处理首先应该学会如何操作矩阵信息。OpenCV提供了Mat类用于存储矩阵数据。本篇内容将讲解Mat类的操作方式以及其支持的运算。
1.1Mat类-介绍
在版本较老的OpenCV版本中,例如OpenCV1.0中,图像存储用的是C语言中的IplImage的结构体,由于其使用后需要用户手动释放内存,程序结束不释放内存容易造成内存泄露的缺点,OpenCV较新版本使用了C++接口中的Mat类,利用自动内存管理技术解决了自动释放内存的问题。
Mat类用来保存矩阵类型的数据,包括向量、矩阵、灰度或彩色图像等数据。Mat类分为矩阵头和指向存储数据的矩阵指针两部分。矩阵头中包括矩阵的尺寸、存储方法、地址和引用次数等。矩阵头的大小是一个常数,不会随着矩阵尺寸的大小而改变。在绝大多数情况下,矩阵头大小远小于矩阵中数据量的大小,因此图像复制和传递过程中主要的内存用在存放矩阵数据。在OpenCV中复制和传递图像时,只复制了矩阵头和指向存储数据的指针,因此,在创建Mat类时,可以先创建矩阵头再赋值数据,方法如下:
cv::Mat a;//创建名为a的矩阵头
Image1 = cv::imread("test.jpg");//向a中赋值图像数据,矩阵指针指向像素数据
cv::Mat a = b;//复制矩阵头a,并命名为b
上面的代码创建了一个名为a的矩阵头,之后读入一张图片并将Image1中的矩阵指针指向该图像的像素数据,最后将矩阵头a中的内容复制给矩阵头b。尽管矩阵头a和b名称不同,但是指向了同一个矩阵数据,通过任意矩阵头修改矩阵中的数据,另一矩阵头指向的数据也会改变。删除a变量时,变量b不会指向一个空数据,只有两个变量都被删除,矩阵数据才会被释放。矩阵头中标记了某个矩阵数据被引用的次数,只有该矩阵数据引用次数为0时才会释放矩阵数据。
根据官方给出的Mat类继承图,如下图所示。Mat类可以存储的数据类型有double、float、uchar、unsigned char,以及自定义模板等。
通过以下代码声明一个指定类型double的Mat类变量
cv::Mat A = Mat_<double>(3,3);//创建一个3*3的矩阵用于存放double类型数据
像素值的最大值决定了图像质量,OpenCV用Mat类主要用于存储图像,如果用8位无符号整数存储16位的图像,会造成严重的图像颜色失真造成数据错误。由于不同位数的编译器对数据长度定义不同,为了在不同环境下因变量位数长度不同而造成程序执行问题,OpenCV根据数值变量存储位数长度定义了数据类型。
OpenCV的数据类型及取值范围:
数据类型 | 具体类型 | 取值范围 |
---|---|---|
CV_8U | 8位无符号整数 | 0~255 |
CV_8S | 8位无符号整数 | -128~127 |
CV_16U | 16位无符号整数 | 0~65535 |
CV_16S | 16位有符号整数 | -32,768~32,768 |
CV_32S | 32位符号整数 | -2,147,483,648~2,147,483,647 |
CV_32F | 32位浮点数 | -FLT_MAX~FLT_MAX,INF,NAN |
CV_64F | 64位浮点数 | _DBL_MAX~DBL_MAX,INF,NAN |
仅有数据类型还不够,还需要定义图像数据的通道(Channel)数。例如:灰度图像数据是单通道,彩色图像数据是3通道或者4通道数据。因此针对此问题,OpenCV定义了通道数标识,C1、C2、C3、C4分别表示单通道、双通道、3通道和4通道。每一种数据类型都存在多个通道的情况,将数据类型和通道数结合就得到了OpenCV对图像数据类型的完整定义。例如:CV_8UC1表示的是8位无符号单通道数据,用于表示8位的灰度图;CV_8UC3表示的是8位3通道类型数据,用于表示8位彩色图。具体使用方式如下:
cv::Mat a(1260,720,CV_8UC3);//创建一个1260*720的3通道矩阵用于存储彩色图像
cv::Mat a(3,3,CV_8UC1); //创建一个3*3的8位无符号整数的单通道矩阵
cv::Mat a(3,3,CV_8U); //创建单通道矩阵,C1标识可以省略
注意:在64位编译器中,uchar和CV_8U都表示无符号整数,但是二者有严格的定义,CV_8U只能用在Mat类内部。如果使用Mat_<CV_8U>(3,3)
和Mat a(3,3,uchar)
,会提示报错。
1.2Mat类-构造与赋值
根据OpenCV源码定义,关于Mat类的构造有20多种,本节将介绍常用的几种构造和赋值方式。
1.2.1Mat类-构造
1)利用默认构造函数
cv::Mat::Mat()
利用默认构造函数构造一个Mat类,这种构造方式不需要输入任何的参数,在后续给变量赋值的适合会自动判断矩阵的类型与大小,实现灵活存储,常用于存储读取的图像数据和某个函数运算的输出结果。
2)根据输入矩阵尺寸和类型构造
cv::Mat::Mat(int rows,
int cols,
int type
)
rows
:构造矩阵的行数;
cols
:矩阵的列数;
type
:矩阵存储的数据类型。此处,除CV_8UC1、CV_64FC4等从1到4通道以外,还可通过CV_8UC(n)中的n来构建多通道矩阵,其中n最大可以取到512。
以上方法在章节1.1中有介绍,通过输入矩阵的行、列以及存储数据类型来构造矩阵,常用在明确所需存储数据尺寸和数据类型的情况下,例如相机的内参、物体的旋转矩阵等。该方法存在变形,通过将行和列组合成Size()
结构进行赋值:
cv::Mat::Mat(cv::Size size()
int type
)
size
:二维数据变量尺寸,通过Size(cols, rows)
进行赋值;
type
:矩阵存储的数据类型。
利用以上方式构造函数时需注意,在Size()
结构中,矩阵的行和列的顺序与直接填写顺序相反,使用Size()
时,列在前、行在后。使用该方法构造如下:
cv::Mat a(cv::Size(720, 1280), CV_8UC1);//构造一个行为1280、列为720的单通道矩阵
cv::Mat b(cv::Size(720, 1280), CV_32FC3);//构造一个行为1280、列为720的3通道矩阵
3)利用已有矩阵构造Mat类
cv::Mat::Mat(const Mat & m);
m
:已经构建完成的Mat类矩阵数据。
以上方法可以构建出已有Mat类变量存储内容一样的变量。这种方法只是复制了Mat类的矩阵头,矩阵指针指向同一个地址,因此如果通过其中一个Mat类修改了矩阵数据,另一个变量中的数据也会改变。
如果想复制两个一模一样的Mat类而彼此之间不受影响,那么可以用b = a.clone()
实现,其中a为之前已经构建的Mat类,b为想和a大小一样的Mat类。
如果需要构建的矩阵尺寸比已有矩阵尺寸小,并且存储的是已有矩阵中的子内容,可以用如下方式:
cv:Mat::Mat(const Mat & m,
const Range & rowRange,
const Range & colRange = Range::all()
)
m
:之前已经构建好的Mat类矩阵数据;
rowRange
:在已有矩阵中需要截取的行数范围,是一个Range变量,例如从第2行到第5行可以表示为Range(2,5),Range范围是左闭右开,[2,5),这一点和Python中的range()
函数类似。
colRange
:在已有矩阵中需要截取的行数范围,是一个Range变量,例如从第2行到第5行可以表示为Range(2,5),当不输入任何值时,表示所有列都会被截取。
以上方式主要用于在原图中进行截取一部分。这种方式构建的Mat类与已有的Mat类指向数据相同,有一个数据发生变化,另一个也会发生改变。具体使用方式如下:
cv::Mat b(a, range(2,5), range(2,5));//从a中截取行数和列数都为2-5,构造b
cv::Mat c(a, range(2,5));//从a中截取2-5行,全部列,构造c
1.2.2Mat类-赋值
构建完Mat类后,变量中并没有数据,需要将数据赋值给它。
1)构造时赋值
cv::Mat::Mat(int rows,
int cols,
int type,
const Scalar & s
)
rows:构造矩阵的行数;
cols :矩阵的列数;
type :矩阵存储的数据类型;
s:给矩阵中每个像素赋值的参数变量,例如Scalar(0,0,255)
。
以上方式在构造Mat类时进行赋值,每个元素要赋的值放入Scalar
结构中,会给矩阵中的每一个元素赋予相同的值。例如:Scalar(0,0,255)
,会将每个像素的3个通道分别赋值为0、0、255。具体使用方法如下:
cv::Mat a(2, 2, CV_8UC3, cv::Scalar(0,0,255));//创建一个3通道8位无符号矩阵,每个元素都为0,0,255
cv::Mat b(2, 2, CV_8UC2, cv::Scalar(0,255)); //创建一个2通道8位无符号矩阵,每个元素都为0,255
cv::Mat c(2, 2, CV_8UC1, cv::Scalar(255)); //创建一个单通道8位无符号矩阵,每个元素都为255
在return
语句之前加上断点进行调试,通过【视图】->【其他窗口】->【Image Watch】查看每一个Mat变量中的数据。结果如下图所示:
注意:Scalar
结构变量的个数一定要与定义中的通道数相对应。如果Scalar
结果中变量的个数大于通道数,则位置在大于通道数之后的数据不会被读取。例如:a(2, 2, CV_8UC2, Scalar(0,0,255))
,每个像素值将是(0, 0),而255不会被读取;如果Scalar
结构中变量的个数小于通道数,则会以0补充。
2)枚举法赋值
该方法将矩阵中所有的元素一一列举,并用数据流的方式赋值给Mat类。具体方式如下:
cv::Mat a = (cv::Mat_<int>(3, 3) << 1, 2, 3, 4, 5, 6, 7, 8, 9);
cv::Mat b = (cv::Mat_<double>(2, 3) << 1.1, 2.2, 3.3, 4.4, 5.5, 6.6);
以上代码第一行创建了一个3×3的矩阵,矩阵中存放的是1~9的9的整数,先将矩阵中的第一行存满,之后存放第二行、第三行,即1、2、3存放在矩阵a的第1行,4、5、6存放在矩阵a的第2行,7、8、9存放在矩阵a的第三行。第二行代码创建了一个2×3的矩阵,数据类型是浮点数,其存放方式和a相同。
使用枚举赋值法时,输入的数据个数一定要和矩阵元素相同,不然会报错。
3)循环法赋值
与枚举法赋值类似,循环赋值法是通过对矩阵进行遍历,然后进行赋值。具体如下:
cv::Mat c = cv::Mat_<int>(3, 3); //定义一个3*3矩阵
for (int i=0; i<c.rows; i++) //矩阵行数遍历
{
for (int j =0; j<c.cols; j++)//矩阵列数遍历
{
c.at<int>(i, j)=i+j;
}
}
上面代码创建了3×3的矩阵,通过for
循环遍历的形式,对矩阵中的元素进行赋值。在给矩阵每个元素赋值时,赋值函数c.at<int>()
中的数据类型要与刚开始创建矩阵时数据类型要一致,否则会报错。
4)类方法赋值
在Mat类中,提供了可以快速赋值的方法,可以初始化指定的矩阵。例如:生成单位矩阵、对角矩阵、元素全为0或1的矩阵等。具体方法如下:
cv::Mat a = cv::Mat::eye(3, 3, CV_8UC1);//生成3*3的单通道单位阵a
cv::Mat b = (cv::Mat_<int>(1, 3) << 1, 2, 3);
cv::Mat c = cv::Mat::diag(b);//利用一维矩阵b生成对角阵c
cv::Mat d = cv::Mat::ones(3, 3, CV_8UC1);//生成全1的3*3的单通道矩阵d
cv::Mat e = cv::Mat::zeros(3, 3, CV_8UC3);//生成全1的3*3的3通道矩阵e
eye()
:构建一个单位矩阵,前两个参数为矩阵的行数和列数,第三个参数为矩阵存放的数据类型和通道数。如果行和列不相等,则在主对角线位位置(1,1),(2,2),(3,3)等对角线位置为1。
diag()
:构建对角矩阵,其参数必须是Mat类型的一维变量,用来存放对角线元素。
ones()
:构建一个全为1的矩阵,前两个参数为矩阵的行数和列数,第三个参数为矩阵存放的数据类型和通道数。
zeros()
:构建一个全为0的矩阵,前两个参数为矩阵的行数和列数,第三个参数为矩阵存放的数据类型和通道数。
5)利用数组方式赋值
这种方式和与枚举法类似,但是该方法可以根据需求改变Mat类矩阵的通道数,可以看做枚举法的扩展。具体使用方式如下:
float a[8] = {1,2,3,4,5,6,7,8};
cv::Mat b = cv::Mat(2, 2, CV_32FC2, a);
cv::Mat c = cv::Mat(2, 4, CV_32FC1, a);
这种赋值方式将数值存放在一个数组中,然后通过Mat类矩阵尺寸和通道数进行拆分,这种拆分方式可以自由定义矩阵的通道数。当矩阵中的元素数目大于数组总数时,将用-1.073,741,8e+08填充赋值给矩阵;当矩阵元素数目小于数组总数时,将矩阵完成赋值后,数组剩余数据不再使用。由数组赋值给矩阵是将矩阵中第一个元素的所有通道依次赋值,然后再赋值第二个元素所有通道,依次递推。【Image Watch】窗口如下所示:
1.3Mat类-运算
Mat类变量支持矩阵的加减乘除运算,即在使用Mat类变量时,将其看做普通的矩阵即可。具体使用方法如下:
cv::Mat a = (cv::Mat_<int>(3, 3) << 1, 2, 3, 4, 5, 6, 7, 8, 9);
cv::Mat b = (cv::Mat_<int>(3, 3) << 1, 2, 3, 4, 5, 6, 7, 8, 9);
cv::Mat c = (cv::Mat_<double>(3, 3) << 1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7, 8.8, 9.9);
cv::Mat d = (cv::Mat_<double>(3, 3) << 1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7, 8.8, 9.9);
cv::Mat e,f,g,h,i,j,k;
double m;
e = a + b;//加
f = c - d;//减
g = 2 * a;//乘
h = d / 2.0;//除
i = a - 1;
j = c * d;//两个矩阵相乘
k = a.mul(b);//矩阵对应位相乘
m = a.dot(b);//矩阵内积(矩阵点积)
两个Mat类变量进行加减运算时,需要保证数据类型相同。如果int和double数据类型的Mat类变量进行运算,将会报错。常数与Mat类运算结果的数据类型仍为Mat类变量的数据类型,例如:double 的常数类型与int类型的Mat变量运算,结果仍然为int类型。Mat类变量减去一个常数,表示Mat类变量中的每一个元素都要减去这个常数。Mat类矩阵相乘“*”时,需要保证满足矩阵相乘原则,并且满足Mat类中的数据类型为CV_32FC1、CV_64FC1、CV_32FC2、CV_64FC2这4种中的一种,也就是数据必须为float或double数据类型。矩阵逐元素乘法,也就是对应位相乘,需要保证两个矩阵数据类型相同。
1.4Mat类-元素读取
三通道的Mat类矩阵类似于三维数据,而计算机的存储空间是一个二维空间,因此Mat类矩阵在计算机存储时需要将三维数据变成二维数据,先存储第一个元素每个通道的数据,再存储第二个元素每个通道的数据,依次递推,如下图所示,蓝、绿、红组成一个元素中每个通道的数据。因此,我们如果找到了每个元素的起始位置,就可以找到这个元素每个通道的数据。
Mat类矩阵属性表:
属性 | 作用 |
---|---|
cols | 矩阵的列数 |
rows | 矩阵的行数 |
step | 以字节为单位的矩阵的有效宽度 |
elemSize() | 每个元素的字节数 |
total() | 矩阵中元素的个数 |
channels() | 矩阵的通道数 |
如果用Mat(3,4,CV_32FC3)
定义一个矩阵,这时行数rows为3;列数cols为4;通道数channels()为3;矩阵中元素个数为3×4=12;每个元素的字节数elemSize()为32/8×channels()=12(一个元素有3个通道,每个通道为32位数据类型,位换算成字节需要除以8);以字节为单位的有效长度step为elemSize()×cols=48。
1.4.1通过at方法读取Mat类矩阵中的元素
通过at方法读取Mat类矩阵中的元素,分为针对单通道读取方法和针对多通道读取方法。
1)at单通道读取方法
cv::Mat a = (cv::Mat_<uchar>(3, 3)<<1, 2, 3, 4, 5, 6, 7, 8, 9);
int vale = (int)a.at<uchar>(0, 0);
通过at方法读取元素需要在后面跟上"<数据类型>",而且必须和定义Mat类矩阵时数据类型相同,否则报数据类型不匹配产生的错误。该方法以坐标的形式给出需要读取元素的位置(行数,列数)。如果矩阵定义的是uchar数据类型,那么需要读取数据的时候最好转换成int数据类型,否则在终端等,并不能正常显示内容。
2)at多通道读取方法
在OpenCV中,针对多通道引入了一个向量类型的类,例如三通道cv::Vec3b、cv::Vec3s、cv::Vec3w、cv::Vec3d、cv::Vec3f、cv::Vec3i有6种类型。命名规则为数字代表通道数,最后的字母代表数据类型,其中b是uchar,s是short,w是ushort、d是double、f是float、i是int。OpenCV的二通道和四通道也遵循这个规则。
cv::Mat b(3, 4, CV_8UC3, cv::Scalar(0, 0, 1));//创建一个三通道,每个数据都为(0,0,1)的矩阵
cv::Vec3b vc3 = b.at<cv::Vec3b>(0, 0);//读取第一个数据
int first = (int)vc3.val[0];//第一个元素,第一个通道的值
int second = (int)vc3.val[1];//第一个元素,第二个通道的值
int third = (int)vc3.val[2];//第一个元素,第三个通道的值
at多通道读取时,也需要数据类型和定义矩阵时数据类型一致。在读取时,可以将cv::Vec3b
改成cv::Vec3i
,减少了后面强制转换类型(int)
。
1.4.2通过ptr指针读取Mat类矩阵的元素
Mat类矩阵数据在存放时,每一行的数据都是挨着存放,如果找到每一行的起始地址,要读取每一行不同位置的数据就可以改变指针的位置即可。
cv::Mat b(3, 4, CV_8UC3, cv::Scalar(0, 0, 1));
for (int i = 0; i < b.rows; i++)
{
uchar* ptr = b.ptr<uchar>(i);
for (int j = 0; j < b.cols*b.channels(); j++)
{
cout << (int)ptr[j] << endl;
}
}
以上程序中,外循环遍历矩阵的行,然后定义一个uchar类型的指针ptr,并且Mat类矩阵也需要指明自己的数据类型uchar,用小括号读取的行数。内循环用于输出每个数据包含的3个通道的数据,输出第一个数据三个通道的数据后,然后再输出第二个数据的,依次类推。指针移动位数在中括号“[]”中给出。如果我们已经明确需要访问的数据所在的行和位,则可直接读取,例如:要读取第3行第2个数据,可以用a.ptr<uchar(2)[1]>
的形式直接访问。
1.4.3通过迭代器访问Mat类矩阵的元素
cv::Mat a(3, 4, CV_8UC3, cv::Scalar(0, 0, 1));
cv::MatIterator_<cv::Vec3b> it = a.begin<cv::Vec3b>();//Mat类开始
cv::MatIterator_<cv::Vec3b> it_end = a.end<cv::Vec3b>();//Mat类结束
for (int i = 0; it != it_end; it++)
{
cout << (*it) << " ";
if ((++i% a.cols) == 0)
{
cout << endl;
}
}
Mat类的迭代器变量类型为cv::MatIterator_<>
,在定义时需要在尖括号(“<>”)中填入定义Mat类时相同的数据类型。读取时同样是将第一个元素中的所有通道数据读完再进行下一个,以此类推。
1.4.4通过矩阵元素地址定位方式访问元素
前面3种读取元素的方式都需要知道Mat类矩阵存储数据的数据类型, 我们也可以通过声明“第X行第X列第X通道”的方式来读取矩阵中的元素,具体方式如下:
(int)(*(a.data + a.step[0] * row + a.step[1] * col + channel));
上面代码中row为某个数据所在行数,col为某个数据所在列数,channel为元素所在通道数。a.data
表示Mat矩阵的数据指针,指向矩阵的第一个像素点;a.step[0]
表示每一行的字节数,a.step[1]
表示每一列的字节数。
2.图像的获取与显示
2.1图像读取函数imread
cv::Mat::imread(const String & filename,
int flags=IMREAD_COLOR
)
filename
:需要读取图像的文件名称,包括图像地址、名称和图像文件扩展名;
flags
:读取图像形式的标志,如将彩色图像按照灰度图读取,默认参数是按照彩色图像格式读取,可选参数如下表所示。
imread()
函数读取图像形式参数表(标志码在不冲突时可以同时声明多个,不同参数之间用“|”隔开):
标志参数 | 简记 | 作用 |
---|---|---|
IMREAD_UNCHHANGED | -1 | 按照图像原样读取,保留Alpha通道(第4通道) |
IMREAD_GRAYSCALE | 0 | 将图像转换成单通道灰度图像后读取 |
IMREAD_COLOR | 1 | 将图像转换成3通道BGR彩色图像 |
IMREAD_ANYDEPTH | 2 | 保留原图像的16、32位深度,不声明该参数则转换成8位读取 |
IMREAD_ANYCOLOR | 4 | 以任何可能的颜色读取图像 |
IMREAD_LOAD_GDAL | 8 | 使用gdal驱动程序加载图像 |
IMREAD_REDUCED_GRAYSCALE_2 | 16 | 将图像转成单通道灰度图像,尺寸缩小1/2。可以更改最后一位数字实现缩小1/4(最后一位改成4)或1/8(最后一位改成8) |
IMREAD_REDUCED_COLOR_2 | 17 | 将图像转成3通道彩色图像,尺寸缩小1/2。可以更改最后一位数字实现缩小1/4(最后一位改成4)或1/8(最后一位改成8) |
IMREAD_IGNORE_ORIENTATION | 128 | 不以EXIF的方向旋转图像 |
imread
函数读取指定图像返回一个Mat类变量,当图像文件不存在、破损或者格式不支持时,则返回空矩阵,表示无法读取。可以通过data
属性是否为空或者empty()
函数是否为真来判断图像是否读取成功,读取失败时data
属性为0,empty()
函数为1。
不同操作系统中读取图像使用的编解码器有所不同,无论在哪个系统中BMP文件和DIB文件都是始终可以读取的。在Windows和masOS系统中,默认情况使用OpenCV自带的编解码器(libjpeg、libpng、libtiff和libjasper),因此可以读取JPEG(jpg、jpeg、jpe)、PNG、TIFF(tiff、tif)文件,在Linux系统中,需要自行安装这些编解码器,安装后同样可以读取这些类型的文件。函数imread
能否读取文件与扩展名无关,而是通过文件内容确定图像类型,假如将一个图像扩展名由png改成exe时,该函数一样可以读取文件,但是将exe文件改成png,该函数无法加载此文件。
imread
函数读取图像的模式可以总结为原样读取、灰度图读取、彩色图读取、多位图读取、在读取时将图像缩小一定尺寸等形式。选择参数将彩色图转换成灰度图是通过编解码器转换,可能会与OpenCV程序中将彩色图像转换成灰度图的结果存在差异。在默认情况下,读取图像的像素数目必须小于230,这个要求在绝大多数图像处领域是不受影响的,但是微星遥感图像、超高分辨率图像的像素可能会超过此阈值,则需要修改OpenCV的环境变量OPENCV_io_MAX_IMAGE_PIXELS
参数来调整能够读取的最大像素数目。
2.2图像窗口函数namedWindow
在显示图像时如果不主动定义图像窗口,程序会在显示图像自动生成一个窗口,但是有时需要在显示图像之前对图像窗进行操作,例如添加滑动条,此时就需要提前创建图像窗口。
void cv::namedWindow(const String & winname,
int flags = WINDOW_AUTOSIZE
)
winname:窗口名称,用作窗口的标识符;
flags :窗口属性设置标志。
函数namedWindow
会创建一个窗口变量,用于显示图像和滑动条,通过窗口的名称可以引用该窗口,如果在创建窗口时已经存在相同名称的窗口,则该函数不会执行任何操作。创建窗口需要占用资源,如果窗口不需要后,可以通过cv::destroyWindow()
函数和cv::destroyAllWindows()
函数关闭窗口,前一个函数需要在括号中指定窗口名字,后一个窗口则会关闭所有窗口。不主动释放窗口资源在程序结束时会自动释放。
函数namedWindow
第二个参数用于声明窗口的属性,主要用于设置窗口大小是否可调、显示的图像是否填满窗口等,具体可选的参数如下(默认情况下,函数加载的标准参数为WINDOW_AUTOSIZE | WINDOW_KEEPRATIO | WINDOW_GUI_EXPANDED
):
标志参数 | 简记 | 作用 |
---|---|---|
WINDOW_NORMAL | 0x00000000 | 显示窗口后,允许用户随意调整窗口大小 |
WINDOW_AUTOSIZE | 0x00000001 | 根据图像大小显示窗口,不允许用户调整大小 |
WINDOW_OPENGL | 0x00001000 | 创建窗口的时候会支持OpenGL |
WINDOW_FULLSCREEN | 1 | 全屏显示窗口 |
WINDOW_FREERATIO | 0x00000100 | 调整图像尺寸以填满窗口 |
WINDOW_KEEPRATIO | 0x00000000 | 保持图像的比例 |
WINDOW_GUI_EXPANDED | 0x00000000 | 创建的窗口允许添加工具栏和状态栏 |
WINDOW_GUI_NORMAL | 0x00000010 | 创建没有状态栏和工具栏的窗口 |
2.3图像显示函数imshow
void cv::imshow(const String & winname,
InputArray mat
)
winname:要显示图像的窗口名字,用字符窜形式赋值;
mat:要显示的图像矩阵。
该函数会在指定的窗口显示图像,如果在此函数之前没有创建该名字的图像窗口,则会以 WINDOW_AUTOSIZE 标志创建一个窗口,显示图像的原始大小;如果创建了图像窗口,那么会缩放图像以适应窗口属性。该函数会根据图像的深度将其缩放,具体缩放规则如下:
- 如果图像是8位无符号类型,那么按照原样显示;
- 如果图像是16位无符号类型或者32位整数类型,那么会将像素除以256,将范围由[0,256×256]映射到[0,256];
- 如果图像是32位或64位浮点类型,那么将图像乘以256,将范围由[0,1]映射到[0,256]。
imshow
函数中第二个参数并不是常见的Mat类,而是InputArray,这个是OpenCV定义的一个类型声明引用或,用作输入参数的标识,遇到它时就将它当做一个需要输入Mat类数据。同样地,OpenCV对输出也定义了OutputArray类型,也可当做输出一个Mat类数据。此函数运行后会继续执行后面的程序,如果后面的程序运行完直接退出程序,可能会使图像一闪而过,因此需要显示图像的程序中,往往会在imshow()
函数后跟cv::waitKey()
函数,用于将程序暂停一段时间,参数默认为0,表示无限时间等待用户按键,等待时间的单位为毫秒。
3.视频加载和摄像头调用
3.1视频数据读取
虽然视频是由很多帧图片构成,但是imread()
函数不能直接读取视频文件,需要专门的视频读取函数对视频进行读取,并将每一帧图像保存到Mat类矩阵中。
cv::VideoCapture::VideoCapture();//默认构造函数
cv::VideoCapture::VideoCapture(const String& filename,
int apiPreference = CAP_ANY
)
filename:读取的视频文件或者图像序列名称;
apiPreference:读取数据时设置的属性,例如编码格式、是否调用OpenNI等。
该函数构造一个能够读取与处理视频文件的视频流。代码的第一行是VideoCapture类的默认构造函数,只是声明了一个能够读取视频数据的类,具体读取什么文件,需要在使用时通过open()
函数指定,例如:cv::VideoCapture cap
,通过cap.open("1.mp4)"
通过VideoCapture类变量cap读取1.mp4视频文件。
第二种构造函数在给出声明变量的同时也将视频数据赋值给变量。可以读取的文件种类包括视频文件(例如:video.avi)、图像序列或者视频流的URL。其中图像序列需要统一命名为“前缀+数字的形式”,通过“前缀+%02d”形式读取,例如:在某个文件中存在图像序列名称Image01.jpg、Image02.jpg…加载这些图像序列时,文件名用“Image+%02d”表示。函数中的读取视频设置属性标签默认的是自动搜索合适的标志,因此使用时,可以只输入视频名称。与imread()
函数类似,构造函数同样具有返回结果,通过isOpened()
函数进行判断,读取成功则返回true,失败则返回false。
通过构造函数将视频文件只是加载到了VideoCapture类变量中,使用视频中的图像时,需要将VideoCapture类变量导出到Mat类变量中,该操作可以通过数据流“<<”运算符将图像按视频顺序由VideoCapture类变量赋值给Mat类变量。VideoCapture类变量全部赋值给Mat类变量后,再次赋值Mat类变量会变为空矩阵,可以通过empty()
判断VideoCapture类变量中是否所有图像读取完毕。
VideoCapture类变量可以通过函数get()
查看视频属性,通过输入指定的标志来获取视频属性,例如视频的像素尺寸、帧数、帧率等。VideoCapture类中get()方法中的常用标志及含义如下表所示:
标志参数 | 简记 | 作用 |
---|---|---|
CAP_PROP_POS_MSEC | 0 | 视频文件的当前位置(以毫秒为单位) |
CAP_PROP_FRAME_WIDTH | 3 | 视频流中图像的宽度 |
CAP_PROP_FRAME_HEIGHT | 4 | 视频流中图像的高度 |
CAP_PROP_FPS | 5 | 视频流中图像的频率(每秒帧数) |
CAP_PROP_FOURCC | 6 | 编解码器的4字符代码 |
CAP_PROP_FRAME_COUNT | 7 | 视频流中图像的帧数 |
CAP_PROP_FORMAT | 8 | 返回的Mat对象的格式 |
CAP_PROP_BRIGHTNESS | 10 | 图像的亮度(仅适用于支持的相机) |
CAP_PROP_COUNTRAST | 11 | 图像对比度(仅适用于相机) |
CAP_PROP_SATURATION | 12 | 图像饱和度(仅适用于相机) |
CAP_PROP_HUE | 13 | 图像的色调(仅适用于相机) |
CAP_PROP_GAIN | 14 | 图像的增益(仅适用于相机) |
使用示例如下:
#include <opencv2\opencv.hpp>
#include <iostream>
using namespace std;
using namespace cv;
int main()
{
system("color F0"); //更改输出界面颜色
VideoCapture video("test.mp4");
if (video.isOpened())
{
cout << "视频中图像的宽度=" << video.get(CAP_PROP_FRAME_WIDTH) << endl;
cout << "视频中图像的高度=" << video.get(CAP_PROP_FRAME_HEIGHT) << endl;
cout << "视频帧率=" << video.get(CAP_PROP_FPS) << endl;
cout << "视频的总帧数=" << video.get(CAP_PROP_FRAME_COUNT);
}
else
{
cout << "请确认视频文件名称是否正确" << endl;
return -1;
}
while (1)
{
Mat frame;
video >> frame;
if (frame.empty())
{
break;
}
imshow("video", frame);
waitKey(1000 / video.get(CAP_PROP_FPS));
}
waitKey();
return 0;
}
运行结果:
3.2摄像头调用
cv::VideoCapture::VideoCapture(int index,
int apiPreference = CAP_ANY
)
index:摄像头ID号;
apiPreference:读取数据时设置的属性,例如编码格式、是否调用OpenNI等。
调用摄像头和读取视频文件,只有第一个参数不同。调用摄像头时,第一个参数是打开摄像头设备的ID,ID的命名从0开始,并通过数据流“>>”符号读取当前摄像头拍摄到的图片。
示例代码:
#include <opencv2\opencv.hpp>
#include <iostream>
using namespace std;
using namespace cv;
int main()
{
system("color F0"); //更改输出界面颜色
VideoCapture video(0);
if (video.isOpened())
{
cout << "视频中图像的宽度=" << video.get(CAP_PROP_FRAME_WIDTH) << endl;
cout << "视频中图像的高度=" << video.get(CAP_PROP_FRAME_HEIGHT) << endl;
cout << "视频帧率=" << video.get(CAP_PROP_FPS) << endl;
cout << "视频的总帧数=" << video.get(CAP_PROP_FRAME_COUNT);
}
else
{
cout << "请确认视频文件名称是否正确" << endl;
return -1;
}
while (1)
{
Mat frame;
video >> frame;
if (frame.empty())
{
break;
}
imshow("video", frame);
waitKey(1000 / video.get(CAP_PROP_FPS));
}
waitKey();
return 0;
}
运行结果:
4.数据保存
4.1图像-保存
OpenCV提供imread()
函数用于将Mat类矩阵保存成图像文件。
bool cv::imread(const String& filename,
InputArray img
Const std::vector<int>& params = std::vector<int>()
)
filename:保存图像的地址和文件名,包括图像格式;
img:将要保存的Mat类矩阵变量;
params :保存图像格式属性设置标志(可选参数,可不设置);
返回值Bool类型:成功保存返回true,否则返回false。
通常使用该函数只能保存8位单通道图像和3通道RGB彩色图像,可以通过更改第三个参数保存为不同格式的图像。不同图像格式能够保存的图像位数如下:
- 16位无符号(CV_16U)图像可以保存成PNG、JPEG、TIFF格式文件;
- 32位浮点(CV_32F)图像可以保存成PFM、TIFF、OpenEXR和Radiance HDR格式文件;
- 4通道(Alpha通道)图像可以保存成PNG格式文件。
第三个参数的设置方式如下所示:
vector<int> compression_params;
compression_params.push_back(IMWRITE_PNG_COMPRESSION);
compression_params.push_back(9);
imwrite("alpha.png", mat, compression_params);
imwrite()
第三个参数可选标志及作用:
标志参数 | 简记 | 作用 |
---|---|---|
IMWRITE_JPEG_QUALITY | 1 | 保存成JPEG格式的文件的图像质量 ,分成0~100等级,默认95 |
IMWRITE_JPEG_PROGRESSIVE | 2 | 增强JPEG格式。启用为1,默认值为0(False) |
IMWRITE_JPEG_OPTIMIZE | 3 | 对JPEG格式进行优化,启用为1,默认参数为0(False) |
IMWRITE_JPEG_LUMA_QUALITY | 5 | JPEG格式文件单独的亮度质量等级,分成0~100,默认为0 |
IMWRITE_JPEG_CHROMA_QUALITY | 6 | JPEG格式文件单独的色度质量等级,分成0~100,默认为0 |
IMWRITE_PNG_COMPRESSION | 16 | 保存成PNG格式问压缩级别,0~9,值越大意味着更小尺寸和更长的压缩时间,默认值为1(最佳速度) |
IMWRITE_TIFF_COMRRESSION | 259 | 保存成TIFF格式文件压缩方案 |
示例代码:
#include <iostream>
#include <opencv2\opencv.hpp>
using namespace std;
using namespace cv;
void AlphaMat(Mat &mat)
{
CV_Assert(mat.channels() == 4);//判断通道数是否为4
for (int i = 0; i < mat.rows; ++i)
{
for (int j = 0; j < mat.cols; ++j)
{
Vec4b& bgra = mat.at<Vec4b>(i, j);
bgra[0] = UCHAR_MAX; // 蓝色通道
bgra[1] = saturate_cast<uchar>((float(mat.cols - j)) / ((float)mat.cols) * UCHAR_MAX); // 绿色通道
bgra[2] = saturate_cast<uchar>((float(mat.rows - i)) / ((float)mat.rows) * UCHAR_MAX); // 红色通道
bgra[3] = saturate_cast<uchar>(0.5 * (bgra[1] + bgra[2])); // Alpha通道
}
}
}
int main(int agrc, char** agrv)
{
// Create mat with alpha channel
Mat mat(480, 640, CV_8UC4);
AlphaMat(mat);
vector<int> compression_params;
compression_params.push_back(IMWRITE_PNG_COMPRESSION); //PNG格式图像压缩标志
compression_params.push_back(9); //设置最高压缩质量
bool result = imwrite("alpha.png", mat, compression_params);
if (!result)
{
cout << "保存成PNG格式图像失败" << endl;
return -1;
}
cout << "保存成功" << endl;
return 0;
}
运行结果:
4.2视频-保存
OpenCV提供的VideoWriter()
类可以实现将多张图片保存成视频文件,可以将多幅图像生成视频或者将摄像头拍摄到数据保存成视频文件。
cv::VideoWriter::VideoWriter();//默认构造函数
cv::VideoWriter::VideoWriter(const String& filename,
int fourcc,
double fps,
Size frameSize,
bool isColor=true
)
filename:保存视频的地址和文件名,包含视频类型;
fourcc:压缩帧的4字符编解码器代码(具体在后续表格中给出,如果赋值-1,则自动搜索合适的编解码器);
fps:保存视频的帧率,即视频中每秒图像的张数(可以实现原视频二倍速或慢动作播放);
frameSize:视频帧的尺寸(需要设置和图像尺寸一致,否则无法保存);
isColor:保存视频是否为彩色视频((默认保存为彩色视频))。
默认构造函数使用方法和VideoCapture()
相同,都是创建一个保存视频的数据流,后续通过open()
函数设置保存文件名称,编解码器、帧数等一系列参数。
视频编码格式表:
OpenCV4.1以后版本 | 作用 |
---|---|
Video Writer::fourcc(‘D’,‘T’,‘V’,‘X’) | MPED-4编码 |
Video Writer::fourcc(‘P’,‘I’,‘M’,‘I’) | MPEG-1编码 |
Video Writer::fourcc(‘M’,‘J’,‘P’,‘G’) | JPEG编码(运行效果一般) |
Video Writer::fourcc(‘M’,‘P’,‘4’,‘2’) | MPEG-4.2编码 |
Video Writer::fourcc(‘D’,‘I’,‘V’,‘3’) | MPED-4.3编码 |
Video Writer::fourcc(‘U’,‘2’,‘6’,‘3’) | H263编码 |
Video Writer::fourcc(‘I’,‘2’,‘6’,‘3’) | H263I编码 |
Video Writer::fourcc(‘F’,‘L’,‘V’,‘1’) | FLV1编码 |
函数VideoWriter()
和VideoCapture()
类似,也是通过isOpened()
函数判断是否成功创建视频流,通过get()
查看视频流中的各种属性。保存视频时将需要生成视频的图像一帧一帧地通过“<<”操作符(或者writer()
函数)赋值给视频流,最后使用release()
关闭视频流。
#include <opencv2\opencv.hpp>
#include <iostream>
using namespace cv;
using namespace std;
int main()
{
Mat img;
VideoCapture video(0); //使用摄像头
//VideoCapture video; //读取视频
//video.open("cup.mp4");
if (!video.isOpened()) // 判断是否调用成功
{
cout << "打开摄像头失败,请确实摄像头是否安装成功";
return -1;
}
video >> img; //获取图像
//检测是否成功获取图像
if (img.empty()) //判断有没有读取图像成功
{
cout << "没有获取到图像" << endl;
return -1;
}
bool isColor = (img.type() == CV_8UC3); //判断相机(视频)类型是否为彩色
VideoWriter writer;
int codec = VideoWriter::fourcc('M', 'J', 'P', 'G'); // 选择编码格式
//OpenCV 4.0版本设置编码格式
//int codec = CV_FOURCC('M', 'J', 'P', 'G');
double fps = 25.0; //设置视频帧率
string filename = "live.avi"; //保存的视频文件名称
writer.open(filename, codec, fps, img.size(), isColor); //创建保存视频文件的视频流
if (!writer.isOpened()) //判断视频流是否创建成功
{
cout << "打开视频文件失败,请确实是否为合法输入" << endl;
return -1;
}
while (1)
{
//检测是否执行完毕
if (!video.read(img)) //判断能都继续从摄像头或者视频文件中读出一帧图像
{
cout << "摄像头断开连接或者视频读取完成" << endl;
break;
}
writer.write(img); //把图像写入视频流
//writer << img;
imshow("Live", img); //显示图像
char c = waitKey(50);
if (c == 27) //按ESC案件退出视频保存
{
break;
}
}
// 退出程序时刻自动关闭视频流
//video.release();
//writer.release();
return 0;
}
4.3XML和YMAL文件的保存和读取
除图像数据之外,有时程序中尺寸较小的Mat类矩阵、字符串、数组数据也需要进行保存,这些数据通常保存成XML文件或者YMAL文件。本节介绍如何利用OpenCV中的函数读取和保存两种格式文件。
XML是一种元标记语言。所谓元标记,就是使用者根据自身需求定义自己的标记,例如可以用、等标记来定义数据含义,如24表示age数据的值为24。XML是一种结构化语言,通过XML可以知道数据之间的隶属关系,例如100150表示color数据中有两个名为red和blue的数据,并且二者数值分别是100和150。XML文件的扩展名是".xml"。
YMAL是一种以数据为中心的语言,通过“变量:数值”的形式来表示每个数据的数值,通过不同的缩进来表示不同数据之间的结果和隶属关系。YMAL可读性高,常用来表达资料序列的格式,它参考了多种语言,包括XML、C语言、Python、Perl等。YMAL文件的扩展名是“.ymal”。
OpenCV4提供了用于生产和读取XML文件和YMAL文件的FileStorage
类。类中定义了初始化类、写入数据和读取数据等方法。使用FileStorage
类首先需要对其初始化,声明需要操作的文件和操作类型。OpenCV4有两种初始化FileStorage
类的方法,分别是不输入任何参数的初始化(可以理解成只定义,未初始化),以及输入文件名称和操作类型的初始化。第二种初始化方法如下:
cv::FileStorae::FileStorage(const String & filename,
int flags,
const String & encoding = String()
)
filename:打开的文件名称,字符串类型,文件扩展名为“.xml”、“.yaml”或者“.yml”。对文件进行读取操作时,需要文件存在。;
flags:对文件进行的操作类型标志,常用参数及参数在下表中给出,由于是FileStorage
类中的方法使用时需要加上前缀,例如FileStorage::WRITE
;
encoding :编码格式,目前不支持UTF-16 XML编码,需要使用UTF-8 XML编码。
标记参数 | 简记 | 含义 |
---|---|---|
READ | 0 | 读取文件中的数据 |
WRITE | 1 | 向文件中重新写入数据,会覆盖之间的数据 |
APPEND | 2 | 向文件中继续写入数据,新数据在原数据之后 |
MEMORY | 4 | 将数据写入或者读取到内存缓冲区 |
打开文件后,通过FileStorage
类中的isOpened()
函数判断是否成功打开文件,成功则返回true,反之false。第一种构造方法只进行了定义,并没有初始化,初始化需要通过FileStorage
类中的open()
函数进行声明,具体如下:
virtual bool cv::FileStorage::open(const String & filename
int flags
const String & encoding = String()
)
filename:打开的文件名称,字符串类型,文件扩展名为“.xml”、“.yaml”或者“.yml”。对文件进行读取操作时,需要文件存在。;
flags:对文件进行的操作类型标志,常用参数及参数在下表中给出,由于是FileStorage
类中的方法使用时需要加上前缀,例如FileStorage::WRITE
;
encoding :编码格式,目前不支持UTF-16 XML编码,需要使用UTF-8 XML编码。
返回值bool类型 :成功打开文件则返回true,否则返回false(也可以通过FileStorage
类中isOpened()
函数判断文件是否打开)。
打开文件后,类似C++中创建的数据流,可以通过“<<”操作符将数据写入文件,或者通过“>>”操作符从文件中读取数据。也可以通过FileStorage
类中中的write()
函数将数据写入文件,具体如下:
void cv::FileStorage::write(const String & name,
int val
)
name:写入文件中的变量名称;
val:变量值。变量数据类型可以选择 double、String、Mat、vector < String>类型的变量值写入到文件中。
在写入数据时,使用操作符合write()
函数类似,都需要什么变量名和变量值,例如:变量名为“age”,值为“24”,可以通过file<<"age"<<24
来实现写入;如果数据是数组类型,可以用“[]”将属于同一个变量的值标记出来,例如:file<<"age"<<"[<<24<<25<<"]"
;如果某些变量隶属于一个变量,可以用“{}”表示隶属关系,例如:file<<“age”<<"{"<<"XiaoLang"<<24<<“BenBen”<<25<<"}"
。
在读取数据时,只需要变量名就可以读取到数据。例如:file["x"]>>xRead
,就是读取文件中变量“x”的值;当变量中含有多个数据或者含有子变量时,需要通过FileNode
节点类型和迭代器FileNodeIterator
进行读取,例如:某个变量是数组类型,首先需要定义一个形如file[“age”]的FileNode
节点类型变量,然后通过迭代器FileNodeIterator
进行遍历读取其中的数据。另一种读取方法是定义了FileNode
节点类型变量,然后在变量后添加“[]”(地址)的形式进行读取,例如:FileNode[0]读取的是变量数组中的第一个数据,FileNode[“XiaoLang”]读取的是"age"变量中的"XiaoLang"变量的数据,依次向后添加“[]”实现多节点数据的读取。具体使用如下:
#include <opencv2/opencv.hpp>
#include <iostream>
#include <string>
using namespace std;
using namespace cv;
int main(int argc, char** argv)
{
system("color F0"); //修改运行程序背景和文字颜色
//string fileName = "datas.xml"; //文件的名称
string fileName = "datas.yaml"; //文件的名称
//以写入的模式打开文件
cv::FileStorage fwrite(fileName, cv::FileStorage::WRITE);
//存入矩阵Mat类型的数据
Mat mat = Mat::eye(3, 3, CV_8U);
fwrite.write("mat", mat); //使用write()函数写入数据
//存入浮点型数据,节点名称为x
float x = 100;
fwrite << "x" << x;
//存入字符串型数据,节点名称为str
String str = "Learn OpenCV 4";
fwrite << "str" << str;
//存入数组,节点名称为number_array
fwrite << "number_array" << "[" << 4 << 5 << 6 << "]";
//存入多node节点数据,主名称为multi_nodes
fwrite << "multi_nodes" << "{" << "month" << 3 << "day" << 26 << "year"
<< 2023 << "time" << "[" << 1 << 3 << 4 << 7 << "]" << "}";
//关闭文件
fwrite.release();
//以读取的模式打开文件
cv::FileStorage fread(fileName, cv::FileStorage::READ);
//判断是否成功打开文件
if (!fread.isOpened())
{
cout << "打开文件失败,请确认文件名称是否正确!" << endl;
return -1;
}
//读取文件中的数据
float xRead;
fread["x"] >> xRead; //读取浮点型数据
cout << "x=" << xRead << endl;
//读取字符串数据
string strRead;
fread["str"] >> strRead;
cout << "str=" << strRead << endl;
//读取含多个数据的number_array节点
FileNode fileNode = fread["number_array"];
cout << "number_array=[";
//循环遍历每个数据
for (FileNodeIterator i = fileNode.begin(); i != fileNode.end(); i++)
{
float a;
*i >> a;
cout << a << " ";
}
cout << "]" << endl;
//读取Mat类型数据
Mat matRead;
fread["mat"] >> matRead;
cout << "mat=" << matRead << endl;
//读取含有多个子节点的节点数据,不使用FileNode和迭代器进行读取
FileNode fileNode1 = fread["multi_nodes"];
int month = (int)fileNode1["month"];
int day = (int)fileNode1["day"];
int year = (int)fileNode1["year"];
cout << "multi_nodes:" << endl
<< " month=" << month << " day=" << day << " year=" << year;
cout << " time=[";
for (int i = 0; i < 4; i++)
{
int a = (int)fileNode1["time"][i];
cout << a << " ";
}
cout << "]" << endl;
system("pause");
//关闭文件
fread.release();
return 0;
}
运行结果: