OpenCV基础

2 数据载入、显示与保存

2.1 图像存储容器

与我们平时看到的图像存在巨大的差异,数字图像在计算机中是以矩阵形式存储的,矩阵中的每一个元素都描述一定的图像信息,如亮度、颜色等。数字图像处理就是通过一系列操作从矩阵数据中提取更深层次信息的过程,因此,学习图像处理首先需要学会如何操作这些矩阵信息。

OpenCV提供了一个Mat类用于存储矩阵数据。

本节介绍Mat的操作方式以及其支持的运算,通过学习可以在程序中灵活使用Mat类型变量。

2.1.1 Mat类介绍

在最早的OpenCV 1.0版本中,图像是使用名为IplImage的C语言结构体进行存储的,因此在很多比较老的OpenCV版本教程中会看到其身影。但是IplImage需要用户管理内存,后来引入了C++接口,提供Mat类。

Mat类用来保存矩阵类型的数据信息,包括向量、矩阵、灰度或彩色图像等数据。 Mat类分为矩阵头和指向存储数据的矩阵指针两部分。矩阵头中包含矩阵的尺寸、存储方法、地址和引用次数等。矩阵头的大小是一个常熟,不会随着矩阵尺寸的大小而改变。在绝大多数情况下,矩阵头大小远小于矩阵中数据量的大小,因此图像复制和传递过程中主要的开销是存放矩阵数据。为了解决这个问题,在OpenCV中复制和传递图像时,只是复制了矩阵头和指向存储数据的指针,因此,在创建Mat类时,可以先创建矩阵头后赋值数据,其方法如下代码所示:

cv::Mat a;//创建一个名为a的矩阵头
a = cv::imread("test.jpg");//向a中赋值图像数据,矩阵指针指向像素数据
cv::Mat b = a;//复制矩阵头,并命名为b

上面的代码首先创建了一个名为a的矩阵头,之后读入一张图像并将a中的矩阵指针指向该图像的像素数据,最后将a矩阵头中的内容复制到b中。

**虽然a、b有各自的矩阵头,但是其矩阵指针指向的是同一个矩阵数据,通过任意一个矩阵头修改矩阵中的数据,另一个矩阵头指向的数据会跟着发生改变。**但是,当删除a变量时,b变量不会指向一个空数据,只有当两个变量都删除后,才会释放矩阵数据。因为矩阵头中引用次数标记了引用某个矩阵数据的次数,只有当矩阵数据引用次数为0的时候才会释放矩阵数据。

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

接下来讲解Mat类里可以存储的数据类型。根据官方给出的Mat类继承关系图(如下所示),发现Mat类可以存储的类型包含doublefloatucharunsigned char,以及自定义模板等。

cv::Mat
cv::Mat_<_Tp>>
cv::Mat_< double>
cv::Mat_< float>
cv::Mat_< uchar>
cv::Mat_< unsigned char>

我们可以通过下面的方式来声明一个存放指定类型的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 ~ 65536
CV_16S 16位符号整数 -32768 ~ 32767
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还定义了通道数标识,C1C2C3C4分别表示单通道、双通道、3通道和4通道。因为每一种数据类型都存在多个通道的情况,所以将数据类型与通道数表示结合便得到了OpenCV中对图像数据类型的完整定义,例如CV_8UC1表示的是8位单通道数据,用于表示8位灰度图,而CV_8UC3表示的是8位3通道数据,用于表示8位彩色图。我们可用通过下列代码的方式创建一个声明通道数和数据类型的Mat类:

cv::Mat a(640, 480, CV_8UC3);//创建一个640*480的3通道矩阵用于存放彩色图像
cv::Mat a(3, 3, CV_8UC1);//创建一个3*3的8位无符号整数的单通道矩阵
cv::Mat a(3, 3, CV_8U);//创建单通道矩阵,C1标识可以省略

注意:
虽然在64位编译器里,ucharCV_8U都表示8位无符号整数,但是两者有严格的定义,CV_8U只能用在Mat类内部的方法,如果用Mat_(3, 3)Mat a(3, 3, uchar),就会提示创建错误。

2.1.2 Mat类构造与赋值

前面一小节介绍了3种构造Mat类变量的方法,但是后两种没有给变量初始化赋值,本小节重点介绍如何灵活构造并赋值Mat类变量。根据OpenCV的源码定义,关于Mat类的构造方式共有20余种,然后,在平时一些简单的应用程序中,很多复杂的构造方式并没有太多的用武之地,因此这里重点讲解在学习和项目中常用的构造与赋值方式。

Mat类的构造

  1. 利用默认构造函数

    cv::Mat::Mat();
    

    利用默认构造函数构造了一个Mat类,这种构造方式不需要输入任何参数,后续给变量赋值的时候会自动判断矩阵的类型与大小,实现灵活的存储,常用于存储读取的图像数据和某个函数的输出结果。

  2. 根据输入矩阵尺寸和类型构造

    cv::Mat::Mat(int rows,
                int cols,
                int type)
    
    • rows:构造矩阵的行数
    • cols:矩阵的列数
    • type:矩阵中存储的类型。此处,除CV_8UC1CV_64FC4等从1到4通道以外,还提供了更多通道的参数,通过CV_8UC(n)中的n来构建更多通道矩阵,其中n最大可以取到512。

    这种构造方法清晰、直观,易于阅读,常用在明确需要存储数据尺寸和数据类型的情况下,例如相机的内参矩阵、物体的旋转矩阵等。它还存在一种变形,通过将行和列组成一个Size结构进行赋值,这个变形的原型如下:

    cv::Mat(Size size(),
            int type)
    
    • size:二维数据变量尺寸,通过Size(cols, rows)进行赋值。

    利用这种方式构造时要格外注意,在Size结构里,矩阵的行和列的顺序与直接写在Mat里的构造方法顺序相反,使用Size()时rows在前,cols在后。如果不注意这一点,行列颠倒,可能会出现数组越界的错误。使用例子如下:

    cv::Mat a(Size(480, 640), CV_8UC1);//构造一个行为640、列为480的的单通道矩阵
    cv::Mat b(Size(480, 640), CV_32FC3);//构造一个 行为640、列为480的3通道矩阵
    
  3. 根据已有矩阵构造

cv::Mat::Mat(const Mat &m);
  • m :已经构建完成的Mat类矩阵数据。
    这样可以构造出与已有Mat类变量存储内容一样的变量。注意,这种构造方式只是复制类Mat类的矩阵头,矩阵指针指向的是同一个地址,因此,如果通过某一个Mat类变量修改了矩阵中的数据,那么另一个变量中的数据也会发生改变。

提示:
如果希望复制一个一模一样的Mat类而彼此之间不会受影响,那么可以使用m = a.clone()实现。

如果需要构造的矩阵尺寸比已有矩阵小,而且存储的是已有矩阵的字内容,那么可以用下列方法构建:

cv::Mat::Mat(const Mat &m,
            const Range &rowRange,
            const Range &colRange = Range::all()
            )
  • m:已经构建完成的Mat类矩阵数据。
  • rowRange:在已有矩阵中需要截取的行数范围,是一个Range变量,例如从第2行到第五行可以表示为Range(2, 5)
  • colRange:在已有矩阵中需要截取的列数范围,是一个Range变量,例如从第2列到第5列可以表示为Range(2, 5)当不输入任何值时,表示所有列都会被截取

这种方式主要用于在原图中截图使用。不过需要注意的是,这种方式构造的Mat类与已有的Mat类享有共同的数据,即如果两个类变中有一个发生更改,另一个也会随之更改。使用例子如下:

cv::Mat b(a, Range(2, 5), Range(2, 5));//从a中截取部分数据构造b
cv::Mat c(a, Range(2,5));//默认最后一个参数构造c

Mat类的赋值

构建完Mat类后,变量里并没有数据,需要将数据赋值给它。针对不同情况,OpenCV 4.1提供了多种赋值方式,下面介绍如何给Mat类变量赋值。

  1. 构造时赋值

    cv::Mat::Mat(int rows,
                int cols,
                int type,
                const Scalar &s
                )
    
    • s:给就只能热衷每个像素赋值的参数变量,例如Scalar(0, 0, 255)

    该种方式是在构造的同事进行赋值,将每个元素要赋予的值放入一个Scalar结构找那个即可。这里需要注意的是,用此方法会将图像中的每个元素赋予相同的数值,例如Scalar(0, 0, 255)会将每个像素的三个通道值分别赋为0、0、255。使用例子如下:

    cv::Mat a(2, 2, CV_8UC3, cv::Scalar(0, 0, 255));//创建一个3通道矩阵,每个像素都是0, 0, 255
    cv::Mat b(2, 2, CV_8UC2, cv::Scalar(0, 255));//创建一个2通道庭,每个像素都是0, 255
    cv::Mat c(2, 2, CV_8UC1, cv::Scalar(255));//创建一个单通道矩阵,每个像素都是255
    
  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.0, 2.1, 3.2, 4.0, 5.1, 6.2);
    

    上面第一行代码创建了一个3X3的矩阵,矩阵中存放的是一个1~9的9个整数,现将矩阵中的第一行存满,之后再存入第2行、第3行,即1、2、3存放在矩阵a的第一行,4、5、6存放在a的第二行,7、8、9存放在第三行。第二行代码创建了一个2X3的矩阵,其存放方式和矩阵a相同。

    提示:
    在采用枚举法时,输入的数据格式一定要与矩阵元素个数相同,例如,在上面的第一行Daum中只输入1~8共8个数时,赋值过程会出现报错。因此本方法常用在矩阵数据比较少的情况下。

  3. 循环法赋值

    与枚举赋值相似:

    cv::Mat c = cv::Mat_<int>(3, 3);//定义一个3*3的矩阵
    for(int i = 0; i < c.rows; ++j){//矩阵行数循环
        for(int j = 0; j < c.cols; ++j){//矩阵列数循环 
            c.at<int>(i, j) = i+j;
        }
    }
    

    上面代码通过for循环的方式,对矩阵中的每一个元素赋值,需要注意的是,在给矩阵每个元素赋值的时候,赋值函数中声明的变量类型要与矩阵定义时的变量类型相同。即如果将上面的改为c.at(i, j)就会报错,无法赋值。

  4. 类方法赋值

    在Mat类里,提供了可以快速赋值的方法,可以初始化指定的矩阵。例如,生成单位矩阵、对角矩阵、所有元素都为0或者1的矩阵等。具体使用方法如下所示:

    cv::Mat a = cv::Mat::eye(3, 3, CV_8UC1);
    cv::Mat b = (cv::Mat_<int>(1, 3) << 1, 2, 3);
    cv::Mat c = cv::Mat::diag(b);
    cv::Mat d = cv::Mat::ones(3, 3, CV_8UC1);
    cv::Mat e = cv::Mat::zeros(4, 2, CV_8UC3);
    

    上面代码中的每个函数的作用及参数的含义介绍如下:

    • eye():构建一个单位矩阵,前两个参数为矩阵的行数和列数,第三个参数为矩阵存放的数据类型与通道数。如果行和列不相等,则在矩阵的(1, 1),(2, 2),(3, 3)等主对角位置处为1.
    • diag():构建对角矩阵,其参数必须是Mat类型的一维变量,用来存放对角元素的数值。
    • ones():构建一个全为1的矩阵,参数含义与eye()相同。
    • zeros():构建一个全为0的矩阵,参数含义与eye()相同。
  5. 利用数组进行赋值

    这种方法与枚举法类似,但是该方法可以根据需求改变Mat类矩阵的通道数,可以看错枚举法的拓展,使用如下所示:

    float a[8] = { 5, 6, 7, 8, 1, 2, 3, 4}
    cv::Mat b = cv::Mat(2, 2, CV_32FC2, a);
    cv::Mat c = cv::Mat(2, 4, CV_32FC1, a);
    

    这种赋值方式首先将需要存入Mat类中的变量放入一个数组中,之后通过设置Mat类矩阵的尺寸和通道数将数组变量拆分成矩阵,这种拆分可以自由定义矩阵的通道数,当矩阵中的元素数据大于数组中的数据时,将用-1.073 741 8e+08填充赋值给矩阵;当矩阵中元素数目小于数组中的数据时,将矩阵赋值完成后偶,数组中剩余的数据将不再赋值。由数组赋值给矩阵的过程是首先将矩阵中第一个元素的所有通道一次赋值,之后再赋值下一个元素。

2.1.3 Mat类支持的运算

在处理数据时,需要对数据进行加减乘除运算,例如对图像进行滤波、增强等操作都需要对像素级别进行加减乘除运算。为了方便运算,Mat类变量支持矩阵的加减乘除运算,即我们在使用Mat类变量时,将其看做普通的矩阵即可,例如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.0, 2.1, 3.2, 4.0, 5.1, 6.2, 2, 2, 2);
cv::Mat d = (cv::Mat_<double>(3, 3) << 1.0, 2.1, 3.2, 4.0, 5.1, 6.2, 2, 2, 2);
e = a+b;
f = c - d;
g = 2*a;
h = d /2.0;
i = a -1;

这里需要注意的是,当两个Mat类变量进行加减运算时,必须保证两个矩阵中的数据类型是相同的,即两个分别保存int和double数据类型的Mat类变量不能进行加减运算。与常规的乘除法不同之处在于,常数与Mat类变量运算结果的数据类型保留Mat类变量的数据类型,例如,double类型的常数与int类型的Mat类变量运算,最后结果仍然为int类型。上面的最后一行代码中,Mat类变量减去一个常数,表示的含义是Mat类变量中的每一个元素都要减去这个常数。

在对图像进行卷积运算时,需要两个矩阵进行乘法运算,OpenCV不但提供了两个Mat类矩阵的乘法运算,而且定义了两个矩阵的内积和对应位的乘法运算。乘法示例如下:

cv::Mat j, m;
double k;
j = c*d;
k = a.dot(b);
m = a.mul(b);

上面分别实现了两个Mat类矩阵的乘法、内积、对应位乘法。
其中*表示两个矩阵的数学乘积。例如存在两个矩阵A3x3和B3x3*的运算结果为矩阵C3x3,C3x3的每个元素表示为:

    cij = ai1b1j + ai2b2j + ai3b3j

需要注意的是,*运算要求第一个矩阵的列数必须与第二个矩阵的行数相同,而且该运算要求Mat类中的数据类型必须是CV_32FC1CV_64FC1CV_32FC2CV_64FC2这4种中的一种,也就是对于一个二维的Mat类矩阵,其保存的数据类型必须是float类型或者double类型。

k = a.dot(b)表示两个矩阵的内积。根据输出结果可以知道dot()方法结果是一个double类型的变量,该运算的目的是求取一个行向量和一个列向量点乘,例如存在两个向量D = [ d1 d2 d3] 和 E = [ e1 e2 e3 ]T,经过dot()方法运算的结果为:
    f = d1e1 + d2e2 + d3e3

需要注意的是,两个矩阵必须拥有相同的元素数目,但是无论输入的两个矩阵的维数是多少,都会将两个矩阵扩展成一个行向量和一个列向量,因此,.dot运算的结果永远是一个double类型的变量。

m = a.mul(b) 表示两个矩阵对应位的乘积。根据输出结果可以知道mul()方法运算结果同样是一个Mat类矩阵。对于两个矩阵A3x3和B3x3,经过mul()方法运算的结果C3x3中每一个元素都可以表示为:
    cij = aijbij

需要注意的是,参与mul()方法运算的两个Mat类矩阵中保存的数据在保证相同的前提下,可以是任何一种类型,并且默认的输出数据类型与两个Mat类矩阵保持一致。在图像处理领域,常用的数据类型是CV_8U。其范围是0~255,当两个比较大的整数相乘时,就会产生结果溢出的现象,输出结果为255,因此,在使用mul()方法时,需要防止出现数据溢出的问题。

2.1.4 Mat类元素的读取

在学习如何读取Mat类矩阵元素之前,首先需要知道Mat类变量在计算机中是如何存储的。多通道的Mat类矩阵类似于三维数据,而计算机的存储空间是一个二维空间,因此Mat类矩阵在计算机中存储是将三维数据变成二维数据,先存储第一个元素每个通道的数据,之后再存储第二个元素每个通道的数据。每一个的元素都按这种方式进行存储,因此,如果我们找到了每个元素的起始位置,那么可以找到这个元素中每个通道的数据。。

下列是Mat列矩阵常用的属性:

属性 作用
cols 矩阵的列数
rows 矩阵的行数
step 以字节为单位的矩阵的有效宽度
elemSize() 每个元素的字节数
total() 矩阵中元素的个数
channels() 矩阵的通道数

这些属性之间互相组合可以得到多数Mat类矩阵的属性,例如step属性与cols属性组合,可以求出每个元素所占据的字节数,再与channels()属性结合,就可以知道每个通道的字节数,进而知道矩阵中存储的数据量的类型。下面通过一个例子具体说明每个属性的用处:用Mat(3, 4, CV_32FC3)定义一个矩阵,这时通道数channels()为3;列数cols为4,行数rows为3;矩阵中元素的个数为3x4,结果为12;每个元素的字节数为 32/8xchannels(),本例子结果为12;以字节为单位的有效长度stepelemSize()xcols,本例结果为48。

常用的Mat类矩阵元素读取方式包括通过at方法进行读取、通过指针ptr进行读取、通过迭代器进行读取、通过矩阵元素的地址定位进行读取。下面详细介绍这4种读取方式。

  1. 通过at方法读取。

    cv::Mat a = (cv::Mat_<uchar>(3, 3) << 1, 2, 3, 4, 5, 6, 7, 8, 9);
    int value = (int)a.at<uchar>(0, 0);
    

    通过at方法读取元素需要在后边跟上<数据类型>,如果此处的数据类型与矩阵定义的数据类型不相同,就会报错。该方法以坐标的形式给出需要读取的元素坐标(行数,列数)。需要说明的是,如果矩阵定义的是uchar类型的数据,那么在需要输入数据的时候,需要强制转换成int类型的数据进行输出,否则输出的结果并不是整数。

    由于单通道图像是一个二维矩阵,因此在at方法最后给出二维平面坐标即可访问对应位置元素。而多通道矩阵每一个元素坐标处都是多个数据,因此引入一个变量用于表示同一元素的多个数据。在OpenCV中,针对三通道矩阵,定义了cv::Vec3bcv::Vec3scv::Vec3wcv::Vec3dcv::Vec3fcv::Vec3i共6种类型用于表示同一个元素的3个通道数据。通过这6中数据类型可以总结出其命名规则,其中的数字表示通道的个数,最后一位十数据类型的缩写,buchar类型的缩写、sshort类型的缩写、wushort类型的缩写、ddouble类型的缩写、ffloat类型的缩写、iint类型的缩写。当然,OpenCV也为二通道和四通道定义了对应的变量类型,命名方式也遵循这个命名规则,例如二通道和四通道的uchar类型分别用cv::Vec2bcv::Vec4b表示。

    下面是通过at方法读取多通道矩阵的代码:

    cv::Mat b(3, 4, CV_8UC3, cv::Scalar(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类型在输入每个通道数据时需要将其变量类型转换成int类型。不过,如果将at方法读出的结果直接赋给cv::Vec3i类型变量,就不需要在输出每个通道数据时进行数据类型的强制转换。

  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类矩阵的变量类型,并在定义最后用小括号声明指针指向Mat类矩阵的哪一行。第二个循环控制用于输出矩阵每一行所有通道的数据。根据存储形式,每一行中存储的数据数量为列数与通道数的乘积,即指针可以向后移动colsxchannels-1位。
    当我们能够确定要访问的数据时,可以直接给出行数和指针后移位数来访问,例如a.ptr(1)[2]

  3. 通过迭代器访问Mat类矩阵中的元素。

    Mat类变量同时也是一个容器变量,因此,Mat类变量拥有迭代器,用于访问Mat类变量中的数据,通过迭代器也可以实现对矩阵中每一个元素的遍历:

    cv::MatIterator_<uchar> it = a.begin<uchar>();
    cv::MatIterator_<uchar>it_end = a.end<uchar>();
    for( int i = 0; it != it_end; ++it ){
        cout << (int)(*it) << " ";
        if((++i % a.cols) == 0){
            cout << endl;
        }
    }
    

    Mat类的迭代器变量类型是cv::MatIterator_<>,在定义时同样需要在尖括号中声明数据的变量类型。Mat类迭代器的起始是Mat.begin<>(),结束是Mat.end<>(),与其他迭代器用法相同,通过++运算进行向下迭代,数据的读取方式是先读取第一个元素的每一个通道,之后读取第二个,直到最后一个元素的最后一个通道。

  4. 通过矩阵元素的地址定位方式访问元素。

前面3种读取元素的方式都需要知道Mat类矩阵存储数据的类型,而且在认知上,我们个你往能够通过声明“i行j列k通道”的方式来读取某个通道内的数据。使用如下:

(int)(*(b.data + b.step[0] * row + b.step[1] * col + channel));

代码中row变量的含义是某个数据所在元素的行数,col变量是数据所在元素的列数,channel变量是某个数据所在元素的通道数。

这种方式与通过指针读取数据的形式类似,都是通过将首个数据的地址指针移动若干位后指向需要的数据,只不过这种方式可以通过直接给出行、列和通道数进行读取,不需要用户再计算某个数据在这行数据存储空间中的位置。

2.2 图像的读取与显示

图像的种类非常多,包括彩色图像、灰度图像、16位深度图、32位深度图。

2.2.1 图像读取函数imread()

函数原型如下:

cv::Mat cv::imread(const String &filename,
                    int flags = IMREAD_COLOR
                    )
  • filename:需要读取图像的文件名称,包含图像地址、名称和图像文件扩展名。
  • flags:读取图像形式的标志,如将彩色图像按照灰度图读取,默认参数是按照彩色图像方式进行读取,可选参数见本小节后表。
  1. 函数用于读取指定的图像并将其返回给一个Mat类变量,当图像文件不存在、破损或者格式不受支持时,则无法读取图像,此时函数返回一个空矩阵,因此可以通过返回矩阵的data属性是否为空或者empty()函数来判断。如果读取图像失败,那么data属性返回值为0,empty()函数返回值为1。

  2. 函数能够读取多种格式的图像文件,但是,在不同操作系统上由于使用的编解码器不同,因此在某个系统中能够读取的图像可能在其他系统中就无法读取。无论在那个系统中,BMP文件和DIB文件都是始终可以读取的。在Windows和macOS中,默认使用OpenCV自带的编解码器(libjpeg, libpng, libtifflibjasper),因此可以读取JPEG(jpg、jpeg、jpe)、PNG、TIFF(tiff、tif)文件,在Linux系统中,需要自行安装这些编解码器,安装后同样可以读取这些类型的文件

  3. 该函数能否读取文件数据与扩展名无关,而是通过文件的内容确定图像的类型,例如,在将一个扩展名由png修改为exe时,该函数一样可以读取该图像。

    针对不同需求可以更改参数,OpenCV 4.1中给出了13中种模式读取图像的形式,总结起来分别是以原样式读取、灰度图读取、彩色图读取、多位数读取、在读取时将图像缩小一定尺寸等形式,具体可选参数如下表。这里需要支出的是,将从彩色图转成灰度图通过编解码器内部转换,可能会与OpenCV程序中将彩色图像转成灰度图的结果存在差异。这些标志参数在功能不冲突的前提下可以同时声明多个,不同参数之间用|隔开。

    标志参数 简记 作用
    IMREAD_UNCHANGED -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。可以更改最后一位数字,如4、8,分别为1/4,1/8。
    IMREAD_IGNORE_ORIENTATION 128 不以EXIF的方向旋转图像

注意:
在默认情况下,读取图像的像素数目必须小于230,这个要求在绝大多数图像处理领域是不受影响的,但是卫星遥感图像、超高分辨率图像的像素数目可能会超过这个阈值。可以通过修改系统变量中的OPENCV_IO_MAX_IMAGE_PIXELS参数调整能够读取的最大像素数目。

2.2.2 图像窗口函数namedWindow()

在我们之前的程序中并没有介绍过窗口函数,因为在显示图像时如果没有主动定义图像窗口,程序会自动生成一个窗口用于显示图像,然而有时需要在显示图像之前对图像窗口进行操作,例如添加滑动条,此时就需要提前创建图像窗口。函数原型如下:

void cv::namedWindow(const String &winname,
                        int flags = WINDOW_AUTOSIZE
                        )
  • winname:窗口名称,用作窗口的标识符。
  • flags:窗口属性设置标志。

该函数会创建一个窗口变量,用于显示图像和滚动条,通过窗口的名称引用该窗口,如果在创建窗口时已经存在具有相同名称的窗口,则该函数不会执行任何操作。创建一个窗口需要占用部分内存资源,因此,通过该函数创建窗口后,在不需要窗口时需要释放内存资源。OpenCV提供了两个关闭窗口资源的函数,分别是cv::destroyWindow()cv::destroyAllWindows()

前一个函数是关闭一个指定名称的窗口,后一个函数是关闭程序中所有的窗口,一般用于程序的最后。不过事实上,在一个简单的程序里,我们并不需要调用这些函数,因为程序退出时会自动关闭应用程序的所有资源和窗口。虽然不主动释放窗口也会在程序结束时释放窗口资源,但是OpenCV 4.0版在结束时会报出释放窗口的错误,而OpenCV 4.1版则不会报错。

该函数的

  • 第一个参数是声明窗口的名称,用于窗口的唯一识别。
  • 第二个参数是声明窗口的属性,主要用于设置窗口的大小是否可调、显示的图像是否填充满窗口等,具体可选参数如下表。默认情况下,函数加载的标志参数为WINDOW_AUTOSIZE | WINDOW_KEEPRATIO | WINDOW_GUI_EXPANDED
标志参数 简记 作用
WINDOW_NORMAL 0x0000 0000 显示图像后,允许用户随意调整窗口大小
WINDOW_AUTOSIZE 0x0000 0001 根据图像大小显示窗口,不允许用户调整大小
WINDOW_OPENGL 0x0000 1000 创建窗口的时候会支持OpenGL
WINDOW_FULLSCREEN 1 全屏显示窗口
WINDOW_FREERATIO 0x0000 0100 调整图像尺寸以充满窗口
WINDOW_KEEPRATIO 0x0000 0000 保持图像的比例
WINDOW_GUI_EXPANDED 0X0000 0000 创建的窗口允许添加工具栏和状态栏
WINDOW_GUI_NORMAL 0X0000 0000 创建没有状态栏和工具栏的窗口

2.2.3 图像显示函数imshow()

函数原型:

void cv::imshow(const String &winname,
                InputArray mat
                )
  • winname:要显示图像的窗口的名字,用字符串形式赋值。
  • mat:要显示的图像矩阵。

该函数会在指定的窗口中显示图像。如果在此函数之前没有创建同名的图像窗口,就会以WINDOW_AUTOSIZE标志创建一个窗口,显示图像的原始大小;如果创建了图像窗口,那么会缩放图像以适应窗口属性。该函数会根据图像的深度将其缩放,具体缩放规则如下。

  • 如果图像是8位无符号类型,那么按照原样显示。
  • 如果图像是16位无符号类型或者32位整数类型,那么会将像素除以256,将范围由[0, 255x256]映射到[0, 255]。
  • 如果图像是32位或者64位浮点类型,那么将像素除以255,即将范围由[0, 1]映射到[0, 255]。

需要说明的是,第二个参数并不是常见的Mat类,而是inputArray,这个是OpenCV定义的一个类型声明引用,用作输入参数的标识,我们遇到它时可以认为是需要输入一个Mat类数据,同样,OpenCV对输出也定义了OutputArray类型,我们同样可以认为是输入出一个Mat类数据。

注意:
此函数运行后会继续执行后面的程序。如果后面程序执行完直接退出,那么显示的图像可能一闪而过,因此在需要显示图像的程序中,往往会在imshow()函数后根由cv::waitKey()函数,用于将程序暂停一段时间。waitKey()函数是以毫秒计的等待时长,如果参数默认或者为0,那么表示等待用户按键结束该函数。

2.3 视频加载与摄像头调用

  • 待办

你可能感兴趣的:(笔记,c++,opencv,计算机视觉,图像处理)