C++版本OpenCv教程

C++版本OpenCv教程(一)Mat—基本的图像容器

目标

我们有多种方法从现实世界获取数字图像:数码相机、扫描仪、计算机断层扫描和磁共振成像等等。在以上任何情况下,我们(人类)看到的都是图像。然而,当将其转换到我们的数字设备时,我们所记录的是图像中每个点的数值。
在这里插入图片描述
例如,在上面的图像中,你可以看到汽车的镜子只不过是一个包含所有像素点的强度值的矩阵。我们获取和存储像素值的方式可能会根据我们的需要而有所不同,但最终计算机世界中的所有图像都可能被简化为数值矩阵和描述矩阵本身的其他信息。OpenCV是一个计算机视觉库,主要致力于处理和操作这些信息。因此,首先需要熟悉OpenCV如何存储和处理图像。

Mat

cv::Mat Class Reference
关于Mat,您需要知道的第一件事是,您不再需要 手动分配它的内存并在不需要时立即释放它。虽然仍可以这样做,但大多数OpenCV函数将自动分配其输出数据。如果您传递一个已经存在的Mat对象(该对象已经为矩阵分配了所需的空间),那么它将被重用。换句话说,我们在任何时候都只使用执行任务所需的内存。

Mat基本上是一个类,有两个数据部分:Mat头(包含矩阵大小、存储方法、矩阵存储地址等信息)和一个指向包含像素值矩阵的指针(采取多少维数 取决于选择的存储方法)。矩阵头的大小是恒定的,然而,矩阵本身的大小可能随着图像的不同而变化,通常会大上几个数量级。

OpenCV是一个图像处理库。它包含了大量的图像处理功能。为了解决计算难题,大多数时候您将使用库的多个函数。因此,将图像传递给函数是一种常见的做法。我们不应该忘记我们讨论的是图像处理算法,这些算法往往需要大量的计算。我们最不希望做的事情就是通过对可能很大的图像进行不必要的复制来进一步降低程序的速度。

为了解决这个问题,OpenCV使用了一个引用计数系统。其思想是每个Mat对象都有自己的矩阵头,但是当两个Mat对象之间共享一个矩阵时,可以通过让两个Mat对象的矩阵指针指向相同的地址。此外,复制操作符将只复制矩阵头和指向大矩阵的指针,而不是数据本身。

Mat A, C;                          // creates just the header parts
A = imread(argv[1], IMREAD_COLOR); // here we'll know the method used (allocate matrix)
Mat B(A);                                 // Use the copy constructor
C = A;                                    // Assignment operator

最后,上述所有对象都指向同一个数据矩阵,使用其中任何一个对象进行修改都会影响到所有其他对象。实际上,不同的对象只是为相同的底层数据提供了不同的访问方法;然而,它们的矩阵头部分是不同的。真正有趣的部分是,您可以创建只引用部分数据的矩阵头。例如,要在图像中创建感兴趣的区域(ROI),只需创建一个带有新边界的新标题:

Mat D (A, Rect(10, 10, 100, 100) ); // using a rectangle
Mat E = A(Range::all(), Range(1,3)); // using row and column boundaries

现在你可能会问-如果矩阵本身可能属于多个Mat对象:当它不再被需要时,谁负责清理它。简而言之:是最后一个使用它的对象。这是通过使用引用计数机制来处理的。每当有人复制一个Mat对象的矩阵头时,就会为矩阵增加一个计数器。每当清除矩阵头时,此计数器就会减少。当计数器达到0时,矩阵被释放。
采用引用次数来释放存储内容是C++中常见的方式,用这种方式可以避免仍有某个变量引用数据时将这个数据删除造成程序崩溃的问题,同时极大的缩减了程序运行时所占用的内存。
但是,有时候还需要复制矩阵本身,所以OpenCV提供了 cv::Mat::clone() 和 cv::Mat::copyTo() 函数。

Mat F = A.clone();
Mat G;
A.copyTo(G);

接下来我们来了解Mat类里可以存储的数据类型,根据官方给出的Mat类继承图,如图2-2所示,我们发现Mat类可以存储的数据类型包含double、float、uchar、unsigned char以及自定义的模板等。
Mat类继承关系图
我们可以通过代码清单2-2的方式声明一个存放指定类型的Mat类变量:

代码清单2-2 声明一个指定类型的Mat类
cv::Mat A = Mat_<double>(3,3);//创建一个3*3的矩阵用于存放double类型数据

由于OpenCV提出Mat类主要用于存储图像,而像素值的最大值又决定了图像的质量,如果用8位无符号整数去存储16位图像,会造成严重的图像颜色失真或造成数据错误。而由于不同位数的编译器对数据长度定义不同,为了避免在不同环境下因变量位数长度不同而造成程序执行问题,OpenCV根据数值变量存储位数长度定义了数据类型,表2-1中列出了OpenCV中的数据类型与取值范围。
在这里插入图片描述
仅有数据类型是不够的,还需要定义图像数据的通道(Channel)数,例如灰度图像数据是单通道数据,彩色图像数据是3通道或者4通道数据。因此针对这个情况,OpenCV还定义了通道数标识,C1、C2、C3、C4分别表示单通道、双通道、3通道和4通道。每一种数据类型都存在多个通道的情况,所以将数据类型与通道数表示结合便得到了OpenCV中对图像数据类型的完整定义,例如CV_8UC1表示的就是8位单通道数据,用于表示8位灰度图,而CV_8UC3表示的是8位3通道数据,用于表示8位彩色图。我们可以通过代码清单2-3的方式创建一个声明通道数和数据类型的Mat类:

代码清单2-3 通过OpenCV数据类型创建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位编辑器里,uchar和CV_8U都表示8位无符号整数,但是两者有严格的定义,CV_8U只能用在Mat类内部的方法。例如用Mat_<CV_8U>(3,3)和Mat a(3,3,uchar)会提示创建错误。

现在修改F或G将不会影响A的Mat头指向的矩阵。你需要记住的是:

  • OpenCV函数的输出图像内存分配是自动的(除非另有指定)。
  • 您不需要考虑使用OpenCV的c++接口进行内存管理。
  • 赋值操作符和复制构造函数只复制Mat头。
  • 可以使用cv::Mat::clone()和cv::Mat::copyTo()函数复制图像的底层(数据)矩阵。

存储方法

这是关于如何存储像素值的。您可以选择颜色空间和使用的数据类型。颜色空间指的是我们如何结合颜色组件来编码一个给定的颜色。最简单的一种是灰度,我们可以使用黑色和白色来制造灰度。这些组合使我们能够创建许多灰度级。

对于创建色彩的方式,我们有更多的方式可供选择。它们每一种都将其分解为三个或四个基本组件,我们可以使用这些组件的组合来创建其他组件。最流行的一种是RGB,主要是因为这也是我们的眼睛构建颜色的方式。它的基色是红、绿、蓝。为了编码一种颜色的透明度,有时会添加第四个元素:alpha (透明度)(A)。

然而, 不同颜色系统各有其优点:

  • RGB是最常见的,我们的眼睛使用类似的模式。但是请记住,OpenCV标准显示系统使用BGR颜色空间组成颜色(红色和蓝色通道互换位置)。
  • HSV和HLS将颜色分解为色调、饱和度和亮度值组成部分,这是我们描述颜色更自然的方式。例如,您可能会忽略最后一个组件,使您的算法对输入图像的光线条件不太敏感。
  • YCrCb被流行的JPEG图像格式所使用。
  • CIE Lab*是一个感知上一致的颜色空间,如果你需要测量一种给定颜色到另一种颜色的距离,它很方便。

每个构建组件都有自己的有效域。他们直接影响所使用的数据类型。存储组件的方式定义了我们对其域的控制。最小的数据类型可能是char,即一个字节或8位。它可以是无符号的(因此可以存储0到255的值)或有符号的(从-127到+127的值)。虽然在三个组件的情况下,这已经给出了1600万种可能的颜色来表示(像在RGB的情况下),但是,我们仍可以通过使用float(4字节= 32位)或double(8字节= 64位)数据类型为每个组件获得更精细的控制。不过,请记住,增加组件的大小也会增加内存中整个图片的大小。

显式创建一个Mat对象

但是,出于调试的目的,查看实际值要方便得多。您可以使用Mat的<<运算符来完成此操作。注意,这只适用于二维矩阵。

虽然Mat作为图像容器工作得很好,但它也是一个通用的矩阵类。因此,创建和操作多维矩阵是可能的。你可以用多种方式创建一个Mat对象:
- cv::Mat::Mat 构造函数

#include<iostream>
//#include <stdio.h>
#include <opencv2/opencv.hpp>
#include "opencv/highgui.h"

using namespace std;
using namespace cv;

int main(int argc,char** argv) {
    Mat M(2,2,CV_8UC3,Scalar(0,0,255));
    cout<<"M = "<<endl;
    cout<<M<<endl;
    return 0;
}

结果:

M = 
[  0,   0, 255,   0,   0, 255;
   0,   0, 255,   0,   0, 255]

对于二维和多通道图像,我们首先定义它们的大小,行和列计数。

然后,我们需要指定用于存储元素的数据类型和每个矩阵点的通道数量。为了做到这一点,我们根据下面的约定构造了多个定义:

CV_[The number of bits per item][Signed or Unsigned][Type Prefix]C[The channel number]
CV_[单元素的bits数][Signed or Unsigned][数据类型前缀]C[通道数]

例如,CV_8UC3意味着我们使用8位长的无符号char类型,每个像素有三个这样的类型来形成三个通道。为最多4个通道预定义了类型。cv::Scalar是四个元素的 short vector。指定它,您就可以用自定义值初始化所有矩阵点。如果需要更多,可以使用上面的宏创建类型,在括号中设置通道编号,如下所示。

- 使用C/ C++数组并通过构造函数进行初始化

int sz[3] = {2,2,2};
Mat L(3,sz, CV_8UC(1), Scalar::all(0));

上面的例子展示了如何创建一个二维以上的矩阵。指定其维度,然后传递一个包含每个维度大小的指针,剩下的保持不变。

- cv::Mat::create 函数

M.create(4,4, CV_8UC(2));
cout << "M = "<< endl << " "  << M << endl << endl;

您不能用这种结构初始化矩阵值。它只会重新分配它的矩阵数据内存,如果新的大小不能适应旧的。

- MATLAB风格的初始化器:cv::Mat:: zero, cv::Mat:: ones,cv::Mat::eye。指定要使用的大小和数据类型:

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]

- 对于小矩阵,你可以使用逗号分隔的初始化器或初始化器列表(最后一种情况需要c++ 11支持):

Mat C = (Mat_<double>(3,3) << 0, -1, 0, -1, 5, -1, 0, -1, 0);
cout << "C = " << endl << " " << C << endl << endl;
C = (Mat_<double>({0, -1, 0, -1, 5, -1, 0, -1, 0})).reshape(3);
cout << "C = " << endl << " " << C << endl << endl;

- 您可以使用cv::randu()函数用随机值填充一个矩阵。你需要为随机值给出一个下限和上限:

Mat R = Mat(3, 2, CV_8UC3);
randu(R, Scalar::all(0), Scalar::all(255));

C++版本OpenCv教程(二)Mat类构造与赋值


目录

Mat类的构造

1.利用默认构造函数

默认构造函数使用方式
cv::Mat::Mat();

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

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

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

这种构造方法我们前文也见过,通过输入矩阵的行、列以及存储数据类型实现构造。这种定义方式清晰、直观、易于阅读,常用在明确需要存储数据尺寸和数据类型的情况下,例如相机的内参矩阵、物体的旋转矩阵等。利用输入矩阵尺寸和数据类型构造Mat类的方法存在一种变形,通过将行和列组成一个Size()结构进行赋值,代码清单2-6中给出了这种构造方法的原型。

用Size()结构构造Mat类
cv::Mat::Mat(Size size(),int  type)
  • size:2D数组变量尺寸,通过Size(cols, rows)进行赋值。
  • type:与代码清单2-5中的参数一致

利用这种方式构造Mat类时要格外注意,在Size()结构里矩阵的行和列的顺序与代码清单2-5中的方法相反,使用Size()时,列在前、行在后。如果不注意同样会构造成功Mat类,但是当我们需要查看某个元素时,我们并不知道行与列颠倒,就会出现数组越界的错误。使用该种方法构造函数如下:

用Size()结构构造Mat示例
cv::Mat a(Size(480, 640), CV_8UC1); //构造一个行为640,列为480的单通道矩阵
cv::Mat b(Size(480, 640), CV_32FC3); //构造一个行为640,列为480的3通道矩

3.利用已有矩阵构造

利用已有矩阵构造Mat类
cv::Mat::Mat( const Mat & m);
m:已经构建完成的Mat类矩阵数据。

这种构造方式非常简单,可以构造出与已有的Mat类变量存储内容一样的变量。注意这种构造方式只是复制了Mat类的矩阵头,矩阵指针指向的是同一个地址,因此如果通过某一个Mat类变量修改了矩阵中的数据,另一个变量中的数据也会发生改变。
【注】如果想复制两个一模一样的Mat类而彼此之间不会受影响,可以使用m=a.clone()实现。
如果需要构造的矩阵尺寸比已有矩阵小,并且存储的是已有矩阵的子内容,那么可以用代码清单2-9中的方法进行构建:

构造已有Mat类的子类

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

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

构造已有Mat类的子类

cv::Mat::Mat(const Mat & m,const Range & rowRange, const Range & colRange = Range::all())

Mat类的赋值

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

1.构造时赋值

在构造时赋值的方法

cv::Mat::Mat(int  rows,int  cols,int  type,const Scalar & s)
  • rows:矩阵的行数
  • cols:矩阵的列数
  • type:存储数据的类型
  • s:给矩阵中每个像素赋值的参数变量,例如Scalar(0, 0, 255)。

该种方式是在构造的同时进行赋值,将每个元素想要赋予的值放入Scalar结构中即可,这里需要注意的是,用此方法会将图像中的每个元素赋值相同的数值,例如Scalar(0, 0, 255)会将每个像素的三个通道值分别赋值0,0,255。我们可以使用如下的形式构造一个已赋值的Mat类

在构造时赋值示例
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

我们在程序return语句之前加上断点进行调试,用Image Watch查看每一个Mat类变量里的数据,结果如图2-3所示,证明我们已成功构造矩阵并赋值。
使用Scalar结构给Mat类赋值结果

Scalar结构中变量的个数一定要与定义中的通道数相对应,如果Scalar结构中变量个数大于通道数,则位置大于通道数之后的数值将不会被读取,例如执行a(2, 2, CV_8UC2, Scalar(0,0,255))后,每个像素值都将是(0,0),而255不会被读取。如果Scalar结构中变量数小于通道数,则会以0补充。

2.枚举赋值法

这种赋值方式是将矩阵中所有的元素都一一枚举出,并用数据流的形式赋值给Mat类。具体赋值形式如代码清单2-13所示。

利用枚举法赋值示例
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);

上面第一行代码创建了一个3×3的矩阵,矩阵中存放的是从1-9的九个整数,先将矩阵中的第一行存满,之后再存入第二行、第三行,即1、2、3存放在矩阵a的第一行,4、5、6存放在矩阵a的第二行,7,8,9存放在矩阵a的第三行。第二行代码创建了一个2×3的矩阵,其存放方式与矩阵a相同。
采用枚举法时,输入的数据个数一定要与矩阵元素个数相同,例如代码清单2-13中第一行代码只输入从1到8八个数,赋值过程会出现报错,因此本方法常用在矩阵数据比较少的情况。

3.循环赋值

与通过枚举法赋值方法相类似,循环法赋值也是对矩阵中的每一位元素进行赋值,但是可以不在声明变量的时候进行赋值,而且可以对矩阵中的任意部分进行赋值。具体赋值形式如代码清单2-14所示。

利用枚举法赋值示例
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循环的方式,对矩阵中的每一位元素进行赋值。需要注意的是,在给矩阵每个元素进行赋值的时候,赋值函数中声明的变量类型要与矩阵定义时的变量类型相同,即上面代码中第1行和第6行中变量类型要相同,如果第6行代码改成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类型的1维变量,用来存放对角元素的数值。
  • ones():构建一个全为1的矩阵,参数含义与eye()相同。
  • zeros():构建一个全为0的矩阵,参数含义与eye()相同。

5.利用数组进行赋值

这种方法与枚举法相类似,但是该方法可以根据需求改变Mat类矩阵的通道数,可以看作枚举法的拓展,在代码清单2-16中给出了这种方法的赋值形式。

利用数组赋值示例
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.0737418e+08填充赋值给矩阵,如果矩阵中元素的数目小于数组中的数据时,将矩阵赋值完成后,数组中剩余数据将不再赋值。由数组赋值给矩阵的过程是首先将矩阵中第一个元素的所有通道依次赋值,之后再赋值下一个元素,为了更好的体会这个过程,我们将定义的b和c矩阵在图2-4中给出。
矩阵b和c中存储的数据

C++版本OpenCv教程(三)Mat类支持的运算

Excerpt

在处理数据时需要对数据进行加减乘除运算,例如对图像进行滤波、增强等操作都需要对像素级别进行加减乘除运算。为了方便运算,Mat类变量支持矩阵的加减乘除运算,即我们在使用Mat类变量时,将其看做普通的矩阵即可,例如Mat类变量与常数相乘遵循矩阵与常数相乘的运算法则。Mat类与常数运算时,可以直接通过加减乘除符号实现。Mat类的加减法运算cv::Mat a = (cv::Mat_(3, 3) << 1, 2, 3, 4, 5, 6, 7, 8, 9);cv::Mat b =


在处理数据时需要对数据进行加减乘除运算,例如对图像进行滤波、增强等操作都需要对像素级别进行加减乘除运算。为了方便运算,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);
cv::Mat e, f, g, h, i;
e = a + b;
f = c - d;
g = 2 * d;
h = d / 2.0;
i = a – 1;

结果:

a = 
[1, 2, 3;
 4, 5, 6;
 7, 8, 9]
b = 
[1, 2, 3;
 4, 5, 6;
 7, 8, 9]
c = 
[1, 2.1, 3.2;
 4.3, 5.4, 6.5;
 7.6, 8.699999999999999, 9.800000000000001]
d = 
[1, 2.1, 3.2;
 4.3, 5.4, 6.5;
 7.6, 8.699999999999999, 9.800000000000001]
e = 
[2, 4, 6;
 8, 10, 12;
 14, 16, 18]
f = 
[0, 0, 0;
 0, 0, 0;
 0, 0, 0]
g = 
[2, 4.2, 6.4;
 8.6, 10.8, 13;
 15.2, 17.4, 19.6]
h = 
[0.5, 1.05, 1.6;
 2.15, 2.7, 3.25;
 3.8, 4.35, 4.9]
i = 
[0, 1, 2;
 3, 4, 5;
 6, 7, 8]

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

在对图像进行卷积运算的时候,需要两个矩阵进行乘法运算,opencv不仅提供了两个Mat类矩阵的乘法运算,而且定义了两个矩阵的内积和对应位的乘法运算。

两个Mat类矩阵的乘法运算
cv::Mat j, m;
double k;
j = c*d;
k = a.dot(b);
m = a.mul(b);

运行结果:

j = 
[34.35, 41.28, 48.21000000000001;
 76.92, 94.73999999999999, 112.56;
 119.49, 148.2, 176.91]
k = 
285
m = 
[1, 4, 9;
 16, 25, 36;
 49, 64, 81]

在上面的代码中定义了两个Mat类变量和一个double变量,分别实现了两个Mat类矩阵的乘法、内积和对应位乘法。第三行代码中的“”运算符表示两个矩阵的数学乘积,例如存在两个矩阵A3×3和B3×3,“”运算结果为C3×3,C3×3中的每一个元素表示为:
cij=ai1b1j+ai2b2j+ai3b3j,需要注意的是。“*”运算要求第一个Mat类矩阵的列数必须与第二个Mat类矩阵的行数相同,而且该运算要求Mat类中的数据类型必须是CV_32FC1、CV_64FC1、CV_32FC2、CV_64FC2这四种的一种,也就是对于一个二维的Mat类矩阵,其保存的数据类型必须是float类型或者double类型。
关于Mat类矩阵的dot运算和mul运算可以参照下面的链接:
Opencv中Mat矩阵相乘——点乘、dot、mul运算详解
【注】Mat类矩阵的dot运算是把两个矩阵每一位上对应的数相乘然后相加返回一个double类型的数值。
Mat类矩阵的mul运算是把两个矩阵每一位上对应的数相乘返回一个Mat类型的矩阵。
需要注意的是,不同于前两种乘法运算,参与mul()方法运算的两个Mat类矩阵中保存的数据在保证相同的前提下,可以使任何一种类型,并且默认的输出数据类型与两个Mat类矩阵保持一致。在图像处理领域,常用的数据类型是CV_8U,其数据范围是0-255,当两个比较大的整数相乘时就会产生结果溢出的现象,输出结果为255,因此在使用mul()方法时需要防止数据溢出的现象。

C++版本OpenCv教程(四)4种读取Mat类元素的的方法

Excerpt

目录通过at方法读取Mat类矩阵中的元素通过指针ptr读取Mat类矩阵中的元素通过迭代器访问Mat类矩阵中的元素通过矩阵元素地址定位方式访问元素对于Mat类矩阵的读取与更改,我们已经在矩阵的循环赋值中见过如何用at方法对矩阵的每一位进行赋值,这只是OpenCV提供的多种读取矩阵元素方式中的一种,本小节将详细介绍如何读取Mat类矩阵中的元素,并对其数值进行修改。在学习如何读取Mat类矩阵元素之前,首先需要知道Mat类变量在计算机中是如何存储的。多通道的Mat类矩阵是一个类似于三维的数据,而计算机的存储空间是


目录

对于Mat类矩阵的读取与更改,我们已经在矩阵的循环赋值中见过如何用at方法对矩阵的每一位进行赋值,这只是OpenCV提供的多种读取矩阵元素方式中的一种,本小节将详细介绍如何读取Mat类矩阵中的元素,并对其数值进行修改。在学习如何读取Mat类矩阵元素之前,首先需要知道Mat类变量在计算机中是如何存储的。多通道的Mat类矩阵是一个类似于三维的数据,而计算机的存储空间是一个二维空间,因此Mat类矩阵在计算机存储时是将三维数据变成二维数据,先存储第一个元素每个通道的数据,之后再存储第二个元素每个通道的数据。每一行的元素都按照这种方式进行存储,因此如果我们找到了每个元素的起始位置,便可以找到这个元素中每个通道的数据。图2-5展示了一个三通道的矩阵的存储方式,其中连续的蓝色、绿色和红色的方块分别代表每个元素的三个通道。
三通道3*3矩阵存储方式
了解了Mat类变量的存储方式之后,我们来看一下Mat类具有的属性,我们在表2-2中列出了常用的属性,同时详细的介绍了每种属性的作用。
在这里插入图片描述
这些属性之间互相组合可以得到多数Mat类矩阵的属性,例如step属性与cols属性组合,可以求出每个元素所占据的字节数,而再与channels()属性结合,就可以知道每个通道的字节数,进而知道矩阵中存储的数据量的类型。接下来通过一个例子来具体说明每个属性的用处,用Mat (3, 4, CV_32FC3)定义一个矩阵,这时通道数channels()为3;列数cols为4;行数rows为3;矩阵中元素的个数为3_4,结果为12;每个元素的字节数为32/8_channels(),最后结果为12;以字节为单位的有效长度step为eleSize()*cols,结果为48。

常用的Mat类矩阵的元素读取方式有:通过at方法进行读取、通过指针ptr进行读取、通过迭代器进行读取、通过矩阵元素的地址定位方式进行读取。接下来将详细的介绍这四种读取方式。

通过at方法读取Mat类矩阵中的元素

通过at方法读取矩阵元素分为针对单通道的读取方法和针对多通道的读取方法,在代码清单2-19中给出了通过at方法读取单通道矩阵元素的代码。

at方法读取Mat类单通道矩阵元素
Mat a = (Mat_<uchar>(3, 3) << 1, 2, 3, 4, 5, 6, 7, 8, 9);
int value = (int)a.at<uchar>(0, 0);
cout<<"value = "<<value<<endl;

运行结果:

value = 1

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

由于单通道图像是一个二维矩阵,因此在at方法的最后给出二维平面坐标即可访问对应位置元素。而多通道矩阵每一个元素坐标处都是多个数据,因此引入一个变量用于表示同一元素多个数据。在openCV 中,针对3通道矩阵,定义了cv::Vec3b、cv::Vec3s、cv::Vec3w、cv::Vec3d、cv::Vec3f、cv::Vec3i六种类型用于表示同一个元素的三个通道数据。通过这六种数据类型可以总结出其命名规则,其中的数字表示通道的个数,最后一位是数据类型的缩写,b是uchar类型的缩写、s是short类型的缩写、w是ushort类型的缩写、d是double类型的缩写、f是float类型的缩写、i是int类型的缩写。当然OpenCV也为2通道和4通道定义了对应的变量类型,其命名方式也遵循这个命名规则,例如2通道和4通道的uchar类型分别用cv::Vec2b和cv::Vec4b表示。代码清单2-20中给出了通过at方法读取多通道矩阵的实现代码。

Mat b(3, 4, CV_8UC3, cv::Scalar(0, 0, 1));
Vec3b vc3 = b.at<Vec3b>(0, 0);
int first = (int)vc3.val[0];
int second = (int)vc3.val[1];
int third = (int)vc3.val[2];
cout<<"b = "<<endl<<b<<endl;
cout<<"first = "<<first<<endl;
cout<<"second = "<<second<<endl;
cout<<"third = "<<third<<endl;

运行结果:

b = 
[  0,   0,   1,   0,   0,   1,   0,   0,   1,   0,   0,   1;
   0,   0,   1,   0,   0,   1,   0,   0,   1,   0,   0,   1;
   0,   0,   1,   0,   0,   1,   0,   0,   1,   0,   0,   1]
first = 0
second = 0
third = 1

在使用多通道变量类型时,同样需要注意at方法中数据变量类型与矩阵的数据变量类型相对应,并且cv::Vec3b类型在输入每个通道数据时需要将其变量类型强制转成int类型。不过,如果直接将at方法读取出的数据直接赋值给cv::Vec3i类型变量,就不需要在输出每个通道数据时进行数据类型的强制转换。

通过指针ptr读取Mat类矩阵中的元素

前面我们分析过Mat类矩阵在内存中的存放方式,矩阵中每一行中的每个元素都是挨着存放,如果找到每一行元素的起始地址位置,那么读取矩阵中每一行不同位置的元素就是将指针在起始位置向后移动若干位即可。在代码清单2-21中给出了通过指针ptr读取Mat类矩阵元素的代码实现。

Mat b(3,4,CV_8UC3,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]<<" ";
    }
    cout<<endl;
}

运行结果:

0 0 1 0 0 1 0 0 1 0 0 1 
0 0 1 0 0 1 0 0 1 0 0 1 
0 0 1 0 0 1 0 0 1 0 0 1 

在程序里,首先有一个大循环用来控制矩阵中每一行,之后定义一个uchar类型的指针ptr,在定义时需要声明Mat类矩阵的变量类型,并在定义最后用小括号声明指针指向的Mat类矩阵的哪一行。第二个循环控制用于输出矩阵中每一行所有通道的数据。根据图2-5中所示的存储形式,每一行中存储的数据数量为列数与通道数的乘积,即指针可以向后移动cols*channels()-1位,如第7行代码所示,指针向后移动的位数在中括号给出。程序中给出了循环遍历Mat类矩阵中的每一个数据的方法,当我们能够确定需要访问的数据时,可以直接通过给出行数和指针后移的位数进行访问,例如当读取第2行数据中第3个数据时,可以用a.ptr(1)[2]这样的形式来直接访问。

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

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

指针ptr读取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< >(),与其他迭代器用法相同,通过“++”运算实现指针位置向下迭代,数据的读取方式是先读取第一个元素的每一个通道,之后再读取第二个元素的每一个通道,直到最后一个元素的最后一个通道。

通过矩阵元素地址定位方式访问元素

前面三种读取元素的方式都需要知道Mat类矩阵存储数据的类型,而且在从认知上,我们更希望能够通过声明“第x行第x列第x通道”的方式来读取某个通道内的数据,代码清单2-23中给出的就是这种读取数据的方式。

代码清单2-23 通过矩阵元素地址定位方式访问元素

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

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

C++版本OpenCv教程(五)图像读取函数imread

Excerpt

我们在前面已经见过了图像读取函数imread()的调用方式,这里我们给出函数的原型。cv::Mat cv::imread(const String & filename,int flags=IMREAD_COLOR)filename:需要读取图像的文件名称,包含图像地址、名称和图像文件扩展名flags:读取图像形式的标志,如将彩色图像按照灰度图读取,默认参数是按照彩色图像格式读取,可选参数在表2-3给出。函数用于读取指定的图像并将其返回给一个Mat类变量,如果图像文件不存在、破损或者


我们在前面已经见过了图像读取函数imread()的调用方式,这里我们给出函数的原型。

cv::Mat cv::imread(const String & filename,int  flags=IMREAD_COLOR)
  • filename:需要读取图像的文件名称,包含图像地址、名称和图像文件扩展名
  • flags:读取图像形式的标志,如将彩色图像按照灰度图读取,默认参数是按照彩色图像格式读取,可选参数在表2-3给出。

函数用于读取指定的图像并将其返回给一个Mat类变量,如果图像文件不存在、破损或者格式不受支持时,则无法读取图像,此时函数返回一个空矩阵,因此可以通过判断返回矩阵的data属性是否为空或者empty()函数是否为真来判断是否成功读取图像,如果读取图像失败,data属性返回值为0,empty()函数返回值为1。函数能够读取多种格式的图像文件,但是在不同操作系统由于使用的编解码器不同,因此在某个系统中能够读取的图像文件可能在其他系统中就无法读取。无论在哪个系统中,bmp文件和dib文件都是始终可以读取的,在Windows和Mac系统中,默认情况下使用OpenCV自带的编解码器(libjpeg,libpng,libtiff和libjasper),因此可以读取JPEG(jpg、jpeg、jpe),PNG,TIFF(tiff、tif)文件,在Linux系统中需要自行安装这些编解码器,安装后同样可以读取这些类型的文件。不过需要说明的是,该函数能否读取文件数据与扩展名无关,而是通过文件的内容确定图像的类型,例如将一个扩展名由png修改成exe时,该函数一样可以读取该图像,但是将扩展名exe改成png,该函数不能加载该文件。

该函数第一个参数以字符串形式给出待读取图像的地址,第二个函数是设置读取图像的形式,默认的参数是以彩色图的形式读取,针对不同需求可以更改参数,在OpenCV 4.1中给出了13种模式读取图像的形式,总结起来分别是以原样式读取、灰度图读取、彩色图读取、多位数读取、在读取时将图像缩小一定尺寸等形式读取,具体可选择的参数及作用在表2-3种给出,这里需要指出的是,将彩色图像转成灰度图通过编解码器内部转换,可能会与OpenCV程序中将彩色图像转成灰度图的结果存在差异。这些标志参数在功能不冲突的前提下可以同时声明多个,不同参数之间用“|”隔开。
在这里插入图片描述

C++版本OpenCv教程(六)namedWindow函数&imshow函数的使用

Excerpt

目录图像窗口函数namedWindow图像显示函数imshow图像窗口函数namedWindow在我们之前的程序中并没有见到窗口函数,因为我们在显示图像时如果没有主动定义图像窗口,程序会自动生成一个窗口用于显示图像,然而有时我们需要在显示图像之前对图像窗口进行操作,例如添加滑动条,此时就需要提前创建图像窗口。创建窗口函数的原型。void cv::namedWindow(const String & winname,int flags = WINDOW_AUTOSIZE)winname:


目录

图像窗口函数namedWindow

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

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

该函数会创建一个窗口变量,用于显示图像和滑动条,通过窗口的名称引用该窗口,如果在创建窗口时已经存在具有相同名称的窗口,则该函数不会执行任何操作。创建一个窗口需要占用部分内存资源,因此通过该函数创建窗口后,在不需要窗口时需要关闭窗口来释放内存资源。OpenCV提供了两个关闭窗口资源的函数,分别是cv::destroyWindow()函数和cv :: destroyAllWindows(),通过名称我们可以知道前一个函数是用于关闭一个指定名称的窗口,即在括号内输入窗口名称的字符串即可将对应窗口关闭,后一个函数是关闭程序中所有的窗口,一般用于程序的最后。不过事实上,在一个简单的程序里,我们并不需要调用这些函数,因为程序退出时会自动关闭应用程序的所有资源和窗口。虽然不主动释放窗口也会在程序结束时释放窗口资源,但是OpenCV 4.0版本在结束时会报出没有释放窗口的错误,而OpenCV 4.1版本则不会报错。

该函数的第一个参数是声明窗口的名称,用于窗口的唯一识别,第二个参数是声明窗口的属性,主要用于设置窗口的大小是否可调、显示的图像是否填充满窗口等,具体可选择的参数及含义在表2-4中给出,默认情况下,函数加载的标志参数为“WINDOW_AUTOSIZE | WINDOW_KEEPRATIO | WINDOW_GUI_EXPANDED”。
在这里插入图片描述

图像显示函数imshow

我们在前面已经见过了图像显示函数imshow()的调用方式,这里我们给出函数的原型。

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

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

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

函数中第一个参数为图像显示窗口的名称,第二个参数是需要显示的图像Mat类矩阵。这里需要特殊说明的是,我们看到第二个参数并不是常见的Mat类,而是InputArray,这个是OpenCV定义的一个类型声明引用,用作输入参数的标识,我们在遇到它时可以认为是需要输入一个Mat类数据。同样,OpenCV对输出也定义了OutputArray类型,我们同样可以认为是输出一个Mat类数据。
注意 此函数运行后会继续执行后面程序,如果后面程序执行完直接退出的话,那么显示的图像有可能闪一下就消失了,因此在需要显示图像的程序中,往往会在imshow()函数后跟有cv::waitKey()函数,用于将程序暂停一段时间。waitKey()函数是以毫秒计的等待时长,如果参数缺省或者为“0”表示等待用户按键结束该函数。

C++版本OpenCv教程(七)视频数据的读取&摄像头的直接调用

Excerpt

目录视频数据的读取摄像头的直接调用视频数据的读取虽然视频文件是由多张图片组成的,但是imread()函数并不能直接读取视频文件,需要由专门的视频读取函数进行视频读取,并将每一帧图像保存到Mat类矩阵中,下面的代码中给出了VideoCapture类在读取视频文件时的构造方式。cv :: VideoCapture :: VideoCapture(); //默认构造函数cv :: VideoCapture :: VideoCapture(const String& filename,int api


视频数据的读取

虽然视频文件是由多张图片组成的,但是imread()函数并不能直接读取视频文件,需要由专门的视频读取函数进行视频读取,并将每一帧图像保存到Mat类矩阵中,下面的代码中给出了VideoCapture类在读取视频文件时的构造方式。

cv :: VideoCapture :: VideoCapture(); //默认构造函数
cv :: VideoCapture :: VideoCapture(const String& filename,int apiPreference =CAP_ANY)
  • filename:读取的视频文件或者图像序列名称
  • apiPreference:读取数据时设置的属性,例如编码格式、是否调用OpenNI等,详细参数及含义在表2-5给出。

该函数是构造一个能够读取与处理视频文件的视频流,在代码清单2-27中的第一行是VideoCapture类的默认构造函数,只是声明了一个能够读取视频数据的类,具体读取什么视频文件,需要在使用时通过open()函数指出,例如cap.open(“1.avi”)是VideoCapture类变量cap读取1.avi视频文件。

第二种构造函数在给出声明变量的同时也将视频数据赋值给变量。可以读取的文件种类包括视频文件(例如video.avi)、图像序列或者视频流的URL。其中读取图像序列需要将多个图像的名称统一为“前缀+数字”的形式,通过“前缀+%02d”的形式调用,例如在某个文件夹中有图片img_00.jpg、img_01.jpg、img_02.jpg……加载时文件名用img_%02d.jpg表示。函数中的读取视频设置属性标签默认的是自动搜索合适的标志,所以在平时使用中,可以将其缺省,只需要输入视频名称即可。与imread()函数一样,构造函数同样有可能读取文件失败,因此需要通过isOpened()函数进行判断,如果读取成功则返回值为true,如果读取失败,则返回值为false。

通过构造函数只是将视频文件加载到了VideoCapture类变量中,当我们需要使用视频中的图像时,还需要将图像由VideoCapture类变量里导出到Mat类变量里,用于后期数据处理,该操作可以通过“>>”运算符将图像按照视频顺序由VideoCapture类变量复制给Mat类变量。当VideoCapture类变量中所有的图像都赋值给Mat类变量后,再次赋值的时候Mat类变量会变为空矩阵,因此可以通过empty()判断VideoCapture类变量中是否所有图像都已经读取完毕。

VideoCapture类变量同时提供了可以查看视频属性的get()函数,通过输入指定的标志来获取视频属性,例如视频的像素尺寸、帧数、帧率等,常用标志和含义在表2-5中给出。
在这里插入图片描述

#include<iostream>
//#include <stdio.h>
#include <opencv2/opencv.hpp>
#include "opencv/highgui.h"

using namespace std;
using namespace cv;

int main(int argc,char** argv) {
    cout<<"OpenCv Version: "<<CV_VERSION<<endl;
    VideoCapture video("/home/wyh/Documents/C++demo/project_video.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)<<endl;
    }
    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;
}

读取视频程序运行结果读取视频程序运行结果

摄像头的直接调用

VideoCapture类还可以调用摄像头,构造方式如代码清单2-29中所示。

cv :: VideoCapture :: VideoCapture(int index,int apiPreference = CAP_ANY)

调用摄像头与读取视频文件相比,只有第一个参数不同。调用摄像头时,第一个参数为要打开的摄像头设备的ID,ID的命名方式从0开始。从摄像头中读取图像数据的方式与从视频中读取图像数据的方式相同,通过“>>”符号读取当前时刻相机拍摄到的图像。并且读取视频时VideoCapture类具有的属性同样可以使用。

C++版本OpenCv教程(八)图像的保存&视频的保存

Excerpt

目录图像的保存视频的保存图像的保存OpenCV提供imwrite()函数用于将Mat类矩阵保存成图像文件,该函数的函数原型在代码清单2-30中给出。bool cv :: imwrite(const String& filename,InputArray img,Const std::vector& params = std::vector())filename:保存图像的地址和文件名,包含图像格式img:将要保存的Mat类矩阵变量p


图像的保存

OpenCV提供imwrite()函数用于将Mat类矩阵保存成图像文件,该函数的函数原型在代码清单2-30中给出。

bool cv :: imwrite(const String& filename,InputArray img,Const std::vector<int>& params = std::vector<int>())
  • filename:保存图像的地址和文件名,包含图像格式
  • img:将要保存的Mat类矩阵变量
  • params:保存图片格式属性设置标志

该函数用于将Mat类矩阵保存成图像文件,如果成功保存,则返回true,否则返回false。可以保存的图像格式参考imread()函数能够读取的图像文件格式,通常使用该函数只能保存8位单通道图像和3通道BGR彩色图像,但是可以通过更改第三个参数保存成不同格式的图像。不同图像格式能够保存的图像位数如下:

  • 16位无符号(CV_16U)图像可以保存成PNG、JPEG、TIFF格式文件;
  • 32位浮点(CV_32F)图像可以保存成PFM、TIFF、OpenEXR和Radiance HDR格式文件;
  • 4通道(Alpha通道)图像可以保存成PNG格式文件。

函数第三个参数在一般情况下不需要填写,保存成指定的文件格式只需要直接在第一个参数后面更改文件后缀即可,但是当需要保存的Mat类矩阵中数据比较特殊时(如16位深度数据),则需要设置第三个参数。第三个参数的设置方式如下所示

vector <int> compression_params;
compression_params.push_back(IMWRITE_PNG_COMPRESSION);
compression_params.push_back(9);
imwrite(filename, img, compression_params)

在这里插入图片描述
为了更好的理解imwrite()函数的使用方式,在代码清单2-32中给出了生成带有Alpha通道的矩阵,并保存成PNG格式图像的程序。程序运行后会生成一个保存了4通道的png格式图像,为了更直观的看到图像结果,我们在图2-8中给出了Image Watch插件中看到的图像和保存成png格式的图像。

#include<iostream>
#include<vector>
//#include <stdio.h>
#include <opencv2/opencv.hpp>
#include "opencv/highgui.h"

using namespace std;
using namespace cv;

void AlphaMat(Mat &mat){
    CV_Assert(mat.channels()==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 argc,char** argv) {
    cout<<"OpenCv Version: "<<CV_VERSION<<endl;
    Mat mat(480,640,CV_8UC4);
    AlphaMat(mat);
    vector<int>comprocession_params;
    comprocession_params.push_back(IMWRITE_PNG_COMPRESSION);//PNG格式图像压缩标志
    comprocession_params.push_back(9);//设置最高压缩质量
    bool result=imwrite("alpha.png",mat,comprocession_params);
    if(!result){
        cout<<"保存成PNG格式图像失败"<<endl;
        return -1;
    }
    cout<<"保存成功"<<endl;
    imshow("alpha.png",mat);
    waitKey(0);
    return 0;
}

程序中和保存后的四通道图像(左:Image Watc, 右::png文件)

视频的保存

有时我们需要将多幅图像生成视频,或者直接将摄像头拍摄到的数据保存成视频文件。OpenCV中提供了VideoWrite()类用于实现多张图像保存成视频文件,该类构造函数的原型在代码清单2-33中给出。

cv :: VideoWriter :: VideoWriter(); //默认构造函数
cv :: VideoWriter :: VideoWriter(const String& filename,
                                       int fourcc,
                                       double  fps,
                                       Size frameSize,
                                       bool  isColor=true
                                       )
  • filename:保存视频的地址和文件名,包含视频格式
  • fourcc:压缩帧的4字符编解码器代码,详细参数在表2-7给出。
  • fps:保存视频的帧率,即视频中每秒图像的张数。
  • framSize:视频帧的尺寸
  • isColor:保存视频是否为彩色视频

代码清单2-33中的第1行默认构造函数的使用方法与VideoCapture()相同,都是创建一个用于保存视频的数据流,后续通过open()函数设置保存文件名称、编解码器、帧数等一系列参数。第二种构造函数需要输入的第一个参数是需要保存的视频文件名称,第二个函数是编解码器的代码,可以设置的编解码器选项在表中给出,如果赋值“-1”则会自动搜索合适的编解码器,需要注意的是其在OpenCV 4.0版本和OpenCV 4.1版本中的输入方式有一些差别,具体差别在表2-7给出。第三个参数为保存视频的帧率,可以根据需求自由设置,例如实现原视频二倍速播放、原视频慢动作播放等。第四个参数是设置保存的视频文件的尺寸,这里需要注意的时,在设置时一定要与图像的尺寸相同,不然无法保存视频。最后一个参数是设置保存的视频是否是彩色的,程序中,默认的是保存为彩色视频。

该函数与VideoCapture()有很大的相似之处,都可以通过isOpened()函数判断是否成功创建一个视频流,可以通过get()查看视频流中的各种属性。在保存视频时,我们只需要将生成视频的图像一帧一帧通过“<<”操作符(或者write()函数)赋值给视频流即可,最后使用release()关闭视频流。
在这里插入图片描述
为了更好的理解VideoWrite()类的使用方式,代码清单2-34中给出了利用已有视频文件数据或者直接通过摄像头生成新的视频文件的例程。读者需要重点体会VideoWrite()类和VideoCapture()类的相似之处和使用时的注意事项。

#include<iostream>
#include<vector>
//#include <stdio.h>
#include <opencv2/opencv.hpp>
#include "opencv/highgui.h"

using namespace std;
using namespace cv;

int main(int argc,char** argv) {
    cout<<"OpenCv Version: "<<CV_VERSION<<endl;
    Mat img;
    VideoCapture video(0);//使用某个摄像头
    //读取视频
    //VideoCapture video
    //video.open("cpu.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 coder=VideoWriter::fourcc('M','J','P','G');//选择编码格式

    double fps=25.0;//设置视频帧率
    string filename="live.avi";//保存的视频文件名称
    writer.open(filename,coder,fps,img.size(),isColor);//创建保存视频文件的视频流
    if(!writer.isOpened()){
        cout<<"打开视频文件失败,请确认是否为合法输入"<<endl;
        return -1;
    }
    while (1){
        //检测是否执行完毕
        if(!video.read(img)){
            cout<<"摄像头断开连接或者视频读取完成"<<endl;
            break;
        }
        writer.write(img);//把图像写入视频流
        imshow("Live",img);//显示图像
        char c=waitKey(50);
        if(c==27){//按ESC键突出视频保存
            break;
        }
    }
    video.release();
    writer.release();
    return 0;
}

C++版本OpenCv教程(九)保存和读取XML和YMAL文件

Excerpt

除了图像数据之外,有时程序中的尺寸较小的Mat类矩阵、字符串、数组等数据也需要进行保存,这些数据通常保存成XML文件或者YAML文件。本小节中将介绍如何利用OpenCV 4中的函数将数据保存成XML文件或者YAML文件以及如何读取这两种文件中的数据。XML是一种元标记语言,所谓元标记就是使用者可以根据自身需求定义自己的标记,例如可以用、等标记来定义数据的含义,例如用24来表示age数据的数值为24。XML是一种结构化的语言,通过XML语言可以知道数据之间的隶属关系,例如100150表示在color数据中


除了图像数据之外,有时程序中的尺寸较小的Mat类矩阵、字符串、数组等

数据也需要进行保存,这些数据通常保存成XML文件或者YAML文件。本小节中将介绍如何利用OpenCV 4中的函数将数据保存成XML文件或者YAML文件以及如何读取这两种文件中的数据。

XML是一种元标记语言,所谓元标记就是使用者可以根据自身需求定义自己的标记,例如可以用、等标记来定义数据的含义,例如用24来表示age数据的数值为24。XML是一种结构化的语言,通过XML语言可以知道数据之间的隶属关系,例如100150表示在color数据中含有两个名为red和blue的数据,两者的数值分别是100和150。通过标记的方式,无论以任何形式保存数据,只要文件满足XML格式,那么读取出来的数据就不会出现混淆和歧义。XML文件的扩展名是“.xml”。

YMAL是一种以数据为中心的语言,通过“变量:数值”的形式来表示每个数据的数值,通过不同的缩进来表示不同数据之间的结构和隶属关系。YMAL可读性高,常用来表达资料序列的格式,它参考了多种语言,包括XML、C语言、Python、Perl等。YMAL文件的扩展名是“.ymal”或者“.yml”。

OpenCV 4中提供了用于生成和读取XML文件和YMAL文件的FileStorage类,类中定义了初始化类、写入数据和读取数据等方法。我们在使用该FileStorage类时首先需要对其进行初始化,初始化可以理解为声明需要操作的文件和操作类型。OpenCV 4提供了两种初始化的方法,分别是不输入任何参数的初始化(可以理解为只定义,并未初始化)和输入文件名称和操作类型的初始化。后者初始化构造函数的函数原型在代码清单2-35中给出。

cv::FileStorage::FileStorage(const String & filename,
                                  int  flags,
                                  const String & encoding = String()
                                  )
  • filename:打开的文件名称。
  • flags:对文件进行的操作类型标志,常用参数及含义在表2-8给出。
  • encodin:编码格式,目前不支持UTF-16 XML编码,需要使用UTF-8 XML编码。
    FileStorage()构造函数中对文件操作类型常用标志及含义
    该函数是FileStorage类的构造函数,用于声明打开的文件名称和操作的类型。函数第一个参数是打开的文件名称,参数是字符串类型,文件的扩展名是“.xml”、“.ymal”或者“.yml”。打开得文件可以已经存在或者未存在,但是当对文件进行读取操作时需要是已经存在的文件。第二个参数是对文件进行的操作类型标志,例如对文件进行读取操作、写入操作等,常用参数及含义在表2-8给出,由于该标志量在FileStorage类中,因此在使用时需要加上类名作为前缀,例如“FileStorage::WRITE”。最后一个参数是文件的编码格式,目前不支持UTF-16 XML编码,需要使用UTF-8 XML编码,通常情况下使用该参数的默认值即可。

打开文件后,可以通过FileStorage类中的isOpened()函数判断是否成功打开文件,如果成功打开文件,该函数返回true,如果打开文件失败,则该函数返回false。

FileStorage类中默认构造函数没有任何参数,因此没有声明打开的文件和操作的类型,此时需要通过FileStorage类中的open()函数单独进行声明。

virtual bool cv::FileStorage::open(const String & filename,
                                         int  flags,
                                         const String & encoding = String()
                                         )
  • filename:打开的文件名称。
  • flags:对文件进行的操作类型标志,常用参数及含义在表2-8给出。
  • encodin:编码格式,目前不支持UTF-16 XML编码,需要使用UTF-8 XML编码。

该函数补充了默认构造函数没有声明打开文件的缺点,函数可以指定FileStorage类打开的文件,如果成功打开文件,则返回值为true,否则为false。函数中所有的参数及含义都与代码清单2-35中的相同,因此这里不再进行赘述。同样,通过该函数打开文件后仍然可以通过FileStorage类中的isOpened()函数判断是否成功打开文件。

打开文件后,类似C++中创建的数据流,可以通过“<<”操作符将数据写入文件中,或者通过“>>”操作符从文件中读取数据。除此之外,还可以通过FileStorage类中的write()函数将数据写入文件中,该函数的函数原型在代码清单2-37中给出。

void cv::FileStorage::write(const String & name,
                   int  val
              )
  • name:写入文件中的变量名称。
  • val:变量值。

该函数能够将不同数据类型的变量名称和变量值写入到文件中。该函数的第一个参数是写入文件中的变量名称。第二个参数是变量值,代码清单2-37中的变量值是int类型,但是在FileStorage类中提供了write()函数的多个重载函数,分别用于实现将double、String、Mat、vector类型的变量值写入到文件中。

使用操作符向文件中写入数据时与write()函数类似,都需要声明变量名和变量值,例如变量名为“age”,变量值为“24”,可以通过“file<<”age”<<24”来实现。如果某个变量的数据是一个数组,可以用“[]”将属于同一个变量的变量值标记出来,例如“file<<”age”<<“[”<<24<<25<<”]””。如果某些变量隶属于某个变量,可以用“{}”表示变量之间的隶属关系,例如“file<<”age”<<“{”<<”Xiaoming”<<24<<”Wanghua”<<25<<”}””。

读取文件中的数据时,只需要通过变量名就可以读取变量值。例如“file [“x”] >> xRead”是读取变量名为x的变量值。但是,当某个变量中含有多个数据或者含有子变量时,就需要通过FileNode节点类型和迭代器FileNodeIterator进行读取,例如某个变量的变量值是一个数组,首先需要定义一个file [“age”]的FileNode节点类型变量,之后通过迭代器遍历其中的数据。另外一种方法可以不使用迭代器,通过在变量后边添加“[]”地址的形式读取数据,例如FileNode[0]表示数组变量中的第一个数据,FileNode[“Xiaoming”]表示“age”变量中的“Xiaoming”变量的数据,依次向后添加“[]”地址实现多节点数据的读取。

为了了解如何生成和读取XML文件和YMAL文件,在代码清单2-38中给出了实现文件写入和读取的示例程序。程序中使用write()函数和“<<”操作符两种方式向文件中写入数据,使用迭代器和“[]”地址两种方式从文件中读取数据。数据的写入和读取方法在前面已经介绍,在代码清单2-38中需要重点了解如何通过程序实现写入与读取。程序生成的XML文件和YMAL文件中的数据在图2-10给出,读取文件数据的结果在图2-9给出。

#include<iostream>
#include<vector>
#include<string>
//#include <stdio.h>
#include <opencv2/opencv.hpp>
#include "opencv/highgui.h"

using namespace std;
using namespace cv;

int main(int argc,char** argv) {
    cout<<"OpenCv Version: "<<CV_VERSION<<endl;
    string filename="datas.yaml";//文件的名称
    FileStorage fwriter(filename,FileStorage::WRITE);

    //存入矩阵Mat类型的数据
    Mat mat=Mat::eye(3,3,CV_8U);
    fwriter.write("mat",mat);//使用write()函数写入数据
    //存入浮点型数据,节点名称为x
    float x=100;
    fwriter<<"x"<<x;
    //存入字符串类型数据,节点名称为str
    string str="Learn OpenCV 4";
    fwriter<<"str"<<str;
    //存入数组,节点名称为number_array
    fwriter<<"number_array"<<"["<<4<<5<<6<<"]";
    //存入多node节点数据,主名为multi_nodes
    fwriter<<"multi_nodes"<<"{"<<"month"<<8<<"day"<<28<<"year"
           <<2020<<"time"<<"["<<0<<1<<2<<3<<"]"<<"}";
    //关闭文件
    fwriter.release();

    //以读取的模式打开文件
    FileStorage fread(filename,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 = "<<mat<<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;
    cout<<" month = "<<month<<" day = "<<day<<" year = "<<year<<" time=[";
    for(int i=0;i<4;++i){
        int a=(int)fileNode1["time"][i];
        cout<<a<<" ";
    }
    cout<<"]"<<endl;

    //关闭文件
    fread.release();
    return 0;
}

在这里插入图片描述
在这里插入图片描述

C++版本OpenCv教程(十)颜色模型与转换

Excerpt

目录RGB颜色模型YUV颜色模型RGB颜色模型前面对于RGB颜色模型已经有所介绍,该模型的命名方式是采用三种颜色的英文首字母组成,分别是红色(Red)、绿色(Green)和蓝色(Blue)。虽然该颜色模型的命名方式是红色在前,但是在OpenCV中却是相反的顺序,第一个通道时蓝色(B)分量,第二个通道时绿色(G)分量,第三个通道时红色(R)分量。根据存储顺序的不同,OpenCV 4中提供了这种顺序的反序格式,用于存储第一个通道是红色分量的图像,但是这两种格式的图像的颜色空间是相同的,颜色空间如图3-1所示


目录

RGB颜色模型

前面对于RGB颜色模型已经有所介绍,该模型的命名方式是采用三种颜色的英文首字母组成,分别是红色(Red)、绿色(Green)和蓝色(Blue)。虽然该颜色模型的命名方式是红色在前,但是在OpenCV中却是相反的顺序,第一个通道时蓝色(B)分量,第二个通道时绿色(G)分量,第三个通道时红色(R)分量。根据存储顺序的不同,OpenCV 4中提供了这种顺序的反序格式,用于存储第一个通道是红色分量的图像,但是这两种格式的图像的颜色空间是相同的,颜色空间如图3-1所示。三个通道对于颜色描述的范围是相同的,因此RGB颜色模型的空间构成是一个立方体。在RGB颜色模型中,所有的颜色都是由这三种颜色通过不同比例的混合得到,如果三种颜色分量都为0,则表示为黑色,如果三种颜色的分量相同且都为最大值,则表示为白色。每个通道都表示某一种颜色由0到1的过程,不同位数的图像表示将这个颜色变化过程细分成不同的层级,例如8U3C格式的图像每个通道将这个过程量化成256个等级,分别由0到255表示。在这个模型的基础上增加第四个通道即为RGBA模型,第四个通道表示颜色的透明度,当没有透明度需求的时候,RGBA模型就会退化成RGB模型。
在这里插入图片描述

YUV颜色模型

YUV模型是电视信号系统所采用的颜色编码方式。这三个变量分别表示是像素的亮度(Y)以及红色分量与亮度的信号差值(U)和蓝色与亮度的差值(V)。这种颜色模型主要用于视频和图像的传输,该模型的产生与电视机的发展历程密切相关。由于彩色电视机在黑白电视机发明之后才产生,因此用于彩色电视机的视频信号需要能够兼容黑白电视机。彩色电视机需要三个通道的数据才能显示彩色,而黑白电视机只需要一个通道的数据即可,因此为了使视频信号能够兼容彩色电视与黑白电视,将RGB编码方式转变成YUV的编码方式,其Y通道是图像的亮度,黑白电视只需要使用该通道就可以显示黑白视频图像,而彩色相机通过将YUV编码转成RGB编码方式,便可以在彩色电视种显示彩色图像,较好的解决了同一个视频信号兼容不同类型电视的问题。RGB模型与YUV模型之间的转换关系如式所示,其中RGB取值范围均为0-255。
在这里插入图片描述

HSV颜色模型

HSV是色度(Hue)、饱和度(Saturation)和亮度(Value)的简写,通过名字也可以看出来该模型通过这三个特性对颜色进行描述。色度是色彩的基本属性,就是平时常说的颜色,例如红色,蓝色等;饱和度是指颜色的纯度,饱和度越高色彩越纯越艳,饱和度越低色彩则逐渐地变灰变暗,饱和度的取值范围是由0到100%;亮度是颜色的明亮程度,其取值范围由0到计算机中允许的最大值。由于色度、饱和度和亮度的取值范围不同,因此其颜色空间模型用锥形表示,其形状如图3-2所示。相比于RGB模型三个颜色分量与最终颜色联系不直观的缺点,HSV模型更加符合人类感知颜色的方式:颜色、深浅以及亮暗。
在这里插入图片描述

Lab颜色模型

Lab颜色模型弥补了RGB模型的不足,是一种设备无关的颜色模型,是一种基于生理特征的颜色模型。在模型中L表示亮度(Luminosity),a和b是两个颜色通道,两者的取值区间都是由-128到+127,其中a通道数值由小到大对应的颜色是从绿色变成红色,b通道数值由小到大对应的颜色是由蓝色变成黄色。其构成的颜色空间是一个球形,形式如图3-3所示。
在这里插入图片描述

GRAY颜色模型

GRAY模型并不是一个彩色模型,他是一个灰度图像的模型,其命名使用的是英文单词gray的全字母大写。灰度图像只有单通道,灰度值根据图像位数不同由0到最大依次表示由黑到白,例如8UC1格式中,由黑到白被量化成了256个等级,通过0-255表示,其中255表示白色。彩色图像具有颜色丰富、信息含量大的特性,但是灰度图在图像处理中依然具有一定的优势。例如,灰度图像具有相同尺寸相同压缩格式所占容量小,易于采集,便于传输等优点。常用的RGB模型转成灰度图的方式如式中所示。
在这里插入图片描述

不同颜色模型间的互相转换

针对图像不同颜色模型之间的相互转换,OpenCV 4提供了cvtColor()函数用于实现转换功能,该函数的函数原型如下所示:

void cv::cvtColor(InputArray src,
                  OutputArray dst,
                  int code,
                  int dstCn = 0)
  • src:待转换颜色模型的原始图像。
  • dst:转换颜色模型后的目标图像。
  • code:颜色空间转换的标志,如由RGB空间到HSV空间。常用标志及含义在表3-1中给出。
  • dstCn:目标图像中的通道数,如果参数为0,则从src和代码中自动导出通道数。

函数用于将图像从一个颜色模型转换为另一个颜色模型,前两个参数用于输入待转换图像和转换颜色空间后目标图像,第三个参数用于声明该函数具体的转换模型空间。第四个参数在一般情况下不需要特殊设置,使用默认参数即可。需要注意的是该函数变换前后的图像取值范围,由于8位无符号图像的像素由0到255,16位无符号图像的像素由0-65535,而32位浮点图像的像素是由0到1,因此一定要注意目标图像的像素范围。在线性变换的情况下,范围问题不需要考虑,目标图像的像素不会超出范围。如果在非线性变换的情况下,应将输入RGB图像归一化到适当的范围以内获得正确的结果,例如将8位无符号图像转成32位浮点图像,需要先将图像像素通过除以255缩放到0到1范围内,以防止产生错误结果。
【注意】如果转换过程中添加了alpha通道(RGB模型中第四个通道,表示透明度),则其值将设置为相应通道范围的最大值:CV_8U为255,CV_16U为65535,CV_32F为1
在这里插入图片描述
为了直观的感受同一张图像在不同颜色空间中的样子,在代码清单3-2中给出了前面几种颜色模型互相转换的程序,运行结果如图3-4所示。需要说明的是Lab颜色模型具有负数,而通过imshow()函数显示的图像无法显示负数,因此在结果中给出了Image Watch插件显示图像在Lab模型中的样子。在程序中,我们为了防止转换后出现数值越界的情况,先将CV_8U类型转成CV_32F类型后再进行颜色模型的转换。

#include<iostream>
#include<vector>
#include<string>
#include <opencv2/opencv.hpp>
#include "opencv/highgui.h"

using namespace std;
using namespace cv;

int main(int argc,char** argv) {
    cout<<"OpenCv Version: "<<CV_VERSION<<endl;
    Mat img=imread("/home/wyh/Documents/C++demo/699342568.jpg");
    if(img.empty()){
        cout<<"请确认图像文件名称是否正确"<<endl;
        return -1;
    }
    Mat dst;
    resize(img,dst,Size(img.cols*0.5,img.rows*0.5));
    Mat gray,HSV,YUV,Lab,img32;
    dst.convertTo(img32,CV_32F,1.0/255);//将CV_8U类型转换成CV_32F类型
    cvtColor(img32,HSV,COLOR_BGR2HSV);
    cvtColor(img32,YUV,COLOR_BGR2YUV);
    cvtColor(img32,Lab,COLOR_BGR2Lab);
    cvtColor(img32,gray,COLOR_BGR2GRAY);
    imshow("原图",dst);
    imshow("HSV",HSV);
    imshow("YUV",YUV);
    imshow("Lab",Lab);
    imshow("gray",gray);
    waitKey(10000);
    return 0;
}

在这里插入图片描述

C++版本OpenCv教程(十一)多通道分离与合并

Excerpt

在图像颜色模型中不同的分量存放在不同的通道中,如果我们只需要颜色模型的某一个分量,例如只需要处理RGB图像中的红色通道,可以将红色通道从三通道的数据中分离出来再进行处理,这种方式可以减少数据所占据的内存,加快程序的运行速度。同时,当我们分别处理完多个通道后,需要将所有通道合并在一起重新生成RGB图像。针对图像多通道的分离与混合,OpenCV 4中提供了split()函数和merge()函数用于解决这些需求。多通道分离函数split()OpenCV 4中针对多通道分离函数split()有两种重载原型,在代


在图像颜色模型中不同的分量存放在不同的通道中,如果我们只需要颜色模型的某一个分量,例如只需要处理RGB图像中的红色通道,可以将红色通道从三通道的数据中分离出来再进行处理,这种方式可以减少数据所占据的内存,加快程序的运行速度。同时,当我们分别处理完多个通道后,需要将所有通道合并在一起重新生成RGB图像。针对图像多通道的分离与混合,OpenCV 4中提供了split()函数和merge()函数用于解决这些需求。

多通道分离函数split()

OpenCV 4中针对多通道分离函数split()有两种重载原型,在代码清单3-4中给出了这两种函数原型。

void cv::split(const Mat & src,
                 Mat * mvbegin
                 )
void cv::split(InputArray m,
                 OutputArrayOfArrays mv
                 )
  • src:待分离的多通道图像。
  • mvbegin:分离后的单通道图像,为数组形式,数组大小需要与图像的通道数相同
  • m:待分离的多通道图像
  • mv:分离后的单通道图像,为向量vector形式
    该函数主要是用于将多通道的图像分离成若干单通道的图像,两个函数原型中不同之处在于前者第二个参数输入的是Mat类型的数组,其数组的长度需要与多通道图像的通道数相等并且提前定义;第二种函数原型的第二个参数输入的是一个vector容器,不需要知道多通道图像的通道数。两个函数原型虽然输入参数的类型不同,但是通道分离的原理是相同的,可以用公式(3.4)表示。
    在这里插入图片描述

多通道合并函数merge()

OpenCV 4中针对多通道合并函数merge()也有两种重载原型,在代码清单3-5中给出了两种原型。

void cv::merge(const Mat * mv,
                  size_t  count,
                  OutputArray dst
                 ) 
void cv::merge(InputArrayOfArrays mv,
                  OutputArray dst
                 )
  • mv:需要合并的图像数组,其中每个图像必须拥有相同的尺寸和数据类型。
  • count:输入的图像数组的长度,其数值必须大于0.
  • mv:需要合并的图像向量vector,其中每个图像必须拥有相同的尺寸和数据类型。
  • dst:合并后输出的图像,与mv[0]具有相同的尺寸和数据类型,通道数等于所有输入图像的通道数总和。

该函数主要是用于将多个图像合并成一个多通道图像,该函数也具有两种不同的函数原型,每一种函数原型都是与split()函数相对应,两种原型分别输入数组形式的图像数据和向量vector形式的图像数据,在输入数组形式数据的原型中,还需要输入数组的长度。合并函数的输出结果是一个多通道的图像,其通道数目是所有输入图像通道数目的总和。这里需要说明的是,用于合并的图像并非都是单通道的,也可以是多个通道数目不相同的图像合并成一个通道更多的图像,虽然这些图像的通道数目可以不相同,但是需要所有图像具有相同的尺寸和数据类型。

图像多通道分离与合并例程

为了使读者更加熟悉图像多通道分离与合并的操作,同时加深对图像不同通道作用的理解,在代码清单3-6中实现了图像的多通道分离与合并的功能。程序中用两种函数原型分别分离了RGB图像和HSV图像,为了验证merge ()函数可以合并多个通道不相同的图像,程序中分别用两种函数原型合并了多个不同通道的图像,合并后图像的通道数为5,不能通过imshow()函数显示,我们用Image Watch插件查看了合并的结果。由于RGB三个通道分离结果显示时都是灰色且相差不大,因此图3-5没有给出其分离后的结果,只给出合并后显示为绿色的合并图像,同时给出HSV分离结果,其他结果读者可以自行运行程序查看。

#include<iostream>
#include<vector>
#include<string>
#include <opencv2/opencv.hpp>
#include "opencv/highgui.h"

using namespace std;
using namespace cv;

int main(int argc,char** argv) {
    cout<<"OpenCv Version: "<<CV_VERSION<<endl;
    Mat img=imread("/home/wyh/Documents/C++demo/699342568.jpg");
    if(img.empty()){
        cout<<"请确认输入图片的名称是否正确"<<endl;
        return -1;
    }
    Mat HSV,dst;
    resize(img,dst,Size(img.cols*0.5,img.rows*0.5));
    cvtColor(dst,HSV,COLOR_BGR2HSV);
    Mat imgs0,imgs1,imgs2;//用于存放数组类型的结果
    Mat imgv0,imgv1,imgv2;//用于存放vector类型的结果
    Mat result0,result1,result2;//多通道合并的结果

    //输入数组参数的多通道分离与合并
    Mat imgs[3];
    split(dst,imgs);
    imgs0=imgs[0];
    imgs1=imgs[1];
    imgs2=imgs[2];
    imshow("RGB-R通道",imgs0);//显示分离后R通道的像素值
    imshow("RGB-G通道",imgs1);//显示分离后G通道的像素值
    imshow("RGB-B通道",imgs2);//显示分离后B通道的像素值
    imgs[2]=dst;//将数组中的图像通道数变成不统一
    merge(imgs,3,result0);//合并图像

    Mat zero=Mat::zeros(dst.rows,dst.cols,CV_8UC1);
    imgs[0]=zero;
    imgs[2]=zero;
    merge(imgs,3,result1);//用于还原G通道的真实情况,合并结果为绿色
    imshow("result1",result1);//显示合并结果

    //输入vector参数的多通道分离与合并
    vector<Mat>imgv;
    split(HSV,imgv);
    imgv0=imgv.at(0);
    imgv1=imgv.at(1);
    imgv2=imgv.at(2);
    imshow("HSV-H通道",imgv0);//显示分离后H通道的像素值
    imshow("HSV-S通道",imgv1);//显示分离后S通道的像素值
    imshow("HSV-V通道",imgv2);//显示分离后V通道的像素值
    imgv.push_back(HSV);//将vector中的图像通道数变成不统一
    merge(imgv,result2);//合并图像
    waitKey(0);
    return 0;
}

在这里插入图片描述

C++版本OpenCv教程(十二)图像像素统计

Excerpt

我们可以将数字图像理解成一定尺寸的矩阵,矩阵中每个元素的大小表示了图像中每个像素的亮暗程度,因此统计矩阵中的最大值,就是寻找图像中灰度值最大的像素,计算平均值就是计算图像像素平均灰度,可以用来表示图像整体的亮暗程度。因此针对矩阵数据的统计工作在图像像素中同样具有一定的意义和作用。在OpenCV 4中集成了求取图像像素最大值、最小值、平均值、均方差等众多统计量的函数,接下来将详细介绍这些功能的相关函数。寻找图像像素最大值与最小值OpenCV 4提供了寻找图像像素最大值、最小值的函数minMaxLoc(),


我们可以将数字图像理解成一定尺寸的矩阵,矩阵中每个元素的大小表示了图像中每个像素的亮暗程度,因此统计矩阵中的最大值,就是寻找图像中灰度值最大的像素,计算平均值就是计算图像像素平均灰度,可以用来表示图像整体的亮暗程度。因此针对矩阵数据的统计工作在图像像素中同样具有一定的意义和作用。在OpenCV 4中集成了求取图像像素最大值、最小值、平均值、均方差等众多统计量的函数,接下来将详细介绍这些功能的相关函数。

寻找图像像素最大值与最小值

OpenCV 4提供了寻找图像像素最大值、最小值的函数minMaxLoc(),该函数的原型在代码清单3-7中给出。

void cv::minMaxLoc(InputArray src,
                   double * minVal,
                   double * maxVal = 0,
                   Point * minLoc = 0,
                   Point * maxLoc = 0,
                   InputArray mask = noArray())
  • src:需要寻找最大值和最小值的图像或者矩阵,要求必须是单通道矩阵
  • minVal:图像或者矩阵中的最小值。
  • maxVal:图像或者矩阵中的最大值。
  • minLoc:图像或者矩阵中的最小值在矩阵中的坐标。
  • maxLoc:图像或者矩阵中的最大值在矩阵中的坐标。
  • mask:掩模,用于设置在图像或矩阵中的指定区域寻找最值。
    这里我们见到了一个新的数据类型Point,该数据类型是用于表示图像的像素坐标,由于图像的像素坐标轴以左上角为坐标原点,水平方向为x轴,垂直方向为y轴,因此Point(x,y)对应于图像的行和列表示为Point(列数,行数)。在OpenCV中对于2D坐标和3D坐标都设置了多种数据类型,针对2D坐标数据类型定义了整型坐标cv::Point2i(或者cv::Point)、double型坐标cv::Point2d、浮点型坐标cv::Point2f,对于3D坐标同样定义了上述的坐标数据类型,只需要将其中的数字“2”变成“3”即可。对于坐标中x、y、z轴的具体数据,可以通过变量的x、y、z属性进行访问,例如Point.x可以读取坐标的x轴数据。

该函数实现的功能是寻找图像中特定区域内的最值,函数第一个参数是输入单通道矩阵,需要注意的是,该变量必须是一个单通道的矩阵数据,如果是多通道的矩阵数据,需要用cv::Mat::reshape()将多通道变成单通道,或者分别寻找每个通道的最值,然后再进行比较寻找到全局最值。对于cv::Mat::reshape()的用法,在代码清单3-8中给出。第二到第五个参数分别是指向最小值、最大值、最小值位置和最大值位置的指针,如果不需要寻找某一个参数,可以将该参数设置为NULL,函数最后一个参数是寻找最值得掩码矩阵,用于标记寻找上述四个值的范围,参数默认值为noArray(),表示寻找范围是矩阵中所有数据。

Mat cv::Mat::reshape(int  cn,int  rows = 0)
  • cn:转换后矩阵的通道数。
  • rows:转换后矩阵的行数,如果参数为零,则转换后行数与转换前相同。
    注意
    如果矩阵中存在多个最大值或者最小值时,minMaxLoc()函数输出最值的位置为按行扫描从左向右第一次检测到最值的位置,同时输入参数时一定要注意添加取地址符。
    为了让读者更加了解minMaxLoc()函数的原理和使用方法,在代码清单3-9中给出寻找矩阵最值的示例程序,在图3-6中给出了程序运行的最终结果,在图3-7给出了创建的两个矩阵和通道变换后的矩阵在Image Watch中查看的内容。
#include<iostream>
#include<vector>
#include<string>
#include <opencv2/opencv.hpp>
#include "opencv/highgui.h"

using namespace std;
using namespace cv;

int main(int argc,char** argv) {
    cout<<"OpenCv Version: "<<CV_VERSION<<endl;
    float a[12]={1,2,3,4,5,10,6,7,8,9,10,0};
    Mat img=Mat(3,4,CV_32FC1,a);//单通道矩阵
    Mat imgs=Mat(2,3,CV_32FC2,a);//多通道矩阵
    double minVal,maxVal;//用于存放矩阵中的最大值和最小值
    Point minIdx,maxIdx;//用于存放矩阵中的最大值和最小值的位置

    /*寻找单通道矩阵中的最值*/
    minMaxLoc(img,&maxVal,&minVal,&minIdx,&maxIdx);
    cout << "img中最大值是:" << maxVal << " " << "在矩阵中的位置:" << maxIdx << endl;
    cout << "img中最小值是:" << minVal << " " << "在矩阵中的位置:" << minIdx << endl;

    /*寻找多通道矩阵中的最值*/
    Mat imgs_re=imgs.reshape(1,4);//将多通道矩阵变成单通道矩阵
    minMaxLoc(imgs_re,&minVal,&maxVal,&minIdx,&maxIdx);
    cout << "img中最大值是:" << maxVal << " " << "在矩阵中的位置:" << maxIdx << endl;
    cout << "img中最小值是:" << minVal << " " << "在矩阵中的位置:" << minIdx << endl;
    return 0;
}

在这里插入图片描述在这里插入图片描述

计算图像的均值和标准方差

图像的均值表示图像整体的亮暗程度,图像的均值越大图像整体越亮。标准方差表示图像中明暗变化的对比程度,标准差越大表示图像中明暗变化越明显。OpenCV 4提供了mean()函数用于计算图像的平均值,提供了meanStdDev()函数用于同时计算图像的均值和标准方差。接下来将详细的介绍这两个函数的使用方法。

cv::Scalar cv::mean(InputArray src,InputArray mask = noArray())
  • src:待求平均值的图像矩阵。
  • mask:掩模,用于标记求取哪些区域的平均值。

该函数用来求取图像矩阵的每个通道的平均值,函数的第一个参数用来输入待求平均值的图像矩阵,其通道数目可以在1到4之间。需要注意的是,该函数的返回值是一个cv::Scalar类型的变量,函数的返回值有4位,分别表示输入图像4个通道的平均值,如果输入图像只有1个通道,那么返回值的后三位都为0,例如输入该函数一个单通道平均值为1的图像,输出的结果为[1,0,0,0],可以通过cv::Scalar[n]查看第n个通道的平均值。该函数的第二个参数用于控制图像求取均值的范围,在第一个参数中去除第二个参数中像素值为0的像素,计算的原理如式(3.5)所示,当不输入第二个参数时,表示求取第一个参数全部像素的平均值。
在这里插入图片描述

其中表示第c个通道的平均值,表示第c个通道像素的灰度值。
meanStdDev()函数可以同时求取图像每个通道的平均值和标准方差,其函数原型在代码清单3-11中给出。

void cv::meanStdDev(InputArray src,OutputArray mean,OutputArray stddev,InputArray mask = noArray())
  • src:待求平均值的图像矩阵。
  • mean:图像每个通道的平均值,参数为Mat类型变量。
  • stddev:图像每个通道的标准方差,参数为Mat类型变量。
  • mask:掩模,用于标记求取哪些区域的平均值和标准方差。

该函数的第一个参数与前面mean()函数第一个参数相同,都可以是1-4通道的图像,不同之处在于该函数没有返回值,图像的均值和标准方差输出在函数的第二个和第三个参数中,区别于mean()函数,用于存放平均值和标准方差的是Mat类型变量,变量中的数据个数与第一个参数通道数相同,如果输入图像只有一个通道,该函数求取的平均值和标准方差变量中只有一个数据。该函数计算原理如式(3.6)所示。
在这里插入图片描述
我们在代码清单3-12中给出了利用上面两个函数计算代码清单3-9中img和imgs两个矩阵的平均值和标准方差,并在图3-8给出了程序运行的结果。

#include<iostream>
#include<vector>
#include<string>
#include <opencv2/opencv.hpp>
#include "opencv/highgui.h"

using namespace std;
using namespace cv;

int main(int argc,char** argv) {
    cout<<"OpenCv Version: "<<CV_VERSION<<endl;
    float a[12]={1,2,3,4,5,10,6,7,8,9,10,0};
    Mat img=Mat(3,4,CV_32FC1,a);//单通道矩阵
    Mat imgs=Mat(2,3,CV_32FC2,a);//多通道矩阵

    cout << "/* 用meanStdDev同时求取图像的均值和标准方差 */" << endl;
    Scalar myMean;
    myMean=mean(imgs);
    cout<<"imgs均值 = "<<myMean<<endl;
    cout<<"imgs第一个通道的均值 = "<<myMean[0]<<" "
        <<"imgs第二个通道的均值 = "<<myMean[1]<<endl;

    cout << "/* 用meanStdDev同时求取图像的均值和标准方差 */" << endl;
    Mat myMeanMat,myStddevMat;

    meanStdDev(img,myMeanMat,myStddevMat);
    cout << "img均值=" << myMeanMat << " " << endl;
    cout << "img标准方差=" << myStddevMat << endl << endl;
    meanStdDev(imgs,myMeanMat,myStddevMat);
    cout << "img均值=" << myMeanMat << " " << endl;
    cout << "img标准方差=" << myStddevMat << endl << endl;
    return 0;
}

在这里插入图片描述

C++版本OpenCv教程(十三)两图像间的像素操作

Excerpt

两张图像的比较运算OpenCV 4中提供了求取两张图像每一位像素较大或者较小灰度值的max()、min()函数,这两个函数分别比较两个图像中每一位元素灰度值的大小,保留较大(较小)的灰度值,这两个函数的函数原型在代码清单3-13中给出。void cv::max(InputArray src1,InputArray src2,OutputArray dst)void cv::min(InputArray src1,InputArray src2,OutputArray dst)src1:第一个图像


两张图像的比较运算

OpenCV 4中提供了求取两张图像每一位像素较大或者较小灰度值的max()、min()函数,这两个函数分别比较两个图像中每一位元素灰度值的大小,保留较大(较小)的灰度值,这两个函数的函数原型在代码清单3-13中给出。

void cv::max(InputArray src1,InputArray src2,OutputArray dst)
void cv::min(InputArray src1,InputArray src2,OutputArray dst)
  • src1:第一个图像矩阵,可以是任意通道数的矩阵。
  • src2:第二个图像矩阵,尺寸和通道数以及数据类型都需要与src1一致。
  • dst:保留对应位置较大(较小)灰度值后的图像矩阵,尺寸、通道数和数据类型与src1一致。

该函数的功能相对来说比较简单,就是比较图像每个像素的大小,按要求保留较大值或者较小值,最后生成新的图像。例如,第一张图像位置像素值为100,第二张图像位置像素值为10,那么输出图像位置像素值为100。在代码清单3-14中给出了这两个函数的代码实现过程以及运算结果,运算结果在图3-9、图3-10和图3-11中给出。这种比较运算主要用在对矩阵类型数据的处理,与掩模图像进行比较运算可以实现抠图或者选择通道的效果。

#include<iostream>
#include<vector>
#include<string>
#include <opencv2/opencv.hpp>
#include "opencv/highgui.h"

using namespace std;
using namespace cv;

int main(int argc,char** argv) {
    cout<<"OpenCv Version: "<<CV_VERSION<<endl;
    float a[12]={1,2,3.3,4,5,9,5,7,8.2,9,10,2};
    float b[12]={1,2.2,3,1,3,10,6,7,8,9.3,10,1};
    Mat imga=Mat(3,4,CV_32FC1,a);
    Mat imgb=Mat(3,4,CV_32FC1,b);
    Mat imgas=Mat(2,3,CV_32FC2,a);
    Mat imgbs=Mat(2,3,CV_32FC2,b);

    //对两个单通道矩阵进行比较远算
    Mat myMax,myMin;
    max(imga,imgb,myMax);
    min(imga,imgb,myMin);

    //对两个多通道矩阵进行比较运算
    Mat myMaxs,myMins;
    max(imgas,imgbs,myMaxs);
    min(imgas,imgbs,myMins);

    //对两张彩色图像进行比较运算
    Mat img0=imread("lena.png");
    Mat img1=imread("noobcv.jpg");
    if(img0.empty()||img1.empty()){
        cout<<"请确认图像文件名称是否正确"<<endl;
        return -1;
    }
    Mat comMin,comMax;
    max(img0,img1,comMax);
    min(img0,img1,comMin);
    imshow("comMin",comMin);
    imshow("comMax",comMax);

    //与掩模进行比较运算
    Mat src1=Mat::zeros(Size(512,512),CV_8UC3);
    Rect rect(100,100,300,300);
    src1(rect)=Scalar(255,255,255);//生成一个300*300的掩模
    Mat comsrc1,comsrc2;
    min(img0,src1,comsrc1);
    imshow("comsrc1",comsrc1);

    Mat src2=Mat(512,512,CV_8UC3,Scalar(0,0,255));//生成一个显示红色的低通掩模
    min(img0,src2,comsrc2);
    imshow("comsrc2",comsrc2);

    //对两张图片灰度图像进行比较运算
    Mat img0G,img1G,comMinG,comMaxG;
    cvtColor(img0,img0G,COLOR_BGR2GRAY);
    cvtColor(img1,img1G,COLOR_BGR2GRAY);
    max(img0G,img1G,comMaxG);
    max(img0G,img1G,comMaxG);
    imshow("comMinG",comMinG);
    imshow("comMaxG",comMaxG);
    return 0;
}

maxAndMin.cpp程序中两个矩阵进行比较运算结果axAndMin.cpp程序中两个彩色图像和灰度图像进行比较运算结果与掩模图像进行比较运算结果

两张图像的逻辑运算

OpenCV 4针对两个图像像素之间的与、或、异或以及非运算提供了**bitwise_and()、bitwise_or()、bitwise_xor()bitwise_not()**四个函数,在代码清单3-15中给出了这四个函数的函数原型。在了解函数用法之前,我们先了解一下图像像素逻辑运算的规则。图像像素间的逻辑运算与数字间的逻辑运算相同,具体规则在图3-12中给出。像素的非运算只能针对一个数值进行,因此在图3-12中对像素求非运算时对图像1的像素值进行非运算。如果像素取值只有0和1的话,那么图中的前4行数据正好对应了所有的运算规则,但是CV_8U类型的图像像素值从0取到255,此时的逻辑运算就需要将像素值转成二进制数后再进行,因为CV_8U类型是8位数据,因此对0求非是11111111,也就是255。在图3-12中最后一行数据中,像素值5对应的二进制为101,像素值6对应的二进制是110,因此与运算得100(4),或运算得111(7),异或运算得011(3),对像素值5进行非运算得11111010(250)。了解了像素的辑运算原理之后,我们再来看OpenCV 4中提供的辑运算函数的使用方法。
图像逻辑运算规则

 -  //像素求与运算
 -  void cv::bitwise_and(InputArray src1,
 -                           InputArray src2,
 -                           OutputArray dst,
 -                           InputArray mask = noArray()
 -                           )
 -  //像素求或运算
 -  void cv::bitwise_or(InputArray src1,
 -                     InputArray src2,
 -                     OutputArray dst,
 -                     InputArray mask = noArray()
 -                       )
 -  //像素求异或运算
 -  void cv::bitwise_xor(InputArray src1,
 -                      InputArray src2,
 -                      OutputArray dst,
 -                      InputArray mask = noArray()
 -                        )
 -  //像素求非运算
 -  void cv::bitwise_not(InputArray src,
 -                      OutputArray dst,
 -                      InputArray mask = noArray()
 -                        )
  • src1:第一个图像矩阵,可以是多通道图像数据。
  • src2:第二个图像矩阵,尺寸和通道数以及数据类型都需要与src1一致。
  • dst:逻辑运算输出结果,尺寸和通道数和数据类型与src1一致。
  • mask:掩模,用于设置图像或矩阵中逻辑运算的范围。

这几个函数都执行相应的逻辑运算,在进行逻辑计算时,一定要保证两个图像矩阵之间的尺寸、数据类型和通道数相同,多个通道进行逻辑运算时不同通道之间是独立进行的。为了更加直观的理解两个图像像素间的逻辑运算,在代码清单3-16中给出两个黑白图像像素逻辑运算的示例程序,最后运行结果在图3-13中给出。

1.  #include <opencv2\opencv.hpp>
2.  #include <iostream>
3.  #include <vector>
4.  
5.  using namespace std;
6.  using namespace cv;
7.  
8.  int main()
9. {
10.    Mat img = imread("lena.png");
11.    if (img.empty())
12.    {
13.      cout << "请确认图像文件名称是否正确" << endl;
14.      return -1;
15.    }
16.    //创建两个黑白图像
17.    Mat img0 = Mat::zeros(200, 200, CV_8UC1);
18.    Mat img1 = Mat::zeros(200, 200, CV_8UC1);
19.    Rect rect0(50, 50, 100, 100);
20.    img0(rect0) = Scalar(255);
21.    Rect rect1(100, 100, 100, 100);
22.    img1(rect1) = Scalar(255);
23.    imshow("img0", img0);
24.    imshow("img1", img1);
25.  
26.    //进行逻辑运算
27.    Mat myAnd, myOr, myXor, myNot, imgNot;
28.    bitwise_not(img0, myNot);
29.    bitwise_and(img0, img1, myAnd);
30.    bitwise_or(img0, img1, myOr);
31.    bitwise_xor(img0, img1, myXor);
32.    bitwise_not(img, imgNot);
33.    imshow("myAnd", myAnd);
34.    imshow("myOr", myOr);
35.    imshow("myXor", myXor);
36.    imshow("myNot", myNot);
37.    imshow("img", img);
38.    imshow("imgNot", imgNot);
39.    waitKey(0);
40.    return 0;
41.  }

在这里插入图片描述

C++版本OpenCv教程(十四)图像二值化

Excerpt

我们在上一节程序中生成了一张只有黑色和白色的图像,这种“非黑即白”的图像像素的灰度值无论在什么数据类型中只有最大值和最小值两种取值,因此称其为二值图像。二值图像色彩种类少,可以进行高度的压缩,节省存储空间,将非二值图像经过计算变成二值图像的过程称为图像的二值化。在OpenCV 4中提供了threshold()和adaptiveThreshold()两个函数用于实现图像的二值化,我们首先介绍threshold()函数的使用方法,该函数的函数原型在代码清单3-17中给出。double cv::threshol


我们在上一节程序中生成了一张只有黑色和白色的图像,这种“非黑即白”的图像像素的灰度值无论在什么数据类型中只有最大值和最小值两种取值,因此称其为二值图像。二值图像色彩种类少,可以进行高度的压缩,节省存储空间,将非二值图像经过计算变成二值图像的过程称为图像的二值化。在OpenCV 4中提供了threshold()和adaptiveThreshold()两个函数用于实现图像的二值化,我们首先介绍threshold()函数的使用方法,该函数的函数原型在代码清单3-17中给出。
double cv::threshold(InputArray src,OutputArray dst,double  thresh,double  maxval,int  type)
  • src:待二值化的图像,图像只能是CV_8U和CV_32F两种数据类型。对于图像通道数目的要求和选择的二值化方法相关。
  • dst:二值化后的图像,与输入图像具有相同的尺寸、数据类型和通道数。
  • thresh:二值化的阈值。
  • maxval:二值化过程的最大值,此函数只在THRESH_BINARY和THRESH_BINARY_INV两种二值化方法中才使用,但是在使用其他方法是也需要输入。
  • type:选择图像二值化方法的标志。

该函数是众多二值化方法的集成,所有的方法都实现了一个功能,就是给定一个阈值,计算所有像素灰度值与这个阈值关系,得到最终的比较结果。函数中有些阈值比较方法输出结果的灰度值并不是二值的,而是具有一个取值范围,不过为了体现其最常用的功能,我们仍然称其为二值化函数或者阈值比较函数。函数的部分参数和返回值都是针对特定的算法才有用,但是即使不使用这些算法在使用函数时也需要明确的给出,不可缺省。函数的最后一个参数是选择二值化计算方法的标志,可以选择二值化方法以及控制哪些参数对函数的计算结果产生影响,该标志可以选择的范围及含义在表3-2中给出。
在这里插入图片描述
接下来将详细的介绍每种标志对应的二值化原理和需要的参数。

THRESH_BINARY和THRESH_BINARY_INV

这两个标志是相反的二值化方法,THRESH_BINARY是将灰度值与阈值(第三个参数thresh)进行比较,如果灰度值大于阈值就将灰度值改为函数中第四个参数maxval的值,否则将灰度值改成0。THRESH_BINARY_INV标志正好与这个过程相反,如果灰度值大于阈值就将灰度值改为0,否则将灰度值改为maxval的值。这两种标志的计算公式在式(3.7)中给出。
在这里插入图片描述

THRESH_TRUNC

这个标志相当于重新给图像的灰度值设定一个新的最大值,将大于新的最大值的灰度值全部重新设置为新的最大值,具体逻辑为将灰度值与阈值thresh进行比较,如果灰度值大于thresh则将灰度值改为thresh,否则保持灰度值不变。这种方法没有使用到函数中的第四个参数maxval的值,因此maxval的值对本方法不产生影响。这种标志的计算公式在式(3.8)中给出。
在这里插入图片描述

THRESH_TOZERO和THRESH_TOZERO_INV

这两个标志是相反的阈值比较方法, THRESH_TOZERO表示将灰度值与阈值thresh进行比较,如果灰度值大于thresh则将保持不变,否则将灰度值改为0。THRESH_TOZERO_INV方法与其相反,将灰度值与阈值thresh进行比较,如果灰度值小于等于thresh则将保持不变,否则将灰度值改为0。这种两种方法都没有使用到函数中的第四个参数maxval的值,因此maxval的值对本方法不产生影响。这两个标志的计算公式在式(3.9)中给出。
在这里插入图片描述
前面五种标志都支持输入多通道的图像,在计算时分别对每个通道进行阈值比较。为了更加直观的理解上述阈值比较方法,我们假设图像灰度值是连续变化的信号,将阈值比较方法比做滤波器,绘制连续信号通过滤波器后的信号形状,结果如图3-14所示,图中红线为设置的阈值,黑线为原始信号通过滤波器后的信号形状。
在这里插入图片描述

THRESH_OTSU和THRESH_TRIANGLE

这两种标志是获取阈值的方法,并不是阈值的比较方法的标志,这两个标志可以和前面5种标志一起使用,例如“THRESH_BINARY| THRESH_OTSU”。前面5种标志在调用函数时都需要人为的设置阈值,如果对图像不了解设置的阈值不合理,会对处理后的效果造成严重的影响,这两个标志分别表示利用**大津法(OTSU)三角形法(TRIANGLE)**结合图像灰度值分布特性获取二值化的阈值,并将阈值以函数返回值的形式给出。因此如果函数最后一个参数设置了这两个标志中的任何一个,那么函数第三个参数thresh将由系统自动给出,但是在调用函数的时候仍然不能缺省,只是程序不会使用这个数值。需要注意的是,目前为止OpenCV 4中针对这两个标志只支持输入CV_8UC1类型的图像。

threshold()函数全局只使用一个阈值,在实际情况中由于光照不均匀以及阴影的存在,全局只有一个阈值会使得在阴影处的白色区域也会被函数二值化成黑色,因此adaptiveThreshold()函数提供了两种局部自适应阈值的二值化方法,该函数的函数原型在代码清单3-18中给出。

 -  void cv::adaptiveThreshold(InputArray src,
 -                                OutputArray dst,
 -                                double  maxValue,
 -                                int  adaptiveMethod,
 -                                int  thresholdType,
 -                                int  blockSize,
 -                                double   C
 -                                   )
  • src:待二值化的图像,图像只能是CV_8UC1数据类型。
  • dst:二值化后的图像,与输入图像具有相同的尺寸、数据类型。
  • maxValue:二值化的最大值。
  • adaptiveMethod:自制应确定阈值的方法,分为均值法ADAPTIVE_THRESH_MEAN_C和高斯法ADAPTIVE_THRESH_GAUSSIAN_C这两种。
  • thresholdType:选择图像二值化方法的标志,只能是THRESH_BINARY和THRESH_BINARY_INV。
  • blockSize:自适应确定阈值的像素邻域大小,一般为3,5,7的奇数。
  • C:从平均值或者加权平均值中减去的常数,可以为正,也可以为负。

该函数将灰度图像转换成二值图像,通过均值法和高斯法自适应的计算blockSize* blockSize邻域内的阈值,之后进行二值化,其原理与前面的相同,这里就不再赘述。

为了直观的体会到图像二值化的效果,在代码清单3-19中给出了分别对彩色图像和灰度图像进行二值化的示例程序,程序运行结果在图3-15、图3-16中给出。

#include<iostream>
#include<vector>
#include<string>
#include <opencv2/opencv.hpp>
#include "opencv/highgui.h"

using namespace std;
using namespace cv;

int main(int argc,char** argv) {
    cout<<"OpenCv Version: "<<CV_VERSION<<endl;
    Mat img=imread("699342568.jpg");
    if(img.empty()){
        cout<<"请确认输入的图片的路径是否正确"<<endl;
        return -1;
    }

    Mat gray;
    cvtColor(img,gray,COLOR_BGR2GRAY);
    Mat img_B,img_B_V,gray_B,gray_B_V,gray_T,gray_T_V,gray_TRUNC;

    //彩色图像二值化
    threshold(img,img_B,125,255,THRESH_BINARY);
    threshold(img,img_B_V,125,255,THRESH_BINARY_INV);
    imshow("img_B",img_B);
    imshow("img_B_V",img_B_V);

    //灰度图像二值化
    threshold(gray,gray_B,125,255,THRESH_BINARY);
    threshold(gray,gray_B_V,125,255,THRESH_BINARY_INV);
    imshow("gray_B",gray_B);
    imshow("gray_B_V",gray_B_V);

    //灰度图像TOZERO变换
    threshold(gray,gray_T,125,255,THRESH_TOZERO);
    threshold(gray,gray_T_V,125,255,THRESH_TOZERO_INV);
    imshow("gray_T",gray_T);
    imshow("gray_T_V",gray_T_V);

    //灰度图像TRUNC变换
    threshold(gray,gray_TRUNC,125,255,THRESH_TRUNC);
    imshow("gray_TRUNC",gray_TRUNC);

    //灰度图像大津法和三角法二值化
    Mat img_Thr=imread("threshold.jpg",IMREAD_GRAYSCALE);
    Mat img_Thr_0,img_Thr_T;
    threshold(img_Thr,img_Thr_0,100,255,THRESH_BINARY|THRESH_OTSU);
    threshold(img_Thr,img_Thr_T,125,255,THRESH_BINARY|THRESH_TRIANGLE);
    imshow("img_Thr",img_Thr);
    imshow("img_Thr_0",img_Thr_0);
    imshow("img_Thr_T",img_Thr_T);

    //灰度图像自适应二值化
    Mat adaptive_mean,adaptive_gauss;
    adaptiveThreshold(img_Thr,adaptive_mean,255,ADAPTIVE_THRESH_MEAN_C,THRESH_BINARY,55,0);
    adaptiveThreshold(img_Thr,adaptive_gauss,255,ADAPTIVE_THRESH_GAUSSIAN_C,THRESH_BINARY,55,0);

    imshow("adaptive_mean",adaptive_mean);
    imshow("adaptive_gauss",adaptive_gauss);
    waitKey(0);
    return 0;
}

在这里插入图片描述在这里插入图片描述

C++版本OpenCv教程(十五)LUT查找表

Excerpt

前面介绍的阈值比较方法中只有一个阈值,如果需要与多个阈值进行比较,就需要用到显示查找表(Look-Up-Table,LUT)。LUT查找表简单来说就是一个像素灰度值的映射表,它以像素灰度值作为索引,以灰度值映射后的数值作为表中的内容。例如我们有一个长度为5的存放字符的数组,LUT查找表就是通过这个数组将0映射成a,将1映射成b,依次类推,其映射关系为。在OpenCV 4中提供了LUT()函数用于实现图像像素灰度值的LUT查找表功能,在代码清单3-20中给出了该函数的原型。 - void cv::LUT(


前面介绍的阈值比较方法中只有一个阈值,如果需要与多个阈值进行比较,就需要用到显示查找表(Look-Up-Table,LUT)。LUT查找表简单来说就是一个像素灰度值的映射表,它以像素灰度值作为索引,以灰度值映射后的数值作为表中的内容。例如我们有一个长度为5的存放字符的数组,LUT查找表就是通过这个数组将0映射成a,将1映射成b,依次类推,其映射关系为。在OpenCV 4中提供了LUT()函数用于实现图像像素灰度值的LUT查找表功能,在代码清单3-20中给出了该函数的原型。

 -  void cv::LUT(InputArray src,
 -                  InputArray lut,
 -                  OutputArray dst
 -                 )
  • src:输入图像矩阵,其数据类型只能是CV_8U
  • lut:256个像素灰度值的查找表,单通道或者与src通道数相同。
  • dst:输出图像矩阵,其尺寸与src相同,数据类型与lut相同。

该函数的第一个输入参数要求的数据类型必须是CV_8U类型,但是可以是多通道的图像矩阵。第二个参数根据其参数说明可以知道输入量是一个1×256的矩阵,其中存放着每个像素灰度值映射后的数值,其形式如图3-17所示。如果第二个参数是单通道,则输入变量中的每个通道都按照一个LUT查找表进行映射如果第二个参数是多通道,则输入变量中的第i个通道按照第二个参数的第i个通道LUT查找表进行映射。与之前的函数不同,函数输出图像的数据类型不与原图像的数据类型保持一致,而是和LUT查找表的数据类型保持一致,这是因为将原灰度值映射到新的空间中,因此需要与新空间中的数据类型保持一致。
LUT查找表设置示例
为了体会LUT查找表处理图像后的效果,在代码清单3-21中给出通过LUT()函数将灰度图像和彩色图像分别处理的示例程序,程序中分别应用单通道和三通道的查找表对彩色图像进行映射,最终结果在图3-18中给出。

#include<iostream>
#include<vector>
#include<string>
#include <opencv2/opencv.hpp>
#include "opencv/highgui.h"

using namespace std;
using namespace cv;

int main(int argc,char** argv) {
    cout<<"OpenCv Version: "<<CV_VERSION<<endl;
    //LUT查找表第一层
    uchar lutFirst[256];
    for(int i=0;i<256;++i){
        if(i<=100)lutFirst[i]=0;
        else if(i>100&&i<=200)lutFirst[i]=100;
        else lutFirst[i]=255;
    }
    Mat lutOne(1,256,CV_8UC1,lutFirst);

    //LUT查找表第二层
    uchar lutSecond[256];
    for(int i=0;i<256;++i){
        if(i<=100)lutSecond[i]=0;
        else if(i>100&&i<=150)lutSecond[i]=100;
        else if(i>150&&i<=200)lutSecond[i]=150;
        else lutSecond[i]=255;
    }
    Mat lutTwo(1,256,CV_8UC1,lutSecond);

    //LUT查找表第三层
    uchar lutThird[256];
    for(int i=0;i<256;++i){
        if(i<=100)lutThird[i]=0;
        else if(i>100&&i<=200)lutThird[i]=100;
        else lutThird[i]=255;
    }
    Mat lutThree(1,256,CV_8UC1,lutThird);

    //拥有三通道的LUT查找表矩阵
    vector<Mat>mergeMats;
    mergeMats.push_back(lutOne);
    mergeMats.push_back(lutTwo);
    mergeMats.push_back(lutThree);
    Mat LutTree;
    merge(mergeMats,LutTree);

    //计算图像的查找表
    Mat img=imread("lena.jpeg");
    if(img.empty()){
        cout<<"请确认输入的图片路径是否正确"<<endl;
        return -1;
    }
    Mat gray,out0,out1,out2;
    cvtColor(img,gray,COLOR_BGR2GRAY);
    LUT(gray,lutOne,out0);
    LUT(img,lutOne,out1);
    LUT(img,LutTree,out2);
    imshow("out0",out0);
    imshow("out1",out1);
    imshow("out2",out2);
    waitKey(0);
    return 0;
}

在这里插入图片描述

C++版本OpenCv教程(十六)图像仿射变换

Excerpt

介绍完图像的缩放和翻转后,接下来将要介绍图像的旋转,但是在OpenCV 4中并没有专门用于图像旋转的函数,而是通过图像的仿射变换实现图像的旋转。实现图像的旋转首先需要确定旋转角度和旋转中心,之后 确定旋转矩阵,最终通过仿射变换实现图像旋转。针对这个流程,OpenCV 4提供了getRotationMatrix2D()函数用于计算旋转矩阵和warpAffine()函数用于实现图像的仿射变换。首先介绍计算旋转矩阵getRotationMatrix2D()函数,该函数的函数原型在代码清单3-31中给出。Mat


介绍完图像的缩放和翻转后,接下来将要介绍图像的旋转,但是在OpenCV 4中并没有专门用于图像旋转的函数,而是通过图像的仿射变换实现图像的旋转。实现图像的旋转首先需要确定旋转角度和旋转中心,之后 确定旋转矩阵,最终通过仿射变换实现图像旋转。针对这个流程,OpenCV 4提供了getRotationMatrix2D()函数用于计算旋转矩阵warpAffine()函数用于实现图像的仿射变换。首先介绍计算旋转矩阵getRotationMatrix2D()函数,该函数的函数原型在代码清单3-31中给出。

Mat cv::getRotationMatrix2D (Point2f center,double  angle,double  scale)
  • center:图像旋转的中心位置。
  • angle:图像旋转的角度,单位为度,正值为逆时针旋转。
  • scale:两个轴的比例因子,可以实现旋转过程中的图像缩放,不缩放输入1。

该函数输入旋转角度和旋转中心,返回图像旋转矩阵,该返回值得数据类型为Mat类,是一个2×3的矩阵。如果我们已知图像旋转矩阵,可以自己生成旋转矩阵而不调用该函数。该函数生成的旋转矩阵与旋转角度和旋转中心的关系如式(3.11)所示。
在这里插入图片描述
其中:
在这里插入图片描述

warpAffine()函数进行仿射变换,就可以实现图像的旋转,在代码清单3-32中给出了warpAffine()函数的函数原型。

void cv::warpAffine(InputArray src,
                    OutputArray dst,
                    InputArray M,
                    Size dsize,
                    int  flags = INTER_LINEAR,
                    int  borderMode = BORDER_CONSTANT,
                    const Scalar& borderValue = Scalar()
)
  • src:输入图像。
  • dst:仿射变换后输出图像,与src数据类型相同,但是尺寸与dsize相同。
  • M:2×3的变换矩阵。
  • dsize:输出图像的尺寸。
  • flags:插值方法标志,可选参数及含义在表3-3和表3-4中给出。
  • borderMode:像素边界外推方法的标志。
  • borderValue:填充边界使用的数值,默认情况下为0。

该函数拥有多个参数,但是多数都与前面介绍的图像尺寸变换具有相同的含义。函数中第三个参数为前面求取的图像旋转矩阵,第四个参数是输出图像的尺寸。函数第五个参数是仿射变换插值方法的标志,这里相比于图像尺寸变换多增加了两个类型,可以与其他插值方法一起使用,这两种类型在表3-4中给出。函数第六个参数为像素边界外推方法的标志,其可以的标志和对应的方法在表3-5中给出。第七个参数是外推标志选择BORDER_CONSTANT时的定值,默认情况下为0。
在这里插入图片描述在这里插入图片描述
在了解函数每个参数的含义之后,为了更好的理解函数作用,需要介绍一下仿射变换的概念。仿射变换就是图像的旋转、平移和缩放操作的统称,可以表示为线性变换和平移变换的叠加。仿射变换的数学表示是先乘以一个线形变换矩阵再加上一个平移向量,其中线性变换矩阵为2×2的矩阵,平移向量为2×1的向量,至此你可能理解了为什么函数需要输入一个2×3的变换矩阵。假设我们存在一个线性变换矩阵和平移矩阵,两者与输入的矩阵之间的关系如式(3.13)中所示。
在这里插入图片描述

根据旋转矩阵和平移矩阵以及图像像素值,仿射变换的数学原理可以用式(3.14)来表示。
在这里插入图片描述
仿射变换又称为三点变换,如果知道变换前后两张图像中三个像素点坐标的对应关系,就可以求得仿射变换中的变换矩阵,OpenCV 4提供了利用三个对应像素点来确定矩阵的函数getAffineTransform(),该函数的函数原型在代码清单3-33中给出。

Mat cv::getAffineTransform(const Point2f src[],const Point2f dst[])
  • src[]:原图像中的三个像素坐标。
  • dst[]:目标图像中的三个像素坐标。

该函数两个输入量都是存放浮点坐标的数组,在生成数组的时候像素点的输入顺序无关,但是需要保证像素点的对应关系,函数的返回值是一个2×3的变换矩阵。

有了前面变换矩阵的求取,就可以利用warpAffine()函数实现矩阵的仿射变换,我们在代码清单3-34的例程中实现了图像的旋转以及图像三点映射的仿射变换,最终结果在图3-23中给出。

#include<iostream>
#include<vector>
#include<string>
#include <opencv2/opencv.hpp>
#include "opencv/highgui.h"

using namespace std;
using namespace cv;

int main(int argc,char** argv) {
    cout<<"OpenCv Version: "<<CV_VERSION<<endl;
    Mat img=imread("699342568.jpg");
    if(img.empty()){
        cout<<"请确认输入的图像;路径是否正确"<<endl;
        return -1;
    }
    Mat img_;
    resize(img,img_,Size(img.rows/2,img.cols/2));
    imshow("src",img_);
    Mat rotation0,rotation1,img_warp0,img_warp1;
    double angle=30;//设置图像旋转的角度
    Size dst_size(img_.rows,img_.cols);//设置输出图像的尺寸
    Point2f center(img_.rows/2.0,img_.cols/2.0);//设置图像的旋转中心
    rotation0=getRotationMatrix2D(center,angle,1);//计算放射变换矩阵
    warpAffine(img_,img_warp0,rotation0,dst_size);//进行仿射变换
    imshow("img_warp0",img_warp0);
    //根据定义的三个点进行仿射变换
    Point2f src_points[3];
    Point2f dst_points[3];
    src_points[0]=Point2f(0,0);//原始图像的三个点
    src_points[1]=Point2f(0,(float)(img_.cols-1));
    src_points[2]=Point2f((float)(img_.rows-1),(float)(img_.cols-1));
    //仿射变换后图像中的三个点
    dst_points[0]=Point2f((float)(img_.rows)*0.11,(float)(img_.cols)*0.20);
    dst_points[1]=Point2f((float)(img_.rows)*0.15,(float)(img_.cols)*0.70);
    dst_points[0]=Point2f((float)(img_.rows)*0.81,(float)(img_.cols)*0.85);
    rotation1=getAffineTransform(src_points,dst_points);//根据对应点求取放射变换矩阵
    warpAffine(img_,img_warp1,rotation1,dst_size);//进行放射变换
    imshow("img_warp1",img_warp1);
    waitKey(0);
    return 0;
}

在这里插入图片描述

C++版本OpenCv教程(十七)图像透视变换

Excerpt

本小节将介绍图像的另一种变换——透视变换。透视变换是按照物体成像投影规律进行变换,即将物体重新投影到新的成像平面,示意图如图3-24所示。透视变换常用于机器人视觉导航研究中,由于相机视场与地面存在倾斜角使得物体成像产生畸变,通常通过透视变换实现对物体图像的校正。透视变换中,透视前的图像和透视后的图像之间的变换关系可以用一个3×3的变换矩阵表示,该矩阵可以通过两张图像中四个对应点的坐标求取,因此透视变换又称作“四点变换”。与仿射变换一样,OpenCV 4中提供了根据四个对应点求取变换矩阵的getPerspec


本小节将介绍图像的另一种变换——透视变换。透视变换是按照物体成像投影规律进行变换,即将物体重新投影到新的成像平面,示意图如图3-24所示。透视变换常用于机器人视觉导航研究中,由于相机视场与地面存在倾斜角使得物体成像产生畸变,通常通过透视变换实现对物体图像的校正。透视变换中,透视前的图像和透视后的图像之间的变换关系可以用一个3×3的变换矩阵表示,该矩阵可以通过两张图像中四个对应点的坐标求取,因此透视变换又称作“四点变换”。与仿射变换一样,OpenCV 4中提供了根据四个对应点求取变换矩阵的getPerspectiveTransform()函数和进行透视变换的warpPerspective()函数,接下来将介绍这两个函数的使用方法,两个函数的函数原型在代码清单3-35和代码清单3-36中给出。
透视变换原理示意图

Mat cv::getPerspectiveTransform (const Point2f src[],
                                 const Point2f dst[],
                                 int  solveMethod = DECOMP_LU
                                 )
  • src[]:原图像中的四个像素坐标。
  • dst[]:目标图像中的四个像素坐标。
  • solveMethod:选择计算透视变换矩阵方法的标志,可以选择参数及含义在表3-6中给出。

该函数两个输入量都是存放浮点坐标的数组,在生成数组的时候像素点的输入顺序无关,但是需要注意像素点的对应关系,函数的返回值是一个3×3的变换矩阵。函数中最后一个参数是根据四个对应点坐标计算透视变换矩阵方法的选择标志,其可以选择的参数标志在表3-6中给出,默认情况下选择的是最佳主轴元素的高斯消元法DECOMP_LU。
在这里插入图片描述

void cv::warpPerspective(InputArray src,
                         OutputArray dst,
                         InputArray M,
                         Size dsize,
                         int  flags = INTER_LINEAR,
                         int  borderMode = BORDER_CONSTANT,
                         const Scalar & borderValue = Scalar()
                         )
  • src:输入图像。
  • dst:透视变换后输出图像,与src数据类型相同,但是尺寸与dsize相同。
  • M:3×3的变换矩阵。
  • dsize:输出图像的尺寸。
  • flags:插值方法标志。
  • borderMode:像素边界外推方法的标志。
  • borderValue:填充边界使用的数值,默认情况下为0

该函数所有参数含义都与warpAffine()函数的参数含义相同,这里不再进行赘述。为了说明该函数在实际应用中的作用,在代码清单3-37中给出了将相机视线不垂直于二维码平面拍摄的图像经过透视变换变成相机视线垂直于二维码平面拍摄的图像。在图3-25中给出了相机拍摄到的二维码图像和经过程序透视变换后的图像。为了寻找透视变换关系,我们需要寻找拍摄图像中二维码四个角点的像素坐标和透视变换后角点对应的理想坐标。本程序中,我们事先通过Image Watch插件查看了拍摄图像二维码四个角点的坐标,并希望透视变换后二维码可以充满全部的图像,因此我们在程序中手动输入四对对应点的像素坐标。但是在实际工程中,二维码的角点坐标可以通过角点检测的方式获取,具体方式我们将在后面介绍。

#include<iostream>
#include<vector>
#include<string>
#include <opencv2/opencv.hpp>
#include "opencv/highgui.h"

using namespace std;
using namespace cv;

int main(int argc,char** argv) {
    cout<<"OpenCv Version: "<<CV_VERSION<<endl;
    Mat img=imread("noobcvqr.png");
    if(img.empty()){
        cout << "请确认图像文件名称是否正确" << endl;
        return -1;
    }
    Point2f src_points[4];
    Point2f dst_points[4];
    //通过Image Watch查看的二维码四个角点坐标
    src_points[0]=Point2f(94.0, 374.0);
    src_points[1]=Point2f(507.0, 380.0);
    src_points[2]=Point2f(1.0, 623.0);
    src_points[3]=Point2f(627.0, 627.0);
    //期望透视变换后二维码四个角点的坐标
    dst_points[0]=Point2f(0.0, 0.0);
    dst_points[1]=Point2f(627.0, 0.0);
    dst_points[2]=Point2f(0.0, 627.0);
    dst_points[3]=Point2f(627.0, 627.0);

    Mat rotation,img_warp;
    rotation=getPerspectiveTransform(src_points,dst_points);
    warpPerspective(img,img_warp,rotation,img.size());
    imshow("img",img);
    imshow("img_warp",img_warp);
    waitKey(0);
    return 0;
}

在这里插入图片描述

C++版本OpenCv教程(十八)极坐标变换

Excerpt

极坐标变换就是将图像在直角坐标系与极坐标系中互相变换,形式如图3-26所示,它可以将一圆形图像变换成一个矩形图像,常用于处理钟表、圆盘等图像。圆形图案边缘上的文字经过及坐标变换后可以垂直的排列在新图像的边缘,便于对文字的识别和检测。OpenCV 4中提供了warpPolar()函数用于实现图像的极坐标变换,该函数的函数原型在代码清单3-38中给出。void cv::warpPolar(InputArray src, OutputArray dst,


极坐标变换就是将图像在直角坐标系与极坐标系中互相变换,形式如图3-26所示,它可以将一圆形图像变换成一个矩形图像,常用于处理钟表、圆盘等图像。圆形图案边缘上的文字经过及坐标变换后可以垂直的排列在新图像的边缘,便于对文字的识别和检测。
极坐标变换示意图
OpenCV 4中提供了warpPolar()函数用于实现图像的极坐标变换,该函数的函数原型在代码清单3-38中给出。

void cv::warpPolar(InputArray src,
                   OutputArray dst,
                   Size dsize,
                   Point2f center,
                   double  maxRadius,
                   int  flags
                   )
  • src:原图像,可以是灰度图像或者彩色图像。
  • dst:极坐标变换后输出图像,与原图像具有相同的数据类型和通道数。
  • dsize:目标图像大小。
  • center:极坐标变换时极坐标的原点坐标。
  • maxRadius:变换时边界圆的半径,它也决定了逆变换时的比例参数。
  • flags:插值方法与极坐标映射方法标志,插值方法在表3-3中给出,极坐标映射方法在表3-7给出,两个方法之间通过“+”或者“|”号进行连接。

该函数实现了图像极坐标变换和半对数极坐标变换。函数第一个参数是需要进行极坐标变换的原始图像,该图像可以是灰度图像也可以是彩色图像。第二个参数是变换后的输出图像,与输入图像具有相同的数据类型和通道数。第三个参数是变换后图像的大小。第四个参数是极坐标变换时极坐标原点在原图像中的位置,该参数同样适用于逆变换中。第五个参数是变换时边界圆的半径,它也决定了逆变换时的比例参数。最后一个参数是变换方法的选择标志,插值方法在表3-3中给出,极坐标映射方法在表3-7给出,两个方法之间通过“+”或者“|”号进行连接。
在这里插入图片描述

该函数可以对图像进行极坐标正变换也可以进行逆变换,关键在于最后一个参数如何选择。为了了解图像极坐标变换的功能以及相关函数的使用,在代码清单3-39给出了对表盘图像进行极坐标正变换和逆变换的示例程序。程序中选取表盘的中心作为极坐标的原点,变换的结果在图3-27给出。

#include<iostream>
#include<vector>
#include<string>
#include <opencv2/opencv.hpp>
#include "opencv/highgui.h"

using namespace std;
using namespace cv;

int main(int argc,char** argv) {
    cout<<"OpenCv Version: "<<CV_VERSION<<endl;
    Mat img=imread("noobcvqr.png");
    if(img.empty()){
        cout << "请确认图像文件名称是否正确" << endl;
        return -1;
    }
    Mat img1,img2;
    Point2f center=Point2f(img.cols/2,img.rows/2);//极坐标在图像中的原点
    //正极坐标变换
    warpPolar(img,img1,Size(300,600),center,center.x,INTER_LINEAR+WARP_POLAR_LINEAR);
    //逆极坐标变换
    warpPolar(img1,img2,Size(img.rows,img.cols),center,center.x,INTER_LINEAR+WARP_POLAR_LINEAR+WARP_INVERSE_MAP);
    imshow("原表盘图",img);
    imshow("表盘极坐标变换结果",img1);
    imshow("逆变换结果",img2);
    waitKey(0);
    return 0;
}

在这里插入图片描述

C++版本OpenCv教程(十九)图像金字塔

Excerpt

高斯金字塔构建图像的高斯金字塔是解决尺度不确定性的一种常用方法。高斯金字塔是指通过下采样不断的将图像的尺寸缩小,进而在金字塔中包含多个尺度的图像,高斯金字塔的形式如图3-30所示,一般情况下,高斯金字塔的最底层为图像的原图,每上一层就会通过下采样缩小一次图像的尺寸,通常情况尺寸会缩小为原来的一半,但是如果有特殊需求,缩小的尺寸也可以根据实际情况进行调整。由于每次图像的尺寸都缩小为原来的一半,图像尺缩小的速度非常快,因此常见高斯金字塔的层数为3到6层。OpenCV 4中提供了**pyrDown()**函数专


高斯金字塔

构建图像的高斯金字塔是解决尺度不确定性的一种常用方法。高斯金字塔是指通过下采样不断的将图像的尺寸缩小,进而在金字塔中包含多个尺度的图像,高斯金字塔的形式如图3-30所示,一般情况下,高斯金字塔的最底层为图像的原图,每上一层就会通过下采样缩小一次图像的尺寸,通常情况尺寸会缩小为原来的一半,但是如果有特殊需求,缩小的尺寸也可以根据实际情况进行调整。由于每次图像的尺寸都缩小为原来的一半,图像尺缩小的速度非常快,因此常见高斯金字塔的层数为3到6层。OpenCV 4中提供了**pyrDown()**函数专门用于图像的下采样计算,便于构建图像的高斯金字塔,该函数的函数原型在代码清单3-51中给出。
图像高斯金字塔原理

void cv::pyrDown(InputArray src,
                 OutputArray dst,
                 const Size & dstsize = Size(),
                 int  borderType = BORDER_DEFAULT
                 )
  • src:输入待下采样的图像。
  • dst:输出下采样后的图像,图像尺寸可以指定,但是数据类型和通道数与src相同,
  • dstsize:输出图像尺寸,可以缺省。
  • borderType:像素边界外推方法的标志,取值范围如表3-5所示

该函数用于实现图像模糊并对其进行下采样,默认状态下函数输出的图像的尺寸为输入图像尺寸的一半,但是也可以通过dstsize参数来设置输出图像的大小,需要注意的是无论输出尺寸为多少都应满足式(3.15)中的条件。该函数首先将原图像与内核矩阵进行卷积,内核矩阵如式(3.16)所示,之后通过不使用偶数行和列的方式对图像进行下采样,最终实现尺寸缩小的下采样图像。
在这里插入图片描述
该函数的功能与resize()函数将图像尺寸缩小一样,但是使用的内部算法不同,对于函数的具体使用方式以及如何构建图像金字塔在代码清单3-53中给出。

拉普拉斯金字塔

拉普拉斯金字塔与高斯金字塔正好相反,高斯金字塔通过底层图像构建上层图像,而拉普拉斯是通过上层小尺寸的图像构建下层大尺寸的图像。拉普拉斯金字塔具有预测残差的作用,需要与高斯金字塔联合一起使用,假设我们已经有一个高斯图像金字塔,对于其中的第i层图像(高斯金字塔最下面为第0层),首先通过下采样得到一尺寸缩小一半的图像,即高斯金字塔中的第i+1层或者不在高斯金字塔中,之后对这张图像再进行上采样,将图像尺寸恢复到第i层图像的大小,最后求取高斯金字塔第i层图像与经过上采样后得到的图像的差值图像,这个差值图像就是拉普拉斯金字塔的第i层图像,整个过程的流程如图3-31所示。
由高斯金字塔求取拉普拉斯金字塔的流程

void cv::pyrUp(InputArray src,
   OutputArray dst,
   const Size & dstsize = Size(),
   int borderType = BORDER_DEFAULT)

该函数所有参数的含义与pyrDown()函数中相同,使用方式也与其一致,因此这里不再进行赘述。

为了了解下采样函数pyrDown()和上采样函数pyrUp()的使用方式,以及高斯金字塔和拉普拉斯金字塔的构建过程,我们在代码清单3-53中给出构建高斯金字塔和拉普拉斯金字塔的示例程序。在例程中我们将原始图像作为高斯金字塔的第0层,之后依次构建高斯金字塔的每一层。完成高斯金字塔的构建之后,我们从上到下取出高斯金字塔中的每一层图像,如果取出的图像是高斯金字塔的最上面一层,则先将其下采样再上采样,之后求取从高斯金字塔中取出的图像与上采样后的图像的差值图像作为拉普拉斯金字塔的最上面一层。如果从高斯金字塔中取出的第i层不是最上面一层,则直接对高斯金字塔中第i+1层图像进行上采样,并计算高斯金字塔第i层图像与上采样结果的差值图像,将差值图像作为拉普拉斯金字塔的第i层。该例程最终的运行结果在图3-32、图3-33中给出。

#include<iostream>
#include<vector>
#include<string>
#include <opencv2/opencv.hpp>
#include "opencv/highgui.h"

using namespace std;
using namespace cv;

int main(int argc,char** argv) {
    cout<<"OpenCv Version: "<<CV_VERSION<<endl;
    Mat img=imread("699342568.jpg");
    if(img.empty()){
        cout<<"请确认输入的图片路径是否正确"<<endl;
        return -1;
    }
    Mat dst;
    resize(img,dst,Size(img.cols/2,img.rows/2));

    vector<Mat> Gauss,Lap;//高斯金字塔和拉普拉斯金字塔
    int level=3;//高斯金字塔下采样次数
    Gauss.push_back(dst);//将原图作为高斯金字塔的第0层
    //构建高斯金字塔
    for(int i=0;i<level;++i){
        Mat gauss;
        pyrDown(Gauss[i],gauss);//下采样
        Gauss.push_back(gauss);
    }
    //构建拉普拉斯金字塔
    for(int i=Gauss.size()-1;i>0;--i){
        Mat lap,upGauss;
        if(i==Gauss.size()-1){//如果是高斯金字塔中的最上面一层图像
            Mat down;
            pyrDown(Gauss[i],down);//下采样
            pyrUp(down,upGauss);//上采样
            lap=Gauss[i]-upGauss;
            Lap.push_back(lap);
        }
        pyrUp(Gauss[i],upGauss);
        lap=Gauss[i-1]-upGauss;
        Lap.push_back(lap);
    }
    //查看两个金字塔中的图像
    for(int i=0;i<Gauss.size();++i){
        string name=to_string(i);
        imshow("G"+name,Gauss[i]);
        imshow("L"+name,Lap[i]);
    }
    waitKey(0);
    return 0;
}

在这里插入图片描述

C++版本OpenCv教程(二十)图像直方图绘制

Excerpt

图像直方图是图像处理中非常重要的像素统计结果,图像直方图不再表征任何的图像纹理信息,而是对图像像素的统计。由于同一物体无论是旋转还是平移在图像中都具有相同的灰度值,因此直方图具有平移不变性、放缩不变性等优点,因此可以用来查看图像整体的变化形式,例如图像是否过暗、图像像素灰度值主要集中在哪些范围等,在特定的条件下也可以利用图像直方图进行图像的识别,例如对数字的识别。图像直方图简单来说就是统计图像中每个灰度值的个数,之后将图像灰度值作为横轴,以灰度值个数或者灰度值所占比率作为纵轴绘制的统计图。通过直方图可以看


图像直方图是图像处理中非常重要的像素统计结果,图像直方图不再表征任何的图像纹理信息,而是对图像像素的统计。由于同一物体无论是旋转还是平移在图像中都具有相同的灰度值,因此直方图具有平移不变性、放缩不变性等优点,因此可以用来查看图像整体的变化形式,例如图像是否过暗、图像像素灰度值主要集中在哪些范围等,在特定的条件下也可以利用图像直方图进行图像的识别,例如对数字的识别。

图像直方图简单来说就是统计图像中每个灰度值的个数,之后将图像灰度值作为横轴,以灰度值个数或者灰度值所占比率作为纵轴绘制的统计图。通过直方图可以看出图像中哪些灰度值数目较多,哪些较少,可以通过一定的方法将灰度值较为集中的区域映射到较为稀疏的区域,从而使得图像在像素灰度值上分布更加符合期望状态。通常情况下,像素灰度值代表亮暗程度,因此通过图像直方图可以分析图像亮暗对比度,并调整图像的亮暗程度。

OpenCV 4中只提供了图像直方图的统计函数calcHist(),该函数能够统计出图像中每个灰度值的个数,但是对于直方图的绘制需要使用者自行绘制。我们首先学习统计灰度值数目的函数**calcHist()**的使用,该函数的原型在代码清单4-1中给出。

void cv::calcHist(const Mat * images,
                        int  nimages,
                        const int * channels,
                        InputArray mask,
                        OutputArray hist,
                        int  dims,
                        const int * histSize,
                        const float ** ranges,
                        bool  uniform = true,
                        bool  accumulate = false 
                        )
  • images:待统计直方图的图像数组,数组中所有的图像应具有相同的尺寸和数据类型,并且数据类型只能是CV_8U、CV_16U和CV_32F三种中的一种,但是不同图像的通道数可以不同。
  • nimages:输入的图像数量
  • channels:需要统计的通道索引数组,第一个图像的通道索引从0到images[0].channels()-1,第二个图像通道索引从images[0].channels()到images[0].channels()+images[1].channels()-1,以此类推。
  • mask:可选的操作掩码,如果是空矩阵则表示图像中所有位置的像素都计入直方图中,如果矩阵不为空,则必须与输入图像尺寸相同且数据类型为CV_8U。
  • hist:输出的统计直方图结果,是一个dims维度的数组。
  • dims:需要计算直方图的维度,必须是整数,并且不能大于CV_MAX_DIMS,在OpenCV 4.0和OpenCV 4.1版本中为32。
  • histSize:存放每个维度直方图的数组的尺寸。
  • ranges:每个图像通道中灰度值的取值范围。
  • uniform:直方图是否均匀的标志符,默认状态下为均匀(true)。
  • accumulate:是否累积统计直方图的标志,如果累积(true),则统计新图像的直方图时之前图像的统计结果不会被清除,该同能主要用于统计多个图像整体的直方图。

该函数用于统计图像中每个灰度值像素的个数,例如统计一张CV_8UC1的图像,需要统计灰度值从0到255中每一个灰度值在图像中的像素个数,如果某个灰度值在图像中没有,那么该灰度值的统计结果就是0。由于该函数具有较多的参数,并且每个参数都较为复杂,因此作者建议读者在使用该函数时只统计单通道图像的灰度值分布,对于多通道图像可以将图像每个通道分离后再进行统计。

为了使读者更加了解函数的使用方法,我们在代码清单4-2中提供了绘制灰度图像的图像直方图的示例程序。在程序中我们首先使用calcHist()函数统计灰度图像里面每个灰度值的数目,之后通过不断绘制矩形的方式实现直方图的绘制。由于图像中部分灰度值像素数目较多,因此我们将每个灰度值数目缩小了20倍后再进行绘制,绘制的直方图在图4-1中所示。在程序中我们使用了OpenCV 4提供的四舍五入的取整函数cvRound(),该函数输入参数为double类型的变量,返回值为对该变量四舍五入后的int型数值。

#include <opencv2/opencv.hpp>
#include <opencv2/highgui/highgui.hpp>
using namespace cv;
using namespace std;

int main(){
    Mat img=imread("699342568.jpg");
    if(img.empty()){
        cout<<"请确认输入的图片路径是否正确"<<endl;
        return -1;
    }
    Mat gray;
    cvtColor(img,gray,COLOR_BGR2GRAY);
    //设置提取直方图的相关变量
    Mat hist;//用于存放直方图计算结果
    const int channels[1]={0};//通道索引
    float inRanges[2]={0,255};
    const float*ranges[1]={inRanges};//像素灰度值范围
    const int bins[1]={256};//直方图的维度,其实就是像素灰度值的最大值
    calcHist(&img,1,channels,Mat(),hist,1,bins,ranges);//计算图像直方图
    //准备绘制直方图
    int hist_w=512;
    int hist_h=400;
    int width=2;
    Mat histImage=Mat::zeros(hist_h,hist_w,CV_8UC3);
    for(int i=1;i<=hist.rows;++i){
        rectangle(histImage,Point(width*(i-1),hist_h-1),
                  Point(width*i-1,hist_h-cvRound(hist.at<float>(i-1)/20)),
                  Scalar(255,255,255),-1);
    }
    namedWindow("histImage",WINDOW_AUTOSIZE);
    imshow("histImage",histImage);
    imshow("gray",gray);
    waitKey(0);
    return 0;
}

在这里插入图片描述

C++版本OpenCv教程(二十一)直方图归一化

Excerpt

前面我们完成了对一张图像像素灰度值的统计,并成功绘制了图像的直方图。但是由于绘制直方图的图像高度小于某些灰度值统计的数目,因此我们在绘制直方图时将所有的数据都缩小为原来的二十分之一之后再进行绘制,目的就是为了能够将直方图完整的绘制在图像中。如果换一张图像的直方图统计结果或者将直方图绘制到一个尺寸更小的图像中时,可能需要将统计数据缩小为原来的三十分之一、五十分之一甚至更低。数据缩小比例与统计结果、将要绘制直方图图像的尺寸相关,因此每次绘制时都需要计算数据缩小的比例。另外,由于像素灰度值统计的数目与图像的尺寸具


前面我们完成了对一张图像像素灰度值的统计,并成功绘制了图像的直方图。但是由于绘制直方图的图像高度小于某些灰度值统计的数目,因此我们在绘制直方图时将所有的数据都缩小为原来的二十分之一之后再进行绘制,目的就是为了能够将直方图完整的绘制在图像中。如果换一张图像的直方图统计结果或者将直方图绘制到一个尺寸更小的图像中时,可能需要将统计数据缩小为原来的三十分之一、五十分之一甚至更低。数据缩小比例与统计结果、将要绘制直方图图像的尺寸相关,因此每次绘制时都需要计算数据缩小的比例。另外,由于像素灰度值统计的数目与图像的尺寸具有直接关系,如果以灰度值数目作为最终统计结果,那么一张图像经过尺寸放缩后的两张图像的直方图将会有巨大的差异,然而直方图可以用来表示图像的明亮程度,从理论上讲通过缩放的两张图像将具有大致相似的直方图分布特性,因此用灰度值的数目作为统计结果具有一定的局限性。

图像的像素灰度值统计结果主要目的之一就是查看某个灰度值在所有像素中所占的比例,因此可以用每个灰度值像素的数目占一幅图像中所有像素数目的比例来表示某个灰度值数目的多少,即将统计结果再除以图像中像素个数。这种方式可以保证每个灰度值的统计结果都是0到100%之间的数据,实现统计结果的归一化,但是这种方式也存在一个弊端,就是再CV_8U类型的图像中,灰度值有256个等级,平均每个像素的灰度值所占比例为0.39%,这个比例非常低,因此为了更直观的绘制图像直方图,常需要将比例扩大一定的倍数后再绘制图像。另一种常用的归一化方式是寻找统计结果中最大数值,把所有结果除以这个最大的数值,以实现将所有数据都缩放到0到1之间。

针对上面这两种归一化方式,OpenCV 4提供了normalize()函数实现多种形式的归一化功能,该函数的函数原型在代码清单4-3中给出。

void cv::normalize(InputArray src,
                   InputOutputArray dst,
                   double  alpha = 1,
                   double   beta = 0,
                   int  norm_type = NORM_L2,
                   int  dtype = -1,
                   InputArray mask = noArray()
                   )
  • src:输入数组矩阵。
  • dst:输入与src相同大小的数组矩阵。
  • alpha:在范围归一化的情况下,归一化到下限边界的标准值
  • beta:范围归一化时的上限范围,它不用于标准规范化。
  • norm_type:归一化过程中数据范数种类标志,常用可选择参数在表4-1中给出
  • dtype:输出数据类型选择标志,如果为负数,则输出数据与src拥有相同的类型,否则与src具有相同的通道数和数据类型。
  • mask:掩码矩阵。

该函数输入一个存放数据的矩阵,通过参数alpha设置将数据缩放到的最大范围,然后通过norm_type参数选择计算范数的种类,之后将输入矩阵中的每个数据分别除以求取的范数数值,最后得到缩放的结果。输出结果是一个CV_32F类型的矩阵,可以将输入矩阵作为输出矩阵,或者重新定义一个新的矩阵用于存放输出结果。该函数的第五个参数用于选择计算数据范数的种类,常用的可选择参数以及计算范数的公式都在表4-1中给出。计算不同的范数,最后的结果也不相同,例如选择NORM_L1标志,输出结果为每个灰度值所占的比例;选择NORM_INF参数,输出结果为除以数据中最大值,将所有的数据归一化到0到1之间。
在这里插入图片描述
为了了解归一化函数normalize()的作用,在代码清单4-4中给出了通过不同方式归一化数组的计算结果,并且分别用灰度值所占比例和除以数据最大值的方式对图像直方图进行归一化操作。为了更加直观的展现归一化后的结果,我们将每个灰度值所占比例放大了30倍,并将绘制直方图的图像高度作为1进行绘制直方图,最终结果在图4-3给出,根据结果显示,无论是否进行归一化,或者采用那种归一化方法,直方图的分布特性都不会改变。

#include <opencv2/opencv.hpp>
#include <opencv2/highgui/highgui.hpp>
using namespace cv;
using namespace std;

int main(){
    vector<double>positiveData{2.0,8.0,10.0};
    vector<double>normalized_L1,normalized_L2,normalized_Inf,normalized_L2SQR;
    //测试不同的归一化方法
    normalize(positiveData,normalized_L1,1.0,0.0,NORM_L1);//绝对值求和归一化
    cout<<"normalized_L1 = ["<<normalized_L1[0]<<", "
        <<normalized_L1[1]<<", "<<normalized_L1[2]<<"]"<<endl;
    normalize(positiveData,normalized_L2,1.0,0.0,NORM_L2);//模长归一化
    cout<<"normalized_L2 = ["<<normalized_L2[0]<<", "
        <<normalized_L2[1]<<", "<<normalized_L2[2]<<"]"<<endl;
    normalize(positiveData,normalized_Inf,1.0,0.0,NORM_INF);//最大值归一化
    cout<<"normalized_Inf = ["<<normalized_Inf[0]<<", "
        <<normalized_Inf[1]<<", "<<normalized_Inf[2]<<"]"<<endl;
    normalize(positiveData,normalized_L2SQR,1.0,0.0,NORM_MINMAX);//偏移归一化
    cout<<"normalized_L2SQR = ["<<normalized_L2SQR[0]<<", "
        <<normalized_L2SQR[1]<<", "<<normalized_L2SQR[2]<<"]"<<endl;

    //将直方图归一化
    Mat img=imread("699342568.jpg");
    if(img.empty()){
        cout<<"请确认输入的图片路径是否正确"<<endl;
        return -1;
    }
    Mat gray;
    cvtColor(img,gray,COLOR_BGR2GRAY);
    Mat hist;
    const int channels[1]={0};
    float inRanges[2]={0,255};
    const float* ranges[1]={inRanges};
    const int bins[1]={256};
    calcHist(&gray,1,channels,Mat(),hist,1,bins,ranges);
    int hist_w=512;
    int hist_h=400;
    int width=2;
    Mat histImage_L1=Mat::zeros(hist_h,hist_w,CV_8UC3);
    Mat histImage_Inf=Mat::zeros(hist_h,hist_w,CV_8UC3);
    Mat hist_L1,hist_Inf;
    normalize(hist,hist_L1,1,0,NORM_L1,-1,Mat());
    for(int i=1;i<=hist_L1.rows;++i){
        rectangle(histImage_L1,Point(width*(i-1),hist_h-1),
                  Point(width*i-1,hist_h-cvRound(hist_h*hist_L1.at<float>(i-1))-1),
                  Scalar(255,255,255),-1);
    }
    normalize(hist,hist_Inf,1,0,NORM_INF,-1,Mat());
    for(int i=1;i<=hist_Inf.rows;++i){
        rectangle(histImage_Inf,Point(width*(i-1),hist_h-1),
                  Point(width*i-1,hist_h-cvRound(hist_h*hist_Inf.at<float>(i-1))-1),
                  Scalar(255,255,255),-1);
    }
    imshow("histImage_L1",histImage_L1);
    imshow("histImage_Inf",histImage_Inf);
    waitKey(0);
    return 0;
}

在这里插入图片描述
在这里插入图片描述

C++版本OpenCv教程(二十二)直方图比较

Excerpt

图像的直方图表示图像像素灰度值的统计特性,因此可以通过比较两张图像的直方图特性比较两张图像的相似程度。从一定程度上来讲,虽然两张图像的直方图分布相似不代表两张图像相似,但是两张图像相似则两张图像的直方图分布一定相似。例如通过插值对图像进行放缩后图像的直方图虽然不会与之前完全一致,但是两者一定具有很高的相似性,因而可以通过比较两张图像的直方图分布相似性对图像进行初步的筛选与识别。OpenCV 4中提供了用于比较两个图像直方图相似性的compareHist()函数,该函数原型在代码清单4-5中给出。doub


图像的直方图表示图像像素灰度值的统计特性,因此可以通过比较两张图像的直方图特性比较两张图像的相似程度。从一定程度上来讲,虽然两张图像的直方图分布相似不代表两张图像相似,但是两张图像相似则两张图像的直方图分布一定相似。例如通过插值对图像进行放缩后图像的直方图虽然不会与之前完全一致,但是两者一定具有很高的相似性,因而可以通过比较两张图像的直方图分布相似性对图像进行初步的筛选与识别。

OpenCV 4中提供了用于比较两个图像直方图相似性的compareHist()函数,该函数原型在代码清单4-5中给出。

double cv::compareHist(InputArray H1,
                       InputArray H2,
                       int  method
                       )
  • H1:第一张图像直方图。
  • H2:第二张图像直方图,与H1具有相同的尺寸
  • method:比较方法标志,可选择参数及含义在表4-2中给出。

该函数前两个参数为需要比较相似性的图像直方图,由于不同尺寸的图像中像素数目可能不相同,为了能够得到两个直方图图像正确的相识性,需要输入同一种方式归一化后的图像直方图,并且要求两个图像直方图具有相同的尺寸。函数第三个参数为比较相似性的方法,选择不同的方法,会得到不同的相似性系数,函数将计算得到的相似性系数以double类型返回。由于不同计算方法的规则不一,因此相似性系数代表的含义也不相同,函数可以选择的计算方式标志在表4-2中给出,接下来介绍每种方法比较相似性的原理。
在这里插入图片描述

HISTCMP_CORREL

该方法名为相关法,其计算相似性原理在式(6.1)中给出,在该方法中如果两个图像直方图完全一致,则计算数值为1;如果两个图像直方图完全不相关,则计算值为0。
在这里插入图片描述
其中
在这里插入图片描述

其中 N是直方图的灰度值个数。

HISTCMP_CHISQR

该方法名为卡方法,其计算相似性原理在式(6.3)中给出,在该方法中如果两个图像直方图完全一致,则计算数值为0,两个图像的相似性越小,计算数值越大。
在这里插入图片描述

HISTCMP_INTERSECT

该方法名为直方图相交法,其计算相似性原理在式(6.4)中给出,在该方法不会将计算结果归一化,因此即使是两个完全一致的图像直方图,来自于不同图像也会有不同的数值,但是其遵循与同一个图像直方图比较时,数值越大相似性越高,数值越小相似性越低。
在这里插入图片描述

HISTCMP_INTERSECT

该方法名为直方图相交法,其计算相似性原理在式(6.4)中给出,在该方法不会将计算结果归一化,因此即使是两个完全一致的图像直方图,来自于不同图像也会有不同的数值,但是其遵循与同一个图像直方图比较时,数值越大相似性越高,数值越小相似性越低。
在这里插入图片描述

HISTCMP_BHATTACHARYYA

该方法名为巴塔恰里雅距离(巴氏距离)法,其计算相似性原理在式(6.5)中给出,在该方法中如果两个图像直方图完全一致,则计算数值为0,两个图像的相似性越小,计算数值越大。
在这里插入图片描述

HISTCMP_CHISQR_ALT

该方法与巴氏距离法相同,常用于替代巴氏距离法用于纹理比较,计算公式如式(6.6),
在这里插入图片描述

HISTCMP_KL_DIV

该方法名为相对熵法,又名Kullback-Leibler散度法,其计算相似性原理在式(6.7)中给出,在该方法中如果两个图像直方图完全一致,则计算数值为0,两个图像的相似性越小,计算数值越大。
在这里插入图片描述
为了验证通过直方图比较两张图像相似性的可行性,在代码清单4-6中提供了三张图像直方图比较的示例程序。在程序中,我们将读取的图像转成灰度图像,之后将图像缩小为原来尺寸的一半,同时读取另外一张图像的灰度图,计算这三张图像的直方图,直方图的结果在图4-4中给出,通过观看直方图的趋势可以发现即使将图像尺寸缩小,两张图像的直方图分布也有一定的相似性。之后利用compareHist()函数对三个直方图进行比较,比较结果也显示图像缩小后的直方图与原来图像的直方图具有很高的相似性,而两张完全不相同的图像的直方图相似性比较小。

#include <opencv2/opencv.hpp>
#include <opencv2/highgui/highgui.hpp>
using namespace cv;
using namespace std;

void drawHist(Mat&hist,int type,string name){//归一化并回执直方图函数
    int hist_w=512;
    int hist_h=400;
    int width=2;
    Mat histImage=Mat::zeros(hist_h,hist_w,CV_8UC3);
    normalize(hist,hist,1,0,type,-1,Mat());
    for(int i=1;i<=hist.rows;++i){
        rectangle(histImage,Point(width*(i-1),hist_h-1),
                  Point(width*i-1,hist_h-cvRound(hist_h*hist.at<float>(i-1))-1),
                  Scalar(255,255,255),-1);
    }
    imshow(name,histImage);
}

int main(){
    Mat img=imread("apple.jpg");
    if(img.empty()){
        cout<<"请确认输入的图片路径是否正确"<<endl;
        return -1;
    }
    Mat gray,hist,gray2,hist2,gray3,hist3;
    cvtColor(img,gray,COLOR_BGR2GRAY);
    resize(gray,gray2,Size(),0.5,0.5);
    gray3=imread("lena.png",IMREAD_GRAYSCALE);
    const int channels[1]={0};
    float inRanges[2]={0,255};
    const float*ranges[1]={inRanges};
    const int bins[1]={256};
    calcHist(&gray,1,channels,Mat(),hist,1,bins,ranges);
    calcHist(&gray2,1,channels,Mat(),hist2,1,bins,ranges);
    calcHist(&gray3,1,channels,Mat(),hist3,1,bins,ranges);
    drawHist(hist,NORM_INF,"hist");
    drawHist(hist2,NORM_INF,"hist2");
    drawHist(hist3,NORM_INF,"hist3");

    //原图直方图与原图直方图的相关系数
    double hist_hist=compareHist(hist,hist,HISTCMP_CORREL);
    cout<<"apple_apple = "<<hist_hist<<endl;

    //原图直方图与缩小原图直方图的相关系数
    double hist_hist2=compareHist(hist,hist2,HISTCMP_CORREL);
    cout<<"apple_apple256 = "<<hist_hist2<<endl;

    //两张不同图像直方图的相关系数
    double hist_hist3=compareHist(hist,hist3,HISTCMP_CORREL);
    cout<<"apple_lena = "<<hist_hist3<<endl;

    waitKey(0);
    return 0;
}

在这里插入图片描述
在这里插入图片描述

C++版本OpenCv教程(二十三)直方图均衡化

Excerpt

如果一个图像的直方图都集中在一个区域,则整体图像的对比度比较小,不便于图像中纹理的识别。例如相邻的两个像素灰度值如果分别是120和121,仅凭肉眼是如法区别出来的。同时,如果图像中所有的像素灰度值都集中在100到150之间,则整个图像想会给人一种模糊的感觉,看不清图中的内容。如果通过映射关系,将图像中灰度值的范围扩大,增加原来两个灰度值之间的差值,就可以提高图像的对比度,进而将图像中的纹理突出显现出来,这个过程称为图像直方图均衡化。在OpenCV 4中提供了equalizeHist()函数用于将图像的直方


如果一个图像的直方图都集中在一个区域,则整体图像的对比度比较小,不便于图像中纹理的识别。例如相邻的两个像素灰度值如果分别是120和121,仅凭肉眼是如法区别出来的。同时,如果图像中所有的像素灰度值都集中在100到150之间,则整个图像想会给人一种模糊的感觉,看不清图中的内容。如果通过映射关系,将图像中灰度值的范围扩大,增加原来两个灰度值之间的差值,就可以提高图像的对比度,进而将图像中的纹理突出显现出来,这个过程称为图像直方图均衡化。

OpenCV 4中提供了equalizeHist()函数用于将图像的直方图均衡化,该函数的函数原型在代码清单4-7中给出。

void cv::equalizeHist(InputArray src,OutputArray dst)
  • src:需要直方图均衡化的CV_8UC1图像。
  • dst:直方图均衡化后的输出图像,与src具有相同尺寸和数据类型。

该函数形式比较简单,但是需要注意该函数只能对单通道的灰度图进行直方图均衡化。对图像的均衡化示例程序在代码清单4-8中给出,程序中我们将一张图像灰度值偏暗的图像进行均衡化,通过结果可以发现经过均衡化后的图像对比度明显增加,可以看清楚原来看不清的纹理。通过绘制原图和均衡化后的图像的直方图可以发现,经过均衡化后的图像直方图分布更加均匀。

#include <opencv2/opencv.hpp>
#include <opencv2/highgui/highgui.hpp>
using namespace cv;
using namespace std;

void drawHist(Mat&hist,int type,string name){//归一化并回执直方图函数
    int hist_w=512;
    int hist_h=400;
    int width=2;
    Mat histImage=Mat::zeros(hist_h,hist_w,CV_8UC3);
    normalize(hist,hist,1,0,type,-1,Mat());
    for(int i=1;i<=hist.rows;++i){
        rectangle(histImage,Point(width*(i-1),hist_h-1),
                  Point(width*i-1,hist_h-cvRound(hist_h*hist.at<float>(i-1))-1),
                  Scalar(255,255,255),-1);
    }
    imshow(name,histImage);
}

int main(){
    Mat img=imread("gearwheel.jpg");
    if(img.empty()){
        cout<<"请确认输入的图片路径是否正确"<<endl;
        return -1;
    }
    Mat gray,hist,hist2;
    cvtColor(img,gray,COLOR_BGR2GRAY);
    Mat equalImg;
    equalizeHist(gray,equalImg);//将图像直方图均衡化
    const int channels[1]={0};
    float inRanges[2]={0,255};
    const float*ranges[1]={inRanges};
    const int bins[1]={256};
    calcHist(&gray,1,channels,Mat(),hist,1,bins,ranges);
    calcHist(&equalImg,1,channels,Mat(),hist2,1,bins,ranges);
    drawHist(hist,NORM_INF,"hist");
    drawHist(hist2,NORM_INF,"hist2");
    imshow("原图",gray);
    imshow("均衡化后的图像",equalImg);
    waitKey(0);
    return 0;
}

在这里插入图片描述

C++版本OpenCv教程(二十四)直方图匹配

Excerpt

直方图均衡化函数可以自动的改变图像直方图的分布形式,这种方式极大的简化了直方图均衡化过程中需要的操作步骤,但是该函数不能指定均衡化后的直方图分布形式。在某些特定的条件下需要将直方图映射成指定的分布形式,这种将直方图映射成指定分布形式的算法称为直方图匹配或者直方图规定化。直方图匹配与直方图均衡化相似,都是对图像的直方图分布形式进行改变,只是直方图均衡化后的图像直方图是均匀分布的,而直方图匹配后的直方图可以随意指定,即在执行直方图匹配操作时,首先要知道变换后的灰度直方图分布形式,进而确定变换函数。直方图匹配操作


直方图均衡化函数可以自动的改变图像直方图的分布形式,这种方式极大的简化了直方图均衡化过程中需要的操作步骤,但是该函数不能指定均衡化后的直方图分布形式。在某些特定的条件下需要将直方图映射成指定的分布形式,这种将直方图映射成指定分布形式的算法称为直方图匹配或者直方图规定化。直方图匹配与直方图均衡化相似,都是对图像的直方图分布形式进行改变,只是直方图均衡化后的图像直方图是均匀分布的,而直方图匹配后的直方图可以随意指定,即在执行直方图匹配操作时,首先要知道变换后的灰度直方图分布形式,进而确定变换函数。直方图匹配操作能够有目的的增强某个灰度区间,相比于直方图均衡化操作,该算法虽然多了一个输入,但是其变换后的结果也更灵活。

由于不同图像间像素数目可能不同,为了使两个图像直方图能够匹配,需要使用概率形式去表示每个灰度值在图像像素中所占的比例。理想状态下,经过图像直方图匹配操作后图像直方图分布形式应与目标分布一致,因此两者之间的累积概率分布也一致。累积概率为小于等于某一灰度值的像素数目占所有像素中的比例。我们用Vs表示原图像直方图的各个灰度级的累积概率,用Vz表示匹配后直方图的各个灰度级累积概率。那么确定由原图像中灰度值n映射成r的条件如式(6.8)所示。
在这里插入图片描述

为了更清楚的说明直方图匹配过程,在图4-7中给出了一个直方图匹配示例。示例中目标直方图灰度值2以下的概率都为0,灰度值3的累积概率为0.16,灰度值4的累积概率为0.35,原图像直方图灰度值为0时累积概率为0.19。0.19距离0.16的距离小于距离0.35的距离,因此需要将原图像中灰度值0匹配成灰度值3。同样,原图像灰度值1的累积概率为0.43,其距离目标直方图灰度值4的累积概率0.35的距离为0.08,而距离目标直方图灰度值5的累积概率0.64的距离为0.21,因此需要将原图像中灰度值1匹配成灰度值4。
在这里插入图片描述
这个寻找灰度值匹配的过程是直方图匹配算法的关键,在代码实现中我们可以通过构建原直方图累积概率与目标直方图累积概率之间的差值表,寻找原直方图中灰度值n的累积概率与目标直方图中所有灰度值累积概率差值的最小值,这个最小值对应的灰度值r就是n匹配后的灰度值。

OpenCV 4中并没有提供直方图匹配的函数,需要自己根据算法实现图像直方图匹配。在代码清单4-9中给出了实现直方图匹配的示例程序。程序中待匹配的原图是一个图像整体偏暗的图像,目标直方图分配形式来自于一张较为明亮的图像,经过图像直方图匹配操作之后,提高了图像的整体亮度,图像直方图分布也更加均匀,程序中所有的结果在图4-8、图4-9给出。

#include <opencv2/opencv.hpp>
#include <opencv2/highgui/highgui.hpp>
using namespace cv;
using namespace std;

void drawHist(Mat&hist,int type,string name){//归一化并回执直方图函数
    int hist_w=512;
    int hist_h=400;
    int width=2;
    Mat histImage=Mat::zeros(hist_h,hist_w,CV_8UC3);
    normalize(hist,hist,1,0,type,-1,Mat());
    for(int i=1;i<=hist.rows;++i){
        rectangle(histImage,Point(width*(i-1),hist_h-1),
                  Point(width*i-1,hist_h-cvRound(hist_h*hist.at<float>(i-1))-1),
                  Scalar(255,255,255),-1);
    }
    imshow(name,histImage);
}

int main(){
    Mat img1=imread("histMatch.png");
    Mat img2=imread("equalLena.png");
    if(img1.empty()||img2.empty()){
        cout<<"请确认输入的图片路径是否正确"<<endl;
        return -1;
    }
    Mat hist1,hist2;
    //计算两张图像的直方图
    const int channels[1]={0};
    float inRanges[2]={0,255};
    const float *ranges[1]={inRanges};
    const int bins[1]={256};
    calcHist(&img1,1,channels,Mat(),hist1,1,bins,ranges);
    calcHist(&img2,1,channels,Mat(),hist2,1,bins,ranges);
    //归一化两张图像
    drawHist(hist1,NORM_INF,"hist1");
    drawHist(hist2,NORM_INF,"hist2");
    //计算两张图向直方图的;累计概率
    float hist1_cdf[256]={hist1.at<float>(0)};
    float hist2_cdf[256]={hist2.at<float>(0)};
    for(int i=1;i<256;++i){
        hist1_cdf[i]=hist1_cdf[i-1]+hist1.at<float>(i);
        hist2_cdf[i]=hist2_cdf[i-1]+hist2.at<float>(i);
    }
    //构建累积概率误差矩阵
    float diff_cdf[256][256];
    for(int i=0;i<256;++i){
        for(int j=0;j<256;++j){
            diff_cdf[i][j]=fabs(hist1_cdf[i]-hist2_cdf[i]);
        }
    }

    //生成LUT映射表
    Mat lut(1,256,CV_8U);
    for(int i=0;i<256;++i){
        //查找源灰度级为i的映射灰度
        //和i的累积概率差值的最小的规定花灰度
        float min=diff_cdf[i][0];
        int index=0;
        //寻找累积概率误差矩阵中每一行中的最小值
        for(int j=1;j<256;++j){
            if(min>diff_cdf[i][j]){
                min=diff_cdf[i][j];
                index=j;
            }
        }
        lut.at<uchar>(i)=(uchar)index;
    }
    Mat result,hist3;
    LUT(img1,lut,result);
    imshow("待匹配图像",img1);
    imshow("匹配的模板图像",img2);
    imshow("直方图匹配结果",result);
    calcHist(&result,1,channels,Mat(),hist3,1,bins,ranges);
    drawHist(hist3,NORM_L1,"hist3");
    waitKey(0);
    return 0;
}

在这里插入图片描述在这里插入图片描述

C++版本OpenCv教程(二十五)图像模板匹配

Excerpt

前面我们通过图像直方图反向投影的方式在图像中寻找模板图像,由于直方图不能直接反应图像的纹理,因此如果两张不同模板图像具有相同的直方图分布特性,那么在同一张图中对这两张模板图像的直方图进行反向投影,最终结果将不具有参考意义。因此,我们在图像中寻找模板图像时,可以直接通过比较图像像素的形式来搜索是否存在相同的内容,这种通过比较像素灰度值来寻找相同内容的方法叫做图像的模板匹配。模板匹配常用于在一幅图像中寻找特定内容的任务中。由于模板图像的尺寸小于待匹配图像的尺寸,同时又需要比较两张图像中的每一个像素的灰度值,因


前面我们通过图像直方图反向投影的方式在图像中寻找模板图像,由于直方图不能直接反应图像的纹理,因此如果两张不同模板图像具有相同的直方图分布特性,那么在同一张图中对这两张模板图像的直方图进行反向投影,最终结果将不具有参考意义。因此,我们在图像中寻找模板图像时,可以直接通过比较图像像素的形式来搜索是否存在相同的内容,这种通过比较像素灰度值来寻找相同内容的方法叫做图像的模板匹配。

模板匹配常用于在一幅图像中寻找特定内容的任务中。由于模板图像的尺寸小于待匹配图像的尺寸,同时又需要比较两张图像中的每一个像素的灰度值,因此常采用在待匹配图像中选择与模板相同尺寸的滑动窗口,通过比较滑动窗口与模板的相似程度,判断待匹配图像中是否含有与模板图像相同的内容,其原理如图4-11所示。
模板匹配示意图

在图4-11中,右侧4×4的图像是模板图像,每个像素中的数字是该像素的灰度值,左侧8×8图像是待匹配图像,模板匹配的流程如下:

Step1:在待匹配图像中选取与模板尺寸大小相同的滑动窗口,如图4-11中的阴影区域所示。

Step2:比较滑动窗口中每个像素与模板中对应像素灰度值的关系,计算模板与滑动窗口的相似性。

Step3:将滑动窗口从左上角开始先向右滑动,滑动到最右边后向下滑动一行,从最左侧重新开始滑动,记录每一次移动后计算的模板与滑动窗口的相似性。

Step4:比较所有位置的相似性,选择相似性最大的滑动窗口作为备选匹配结果。

OpenCV 4中提供了用于图像模板匹配的函数matchTemplate(),该函数能够实现模板匹配过程中图像与模板相似性的计算,在代码清单4-12中给出了函数原型。

void cv::matchTemplate(InputArray image,
                       InputArray templ,
                       OutputArray result,
                       int  method,
                       InputArray mask = noArray()
                       )
  • image:待模板匹配的原图像,图像数据类型为CV_8U和CV_32F两者中的一个。
  • templ:模板图像,需要与image具有相同的数据类型,但是尺寸不能大于image。
  • result:模板匹配结果输出图像,图像数据类型为CV_32F。如果image的尺寸为W×H,模板图像尺寸为w×h,则输出图像的尺寸为(W-w+1)×(H-h+1)。
  • method:模板匹配方法标志,可选择参数及含义在表4-3中给出。
  • mask:匹配模板的掩码,必须与模板图像具有相同的数据类型和尺寸,默认情况下不设置,目前仅支持在TM_SQDIFF和TM_CCORR_NORMED这两种匹配方法时使用。

该函数同时支持灰度图像和彩色图像两种图像的模板匹配。函数前两个参数为输入的原图像和模板图像,由于是在原图像中搜索是否存在与模板图像相同的内容,因此需要模板图像的尺寸小于原图像,并且两者必须具有相同的数据类型。第三个参数为相似性矩阵,滑动窗口与模板的相似性系数存放在滑动窗口左上角第一个像素处,因此输出的相似性矩阵尺寸要小于原图像的尺寸,如果image的尺寸为W×H,模板图像尺寸为w×h,则输出图像的尺寸为(W-w+1)×(H-h+1)。因为在模板匹配中原图像不需要进行尺寸的外延,所以滑动窗口左上角可以移动的范围要小于原图像的尺寸。无论输入的是彩色图像还是灰度图像,函数输出结果都是单通道矩阵。了解相似性系数记录的方式便于寻找到与模板最相似的滑动窗口,继而在原图中标记出与模板相同的位置。函数第四个参数是滑动窗口与模板相似性系数的计算方式,OpenCV 4提供了多种计算方法,所有可以选择的标志参数在表4-3中给出,接下来对每一种方法进行详细介绍。
在这里插入图片描述

1.TM_SQDIFF

该方法名为平方差匹配法,计算的公式如式(6.9)所示,这种方法利用平方差来进行匹配,当模板与滑动窗口完全匹配时计算数值为0,两者匹配度越低计算数值越大。
在这里插入图片描述

2.TM_SQDIFF_NORMED

该方法名为归一化平方差匹配方法,计算公式如式(6.10)所示,这种方法是将平方差方法进行归一化,使得输入结果缩放到了0到1之间,当模板与滑动窗口完全匹配时计算数值为0,两者匹配度越低计算数值越大。
在这里插入图片描述

3.TM_CCORR

该方法名为相关匹配法,计算公式如式(6.11)所示,这类方法采用模板和图像间的乘法操作,所以数值越大表示匹配效果越好,0表示最坏的匹配结果。
在这里插入图片描述

4.TM_CCORR_NORMED

该方法名为归一化相关匹配法,计算公式如式(6.12)所示,这种方法是将相关匹配法进行归一化,使得输入结果缩放到了0到1之间,当模板与滑动窗口完全匹配时计算数值为1,两者完全不匹配时计算结果为0。
在这里插入图片描述

5.TM_CCOEFF

该方法名为系数匹配法,计算公式如式(6.13)所示,这种方法采用相关匹配方法对模板减去均值的结果和原图像减去均值的结果进行匹配,这种方法可以很好的解决模板图像和原图像之间由于亮度不同而产生的影响。该方法中模板与滑动窗口匹配度越高计算数值越大,匹配度越低计算数值越小,并且该方法计算结果可以为负数。
在这里插入图片描述
其中:
在这里插入图片描述

6.TM_CCOEFF_NORMED

该方法名为归一化系数匹配法,计算公式如式(6.15)所示,这种方法将系数匹配方法进行归一化,使得输入结果缩放到了1到-1之间,当模板与滑动窗口完全匹配时计算数值为1,当两者完全不匹配时计算结果为-1。
在这里插入图片描述
了解不同的计算相似性方法时,重点需要知道在每种方法中最佳匹配结果的数值应该是较大值还是较小值,由于matchTemplate()函数的输出结果是存有相关性系数的矩阵,因此需要通过minMaxLoc()函数去寻找输入矩阵中的最大值或者最小值,进而确定模板匹配的结果。

通过寻找输出矩阵的最大值或者最小值得到的只是一个像素点,需要以该像素点为矩形区域的左上角,绘制与模板图像同尺寸的矩形框,标记出最终匹配的结果。为了了解图像模板匹配相关函数的使用方法,在代码清单4-13中给出了在彩色图像中进行模板匹配的示例程序。程序中采用TM_CCOEFF_NORMED方法计算相关性系数,通过minMaxLoc()函数寻找相关性系数中的最大值,确定最佳匹配值的像素点坐标,之后在原图中绘制出与模板最佳匹配区域的范围,程序的运行结果在图4-12中给出。

#include <opencv2/opencv.hpp>
#include <opencv2/highgui/highgui.hpp>
using namespace cv;
using namespace std;

int main(){
    Mat img=imread("luffy.jpg");
    Mat temp=imread("luffy_face.png");
    if(img.empty()||temp.empty()){
        cout<<"请确认输入的图片路径是否正确"<<endl;
        return -1;
    }
    Mat result;
    matchTemplate(img,temp,result,TM_CCOEFF_NORMED);//模板匹配
    double maxVal,minVal;
    Point minLoc,maxLoc;
    //寻找匹配结果中的最大值和最小值以及坐标位置
    minMaxLoc(result,&minVal,&maxVal,&minLoc,&maxLoc);
    //回执最佳匹配结果
    rectangle(img,Rect(maxLoc.x,maxLoc.y,temp.cols,temp.rows),Scalar(0,0,255),2);
    imshow("原图",img);
    imshow("模板图像",temp);
    imshow("result",result);
    waitKey(0);
    return 0;
}

在这里插入图片描述在这里插入图片描述

C++版本OpenCv教程(二十六)图像中添加椒盐噪声

Excerpt

椒盐噪声又被称作脉冲噪声,它会随机改变图像中的像素值,是由相机成像、图像传输、解码处理等过程产生的黑白相间的亮暗点噪声,其样子就像在图像上随机的撒上一些盐粒和黑椒粒,因此被称为椒盐噪声。目前为止OpenCV 4中没有提供专门用于为图像添加椒盐噪声的函数,需要使用者根据自己需求去编写生成椒盐噪声的程序,本小节将会带领读者一起实现在图像中添加椒盐噪声。考虑到椒盐噪声会随机产生在图像中的任何一个位置,因此对于椒盐噪声的生成需要使用到OpenCV 4中能够产生随机数的函数rand(),为了能够生成不同数据类型的随


椒盐噪声又被称作脉冲噪声,它会随机改变图像中的像素值,是由相机成像、图像传输、解码处理等过程产生的黑白相间的亮暗点噪声,其样子就像在图像上随机的撒上一些盐粒和黑椒粒,因此被称为椒盐噪声。目前为止OpenCV 4中没有提供专门用于为图像添加椒盐噪声的函数,需要使用者根据自己需求去编写生成椒盐噪声的程序,本小节将会带领读者一起实现在图像中添加椒盐噪声。

考虑到椒盐噪声会随机产生在图像中的任何一个位置,因此对于椒盐噪声的生成需要使用到OpenCV 4中能够产生随机数的函数rand(),为了能够生成不同数据类型的随机数,该函数拥有多种演变形式,在代码清单5-3中给出了这几种形式的函数原型。

int cvflann::rand()
double cvflann::rand_double(double high = 1.0,double low = 0 )
int cvflann::rand_int(int high = RAND_MAX,int low = 0 )
  • high:输出随机数的最大值
  • low:输出随机数的最小值

这三个函数都可以用来生成随机数,区别在于
第一个函数rand()不需要输入任何的参数,返回的随机数为int类型;
第二个函数rand_double()需要输入随机数的上下边界,默认状态下生成的随机数在0到1之间,返回的随机数为double类型;第三个函数rand_int()也需要输入随机数的上下边界,不同的是该函数默认状态下的最大值为RAND_MAX,这是一个由系统定义的宏变量,在笔者的计算机中这个变量表示的是整数32767,该函数会返回的随机数为int类型。这三个函数的功能和使用方式上都比较简单,这里有个小技巧,rand()函数虽然没有给出随机数的取值范围,但是可以采用求取余数的方式来实现对随机数范围的设置,例如使用rand()函数随机生成一个0到100之间的整数,可以使用“int a = rand()%100”语句来实现,因为无论任何数除以100后的余数一定在0到100之间。

注意
该函数与之前所有的函数不相同之处在于该函数并不在cv的命名空间中,而是在cvflann类中,因此在使用的时候一定要在函数前添加前缀,如cvflann::rand()。有些读者在使用rand()函数时不添加cvflann命名空间的前缀也可以使用,是因为该函数不仅在OpenCV 4中有,在stdlib.h头文件中同样有这个函数,只有在函数前面添加了命名空间前缀时使用的才是OpenCV 4中的随机数生成函数。

了解随机函数之后,在图像中添加椒盐噪声大致分为以下4个步骤

Step1:确定添加椒盐噪声的位置。根据椒盐噪声会随机出现在图像中任何一个位置的特性,我们可以通过随机数函数生成两个随机数,分别用于确定椒盐噪声产生的行和列。

Step2:确定噪声的种类。不仅椒盐噪声的位置是随机的,噪声点是黑色的还是白色的也是随机的,因此可以再次生成的随机数,通过判断随机数的奇偶性确定该像素是黑色噪声点还是白色噪声点。

Step3:修改图像像素灰度值。判断图像通道数,通道数不同的图像中像素表示白色的方式也不相同。也可以根据需求只改变多通道图像中某一个通道的数值。

Step4:得到含有椒盐噪声的图像。

依照上述思想,在代码清单5-4中给出在图像中添加椒盐噪声的示例程序,程序中判断了输入图像是灰度图还是彩色图,但是没有对彩色图像的单一颜色通道产生椒盐噪声。如果需要对某一通道产生椒盐噪声,只需要单独处理彩色图像每个通道即可。程序在图像中添加椒盐噪声的结果如图5-6、图5-7所示,由于椒盐噪声是随机添加的,因此每次运行结果会有所差异。

#include <opencv2/opencv.hpp>
#include <opencv2/highgui/highgui.hpp>
using namespace cv;
using namespace std;

//椒盐噪声函数
void saltAndPepper(Mat image,int n){
    for(int k=0;k<n;++k){
        //随机确定图像中位置
        int i,j;
        i=rand()%image.cols;//取余数运算,保证在图像的列数内
        j=rand()%image.rows;//取余数运算,保证在图像的行数内
        int write_black=rand()%2;//判定为白色噪声还是黑色噪声的变量
        if(write_black==0){//添加白色噪声
            if(image.type()==CV_8UC1) {//处理灰度图像
                image.at<uchar>(j,i)=255;//白色噪声
            }
            else if(image.type()==CV_8UC3){//处理彩色图像
                image.at<Vec3b>(j,i)[0]=255;//Vec3b为opencv定义的3个值的向量类型
                image.at<Vec3b>(j,i)[1]=255;//[]制定通道,B:0,G:1,R:2
                image.at<Vec3b>(j,i)[2]=255;
            }
        }
        else{//添加黑噪声
            if(image.type()==CV_8UC1){
                image.at<uchar>(j,i)=0;
            }
            else if(image.type()==CV_8UC3){
                image.at<Vec3b>(j,i)[0]=0;//Vec3b为opencv定义的3个值的向量类型
                image.at<Vec3b>(j,i)[1]=0;//[]制定通道,B:0,G:1,R:2
                image.at<Vec3b>(j,i)[2]=0;
            }
        }
    }
}

int main(){
    Mat img_=imread("luffy.jpg");
    Mat img;
    resize(img_,img,Size(img_.cols/2,img_.rows/2));
    Mat gray;
    cvtColor(img,gray,COLOR_BGR2GRAY);
    if(img.empty()||gray.empty()){
        cout<<"请确认输入的路径是否正确"<<endl;
        return -1;
    }
    imshow("luffy原图",img);
    imshow("luffy_gray原图",gray);
    saltAndPepper(img,10000);//彩色图像添加椒盐噪声
    saltAndPepper(gray,10000);//灰度图像添加椒盐噪声
    imshow("luffy添加噪声",img);
    imshow("luffy_gray添加噪声",gray);
    waitKey(0);
    return 0;
}

在这里插入图片描述

C++版本OpenCv教程(二十七)图像中添加高斯噪声

Excerpt

高斯白噪声是指噪声分布的概率密度服从高斯分布(正态分布)的一类噪声,其产生的主要原因是由于相机在拍摄时视场较暗且亮度不均匀造成的,同事相机长时间工作使得温度过高也会引起高斯噪声,另外电路元器件自身噪声和互相影响也是造成高斯噪声的重要原因之一。高斯噪声的概率密度函数如式(5.2)所示,其中Z表示图像的灰度值,μ表示像素值的平均值或者期望值,…


在这里插入图片描述
OpenCV 4中同样没有专门为图像添加高斯噪声的函数,对照在图像中添加椒盐噪声的过程,我们可以根据需求利用能够产生随机数的函数来完成在图像中添加高斯噪声的任务。在OpenCV 4中提供了fill()函数可以产生均匀分布或者高斯分布(正态分布)的随机数,我们可以利用该函数产生符合高斯分布的随机数,之后在图像中加入这些随机数即可,我们首先了解该函数的使用方式,该函数的函数原型在代码清单5-5中给出。

void cv::RNG::fill(InputOutputArray mat,
                   int  distType,
                   InputArray a,
                   InputArray b,
                   bool  saturateRange = false 
                   )
  • mat:用于存放随机数的矩阵,目前只支持低于5通道的矩阵。
  • distType:随机数分布形式选择标志,目前生成的随机数支持均匀分布(RNG::UNIFORM,0)和高斯分布(RNG::NORMAL,1)。
  • a:确定分布规律的参数。当选择均匀分布时,该参数表示均匀分布的最小下限;当选择高斯分布时,该参数表示高斯分布的均值。
  • b:确定分布规律的参数。当选择均匀分布时,该参数表示均匀分布的最大上限;当选择高斯分布时,该参数表示高斯分布的标准差。
  • saturateRange:预饱和标志,仅用于均匀分布。

该函数用于生成指定分布形式的随机数填充矩阵,可以生成符合均匀分布的随机数符合高斯分布随机数。函数的第一个参数输入用于存储生成随机数的矩阵,但是矩阵的通道数必须小于等于4。第二个参数是选择随机数分布形式的标志,该函数目前只支持两种分布形式,分别是均匀分布(RNG::UNIFORM,简记0)高斯分布(RNG::NORMAL,简记1)。函数的第三个和第四个参数为确定随机数分布规律的参数,第三个参数在均匀分布时表示均匀分布的最小下限,在高斯分布时表示高斯分布的均值;第四个参数在均匀分布时表示均匀分布的最大上限,在高斯分布时表示高斯分布的标准差。最后一个参数是预饱和标志,仅用于均匀分布,我们使用其默认式即可。需要注意的是该函数属于OpenCV 4的RNG类,是一个非静态成员函数,因此在使用的时候不能像使用正常函数一样的直接使用,而需要首先创建一个RNG类的变量,之后通过访问这个变量中函数进行调用这个函数,具体使用方式在代码清单5-6中给出。

cv::RNG rng;
rng.fill(mat, RNG::NORMAL, 10, 20);

在图像中添加高斯噪声大致分为以下4个步骤:
Step1:首先需要创建一个与图像尺寸、数据类型以及通道数相同的Mat类变量.
Step2:通过调用fill()函数在Mat类变量中产生符合高斯分布的随机数。
Step3:将原图像和含有高斯分布的随机数矩阵相加。
Step4:得到添加高斯噪声之后的图像。

依照上述思想,在代码清单5-7中给出了在图像中添加高斯噪声的示例程序,程序实现了对灰度图像和彩色图像添加高斯噪声,在图像中添加高斯噪声的结果如图5-8、图5-9所示,由于高斯噪声是随机生成的,因此每次运行结果会有差异。

#include <opencv2/opencv.hpp>
#include <opencv2/highgui/highgui.hpp>
using namespace cv;
using namespace std;

int main(){
    Mat img_=imread("luffy.jpg");
    Mat luffy;
    resize(img_,luffy,Size(img_.cols/2,img_.rows/2));
    Mat luffy_gray;
    cvtColor(luffy,luffy_gray,COLOR_BGR2GRAY);
    if(luffy.empty()||luffy_gray.empty()){
        cout<<"请确认输入的路径是否正确"<<endl;
        return -1;
    }
    //生成与原图像相同尺寸、数据类型和通道类型的矩阵
    Mat luffy_noise=Mat::zeros(luffy.rows,luffy.cols,luffy.type());
    Mat luffy_gray_noise=Mat::zeros(luffy.rows,luffy.cols,luffy_gray.type());
    imshow("luffy原图",luffy);
    imshow("luffy_gray原图",luffy_gray);
    RNG rng;//创建一个RNG类
    rng.fill(luffy_noise,RNG::NORMAL,10,20);//生成三通道的高斯分布随机数
    rng.fill(luffy_gray_noise,RNG::NORMAL,15,30);//生成三通道的高斯分布随机数
    imshow("三通道高斯噪声",luffy_noise);
    imshow("单通道高斯噪声",luffy_gray_noise);
    luffy=luffy+luffy_noise;//在彩色图像中添加高斯噪声
    luffy_gray=luffy_gray+luffy_gray_noise;//在灰度图像中添加高斯噪声
    //显示添加高斯噪声后的图像
    imshow("lufy添加噪声",luffy);
    imshow("lufy_gray添加噪声",luffy_gray);
    waitKey(0);
    return 0;
}

在这里插入图片描述

C++版本OpenCv教程(二十八)均值滤波

Excerpt

我们在测量数据时,往往会多次测量最后求取所有数据的平均值作为最终结果,均值滤波的思想和测量数据时多次测量求取平均值的思想一致。均值滤波将滤波器内所有的像素值都看作中心像素值的测量,将滤波器内所有的像数值的平均值作为滤波器中心处图像像素值。滤波器内的每个数据表示对应的像素在决定中心像素值的过程中所占的权重,由于滤波器内所有的像素值在决定中心像素值的过程中占有相同的权重,因此滤波器内每个数据都相等。均值滤波的优点是在像素值变换趋势一致的情况下,可以将受噪声影响而突然变化的像素值修正到接近周围像素值变化的一致性下


我们在测量数据时,往往会多次测量最后求取所有数据的平均值作为最终结果,均值滤波的思想和测量数据时多次测量求取平均值的思想一致。均值滤波将滤波器内所有的像素值都看作中心像素值的测量,将滤波器内所有的像数值的平均值作为滤波器中心处图像像素值。滤波器内的每个数据表示对应的像素在决定中心像素值的过程中所占的权重,由于滤波器内所有的像素值在决定中心像素值的过程中占有相同的权重,因此滤波器内每个数据都相等。均值滤波的优点是在像素值变换趋势一致的情况下,可以将受噪声影响而突然变化的像素值修正到接近周围像素值变化的一致性下。但是这种滤波方式会缩小像素值之间的差距,使得细节信息变得更加模糊,滤波器范围越大,变模糊的效果越明显。

OpenCV 4中提供了blur()函数用于实现图像的均值滤波,该函数的函数原型在代码清单5-8中给出。

void cv::blur(InputArray  src,
              OutputArray  dst,
              Size  ksize,
              Point  anchor = Point(-1,-1),
              int  borderType = BORDER_DEFAULT 
              )
  • 待均值滤波的图像,图像的数据类型必须是CV_8U、CV_16U、CV_16S、CV_32F和CV_64F这五种数据类型之一。
  • dst:均值滤波后的图像,与输入图像具有相同的尺寸和数据类型。
  • ksize:卷积核尺寸。
  • anchor:内核的基准点(锚点),其默认值为(-1,-1)代表内核基准点位于kernel的中心位置。基准点即卷积核中与进行处理的像素点重合的点,其位置必须在卷积核的内部。
  • borderType:像素外推法选择标志,取值范围在表3-5中给出,默认参数为BORDER_DEFAULT,表示不包含边界值倒序填充。

该函数的第一个参数为待滤波图像,可以是彩色图像也可以是灰度图像,甚至可以是保存成Mat类型的多维矩阵数据。第二个参数滤波后的图像,保持与输入图像相同的数据类型、尺寸以及通道数。第三个参数是滤波器的尺寸,输入滤波器的尺寸后函数会自动确定滤波器,其形式如式
所示。
在这里插入图片描述
函数的第四个参数为确定滤波器的基准点,默认状态下滤波器的几何中心就是基准点,不过也可以根据需求自由的调整,在均值滤波中调整基准点的位置主要影响图像外推的方向和外推的尺寸第五个参数是图像外推方法选择标志,根据需求可以自由的选择。原图像边缘位置滤波计算过程需要使用到外推的像素值,但是这些像素值并不能真实反应图像像素值的变化情况,因此在滤波后的图像里边缘处的信息可能会出现巨大的改变,这属于正常现象。如果在边缘处有比较重要的信息,可以适当缩小滤波器尺寸、选择合适的滤波器基准点或者使用合适的图像外推算法。

为了更加了解均值滤波函数blur()的使用方法以及均值滤波的处理效果,在代码清单5-9中给出了利用不同尺寸的均值滤波器分别处理不含有噪声的图像、含有椒盐噪声的图像和含有高斯噪声的图像,处理结果在图5-10、图5-11、图5-12中给出。通过结果可以发现,滤波器的尺寸越大,滤波后图像变得越模糊。

#include <opencv2/opencv.hpp>
#include <opencv2/highgui/highgui.hpp>
using namespace cv;
using namespace std;

int main(){
    Mat img=imread("luffy.jpg");
    Mat luffy,luffy_gray;
    resize(img,luffy,Size(img.rows/2,img.cols/2));
    Mat luffy_gauss=imread("luffy_gauss.jpg");
    Mat luffy_salt=imread("luffy_salt.jpg");
    if(luffy.empty()||luffy_gauss.empty()||luffy_salt.empty()){
        cout<<"请确认输入的图片路径是否正确"<<endl;
        return -1;
    }
    Mat result_3,result_9;//存放不含早生的滤波结果,后面数字代表滤波器尺寸
    Mat result_3gauss,result_9gauss;//存放含有高斯噪声滤波结果,后面数字代表滤波器尺寸
    Mat result_3salt,result_9salt;//存放含有椒盐噪声滤波结果,后面数字代表滤波器尺寸
    //调用均值滤波函数blur()进行滤波
    blur(luffy,result_3,Size(3,3));
    blur(luffy,result_9,Size(9,9));
    blur(luffy_gauss,result_3gauss,Size(3,3));
    blur(luffy_gauss,result_9gauss,Size(9,9));
    blur(luffy_salt,result_3salt,Size(3,3));
    blur(luffy_salt,result_9salt,Size(9,9));

    //显示不含噪声图像
    imshow("luffy ", luffy);
    imshow("result_3", result_3);
    imshow("result_9", result_9);
    //显示含有高斯噪声图像
    imshow("luffy_gauss", luffy_gauss);
    imshow("result_3gauss", result_3gauss);
    imshow("result_9gauss", result_9gauss);
    //显示含有椒盐噪声图像
    imshow("luffy_salt", luffy_salt);
    imshow("result_3salt", result_3salt);
    imshow("result_9salt", result_9salt);
    waitKey(0);
    return 0;
}

在这里插入图片描述

C++版本OpenCv教程(二十九)方框滤波

Excerpt

方框滤波是均值滤波的一般形式,在均值滤波中,将滤波器中所有的像素值求和后的平均值作为滤波后结果,方框滤波也是求滤波器内所有像素值的之和,但是方框滤波可以选择不进行归一化,就是将所有像素值的和作为滤波结果,而不是所有像素值的平均值。OpenCV 4中提供了boxFilter()函数实现方框滤波,该函数的函数原型在代码清单5-10中给出。void cv::boxFilter(InputArray src, OutputArray dst,


方框滤波是均值滤波的一般形式,在均值滤波中,将滤波器中所有的像素值求和后的平均值作为滤波后结果,方框滤波也是求滤波器内所有像素值的之和,但是方框滤波可以选择不进行归一化,就是将所有像素值的和作为滤波结果,而不是所有像素值的平均值。

OpenCV 4中提供了boxFilter()函数实现方框滤波,该函数的函数原型在代码清单5-10中给出。

void cv::boxFilter(InputArray  src,
                   OutputArray  dst,
                   int  ddepth,
                   Size  ksize,
                   Point  anchor = Point(-1,-1),
                   bool  normalize = true,
                   int  borderType = BORDER_DEFAULT 
                   )
  • src:输入图像。
  • dst:输出图像,与输入图像具有相同的尺寸和通道数。
  • ddepth:输出图像的数据类型(深度),根据输入图像的数据类型不同拥有不同的取值范围,具体的取值范围在表5-1给出,当赋值为-1时,输出图像的数据类型自动选择。
  • ksize:卷积核尺寸。
  • anchor:内核的基准点(锚点),其默认值为(-1,-1)代表内核基准点位于kernel的中心位置。基准点即卷积核中与进行处理的像素点重合的点,其位置必须在卷积核的内部。
  • normalize:是否将卷积核进行归一化的标志,默认参数为true,表示进行归一化。
  • borderType:像素外推法选择标志,取值范围在表3-5中给出,默认参数为BORDER_DEFAULT,表示不包含边界值倒序填充。

该函数的使用方式与均值滤波函数blur()几乎一样,但是该函数可以选择输出图像的数据类型,除此之外,该函数的第六个参数表示是否对滤波器内所有的数值进行归一化操作,参数默认状态下需要对滤波器内所有的数值进行归一化。此时,在不考虑数据类型的情况下,框滤波函数boxFilter()和均值滤波函数blur()会具有相同的滤波结果。

除了对滤波器内每个像素值直接求和外,OpenCV 4还提供了sqrBoxFilter()函数实现对滤波器内每个像数值的平方求和,之后根据输入参数选择是否进行归一化操作,该函数的函数原型在代码清单5-11中给出。

void cv::sqrBoxFilter(InputArray  src,
                      OutputArray  dst,
                      int  ddepth,
                      Size  ksize,
                      Point  anchor = Point(-1, -1),
                      bool  normalize = true,
                      int  borderType = BORDER_DEFAULT 
                      )

该函数是在boxFilter()函数功能基础上进行扩展功能,因此两者具有相同的输入参数需求,这里对函数的参数不再进行逐一解释。CV_8U数据类型的图像像素值从0到255,计算平方后数据会变得更大,即使归一化操作也不能保证像素值不会超过最大值,但是CV_32F数据类型的图像像素值是从0到1之间的小数,在0到1之间的数计算平方会变得更小,但是始终保持在0到1之间。因此该函数在处理图像滤波的任务时主要针对的是CV_32数据类型的图像,而且根据计算关系可知,在归一化后图像在变模糊的同时亮度也会变暗。

为了更加了解方框滤波的计算原理,清楚归一化操作和未归一化操作对滤波结果的影响,在代码清单5-12中给出了分别利用方框滤波处理矩阵数据和图像的示例程序。程序中我们创建了一个Mat类型的数据,之后用sqrBoxFilter()函数进行方框滤波,并在图5-13给出归一化后和未归一化后的结果,同时使用boxFilter()函数和sqrBoxFilter()对图像进行方框滤波操作,处理结果如图5-12中所示。

#include <opencv2/opencv.hpp>
#include <opencv2/highgui/highgui.hpp>
using namespace cv;
using namespace std;

int main(){
    Mat img=imread("luffy.jpg",IMREAD_ANYDEPTH);//用于方框滤波
    Mat luffy;
    resize(img,luffy,Size(img.rows/2,img.cols/2));
    if(luffy.empty()){
        cout<<"请确认输入的图片路径是否正确"<<endl;
        return -1;
    }
    //验证方框滤波算法的数据矩阵
    float points[25]={
            1,2,3,4,5,
            6,7,8,9,10,
            11,12,13,14,15,
            16,17,18,19,20,
            21,22,23,24,25
    };
    Mat data(5,5,CV_32FC1,points);
    //将CV_8U类型数据转换成CV_32F类型
    Mat luffy_32F;
    luffy.convertTo(luffy_32F,CV_32F,1.0/255);
    Mat resultNorm,result,dataSqrNorm,dataSqr,luffy_32FSqr;
    //方框滤波boxfilter()和sqrBoxfilter()
    boxFilter(luffy,resultNorm,-1,Size(3,3),Point(-1,-1),true);//进行归一化
    boxFilter(luffy,result,-1,Size(3,3),Point(-1,-1), false);//不进行归一化
    sqrBoxFilter(data,dataSqrNorm,-1,Size(3,3),Point(-1,-1),true,BORDER_CONSTANT);//进行归一化
    sqrBoxFilter(data,dataSqr,-1,Size(3,3),Point(-1,-1),false,BORDER_CONSTANT);//不进行归一化
    sqrBoxFilter(luffy_32F,luffy_32FSqr,-1,Size(3,3),Point(-1,-1), true,BORDER_CONSTANT);
    //显示处理结果
    imshow("resultNorm",resultNorm);
    imshow("result",result);
    imshow("luffy_32Sqr",luffy_32FSqr);
    waitKey(0);
    return 0;
}

在这里插入图片描述在这里插入图片描述

C++版本OpenCv教程(三十)高斯滤波

Excerpt

高斯噪声是一种常见的噪声,图像采集的众多过程中都容易引入高斯噪声,因此针对高斯噪声的高斯滤波也广泛应用于图像去噪领域。高斯滤波器考虑了像素离滤波器中心距离的影响,以滤波器中心位置为高斯分布的均值,根据高斯分布公式和每个像素离中心位置的距离计算出滤波器内每个位置的数值,从而形成一个形如图5-15所示的高斯滤波器。之后将高斯滤波器与图像之间进行滤波操作,进而实现对图像的高斯滤波。OpenCV 4提供了对图像进行高斯滤波操作的GaussianBlur()函数,该函数的函数原型在代码清单5-13中给出。voi


高斯噪声是一种常见的噪声,图像采集的众多过程中都容易引入高斯噪声,因此针对高斯噪声的高斯滤波也广泛应用于图像去噪领域。高斯滤波器考虑了像素离滤波器中心距离的影响,以滤波器中心位置为高斯分布的均值,根据高斯分布公式和每个像素离中心位置的距离计算出滤波器内每个位置的数值,从而形成一个形如图5-15所示的高斯滤波器。之后将高斯滤波器与图像之间进行滤波操作,进而实现对图像的高斯滤波。
在这里插入图片描述
OpenCV 4提供了对图像进行高斯滤波操作的GaussianBlur()函数,该函数的函数原型在代码清单5-13中给出。

void cv::GaussianBlur(InputArray  src,
                       OutputArray  dst,
                       Size  ksize,
                       double  sigmaX,
                       double  sigmaY = 0,
                       int  borderType = BORDER_DEFAULT 
                       )
  • src:待高斯滤波图像,图像可以具有任意的通道数目,但是数据类型必须为CV_8U,CV_16U,CV_16S,CV_32F或CV_64F。
  • dst:输出图像,与输入图像src具有相同的尺寸、通道数和数据类型。
  • ksize:高斯滤波器的尺寸,滤波器可以不为正方形,但是必须是正奇数。如果尺寸为0,则由标准偏差计算尺寸。
  • sigmaX:X方向的高斯滤波器标准偏差。
  • sigmaY:Y方向的高斯滤波器标准偏差;如果输入量为0,则将其设置为等于sigmaX,如果两个轴的标准差均为0,则根据输入的高斯滤波器尺寸计算标准偏差。
  • borderType:像素外推法选择标志,取值范围在表3-5中给出,默认参数为BORDER_DEFAULT,表示不包含边界值倒序填充。

该函数能够根据输入参数自动生成高斯滤波器,实现对图像的高斯滤波,函数的前两个参数与前面介绍的滤波函数的参数含义相同。该函数第三个参数是高斯滤波器的尺寸,与前面函数不同的是,该函数除了必须是正奇数以外,还允许输入尺寸为0,当输入的尺寸为0时,会根据输入的标准偏差计算滤波器的尺寸。函数第四个和第五个参数为X方向和Y方向的标准偏差,当Y方向参数为0时表示Y方向的标准偏差与X方向相同,当两个参数都为0时,则根据输入的滤波器尺寸计算两个方向的标准偏差数值。但是为了能够使计算结果符合自己的预期,建议将第三个参数、第四个参数和第五个参数都明确的给出。

高斯滤波器的尺寸和标准偏差存在着一定的互相转换关系,OpenCV 4提供了输入滤波器单一方向尺寸和标准偏差生成单一方向高斯滤波器的getGaussianKernel()函数,在函数的定义中给出了滤波器尺寸和标准偏差存在的关系,这个关系不是数学中存在的关系,而是OpenCV 4为了方便而自己设定的关系。在了解这个关系之前,我们首先了解以下getGaussianKernel()函数,该函数的函数原型在代码清单5-14中给出。

Mat cv::getGaussianKernel(int  ksize,
                          double  sigma,
                          int  ktype = CV_64F 
                          )
  • ksize:高斯滤波器的尺寸。
  • sigma:高斯滤波的标测差。
  • ktype:滤波器系数的数据类型,可以是CV_32F或者CV_64F,默认数据类型为CV_64F。

该函数用于生成指定尺寸的高斯滤波器,需要注意的是该函数生成的是一个ksize×1的Mat类矩阵。函数第一个参数是高斯滤波器的尺寸,这个参数必须是一个正奇数。第二个参数表示高斯滤波的标准差,这个参数如果是一个负数,则调用程序中默认的高斯滤波器尺寸与标准差的公式,其计算公式如式(5.4)所示。
在这里插入图片描述
生成一个二维的高斯滤波器需要调用两次getGaussianKernel()函数,将X方向的一维高斯滤波器和Y方向的一维高斯滤波器相乘,得到最终的二维高斯滤波器。例如计算的X方向的一维滤波器和Y方向的一维滤波器均如式(5.5)所示。
在这里插入图片描述
最终二维高斯滤波器计算过程和结果如式(5.6)所示。
在这里插入图片描述
为了了解高斯滤波对不同噪声的去除效果,在代码清单5-15中利用高斯滤波分别处理不含有噪声的图像、含有椒盐噪声的图像和含有高斯噪声的图像,处理结果在图5-16、图5-17、图5-18中给出。通过结果可以发现,高斯滤波对高斯噪声去除效果较好,但是同样会对图像造成模糊,并且滤波器的尺寸越大,滤波后图像变得越模糊。

#include <opencv2/opencv.hpp>
#include <opencv2/highgui/highgui.hpp>
using namespace cv;
using namespace std;

int main(){
    Mat img=imread("luffy.jpg",IMREAD_ANYDEPTH);//用于方框滤波
    Mat luffy;
    resize(img,luffy,Size(img.rows/3,img.cols/3));
    Mat luffy_gauss=imread("luffy_gauss.jpg",IMREAD_ANYDEPTH);
    Mat luffy_salt=imread("luffy_salt.jpg",IMREAD_ANYDEPTH);
    if(luffy.empty()||luffy_gauss.empty()||luffy_salt.empty()){
        cout<<"请确认输入的图片路径是否正确"<<endl;
        return -1;
    }
    Mat result_5,result_9;//存放不含早生结果,后面数组代表滤波器尺寸
    Mat result_5gauss,result_9gauss;//存放含有高斯噪声滤波结果,后面数字代表滤波器尺寸
    Mat result_5salt,result_9salt;//存放含有椒盐噪声的滤波结果,后面数字代表滤波器尺寸
    //调用均值滤波函数blur()进行滤波
    GaussianBlur(luffy,result_5,Size(5,5),10,20);
    GaussianBlur(luffy,result_9,Size(9,9),10,20);
    GaussianBlur(luffy_gauss,result_5gauss,Size(5,5),10,20);
    GaussianBlur(luffy_gauss,result_9gauss,Size(9,9),10,20);
    GaussianBlur(luffy_salt,result_5salt,Size(5,5),10,20);
    GaussianBlur(luffy_salt,result_9salt,Size(9,9),10,20);
    //显示不含早生图像
    imshow("luffy",luffy);
    imshow("result_5",result_5);
    imshow("result_9",result_9);
    //显示含有高斯噪声图像
    imshow("luffy_gauss",luffy_gauss);
    imshow("result_5gauss",result_5gauss);
    imshow("result_9gauss",result_9gauss);
    //显示含有椒盐噪声图像
    imshow("luffy_salt",luffy_salt);
    imshow("result_5salt",result_5salt);
    imshow("result_9salt",result_9salt);
    waitKey(0);
    return 0;
}

在这里插入图片描述

C++版本OpenCv教程(三十一)可分离滤波

Excerpt

前面介绍的滤波函数使用的滤波器都是固定形式的滤波器,有时我们需要根据实际需求调整滤波模板,例如在滤波计算过程中滤波器中心位置的像素值不参与计算,滤波器中参与计算的像素值不是一个矩形区域等。OpenCV 4无法根据每种需求单独编写滤波函数,因此OpenCV 4提供了根据自定义滤波器实现图像滤波的函数,就是我们本章最开始介绍的卷积函数filter2D(),不过根据函数的名称,这里称呼为滤波函数更为准确一些,输入的卷积模板也应该称为滤波器或者滤波模板。该函数的使用方式我们在一开始已经介绍,只需要根据需求定义一个卷


前面介绍的滤波函数使用的滤波器都是固定形式的滤波器,有时我们需要根据实际需求调整滤波模板,例如在滤波计算过程中滤波器中心位置的像素值不参与计算,滤波器中参与计算的像素值不是一个矩形区域等。OpenCV 4无法根据每种需求单独编写滤波函数,因此OpenCV 4提供了根据自定义滤波器实现图像滤波的函数,就是我们本章最开始介绍的卷积函数filter2D(),不过根据函数的名称,这里称呼为滤波函数更为准确一些,输入的卷积模板也应该称为滤波器或者滤波模板。该函数的使用方式我们在一开始已经介绍,只需要根据需求定义一个卷积模板或者滤波器,便可以实现自定义滤波。

无论是图像卷积还是滤波,在原图像上移动滤波器的过程中每一次的计算结果都不会影响到后面过程的计算结果,因此图像滤波是一个并行的算法,在可以提供并行计算的处理器中可以极大的加快图像滤波的处理速度。除此之外,图像滤波还具有可分离性,这个性质我们在高斯滤波中有简单的接触,可分离性指的是先对X(Y)方向滤波,再对Y(X)方向滤波的结果与将两个方向的滤波器联合后整体滤波的结果相同。两个方向的滤波器的联合就是将两个方向的滤波器相乘,得到一个矩形的滤波器,例如X方向的滤波器为 ,Y方向的滤波器为 ,两个方向联合滤波器可以用式(5.7)计算,无论先进行X方向滤波还是Y方向滤波,两个方向联合滤波器都是相同的。
在这里插入图片描述
因此在高斯滤波中,我们利用getGaussianKernel()函数分别得到X方向和Y方向的滤波器,之后通过生成联合滤波器或者分别用两个方向的滤波器进行滤波的计算结果相同。但是两个方向联合滤波需要在使用filter2D()函数滤波之前计算联合滤波器,而两个方向分别滤波需要调用两次filter2D()函数,增加了通过代码实现的复杂性,因此OpenCV 4提供了可以输入两个方向滤波器实现滤波的滤波函数sepFilter2D(),该函数的函数原型在代码清单5-16中给出。

void cv::sepFilter2D(InputArray  src,
                     OutputArray  dst,
                     int  ddepth,
                     InputArray  kernelX,
                     InputArray  kernelY,
                     Point  anchor = Point(-1,-1),
                     double  delta = 0,
                     int  borderType = BORDER_DEFAULT 
                     )
  • src:待滤波图像
  • dst:输出图像,与输入图像src具有相同的尺寸、通道数和数据类型。
  • ddepth:输出图像的数据类型(深度),根据输入图像的数据类型不同拥有不同的取值范围,具体的取值范围在表5-1给出,当赋值为-1时,输出图像的数据类型自动选择。
  • kernelX:X方向的滤波器,
  • kernelY:Y方向的滤波器。
  • anchor:内核的基准点(锚点),其默认值为(-1,-1)代表内核基准点位于kernel的中心位置。基准点即卷积核中与进行处理的像素点重合的点,其位置必须在卷积核的内部。
  • delta:偏值,在计算结果中加上偏值。
  • borderType:像素外推法选择标志,取值范围在表3-5中给出。默认参数为BORDER_DEFAULT,表示不包含边界值倒序填充。

该函数将可分离的线性滤波器分离成X方向和Y方向进行处理,与filter2D()函数不同之处在于,filter2D()函数需要通过滤波器的尺寸区分滤波操作是作用在X方向还是Y方向,例如滤波器尺寸为K×1时是Y方向滤波,1×K尺寸的滤波器是X方向滤波。而sepFilter2D()函数通过不同参数区分滤波器是作用在X方向还是Y方向,无论输入滤波器的尺寸是K×1还是1×K,都不会影响滤波结果。

为了更加了解线性滤波的可分离性,在代码清单5-17中给出了利用filter2D()函数和sepFilter2D()函数实现滤波的示例程序。程序中利用filter2D()函数依次进行Y方向和X方向滤波,将结果与两个方向联合滤波器滤波结果相比较,验证两种方式计算结果的一致性。同时将两个方向的滤波器输入sepFilter2D()函数中,验证该函数计算结果是否与前面的计算结果一致。最后利用自定义的滤波器,对图像依次进行X方向滤波和Y方向滤波,查看滤波结果是否与使用联合滤波器的滤波结果一致。程序的计算结果依次在图5-19、图5-20给出。

#include <opencv2/opencv.hpp>
#include <opencv2/highgui/highgui.hpp>
using namespace cv;
using namespace std;

int main(){
    float points[25]={1,2,3,4,5,
                      6,7,8,9,10,
                      11,12,13,14,15,
                      16,17,18,19,20,
                      21,22,23,24,25};
    Mat data(5,5,CV_32FC1,points);
    //X方向、Y方向和联合滤波器的构建
    Mat a=(Mat_<float>(3,1)<<-1,3,-1);//构造一个kernel,这也是构造Mat矩阵的一种方法
    Mat b=a.reshape(1,1);
    Mat ab=a*b;
    cout<<"a = "<<endl;
    cout<<a<<endl;
    cout<<"b = "<<endl;
    cout<<b<<endl;
    cout<<"ab = "<<endl;
    cout<<ab<<endl;
    //验证高斯滤波的可分离性
    Mat gaussX=getGaussianKernel(3,1);
    Mat gaussData,gaussDataXY;
    GaussianBlur(data,gaussData,Size(3,3),1,1,BORDER_CONSTANT);
    sepFilter2D(data,gaussDataXY,-1,gaussX,gaussX,Point(-1,-1),0,BORDER_CONSTANT);
    //输入烦两种高斯滤波的计算结果
    cout<<"gaussData = "<<endl;
    cout<<gaussData<<endl;
    cout<<"gaussDataXY = "<<endl;
    cout<<gaussDataXY<<endl;
    //线性滤波的可分离性
    Mat dataYX,dataY,dataXY,dataXY_sep;
    filter2D(data,dataY,-1,a,Point(-1,-1),0,BORDER_CONSTANT);
    filter2D(dataY,dataYX,-1,b,Point(-1,-1),0,BORDER_CONSTANT);
    filter2D(data,dataXY,-1,ab,Point(-1,-1),0,BORDER_CONSTANT);
    sepFilter2D(data,dataXY_sep,-1,b,b,Point(-1,-1),0,BORDER_CONSTANT);
    //输出分离滤波和联合滤波的计算结果
    cout<<"dataY = "<<endl;
    cout<<dataY<<endl;
    cout<<"dataYX = "<<endl;
    cout<<dataYX<<endl;
    cout<<"dataXY = "<<endl;
    cout<<dataXY<<endl;
    cout<<"dataXY_sep = "<<endl;
    cout<<dataXY_sep<<endl;

    //对图像的分离操作
    Mat img=imread("luffy.jpg");
    Mat luffy;
    resize(img,luffy,Size(img.rows/2,img.cols/2));
    if(luffy.empty()){
        cout<<"请确认输入的图片路径是否正确"<<endl;
        return -1;
    }
    Mat imgXY,imgY,imgYX;
    filter2D(luffy,imgY,-1,a,Point(-1,-1),0,BORDER_CONSTANT);
    filter2D(imgY,imgYX,-1,b,Point(-1,-1),0,BORDER_CONSTANT);
    filter2D(luffy,imgXY,-1,ab,Point(-1,-1),0,BORDER_CONSTANT);
    imshow("luffy",luffy);
    imshow("imgY",imgY);
    imshow("imgYX",imgYX);
    imshow("imgXY",imgXY);
    waitKey(0);
    return 0;
}

运行结果:

a = 
[-1;
 3;
 -1]
b = 
[-1, 3, -1]
ab = 
[1, -3, 1;
 -3, 9, -3;
 1, -3, 1]
gaussData = 
[1.7207065, 2.8222058, 3.5481372, 4.2740688, 3.430702;
 4.6296568, 7, 8, 9, 6.9852448;
 8.2593136, 12, 13, 14, 10.614902;
 11.88897, 17, 18, 19, 14.244559;
 10.270683, 14.600147, 15.326078, 16.05201, 11.98068]
gaussDataXY = 
[1.7207065, 2.8222058, 3.5481372, 4.2740688, 3.430702;
 4.6296568, 7, 8, 9, 6.9852448;
 8.2593136, 12, 13, 14, 10.614902;
 11.88897, 17, 18, 19, 14.244559;
 10.270683, 14.600147, 15.326078, 16.05201, 11.98068]
dataY = 
[-3, -1, 1, 3, 5;
 6, 7, 8, 9, 10;
 11, 12, 13, 14, 15;
 16, 17, 18, 19, 20;
 47, 49, 51, 53, 55]
dataYX = 
[-8, -1, 1, 3, 12;
 11, 7, 8, 9, 21;
 21, 12, 13, 14, 31;
 31, 17, 18, 19, 41;
 92, 49, 51, 53, 112]
dataXY = 
[-8, -1, 1, 3, 12;
 11, 7, 8, 9, 21;
 21, 12, 13, 14, 31;
 31, 17, 18, 19, 41;
 92, 49, 51, 53, 112]
dataXY_sep = 
[-8, -1, 1, 3, 12;
 11, 7, 8, 9, 21;
 21, 12, 13, 14, 31;
 31, 17, 18, 19, 41;
 92, 49, 51, 53, 112]

在这里插入图片描述

C++版本OpenCv教程(三十二 )中值滤波

Excerpt

中值滤波就是用滤波器范围内所有像素值的中值来替代滤波器中心位置像素值的滤波方法,是一种基于排序统计理论的能够有效抑制噪声的非线性信号处理方法。中值滤波计算方式如图5-21所示,将滤波器范围内所有的像素值按照由小到大的顺序排列,选取排序序列的中值作为滤波器中心处黄色像素的新像素值,之后将滤波器移动到下一个位置,重复进行排序取中值的操作,直到将图像所有的像素点都被滤波器中心对应一遍。中值滤波不依赖于滤波器内那些与典型值差别很大的值,因此对斑点噪声和椒盐噪声的处理具有较好的处理效果。相比于均值滤波,中值滤波对于


中值滤波就是用滤波器范围内所有像素值的中值来替代滤波器中心位置像素值的滤波方法,是一种基于排序统计理论的能够有效抑制噪声的非线性信号处理方法。中值滤波计算方式如图5-21所示,将滤波器范围内所有的像素值按照由小到大的顺序排列,选取排序序列的中值作为滤波器中心处黄色像素的新像素值,之后将滤波器移动到下一个位置,重复进行排序取中值的操作,直到将图像所有的像素点都被滤波器中心对应一遍。中值滤波不依赖于滤波器内那些与典型值差别很大的值,因此对斑点噪声和椒盐噪声的处理具有较好的处理效果。

相比于均值滤波,中值滤波对于脉冲干扰信号和图像扫描噪声的处理效果更佳,同时在一定条件下中值滤波对图像的边缘信息保护效果更佳,可以避免图像细节的模糊,但是当中值滤波尺寸变大之后同样会产生图像模糊的效果。在处理时间上,中值滤波所消耗的时间要远大于均值滤波消耗的时间。
在这里插入图片描述
OpenCV 4提供了对图像进行中值滤波操作的medianBlur()函数,该函数的函数原型在代码清单5-18中给出。

void cv::medianBlur(InputArray src,
                    OutputArray  dst,
                    int  ksize 
                    )
  • src:待中值滤波的图像,可以是单通道,三通道和四通道,数据类型与滤波器的尺寸相关,当滤波器尺寸为3或5时,图像可以是CV_8U,CV_16U或CV_32F类型,对于较大尺寸的滤波器,数据类型只能是CV_8U。
  • dst:输出图像,与输入图像src具有相同的尺寸和数据类型。
  • ksize:滤波器尺寸,必须是大于1的奇数,例如:3、5、7……

该函数只能处理符合图像信息的Mat类数据,2通道或者更多通道的Mat类矩阵不能被该函数处理,并且对于图像数据类型的要求也和滤波器的尺寸有着密切的关系。函数第一个参数是待中值滤波的图像,可以是单通道,三通道和四通道,数据类型与滤波器的尺寸相关。当滤波器尺寸为3或5时,图像可以是CV_8U,CV_16U或CV_32F类型,对于较大尺寸的滤波器,数据类型只能是CV_8U。第二个参数是输出图像,输出图像的尺寸和数据类型与输入图像相同。最后一个参数是滤波其的尺寸,区别于之前的线性滤波,中值滤波的滤波器必须是正方形且尺寸为大于1的奇数。该函数对于多通道的彩色图像是针对每个通道的内部数据进行中值滤波操作。

为了了解**中值滤波函数medianBlur()**的使用方法,在代码清单5-19中给出了对含有椒盐噪声的灰度图像和彩色图像进行中值滤波的示例程序,程序中分别用3×3和9×9的滤波器对图像进行中值滤波,程序的运行结果在图5-22、图5-23给出,通过结果可以看出,9×9的中值滤波同样会对整个图像造成模糊的现象。

#include <opencv2/opencv.hpp>
#include <opencv2/highgui/highgui.hpp>
using namespace cv;
using namespace std;

int main(){
    Mat luffy=imread("luffy_salt.jpg");
    Mat img,gray;
    resize(luffy,img,Size(luffy.rows,luffy.cols));
    cvtColor(img,gray,COLOR_BGR2GRAY);
    if(img.empty()||gray.empty()){
        cout<<"请确认输入烦人图片路径是否正确"<<endl;
        return -1;
    }
    Mat imgResult3,grayResult3,imgResult9,grayResult9;
    //分别对含有椒盐噪声的彩色图像进行滤波,滤波函数为3×3
    medianBlur(img,imgResult3,3);
    medianBlur(gray,grayResult3,3);
    //加大滤波模板,图像滤波结果会变模糊
    medianBlur(img,imgResult9,9);
    medianBlur(gray,grayResult9,9);
    //显示滤波处理结果
    imshow("img",img);
    imshow("gray",gray);
    imshow("imgResult3",imgResult3);
    imshow("grayResult3",grayResult3);
    imshow("imgResult9",imgResult9);
    imshow("grayResult9",grayResult9);
    waitKey(0);
    return 0;
}

在这里插入图片描述

C++版本OpenCv教程(三十三)双边滤波

Excerpt

前面我们介绍的滤波方法都会图像照成模糊,使得边缘信息变弱或者消失,因此需要一种能够对图像边缘信息进行保留的滤波算法,双边滤波就是经典的常用的能够保留图像边缘信息的滤波算法之一。双边滤波是一种综合考虑滤波器内图像空域信息和滤波器内图像像素灰度值相似性的滤波算法,可以实现在保留区域信息的基础上实现对噪声的去除、对局部边缘的平滑。双边滤波对高频率的波动信号起到平滑的作用,同时保留大幅值的信号波动,进而实现对保留图像中边缘信息的作用。双边滤波的示意图如图5-24所示,双边滤波器是两个滤波器的结合,分别考虑空域信息和


前面我们介绍的滤波方法都会图像照成模糊,使得边缘信息变弱或者消失,因此需要一种能够对图像边缘信息进行保留的滤波算法,双边滤波就是经典的常用的能够保留图像边缘信息的滤波算法之一双边滤波是一种综合考虑滤波器内图像空域信息和滤波器内图像像素灰度值相似性的滤波算法,可以实现在保留区域信息的基础上实现对噪声的去除、对局部边缘的平滑。双边滤波对高频率的波动信号起到平滑的作用,同时保留大幅值的信号波动,进而实现对保留图像中边缘信息的作用。双边滤波的示意图如图5-24所示,双边滤波器是两个滤波器的结合,分别考虑空域信息和值域信息,使得滤波器对边缘附近的像素进行滤波时,距离边缘较远的像素值不会对边缘上的像素值影响太多,进而保留了边缘的清晰性。
在这里插入图片描述
双边滤波原理的数学表示如式(5.9)中所示。
在这里插入图片描述
其中ω(i,j,k,l)为加权系数,其取值决定于空域滤波器和值域滤波器的乘积,空域滤波器的表示形式如式(5.9)所示,值域表示形式如式(5.10)所示。
在这里插入图片描述在这里插入图片描述
两者相乘后,会产生形如式(5.11)所示的依赖于数据的双边滤波器。
在这里插入图片描述
两者相乘后,会产生形如式(5.11)所示的依赖于数据的双边滤波器。
在这里插入图片描述
OpenCV 4提供了对图像进行双边滤波操作的bilateralFilter()函数,该函数的函数原型在代码清单5-20中给出。

void cv::bilateralFilter(InputArray  src,
                         OutputArray  dst,
                         int  d,
                         double  sigmaColor,
                         double  sigmaSpace,
                         int  borderType = BORDER_DEFAULT 
                         )
  • src:待双边滤波图像,图像数据类型为必须是CV_8U、CV_32F和CV_64F三者之一,并且通道数必须为单通道或者三通道。
  • dst:双边滤波后的图像,尺寸和数据类型与输入图像src相同。
  • d:滤波过程中每个像素邻域的直径,如果这个值是非正数,则由第五个参数sigmaSpace计算得到。
  • sigmaColor:颜色空间滤波器的标准差值,这个参数越大表明该像素领域内有越多的颜色被混合到一起,产生较大的半相等颜色区域。
  • sigmaSpace:空间坐标中滤波器的标准差值,这个参数越大表明越远的像素会相互影响,从而使更大领域中有足够相似的颜色获取相同的颜色。当第三个参数d大于0时,邻域范围由d确定,当第三个参数小于等于0时,邻域范围正比于这个参数的数值。
  • borderType:像素外推法选择标志,取值范围在表3-5中给出,默认参数为BORDER_DEFAULT,表示不包含边界值倒序填充。

该函数可以对图像进行双边滤波处理,在减少噪声的同时保持边缘的清晰。该函数第一个参数是待进行双边滤波的图像,该函数要求只能输入单通道的灰度图和三通道的彩色图像,并且对于图像的数据类型也有严格的要求,必须是CV_8U、CV_32F和CV_64F三者之一。函数第三个参数是滤波器的直径,当滤波器的直径大于5时,函数的运行速度会变慢,因此如果需要在实时系统中使用该函数,建议将滤波器的半径设置为5,对于离线处理含有大量噪声的滤波图像时,可以将滤波器的半径设为9,当滤波器半径为非正数的时候,会根据空间滤波器的标准差计算滤波器的直径。函数第四个和第五个参数是两个滤波器的标准差值,为了简单起见可以将两个参数设置成相同的数值,当他们小于10时,滤波器对图像的滤波作用较弱,当他们大于150时滤波效果会非常的强烈,使图像看起来具有卡通的效果。该函数运行时间比其他滤波方法时间要长,因此在实际工程中使用的时候,选择合适的参数十分重要。另外比较有趣的现象是,使用双边滤波会具有美颜效果。

为了了解双边函数bilateralFilter()的使用方法,在代码清单5-21中给出了利用双边滤波函数bilateralFilter()对含有人脸的图像进行滤波的示例程序,滤波结果在图5-25、图5-26给出。通过结果可以知道,滤波器的直径对于滤波效果具有重要的影响,滤波器直径越大,滤波效果越明显;同时当滤波器半径相同时,标准差值越大,滤波效果越明显。另外通过结果也可以看出双边滤波确实能对人脸起到美颜的效果。

#include <opencv2\opencv.hpp>
#include <iostream>

using namespace cv;
using namespace std;

int main()
{
  //读取两张含有人脸的图像
  Mat img1 = imread("img1.png", IMREAD_ANYCOLOR);
  Mat img2 = imread("img2.png", IMREAD_ANYCOLOR);
  if (img1.empty()||img2.empty())
  {
    cout << "请确认图像文件名称是否正确" << endl;
    return -1;
  }
  Mat result1, result2, result3, result4;

  //验证不同滤波器直径的滤波效果
  bilateralFilter(img1, result1, 9, 50, 25 / 2);
  bilateralFilter(img1, result2, 25, 50, 25 / 2);

  //验证不同标准差值的滤波效果
  bilateralFilter(img2, result3, 9, 9, 9);
  bilateralFilter(img2, result4, 9, 200, 200);

  //显示原图
  imshow("img1", img1);
  imshow("img2", img2);
  //不同直径滤波结果
  imshow("result1", result1);
  imshow("result2", result2);
  //不同标准差值滤波结果
  imshow("result3 ", result3);
  imshow("result4", result4);

  waitKey(0);
  return 0;
}

在这里插入图片描述在这里插入图片描述

C++版本OpenCv教程(三十四 )边缘检测原理

Excerpt

图像的边缘指的是图像中像素灰度值突然发生变化的区域,如果将图像的每一行像素和每一列像素都描述成一个关于灰度值的函数,那么图像的边缘对应在灰度值函数中是函数值突然变大的区域。函数值的变化趋势可以用函数的导数描述。当函数值突然变大时,导数也必然会变大,而函数值变化较为平缓区域,导数值也比较小,因此可以通过寻找导数值较大的区域去寻找函数中突然变化的区域,进而确定图像中的边缘位置。图5-27给出一张含有边缘的图像,图像每一行的像素灰度值变化可以用图中下方的曲线表示。通过像素灰度值曲线可以看出图像边缘位于曲线变化


图像的边缘指的是图像中像素灰度值突然发生变化的区域,如果将图像的每一行像素和每一列像素都描述成一个关于灰度值的函数,那么图像的边缘对应在灰度值函数中是函数值突然变大的区域。函数值的变化趋势可以用函数的导数描述。当函数值突然变大时,导数也必然会变大,而函数值变化较为平缓区域,导数值也比较小,因此可以通过寻找导数值较大的区域去寻找函数中突然变化的区域,进而确定图像中的边缘位置。图5-27给出一张含有边缘的图像,图像每一行的像素灰度值变化可以用图中下方的曲线表示。
在这里插入图片描述

通过像素灰度值曲线可以看出图像边缘位于曲线变化最陡峭的区域,对灰度值曲线求取一阶导数可以得到图5-28中所示的曲线,通过曲线可以看出曲线的最大值区域就是图像中的边缘。
在这里插入图片描述

由于图像是离散的信号,我们可以用临近的两个像素差值来表示像素灰度值函数的导数,求导形式可以用式(5.12)来表示。
在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述
图像的边缘有可能是由高像素值变为低像素值,也有可能是由低像素值变成高像素值,通过式(5.13)和式(5.14)得到的正数值表示需要像素值突然由低变高,得到的负数值表示像素值由高到低,这两种都是图像的边缘,因此为了在图像中同时表示出这两种边缘信息,需要将计算的结果求取绝对值。OpenCV 4中提供了convertScaleAbs()函数用计算矩阵中所有数据的绝对值,该函数的函数原型在代码清单5-22中给出。

void cv::convertScaleAbs(InputArray  src,
                         OutputArray  dst,
                         double  alpha = 1,
                         double  beta = 0 
                         )
  • src:输入矩阵。
  • dst:计算绝对值后输入矩阵。
  • alpha:缩放因子,默认参数为只求取绝对值不进行缩放。
  • beta:在原始数据上添加的偏值,默认参数表示不增加偏值。

该函数可以求取矩阵中所有数据的绝对值。函数前两个参数分别为输入、输出矩阵,两个参数可以是相同的变量。函数第三个和第四个参数为对绝对值的缩放和原始数据上的偏移。函数的计算原理如式(5.15)所示。
在这里插入图片描述
图像的边缘包含X方向的边缘和Y方向的边缘,因此分别求取两个方向的边缘后,对两个方向的边缘求取并集就是整幅图像的边缘,即将图像两个方向边缘结果相加得到整幅图像的边缘信息。为了验证这种滤波方式对于图像边缘提取的效果,在代码清单5-23中给出了利用filter2D()函数实现图像边缘检测的算法,检测的结果在图5-29中给出。需要说明的是,由于求取边缘的结果可能会有复数,不在原始图像的CV_8U的数据类型内,因此滤波后的图像数据类型不要用“-1”,而应该改为CV_16S。代码清单5-23 myEdge.cpp图像边缘检测

#include <opencv2\opencv.hpp>
#include <iostream>

using namespace cv;
using namespace std;

int main()
{
  //创建边缘检测滤波器
  Mat kernel1 = (Mat_<float>(1, 2) << 1, -1);  //X方向边缘检测滤波器
  Mat kernel2 = (Mat_<float>(1, 3) << 1, 0, -1);  //X方向边缘检测滤波器
  Mat kernel3 = (Mat_<float>(3, 1) << 1, 0, -1);  //X方向边缘检测滤波器
  Mat kernelXY = (Mat_<float>(2, 2) << 1, 0, 0, -1);  //由左上到右下方向边缘检测滤波器
  Mat kernelYX = (Mat_<float>(2, 2) << 0, -1, 1, 0);  //由右上到左下方向边缘检测滤波器

  //读取图像,黑白图像边缘检测结果较为明显
  Mat img = imread("equalLena.png", IMREAD_ANYCOLOR);
  if (img.empty())
  {
    cout << "请确认图像文件名称是否正确" << endl;
    return -1;
  }
  Mat result1, result2, result3, result4, result5, result6;

  //检测图像边缘
  //以[1 -1]检测水平方向边缘
  filter2D(img, result1, CV_16S, kernel1);
  convertScaleAbs(result1, result1);

  //以[1 0 -1]检测水平方向边缘
  filter2D(img, result2, CV_16S, kernel2);
  convertScaleAbs(result2, result2);

  //以[1 0 -1]'检测由垂直方向边缘
  filter2D(img, result3, CV_16S, kernel3);
  convertScaleAbs(result3, result3);

  //整幅图像的边缘
  result6 = result2 + result3;
  //检测由左上到右下方向边缘
  filter2D(img, result4, CV_16S, kernelXY);
  convertScaleAbs(result4, result4);

  //检测由右上到左下方向边缘
  filter2D(img, result5, CV_16S, kernelYX);
  convertScaleAbs(result5, result5);

  //显示边缘检测结果
  imshow("result1", result1);
  imshow("result2", result2);
  imshow("result3", result3);
  imshow("result4", result4);
  imshow("result5", result5);
  imshow("result6", result6);
  waitKey(0);
  return 0;
}

在这里插入图片描述

C++版本OpenCv教程(三十五 )Laplacian算子

Excerpt

上述的边缘检测算子都具有方向性,因此需要分别求取X方向的边缘和Y方向的边缘,之后将两个方向的边缘综合得到图像的整体边缘。Laplacian算子具有各方向同性的特点,能够对任意方向的边缘进行提取,具有无方向性的优点,因此使用Laplacian算子提取边缘不需要分别检测X方向的边缘和Y方向的边缘,只需要一次边缘检测即可。Laplacian算子是一种二阶导数算子,对噪声比较敏感,因此常需要配合高斯滤波一起使用。Laplacian算子的定义如式(5.20)所示。OpenCV 4提供了通过Laplacian算子


上述的边缘检测算子都具有方向性,因此需要分别求取X方向的边缘和Y方向的边缘,之后将两个方向的边缘综合得到图像的整体边缘。Laplacian算子具有各方向同性的特点,能够对任意方向的边缘进行提取,具有无方向性的优点,因此使用Laplacian算子提取边缘不需要分别检测X方向的边缘和Y方向的边缘,只需要一次边缘检测即可。Laplacian算子是一种二阶导数算子,对噪声比较敏感,因此常需要配合高斯滤波一起使用。

Laplacian算子的定义如式(5.20)所示。
在这里插入图片描述

OpenCV 4提供了通过Laplacian算子提取图像边缘的**Laplacian()**函数,该函数的函数原型在代码清单5-30中给出。

void cv::Laplacian(InputArray  src,
                   OutputArray  dst,
                   int  ddepth,
               int  ksize = 1,
                   double  scale = 1,
                   double  delta = 0,
                   int  borderType = BORDER_DEFAULT
                   )
  • src:输入原图像,可以是灰度图像或彩色图像。
  • dst:输出图像,与输入图像src具有相同的尺寸和通道数
  • ddepth:输出图像的数据类型(深度),根据输入图像的数据类型不同拥有不同的取值范围,具体的取值范围在表5-1给出,当赋值为-1时,输出图像的数据类型自动选择。
  • ksize:滤波器的大小,必须为正奇数。
  • scale:对导数计算结果进行缩放的缩放因子,默认系数为1,表示不进行缩放。
  • delta:偏值,在计算结果中加上偏值。
  • borderType:像素外推法选择标志,取值范围在表3-5中给出,默认参数为BORDER_DEFAULT,表示不包含边界值倒序填充。

该函数利用Laplacian算子提取图像中的边缘信息,与Soble()函数相同,函数的前两个参数分别为输入图像和输出图像,第三个参数为输出图像的数据类型,这里需要注意由于提取边缘信息时有可能会出现负数,因此不要使用CV_8U数据类型的输出图像,否则会使得图像边缘提取不准确。函数第四个参数是滤波器尺寸的大小,必须是正奇数,当该参数的值大于1时,该函数通过Sobel算子计算出图像X方向和Y方向的二阶导数,将两个方向的导数求和得到Laplacian算子,其计算公式如式(5.21)所示。
在这里插入图片描述

当第四个参数等于1时, Laplacian算子如式(5.22)所示。
在这里插入图片描述

函数最后两个参数为图像缩放因子和图像外推填充方法的标志,多数情况下并不需要设置,只需要采用默认参数即可。

为了更好的理解Laplacian ()函数的使用方法,在代码清单5-31中给出了利用Laplacian ()函数检测图像边缘的示例程序。由于Laplacian算子对图像中的噪声较为敏感,因此程序中使用Laplacian算子分别对高斯滤波后的图像和未高斯滤波的图像进行边缘检测,检测结果在图5-34中给出。通过结果可以发现,图像去除噪声后通过Laplacian算子提取边缘变得更加准确。

#include <opencv2/opencv.hpp>
#include <opencv2/highgui/highgui.hpp>
using namespace cv;
using namespace std;

int main(){
    //读取图像,黑白图像边缘检测结果较为明显
    Mat img=imread("luffy.jpg");
    Mat luffy,luffy_gray;
    resize(img,luffy,Size(img.rows/2,img.cols/2));
    cvtColor(luffy,luffy_gray,COLOR_BGR2GRAY);
    if(luffy_gray.empty()){
        cout<<"请确认输入的图片路径是否正确"<<endl;
        return -1;
    }
    Mat result,result_g,result_G;

    //未滤波提取边缘
    Laplacian(luffy_gray,result,CV_16S,3,1,0);
    convertScaleAbs(result,result);

    //滤波后提取边缘
    GaussianBlur(luffy_gray,result_g,Size(3,3),5,0);//高斯滤波
    Laplacian(result_g,result_G,CV_16S,3,1,0);
    convertScaleAbs(result_G,result_G);

    //显示图像
    imshow("result",result);
    imshow("result_G",result_G);
    waitKey(0);
    return 0;
}

在这里插入图片描述

C++版本OpenCv教程(三十六 )Canny算法

Excerpt

本节中最后介绍的边缘检测算法是Canny算法,该算法不容易受到噪声的影响,能够识别图像中的弱边缘和强边缘,并结合强弱边缘的位置关系,综和给出图像整体的边缘信息。Canny边缘检测算法是目前最优越的边缘检测算法之一,该方法的检测过程分为以下5个步骤:Step1:使用高斯滤波平滑图像,减少图像中噪声。一般情况下使用式(5.23)所示的5×5的高斯滤波器。Step2:计算图像中每个像素的梯度方向和幅值。首先通过Sobel算子分别检测图像X方向的边缘和Y方向的边缘,之后利用式(5.24)计算梯度的方向


本节中最后介绍的边缘检测算法是Canny算法,该算法不容易受到噪声的影响,能够识别图像中的弱边缘和强边缘,并结合强弱边缘的位置关系,综和给出图像整体的边缘信息。Canny边缘检测算法是目前最优越的边缘检测算法之一,该方法的检测过程分为以下5个步骤:

  • Step1:使用高斯滤波平滑图像,减少图像中噪声。一般情况下使用式(5.23)所示的5×5的高斯滤波器
    在这里插入图片描述

  • Step2:计算图像中每个像素的梯度方向和幅值。首先通过Sobel算子分别检测图像X方向的边缘和Y方向的边缘,之后利用式(5.24)计算梯度的方向和幅值。
    在这里插入图片描述

为了简便,梯度方向常取值0°、45°、90°和135°这个四个角度之一。

  • Step3:应用非极大值抑制算法消除边缘检测带来的杂散响应。首先将当前像素的梯度强度与沿正负梯度方向上的两个像素进行比较,如果当前像素的梯度强度与另外两个像素梯度强度相比最大,则该像素点保留为边缘点,否则该像素点将被抑制。
  • Step4:应用双阈值法划分强边缘和弱边缘。将边缘处的梯度值与两个阈值进行比较,如果某像素的梯度幅值小于较小的阈值,则会被去除掉;如果某像素的梯度幅 值大于较小阈值但小于较大阈值,则将该像素标记为弱边缘;如果某像素的梯度幅值大于较大阈值,则将该像素标记为强边缘。
  • Step5:消除孤立的弱边缘。在弱边缘的8邻域范围寻找强边缘,如果8邻域内存在强边缘,则保留该弱边缘,否则将删除弱边缘,最终输出边缘检测结果。

Canny算法具有复杂的流程,然而在OpenCV 4中提供了Canny()函数用于实现Canny算法检测图像中的边缘,极大的简化了使用Canny算法提取边缘信息的过程。Canny()函数的函数原型在代码清单5-32中给出。

void cv::Canny(InputArray  image,
               OutputArray  edges,
               double  threshold1,
               double  threshold2,
               int  apertureSize = 3,
               bool  L2gradient = false 
               )
  • image:输入图像,必须是CV_8U的单通道或者三通道图像。
  • edges:输出图像,与输入图像具有相同尺寸的单通道图像,且数据类型为CV_8U。
  • threshold1:第一个滞后阈值。
  • threshold2:第二个滞后阈值。
  • apertureSize:Sobel算子的直径。
  • L2gradient:计算图像梯度幅值方法的标志,幅值的两种计算方式如式(5.25)所示。
    在这里插入图片描述

该函数利用Canny算法提取图像中的边缘信息。第一个参数是需要提取边缘的输入图像,目前只支持数据类型为CV_8U的图像,输入图像可以是灰度图像或者彩色图像。第二个参数是边缘检测结果的输出图像,图像是数据类型为为CV_8U的单通道灰度图像。函数第三个和第四个参数是Canny算法中用于区分强边缘和弱边缘的两个阈值,两个参数不区分较大阈值和较小阈值,函数会自动比较区分两个阈值的大小,不过一般情况下,较大阈值与较小阈值的比值在2:1到3:1之间。函数最后一个参数是计算梯度幅值方法的选择标志,无特殊需求的情况下,使用默认值即可。

为了更好的理解Canny()函数的使用方法,在代码清单5-33中给出了利用Canny()函数检测图像边缘的示例程序。程序中通过设置不同的阈值来比较阈值的大小对图像边缘检测效果的影响,程序的输出结果在图5-35给出。通过结果可以发现,较高的阈值会降低噪声信息对图像提取边缘结果的影响,但是同时也会减少结果中的边缘信息。同时程序中先对图像进行高斯模糊后再进行边缘检测,结果表明高斯模糊在边缘纹理较多的区域能减少边缘检测的结果,但是对纹理较少的区域影响较小。

#include <opencv2/opencv.hpp>
#include <opencv2/highgui/highgui.hpp>
using namespace cv;
using namespace std;

int main(){
    //读取图像,黑白图像边缘检测结果较为明显
    Mat img=imread("luffy.jpg");
    Mat luffy,luffy_gray;
    resize(img,luffy,Size(img.rows/2,img.cols/2));
    cvtColor(luffy,luffy_gray,COLOR_BGR2GRAY);
    if(luffy_gray.empty()){
        cout<<"请确认输入的图片路径是否正确"<<endl;
        return -1;
    }
    Mat resultHigh,resultLow,resultG;

    //大阈值检测图像边缘
    Canny(luffy_gray,resultHigh,100,200,3);

    //小阈值检测图像边缘
    Canny(luffy_gray,resultLow,20,40,3);

    //高斯滤波后检测图像边缘
    GaussianBlur(luffy_gray,resultG,Size(3,3),5);
    Canny(resultG,resultG,100,200,3);

    //显示图像
    imshow("resultHigh",resultHigh);
    imshow("resultLow",resultLow);
    imshow("resultG",resultG);
    waitKey(0);
    return 0;
}

在这里插入图片描述

C++版本OpenCv教程(三十七 )图像距离变换

Excerpt

图像中两个像素之间的距离有多种定义方式,图像处理中常用的距离有欧式距离、街区距离和棋盘距离,本节中将重点介绍这三种距离的定义方式,以及如何利用两个像素间的距离来描述一幅图像。欧式距离两个像素点之间的直线距离。与直角坐标系中两点之间的直线距离求取方式相同,分别计算两个像素在X方向和Y方向上的距离,之后利用勾股定理得到两个像素之间的距离,数学表示形式如式(6.1)所示。街区距离两个像素点X方向和Y方向的距离之和。欧式距离表示的是从一个像素点到另一个像素点的最短距离,然而有时我们并不能以两个点之间连线


图像中两个像素之间的距离有多种定义方式,图像处理中常用的距离有欧式距离、街区距离和棋盘距离,本节中将重点介绍这三种距离的定义方式,以及如何利用两个像素间的距离来描述一幅图像。

欧式距离

两个像素点之间的直线距离。与直角坐标系中两点之间的直线距离求取方式相同,分别计算两个像素在X方向和Y方向上的距离,之后利用勾股定理得到两个像素之间的距离,数学表示形式如式(6.1)所示。
在这里插入图片描述
在这里插入图片描述在这里插入图片描述

街区距离

两个像素点X方向和Y方向的距离之和。欧式距离表示的是从一个像素点到另一个像素点的最短距离,然而有时我们并不能以两个点之间连线的方向前进,例如在一个城市内两点之间的连线可能存在障碍物的阻碍,因此从一个点到另一个点需要沿着街道行走,因此这种距离的度量方式被称为街区距离。街区距离就是由一个像素点到另一个像素点需要沿着X方向和Y方向一共行走的距离,数学表示形式如式(6.2)所示。
在这里插入图片描述在这里插入图片描述在这里插入图片描述

棋盘距离

两个像素点X方向距离和Y方向距离的最大值。与街区距离相似,棋盘距离也是假定两个像素点之间不能够沿着连线方向靠近,像素点只能沿着X方向和Y方向移动,但是棋盘距离并不是表示由一个像素点移动到另一个像素点之间的距离,而是表示两个像素点移动到同一行或者同一列时需要移动的最大距离,数学表示形式如式(6.3)所示。
在这里插入图片描述在这里插入图片描述在这里插入图片描述
OpenCV 4中提供了用于计算图像中不同像素之间距离的**distanceTransform()**函数,该函数有两个原型,在代码清单6-1中给出了第一种函数原型。

void cv::distanceTransform(InputArray  src,
                           OutputArray  dst,
                           OutputArray  labels,
                           int  distanceType,
                           int  maskSize,
                           int  labelType = DIST_LABEL_CCOMP 
                           )

该函数原型在对图像进行距离变换的同时会生成Voronoi图,但是有时只是为了实现对图像的距离变换,并不需要使用Voronoi图,而使用该函数必须要求创建一个Mat类变量用于存放Voronoi图,占用了内存资源,因此distanceTransform()函数的第二种函数原型中取消了生成Voronoi图,只输出距离变换后的图像,该种函数原型在代码清单6-2中给出。

void distanceTransform(InputArray  src, 
                       OutputArray  dst,
                       int  distanceType, 
                       int  maskSize, 
                       int  dstType = CV_32F
                       )
  • src:输入图像,数据类型为CV_8U的单通道图像
  • dst:输出图像,与输入图像具有相同的尺寸,数据类型为CV_8U或者CV_32F的单通道图像。
  • distanceType:选择计算两个像素之间距离方法的标志,其常用的距离度量方法在表6-1给出。
  • maskSize:距离变换掩码矩阵的大小,参数可以选择的尺寸为DIST_MASK_3(3×3)和DIST_MASK_5(5×5)。
  • dstType:输出图像的数据类型,可以是CV_8U或者CV_32F。

该函数原型中的主要参数含义与前一种函数原型相同,前两个参数为输入图像和输出图像,第三个参数和为距离变换过程中使用的距离种类。函数中第四个参数是距离变换掩码矩阵的大小,由于街区距离(Dist_L1)和棋盘距离(Dist_C)对掩模尺寸没有要求,因此该参数在选择街区距离和棋盘距离时被强制设置为3,同样掩模尺寸的大小对欧式距离(Dist_L2)计算的精度有影响,为了获取较为精确的时,一般使用5×5的掩模矩阵。函数最后一个参数是输出图像的数据类型,虽然可以在CV_8U和CV_32F两个类型中任意选择,但是图像输出时实际的数据类型与距离变换时选择的距离种类有着密切的联系,CV_8U只能使用在计算街区距离的条件下,当计算欧式距离和棋盘距离时,即使该参数设置为CV_8U,实际的输出图像的数据类型也是CV_32F。

由于distanceTransform()函数是计算图像中非0像素距离0像素的最小距离,而图像中0像素表示黑色,因此为了保证能够清楚的观察到距离变换的结果,不建议使用尺寸过小或者黑色区域较多的图像,否则distanceTransform()函数处理后的图像中几乎全为黑色,不利于观察。

为了了解distanceTransform()函数使用方式以及验证5×5矩阵中所有元素离中心位置的距离,在代码清单6-3中给出利用distanceTransform()函数计算像素间的距离以及实现图像的距离变换。由于distanceTransform()函数计算图像中非0像素距离0像素的最近距离,因此为了能够计算5×5矩阵中所有元素离中心位置的距离,在程序中创造一个5×5的矩阵,矩阵的中心元素为0,其余值全为1,计算结果通过Image Watch查看如图6-4所示。为了验证图像中0元素数目对图像距离变换结果的影响,程序中首先将图像二值化,之后将二值化图像黑白像素反转,之后利用distanceTransform()函数实现距离变换,程序的计算结果在图6-4给出。由于riceBW图像黑色区域较多,如果距离变换结果的数据类型为CV_8U,那么查看图像时将全部为黑色,因此将距离变换结果的数据类型设置为CV_32F,所以查看图像时与原二值图像一致,但是内部的数据不一致。

#include <opencv2/opencv.hpp>
#include <opencv2/highgui/highgui.hpp>
using namespace cv;
using namespace std;

int main(){
    //构建建议矩阵,用于求取像素之间的距离
    Mat a=(Mat_<uchar>(5,5)<<1,1,1,1,1,
            1,1,1,1,1,
            1,1,0,1,1,
            1,1,1,1,1,
            1,1,1,1,1);
    Mat dist_L1,dist_L2,dist_C,dist_l12;

    //计算街区距离
    distanceTransform(a,dist_L1,1,3,CV_8U);
    cout<<"街区距离: "<<endl<<dist_L1<<endl;

    //计算欧氏距离
    distanceTransform(a,dist_L2,3,5,CV_8U);
    cout<<"欧氏距离: "<<endl<<dist_L2<<endl;

    //计算棋盘距离
    distanceTransform(a,dist_C,3,5,CV_8U);
    cout<<"棋盘距离: "<<endl<<dist_C<<endl;

    Mat rice=imread("rice.jpeg",IMREAD_ANYDEPTH);
    if(rice.empty()){
        cout<<"请确认输入的图片路径是否正确"<<endl;
        return -1;
    }
    Mat riceBW,riceBW_INV;

    //将图像转换成二值图像,同时把黑白区域颜色互换
    threshold(rice,riceBW,50,255,THRESH_BINARY);
    threshold(rice,riceBW_INV,50,255,THRESH_BINARY_INV);

    //距离变换
    Mat dist,dist_INV;
    distanceTransform(riceBW,dist,1,3,CV_32F);//为了显示清晰,将数据类型变成CV_32F
    distanceTransform(riceBW_INV,dist_INV,1,3,CV_8U);

    //显示变换结果
    imshow("riceBW", riceBW);
    imshow("dist", dist);
    imshow("riceBW_INV", riceBW_INV);
    imshow("dist_INV", dist_INV);
    waitKey(0);
    return 0;
}

运行结果:

街区距离: 
[  4,   3,   2,   3,   4;
   3,   2,   1,   2,   3;
   2,   1,   0,   1,   2;
   3,   2,   1,   2,   3;
   4,   3,   2,   3,   4]
欧氏距离: 
[2, 2, 2, 2, 2;
 2, 1, 1, 1, 2;
 2, 1, 0, 1, 2;
 2, 1, 1, 1, 2;
 2, 2, 2, 2, 2]
棋盘距离: 
[2, 2, 2, 2, 2;
 2, 1, 1, 1, 2;
 2, 1, 0, 1, 2;
 2, 1, 1, 1, 2;
 2, 2, 2, 2, 2]

在这里插入图片描述

C++版本OpenCv教程(三十八 )图像连通域分析

Excerpt

图像的连通域是指图像中具有相同像素值并且位置相邻的像素组成的区域,连通域分析是指在图像中寻找出彼此互相独立的连通域并将其标记出来。提取图像中不同的连通域是图像处理中较为常用的方法,例如在车牌识别、文字识别、目标检测等领域对感兴趣区域分割与识别。一般情况下,一个连通域内只包含一个像素值,因此为了防止像素值波动对提取不同连通域的影响,连通域分析常处理的是二值化后的图像。了解图像连通域分析方法之前,首先需要了解图像邻域的概念。图像中两个像素相邻有两种定义方式,分别是4-邻域和8-邻域,这两种领域的定义方式在图6


图像的连通域是指图像中具有相同像素值并且位置相邻的像素组成的区域,连通域分析是指在图像中寻找出彼此互相独立的连通域并将其标记出来。提取图像中不同的连通域是图像处理中较为常用的方法,例如在车牌识别、文字识别、目标检测等领域对感兴趣区域分割与识别。一般情况下,一个连通域内只包含一个像素值,因此为了防止像素值波动对提取不同连通域的影响,连通域分析常处理的是二值化后的图像
了解图像连通域分析方法之前,首先需要了解图像邻域的概念。图像中两个像素相邻有两种定义方式,分别是4-邻域和8-邻域,这两种领域的定义方式在图6-7给出。4-邻域的定义方式如图6-7中的左侧所示,在这种定义下,两个像素相邻必须在水平和垂直方向上相邻,相邻的两个像素坐标必须只有一位不同而且只能相差1个像素,例如点
P0(x,y)的4-邻域的4个像素点分别为P1(x-1,y)、P2(x+1,y)、P3(x,y-1)和P4(x,y+1).8-邻域的定义方式如图6-7中的右侧所示,这种定义下两个像素相邻允许在对角线方向相邻,相邻的两个像素坐标在X方向和Y方向上的最大差值为1,例如点P0(x,y)的8-邻域的8个像素点分别为P1(x-1,y)、P2(x+1,y)、P3(x,y-1)、P4(x,y+1)、P5(x-1,y-1)、P6(x+1,y-1)、P7(x-1,y+1)和P8(x+1,y+1).根据两个像素相邻的定义方式不同,得到的连通域也不相同,因此在分析连通域的同时,一定要声明是在哪种种邻域条件下分析得到的结果。
图6-7 4-邻域和8-邻域的定义方式示意图

常用的图像邻域分析法有两遍扫描法种子填充法。两遍扫描法会遍历两次图像,第一次遍历图像时会给每一个非0像素赋予一个数字标签,当某个像素的上方和左侧邻域内的像素已经有数字标签时,取两者中的最小值作为当前像素的标签,否则赋予当前像素一个新的数字标签。第一次遍历图像的时候同一个连通域可能会被赋予一个或者多个不同的标签,如图6-8所示,因此第二次遍历需要将这些属于同一个连通域的不同标签合并,最后实现同一个邻域内的所有像素具有相同的标签。
图6-8 两遍扫描法中第一遍扫描的结果

种子填充法源于计算机图像学,常用于对某些图形进行填充。该方法首先将所有非0像素放到一个集合中,之后在集合中随机选出一个像素作为种子像素,根据邻域关系不断扩充种子像素所在的连通域,并在集合中删除掉扩充出的像素,直到种子像素所在的连通域无法扩充,之后再从集合中随机选取一个像素作为新的种子像素,重复上述过程直到集合中没有像素。

OpenCV 4提供了用于提取图像中不同连通域的**connectedComponents()**函数,该函数有两个函数原型,第一种函数原型在代码清单6-4中给出。

int cv::connectedComponents(InputArray  image,
                        OutputArray  labels,
                        int  connectivity,
                            int  ltype,
                        int  ccltype 
                            )
  • image:待标记不同连通域的单通道图像,数据类型必须为CV_8U。
  • labels:标记不同连通域后的输出图像,与输入图像具有相同的尺寸。
  • connectivity:标记连通域时使用的邻域种类,4表示4-邻域,8表示8-邻域。
  • ltype:输出图像的数据类型,目前支持CV_32S和CV_16U两种数据类型。
  • ccltype:标记连通域时使用的算法类型标志,可以选择的参数及含义在表6-3中给出。
    表6-3 connectedComponents()函数中标记连通域算法类型可选择标志
    该函数用于计算二值图像中连通域的个数,并在图像中将不同的连通域用不同的数字标签标记出,其中标签0表示图像中的背景区域,同时函数具有一个int类型的返回数据,用于表示图像中连通域的数目。函数的第一个参数是待标记连通域的输入图像,函数要求输入图像必须是数据类型为CV_8U的单通道灰度图像,而且最好是经过二值化的二值图像。函数第二个参数是标记连通域后的输出图像,图像尺寸与第一个参数的输入图像尺寸相同,图像的数据类型与函数的第四个参数相关。函数第三个参数是统计连通域时选择的邻域种类,函数支持两种邻域,分别用4表示4-邻域,8表示8-邻域。函数第四个参数为输出图像的数据类型,可以选择的参数为CV_32S和CV_16U两种。函数的最后一个参数是标记连通域时使用算法的标志,可以选择的参数及含义在表6-3给出,目前只支持Grana(BBDT)和Wu(SAUF)两种算法。

上述函数原型的所有参数都没有默认值,在调用时需要设置全部参数,增加了使用的复杂程度,因此OpenCV 4提供了connectedComponents()函数的简易原型,减少了参数数量以及为部分参数增加了默认值,简易原型在代码清单6-5中给出。

int cv::connectedComponents(InputArray  image,
                            OutputArray  labels,
                            int  connectivity = 8,
                            int  ltype = CV_32S 
                            )
  • image:待标记不同连通域的图像单通道,数据类型必须为CV_8U。
  • labels:标记不同连通域后的输出图像,与输入图像具有相同的尺寸。
  • connectivity:标记连通域时使用的邻域种类,4表示4-邻域,8表示8-邻域,默认参数为8。
  • ltype:输出图像的数据类型,目前支持CV_32S和CV_16U两种数据类型,默认参数为CV_32S。

该函数原型只有四个参数,前两个参数分别表示输入图像和输出图像,第三个参数表示统计连通域时选择的邻域种类,分别用4表示4-邻域,8表示8-邻域,参数的默认值为8。最后一个参数表示输出图像的数据类型,可以选择的参数为CV_32S和CV_16U两种,参数的默认值为CV_32S。该函数原型有两个参数具有默认值,在使用时最少只需要两个参数,极大的方便了函数的调用。

为了了解connectedComponents()函数使用方式,在代码清单6-6中给出利用connectedComponents()函数统计图像中连通域数目的示例程序。程序中首先将图像转换成灰度图像,然后将灰度图像二值化为二值图像,之后利用connectedComponents()函数对图像进行连通域的统计。根据统计结果,将数字不同的标签设置成不同的颜色,以区分不同的连通域,程序运行的结果如图6-9所示。

#include <opencv2/opencv.hpp>
#include <opencv2/highgui/highgui.hpp>
using namespace cv;
using namespace std;

int main(){
    //对图像进行距离变换
    Mat img=imread("rice.jpeg");
    if(img.empty()){
        cout<<"请确认输入的图片路径是否正确"<<endl;
        return -1;
    }
    Mat rice,riceBW;
    //将图像转成二值图像,用于统计连通域
    cvtColor(img,rice,COLOR_BGR2GRAY);
    threshold(rice,riceBW,50,255,THRESH_BINARY);

    //生成随机颜色,用于区分不同连通域
    RNG rng(10086);
    Mat out;
    int number=connectedComponents(riceBW,out,8,CV_16U);//统计图像连通域的个数
    vector<Vec3b>colors;
    for(int i=0;i<number;++i){
        //使用均匀分布的随机确定颜色
        Vec3b vec3=Vec3b(rng.uniform(0,256),rng.uniform(0,256),rng.uniform(0,256));
        colors.push_back(vec3);
    }

    //以不同颜色标记出不同的连通域
    Mat result=Mat::zeros(rice.size(),img.type());
    int w=result.cols;
    int h=result.rows;
    for(int row=0;row<h;++row){
        for(int col=0;col<w;++col){
            int label=out.at<uint16_t>(row,col);
            if(label==0){//背景的黑色不改变
                continue;
            }
            result.at<Vec3b>(row,col)=colors[label];
        }
    }

    //显示结果
    imshow("原图",img);
    imshow("标记后的图像",result);
    waitKey(0);
    return 0;
}

在这里插入图片描述
connectedComponents()函数虽然可以实现图像中多个连通域的统计,但是该函数只能通过标签将图像中的不同连通域区分开,无法得到更多的统计信息。有时我们希望得到每个连通域中心位置或者在图像中标记出连通域所在的矩形区域,connectedComponents()函数便无法胜任这项任务,因为该函数无法得到更多的信息。为了能够获得更多有关连通域的信息,OpenCV 4提供了**connectedComponentsWithStats ()**函数用于标记出图像中不同连通域的同时统计连通域的位置、面积的信息,该函数的函数原型在代码清单6-7中给出。

int cv::connectedComponentsWithStats(InputArray  image,
                                     OutputArray  labels,
                                     OutputArray  stats,
                                     OutputArray  centroids,
                                     int  connectivity,
                                     int  ltype,
                                     int  ccltype 
                                 )
  • image:待标记不同连通域的单通道图像,数据类型必须为CV_8U。
  • labels:标记不同连通域后的输出图像,与输入图像具有相同的尺寸。
  • stats:含有不同连通域统计信息的矩阵,矩阵的数据类型为CV_32S。矩阵中第i行是标签为i的连通域的统计特性,存储的统计信息种类在表6-4中给出。
  • centroids:每个连通域的质心坐标,数据类型为CV_64F。
  • connectivity:标记连通域时使用的邻域种类,4表示4-邻域,8表示8-邻域。
  • ltype:输出图像的数据类型,目前支持CV_32S和CV_16U两种数据类型。
  • ccltype:标记连通域使用的算法类型标志,可以选择的参数及含义在表6-3中给出。

该函数能够在图像中不同连通域标记标签的同时统计每个连通域的中心位置、矩形区域大小、区域面积等信息。函数的前两个参数含义与connectedComponents()函数的前两个参数含义一致,都是输入图像和输出图像。函数的第三个参数为每个连通域统计信息矩阵,如果图像中有N个连通域,那么该参数输出的矩阵尺寸为N×5,矩阵中每一行分别保存每个连通域的统计特性,详细的统计特性在表6-4中给出,如果想读取包含第i个连通域的边界框的水平长度,可以通过stats.at(i, CC_STAT_WIDTH)或者stats.at(i, 0)进行读取。函数的第四个参数为每个连通域质心的坐标,如果图像中有N个连通域,那么该参数输出的矩阵尺寸为N×2,矩阵中每一行分别保存每个连通域质心的x坐标和y坐标,可以通过centroids.at(i, 0)和 centroids.at(i, 1) 分别读取第i个连通域质心的x坐标和y坐标。函数第五个参数是统计连通域时选择的邻域种类,函数支持两种邻域,分别用4表示4-邻域,8表示8-邻域。函数第六个参数为输出图像的数据类型,可以选择的参数为CV_32S和CV_16U两种。函数的最后一个参数是标记连通域使用的算法,可以选择的参数在表6-3给出,目前只支持Grana(BBDT)和Wu(SAUF)两种算法。
表6-4 connectedComponentsWithStats ()函数中统计的连通域信息种类
上述函数原型的所有参数都没有默认值,在调用时需要设置全部参数,增加了使用的复杂程度,因此OpenCV 4提供了ConnectedComponentsWithStats()函数的简易原型,减少了参数数量以及为部分参数增加了默认值,简易原型在代码清单6-8中给出。

int cv::connectedComponentsWithStats(InputArray  image,
                                 OutputArray  labels,
                                 OutputArray  stats,
                                 OutputArray  centroids,
                                 int  connectivity = 8,
                                 int  ltype = CV_32S 
                                 )
  • image:待标记不同连通域的单通道图像,数据类型必须为CV_8U。
  • labels:标记不同连通域后的输出图像,与输入图像具有相同的尺寸。
  • stats:不同连通域的统计信息矩阵,矩阵的数据类型为CV_32S。矩阵中第i行是标签为i的连通域的统计特性,存储的统计信息种类在表6-4中给出。
  • centroids:每个连通域的质心坐标,数据类型为CV_64F。
  • connectivity:标记连通域时使用的邻域种类,4表示4-邻域,8表示8-邻域,默认参数值为8。
  • ltype:输出图像的数据类型,目前只支持CV_32S和CV_16U这两种数据类型,默认参数值为CV_32S。

该函数原型只有六个参数,前两个参数分别表示输入图像和输出图像,第三个参数表示每个连通域的统计信息,第四个参数表示每个连通域的质心位置。后两个参数分别表示统计连通域时选择的邻域种类,分别用4表示4-邻域,8表示8-邻域,参数的默认值为8。最后一个参数表示输出图像的数据类型,可以选择的参数为CV_32S和CV_16U两种,参数的默认值为CV_32S。该函数原型有两个参数具有默认值,在使用时最少只需要四个参数,极大的方便了函数的调用。

为了了解connectedComponentsWithStats ()函数使用方式,在代码清单6-9中给出利用该函数统计图像中连通域数目并将每个连通域信息在图像中进行标注的示例程序。程序中首先将图像转换成灰度图像,然后将灰度图像二值化为二值图像,之后利用connectedComponentsWithStats ()函数对图像进行连通域的统计。根据统计结果,用不同颜色的矩形框将连通域围起来,并标记出每个连通域的质心,标出连通域的标签数字,以区分不同的连通域,程序运行的结果如图6-10所示。最后输出每个连通域的面积,输入结果在图6-11给出。
在这里插入图片描述在这里插入图片描述

C++版本OpenCv教程(三十九)图像腐蚀

Excerpt

图像的腐蚀过程与图像的卷积操作类似,都需要模板矩阵来控制运算的结果,在图像的腐蚀和膨胀中这个模板矩阵被称为结构元素。与图像卷积相同,结构元素可以任意指定图像的中心点,并且结构元素的尺寸和具体内容都可以根据需求自己定义。定义结构元素之后,将结构元素的中心点依次放到图像中每一个非0元素处,如果此时结构元素内所有的元素所覆盖的图像像素值均不为0,则保留结构元素中心点对应的图像像素,否则将删除结构元素中心点对应的像素。图像的腐蚀过程示意图如图6-12所示,图6-12中左侧为待腐蚀的原图像,中间为结构元素,首先将结构


图像的腐蚀过程与图像的卷积操作类似,都需要模板矩阵来控制运算的结果,在图像的腐蚀和膨胀中这个模板矩阵被称为结构元素。与图像卷积相同,结构元素可以任意指定图像的中心点,并且结构元素的尺寸和具体内容都可以根据需求自己定义。定义结构元素之后,将结构元素的中心点依次放到图像中每一个非0元素处,如果此时结构元素内所有的元素所覆盖的图像像素值均不为0,则保留结构元素中心点对应的图像像素,否则将删除结构元素中心点对应的像素。图像的腐蚀过程示意图如图6-12所示,图6-12中左侧为待腐蚀的原图像,中间为结构元素,首先将结构元素的中心与原图像中的A像素重合,此时结构元素中心点的左侧和上方元素所覆盖的图像像素值均为0,因此需要将原图像中的A像素删除;当把结构元素的中心点与B像素重合时,此时结构元素中所有的元素所覆盖的图像像素值均为1,因此保留原图像中的B像素。将结构元素中心点依次与原图像中的每个像素重合,判断每一个像素点是否保留或者删除,最终原图像腐蚀的结果如图6-12中右侧图像所示。
图6-12 图像腐蚀结果示意图

图像腐蚀可以用“Θ”表示,其数学表示形式如式(6.4)所示,通过公式可以发现,其实对图像A的腐蚀运算就是寻找图像中能够将结构元素B全部包含的像素点。
在这里插入图片描述

图像腐蚀过程中使用的结构元素可以根据需求自己生成,但是为了研究人员的使用方便,OpenCV 4提供了**getStructuringElement()**函数用于生成常用的矩形结构元素、十字结构元素和椭圆结构元素。该函数的函数原型在代码清单6-10中给出。

Mat cv::getStructuringElement(int  shape,
                              Size  ksize,
                              Point  anchor = Point(-1,-1) 
                              )
  • shape:结构元素的种类,可以选择的参数及含义在表6-5中给出。
  • ksize:结构元素的尺寸大小
  • anchor:中心点的位置,默认参数为结构元素的几何中心点。

该函数用于生成图像形态学操作中常用的矩形结构元素、十字结构元素和椭圆结构元素。函数第一个参数为生成结构元素的种类,可以选择的参数及含义在表6-5给出,函数第二个参数是结构元素的尺寸大小,能够影响到图像腐蚀的效果,一般情况下,结构元素的种类相同时,结构元素的尺寸越大腐蚀效果越明显。函数的最后一个参数是结构元素的中心点,只有十字结构元素的中心点位置会影响图像腐蚀后的轮廓形状,其他种类的结构元素的中心点位置只影响形态学操作结果的平移量。
表6-5 getStructuringElement()函数结构元素形状可选择参数
OpenCV 4提供了用于图像腐蚀的erode()函数,该函数的函数原型在代码清单6-11中给出。

void cv::erode(InputArray  src,
               OutputArray  dst,
               InputArray  kernel,
               Point  anchor = Point(-1,-1),
               int  iterations = 1,
               int  borderType = BORDER_CONSTANT,
               const Scalar &  borderValue = morphologyDefaultBorderValue() 
               )
  • src:输入的待腐蚀图像,图像的通道数可以是任意的,但是图像的数据类型必须是CV_8U,CV_16U,CV_16S,CV_32F或CV_64F之一。
  • dst:腐蚀后的输出图像,与输入图像src具有相同的尺寸和数据类型。
  • kernel:用于腐蚀操作的结构元素,可以自己定义,也可以用getStructuringElement()函数生成。
  • anchor:中心点在结构元素中的位置,默认参数为结构元素的几何中心点
  • iterations:腐蚀的次数,默认值为1。
  • borderType:像素外推法选择标志,取值范围在表3-5中给出。默认参数为BORDER_DEFAULT,表示不包含边界值倒序填充。
  • borderValue:使用边界不变外推法时的边界值。

该函数根据结构元素对输入图像进行腐蚀,在腐蚀多通道图像时每个通道独立进行腐蚀运算。
函数的第一个参数为待腐蚀的图像,图像通道数可以是任意的,但是图像的数据类型必须是CV_8U,CV_16U,CV_16S,CV_32F或CV_64F之一。
函数第二个参数为腐蚀后的输出图像,与输入图像具有相同的尺寸和数据类型。
函数第三个和第四个参数都是与结构元素相关的参数,第三个参数为结构元素,第四个参数为结构元素的中心位置,第四个参数的默认值为Point(-1,-1),表示结构元素的几何中心处为结构元素的中心点。
函数第五个参数是使用结构元素腐蚀的次数,腐蚀次数越多效果越明显,参数默认值为1,表示只腐蚀1次。
函数第六个参数是图像像素外推法的选择标志,
第七个参数为使用边界不变外推法时的边界值,这两个参数对图像中主要部分的腐蚀操作没有影响,因此在多数情况下使用默认值即可。

需要注意的是该函数的腐蚀过程只针对图像中的非0像素,因此如果图像是以0像素为背景,那么腐蚀操作后会看到图像中的内容变得更瘦更小;如果图像是以255像素为背景,那么腐蚀操作后会看到图像中的内容变得更粗更大。

为了更加了解图像腐蚀的效果以及erode()函数的使用方法,在代码清单6-12中给出了对图6-12中的原图像进行腐蚀的示例程序,程序运行结果如图6-13所示。在程序中分别利用矩形结构元素和十字结构元素对像素值为0做背景的图像和像素值为255做背景的图像进行腐蚀,结果在图6-14、图6-15给出。最后利用图像腐蚀操作对代码清单6-6中二值化后的图像进行滤波,之后统计连通域个数,实现对原图像中的米粒进行计数,程序运行结果在图6-16给出。通过结果可以发现,腐蚀操作可以去除由噪声引起的较小的连通域,得到了正确的米粒数。

#include <opencv2/opencv.hpp>
#include <opencv2/highgui/highgui.hpp>
using namespace cv;
using namespace std;

//绘制包含区域函数
void drawState(Mat &img,int number,Mat centroids,Mat stats,String str){
    RNG rng(10086);
    vector<Vec3b>colors;
    for(int i=0;i<number;++i){
        //使用均匀分布的随机数确定颜色
        Vec3b vec3=Vec3b(rng.uniform(0,256),rng.uniform(0,256),rng.uniform(0,256));
        colors.push_back(vec3);
    }

    for(int i=1;i<number;++i){
        //中心位置
        int center_x=centroids.at<double>(i,0);
        int center_y=centroids.at<double>(i,1);
        //矩形边框
        int x=stats.at<int>(i,CC_STAT_LEFT);
        int y=stats.at<int>(i,CC_STAT_TOP);
        int w=stats.at<int>(i,CC_STAT_WIDTH);
        int h=stats.at<int>(i,CC_STAT_HEIGHT);

        //中心位置绘制
        circle(img,Point(center_x,center_y),2,Scalar(0,255,0),2,0,0);
        //外接矩形
        Rect rect(x,y,w,h);
        rectangle(img,rect,colors[i],1,0,0);
        putText(img,format("%d",i),Point(center_x,center_y),FONT_HERSHEY_SIMPLEX,0.5,Scalar(0,0,255),1)
    }
    imshow(str,img);
}

int main(){
    //生成用于腐蚀的原图像
    Mat src=(Mat_<uchar>(6,6)<<0,0,0,0,255,0,
            0,255,255,255,255,255,
            0,255,255,255,255,0,
            0,255,255,255,255,0,
            0,255,255,255,255,0,
            0,0,0,0,255,0);
    Mat struct1,struct2;
    struct1=getStructuringElement(0,Size(3,3));//矩形结构元素
    struct2=getStructuringElement(1,Size(3,3));//十字结构元素

    Mat erodeSrc;//存放腐蚀后的图像
    erode(src,erodeSrc,struct2);
    namedWindow("src",WINDOW_GUI_NORMAL);
    namedWindow("erodeSrc",WINDOW_GUI_NORMAL);
    imshow("src",src);
    imshow("erodeSrc",erodeSrc);

    Mat LearnCV_black=imread("LearnCV_black.png",IMREAD_ANYCOLOR);
    Mat LearnCV_write=imread("LearnCV_write.png",IMREAD_ANYCOLOR);
    Mat erode_black1,erode_black2,erode_write1,erode_write2;
    //黑背景图像腐蚀
    erode(LearnCV_black,erode_black1,struct1);
    erode(LearnCV_black,erode_black2,struct2);
    imshow("LearnCV_black",LearnCV_black);
    imshow("erode_black1",erode_black1);
    imshow("erode_back2",erode_black2);

    //白色背景腐蚀
    erode(LearnCV_write,erode_write1,struct1);
    erode(LearnCV_write,erode_write2,struct2);
    imshow("Learn_write",LearnCV_write);
    imshow("erode_write1",erode_write1);
    imshow("erode_write2",erode_write2);

    //验证腐蚀对小连通域的去除
    Mat img=imread("rice.jpeg");
    Mat img2;
    copyTo(img,img2,img);//克隆一个单独的图像,用于后期图像绘制
    Mat rice,riceBW;

    //将图像转成二值图像,用于统计连通域
    cvtColor(img,rice,COLOR_BGR2GRAY);
    threshold(rice,riceBW,50,255,THRESH_BINARY);

    Mat out,stats,centroids;
    //统计图像中连通域的个数
    int number=connectedComponentsWithStats(riceBW,out,stats,centroids,8,CV_16U);
    drawState(img,number,centroids,stats,"未腐蚀时统计连通域");//绘制图像

    erode(riceBW,riceBW,struct1);//对图像进行腐蚀
    number=connectedComponentsWithStats(riceBW,out,stats,centroids,8,CV_16U);
    drawState(img2,number,centroids,stats,"腐蚀后统计连通域");//绘制图像

    waitKey(0);
    return 0;
}

图6-13 用十字结构元素腐蚀示例
图6-14 myErode.cpp程序中黑背景图像腐蚀结果
图6-15 myErode.cpp程序中白背景图像腐蚀结果图6-15 myErode.cpp程序中白背景图像腐蚀结果

C++版本OpenCv教程(四十)图像膨胀

Excerpt

图像的膨胀与图像腐蚀是一对相反的过程,与图像腐蚀相似,图像膨胀同样需要结构元素用于控制图像膨胀的效果。结构元素可以任意指定结构的中心点,并且结构元素的尺寸和具体内容都可以根据需求自己定义。定义结构元素之后,将结构元素的中心点依次放到图像中每一个非0元素处,如果原图像中某个元素被结构元素覆盖,但是该像素的像素值不与结构元素中心点对应的像素点的像素值相同,那么将原图像中的该像素的像素值修改为结构元素中心点对应点的像素值。图像的膨胀过程示意图如图6-12所示,图6-12中左侧为待膨胀的原图像,中间为结构元素,首先


图像的膨胀与图像腐蚀是一对相反的过程,与图像腐蚀相似,图像膨胀同样需要结构元素用于控制图像膨胀的效果。结构元素可以任意指定结构的中心点,并且结构元素的尺寸和具体内容都可以根据需求自己定义。定义结构元素之后,将结构元素的中心点依次放到图像中每一个非0元素处,如果原图像中某个元素被结构元素覆盖,但是该像素的像素值不与结构元素中心点对应的像素点的像素值相同,那么将原图像中的该像素的像素值修改为结构元素中心点对应点的像素值。图像的膨胀过程示意图如图6-12所示,图6-12中左侧为待膨胀的原图像,中间为结构元素,首先将结构元素的中心与原图像中的A像素重合,将结构元素覆盖的所有像素的像素值都修改为1,将结构元素中心点依次与原图像中的每个像素重合,判断是否有需要填充的像素。原图像膨胀的结果如图6-17中右侧图像所示。
在这里插入图片描述

图像膨胀数学表示形式如式(6.5)所示,通过公式可以发现,其实图像A的膨胀运算就是生成能够将结构元素B全部包含的图像。
在这里插入图片描述

OpenCV 4提供了用于图像膨胀的dilate()函数,该函数的函数原型在代码清单6-13中给出。

void cv::dilate(InputArray  src,
          OutputArray  dst,
          InputArray  kernel,
          Point  anchor = Point(-1,-1),
int  iterations = 1,
int  borderType = BORDER_CONSTANT,
const Scalar &  borderValue = morphologyDefaultBorderValue()
          )
  • src:输入的待膨胀图像,图像的通道数可以是任意的,但是图像的数据类型必须是CV_8U,CV_16U,CV_16S,CV_32F或CV_64F之一。
  • dst:膨胀后的输出图像,与输入图像src具有相同的尺寸和数据类型。
  • kernel:用于膨胀操作的结构元素,可以自己定义,也可以用getStructuringElement()函数生成。
  • anchor:中心点在结构元素中的位置,默认参数为结构元素的几何中心点
  • iterations:膨胀的次数,默认值为1。
  • borderType:像素外推法选择标志,取值范围在表3-5中给出。默认参数为BORDER_DEFAULT,表示不包含边界值倒序填充。
  • borderValue:使用边界不变外推法时的边界值。

该函数根据结构元素对输入图像进行膨胀,在膨胀多通道图像时每个通道独立进行膨胀运算。函数的第一个参数为待膨胀的图像,图像通道数可以是任意的,但是图像的数据类型必须是CV_8U,CV_16U,CV_16S,CV_32F或CV_64F之一。函数第二个参数为膨胀后的输出图像,与输入图像具有相同的尺寸和数据类型。函数第三个和第四个参数都是与结构元素相关的参数,第三个参数为结构元素,膨胀时使用的结构元素尺寸越大效果越明显,第四个参数为结构元素的中心位置,第四个参数的默认值为Point(-1,-1),表示结构元素的几何中心处为结构元素的中心点。函数第五个参数是使用结构元素膨胀的次数,膨胀次数越多效果越明显,默认参数为1,表示只膨胀1次。函数第六个参数是图像像素外推法的选择标志,第七个参数为使用边界不变外推法时的边界值,这两个参数对图像中主要部分的膨胀操作没有影响,因此在多数情况下使用默认值即可。

需要注意的是该函数的膨胀过程只针对图像中的非0像素,因此如果图像是以0像素为背景,那么膨胀操作后会看到图像中的内容变得更粗更大;如果图像是以255像素为背景,那么膨胀操作后会看到图像中的内容变得更细更小。

为了更加了解图像膨胀的效果以及dilate()函数的使用方法,在代码清单6-14中给出了对图6-17中的原图像进行膨胀的示例程序,程序运行结果如图6-18所示。另外,程序中分别利用矩形结构元素和十字结构元素对像素值为0做背景的图像和像素值为255做背景的图像进行膨胀,结果在图6-19、图6-20给出。最后为了验证膨胀与腐蚀效果之间的关系,求取黑背景图像的腐蚀结果与白背景图像的膨胀结果进行逻辑和、逻辑异或运算,证明两个过程的相反性,结果在图6-21给出。

#include <opencv2\opencv.hpp>
#include <iostream>
#include <vector>

using namespace cv;
using namespace std;

int main()
{
//生成用于腐蚀的原图像
Mat src = (Mat_<uchar>(6, 6) << 0, 0, 0, 0, 255, 0,
0, 255, 255, 255, 255, 255,
0, 255, 255, 255, 255, 0,
0, 255, 255, 255, 255, 0,
0, 255, 255, 255, 255, 0,
0, 0, 0, 0, 0, 0);
Mat struct1, struct2;
struct1 = getStructuringElement(0, Size(3, 3));  //矩形结构元素
struct2 = getStructuringElement(1, Size(3, 3));  //十字结构元素

Mat erodeSrc;  //存放腐蚀后的图像
dilate(src, erodeSrc, struct2);
namedWindow("src", WINDOW_GUI_NORMAL);
namedWindow("dilateSrc", WINDOW_GUI_NORMAL);
imshow("src", src);
imshow("dilateSrc", erodeSrc);

Mat LearnCV_black = imread("LearnCV_black.png", IMREAD_ANYCOLOR);
Mat LearnCV_write = imread("LearnCV_write.png", IMREAD_ANYCOLOR);
if (LearnCV_black.empty()||LearnCV_write.empty())
{
cout << "请确认图像文件名称是否正确" << endl;
return-1;
}

Mat dilate_black1, dilate_black2, dilate_write1, dilate_write2;
//黑背景图像膨胀
dilate(LearnCV_black, dilate_black1, struct1);
dilate(LearnCV_black, dilate_black2, struct2);
imshow("LearnCV_black", LearnCV_black);
imshow("dilate_black1", dilate_black1);
imshow("dilate_black2", dilate_black2);

//白背景图像膨胀
dilate(LearnCV_write, dilate_write1, struct1);
dilate(LearnCV_write, dilate_write2, struct2);
imshow("LearnCV_write", LearnCV_write);
imshow("dilate_write1", dilate_write1);
imshow("dilate_write2", dilate_write2);

//比较膨胀和腐蚀的结果
Mat erode_black1, resultXor, resultAnd;
erode(LearnCV_black, erode_black1, struct1);
bitwise_xor(erode_black1, dilate_write1, resultXor);
bitwise_and(erode_black1, dilate_write1, resultAnd);
imshow("resultXor", resultXor);
imshow("resultAnd", resultAnd);
waitKey(0);
return0;
}

图6-18 用十字结构元素膨胀示例
图6-19 myErode.cpp程序中黑背景图像膨胀结果
图6-20 myErode.cpp程序中白背景图像膨胀结果
图6-21 myErode.cpp程序中腐蚀与膨胀关系验证结果

C++版本OpenCv教程(四十一)形态学应用

Excerpt

图像形态学腐蚀可以将细小的噪声区域去除,但是会将图像主要区域的面积缩小,造成主要区域的形状发生改变;图像形态学膨胀可以扩充每一个区域的面积,填充较小的空洞,但是同样会增加噪声的面积。根据两者的特性将图像腐蚀和膨胀适当的结合,便可以既去除图像中的噪声,又不缩小图像中主要区域的面积;既填充了较小的空洞,又不增加噪声所占的面积。因此,本节中将介绍如何利用不同顺序的图像腐蚀和膨胀实现图像的开运算、闭运算、形态学梯度、顶帽运算、黑帽运算以及击中击不中变换等操作。开运算图像开运算可以去除图像中的噪声,消除较小连通域


图像形态学腐蚀可以将细小的噪声区域去除,但是会将图像主要区域的面积缩小,造成主要区域的形状发生改变;图像形态学膨胀可以扩充每一个区域的面积,填充较小的空洞,但是同样会增加噪声的面积。根据两者的特性将图像腐蚀和膨胀适当的结合,便可以既去除图像中的噪声,又不缩小图像中主要区域的面积;既填充了较小的空洞,又不增加噪声所占的面积。因此,本节中将介绍如何利用不同顺序的图像腐蚀和膨胀实现图像的开运算、闭运算、形态学梯度、顶帽运算、黑帽运算以及击中击不中变换等操作。

开运算

图像开运算可以去除图像中的噪声,消除较小连通域,保留较大连通域,同时能够在两个物体纤细的连接处将两个物体分离,并且在不明显改变较大连通域的面积的同时能够平滑连通域的边界。开运算是图像腐蚀和膨胀操作的结合,首先对图像进行腐蚀,消除图像中的噪声和较小的连通域,之后通过膨胀运算弥补较大连通域因腐蚀而造成的面积减小。图6-22给出了图像开运算的三个阶段,图6-22中左侧图像是待开运算的原图像,中间的图像是利用3×3矩形结构元素对原图像进行腐蚀后的图像,通过结果可以看到较小的连通域已经被去除,但是较大的连通域也在边界区域产生了较大的面积缩减,之后对腐蚀后的图像进行膨胀运算,得到图6-22中右侧图像。通过结果可以看出,膨胀运算弥补了腐蚀运算造成的边界面积缩减,使得开运算的结果只去除了较小的连通域,保留了较大的连通域。
在这里插入图片描述

开运算是对图像腐蚀和膨胀的组合,OpenCV 4没有提供只用于图像开运算的函数,而是提供了图像腐蚀和膨胀运算不同组合形式的**morphologyEx()**函数,以实现图像的开运算、闭运算、形态学提取、顶帽运算、黑帽运算以及击中击不中变换,该函数的函数原型在代码清单6-15中给出。

void cv::morphologyEx(InputArray  src,
                      OutputArray  dst,
                      int  op,
                      InputArray  kernel,
                      Point  anchor = Point(-1,-1),
                      int  iterations = 1,
                      int  borderType = BORDER_CONSTANT,
                      const Scalar &  borderValue = morphologyDefaultBorderValue()  
                      )
  • src:输入图像,图像的通道数可以是任意的,但是图像的数据类型必须是CV_8U,CV_16U,CV_16S,CV_32F或CV_64F之一。
  • dst:形态学操作后的输出图像,与输入图像具有相同的尺寸和数据类型。
  • op:形态学操作类型的标志,可以选择的标志及含义在表6-6中给出。
  • kernel:结构元素,可以自己生成,也可以用getStructuringElement()函数生成。
  • anchor:中心点在结构元素中的位置,默认参数为结构元素的几何中心点
  • iterations:处理的次数
  • borderType:像素外推法选择标志,取值范围在表3-5中给出。默认参数为BORDER_DEFAULT,表示不包含边界值倒序填充。
  • borderValue:使用边界不变外推法时的边界值。

该函数根据结构元素对输入图像进行多种形态学操作,在处理多通道图像时每个通道独立进行处理。
函数的第一个参数为待形态学处理的图像,图像通道数可以是任意的,但是图像的数据类型必须是CV_8U,CV_16U,CV_16S,CV_32F或CV_64F之一。
函数第二个参数为形态学处理后的输出图像,与输入图像具有相同的尺寸和数据类型。
函数第三个参数是形态学操作类型的选择标志,可以选择的形态学操作类型有开运算、闭运算、形态学梯度、顶帽运算、黑帽运算以及击中击不中变换,详细的参数在表6-6给出。
函数第四个和第五个参数都是与结构元素相关的参数,第四个参数为结构元素,使用的结构元素尺寸越大效果越明显,第四个参数为结构元素的中心位置,第五个参数的默认值为Point(-1,-1),表示结构元素的几何中心处为结构元素的中心点。
函数第六个参数是使用结构元素处理的次数,处理次数越多效果越明显。
函数第七个参数是图像像素外推法的选择标志,
第八个参数为使用边界不变外推法时的边界值,这两个参数对图像中主要部分的形态学操作没有影响,因此在多数情况下使用默认值即可。
表6-6 morphologyEX()函数中形态学操作类型标志可选参数及含义

该函数实现了多种形态学操作,对于函数的使用方法将在介绍该函数涉及到的所有形态学操作相关概念后在代码清单6-16中给出。

闭运算

图像闭运算可以去除连通域内的小型空洞,平滑物体轮廓,连接两个临近的连通域闭运算是图像膨胀和腐蚀操作的结合,首先对图像进行膨胀,填充连通域内的小型空洞,扩大连通域的边界,将临近的两个连通域连接,之后通过腐蚀运算减少由膨胀运算引起的连通域边界的扩大以及面积的增加。图6-23给出了图像闭运算的三个阶段,图6-23中左侧图像是待闭运算的原图像,中间的图像是利用3×3矩形结构元素对原图像进行膨胀后的图像,通过结果可以看到较大连通域内的小型空洞已经被填充,同时临近的两个连通域也连接了在一起,但是连通域的边界明显扩张,整体的面积增加,之后对膨胀后的图像进行腐蚀运算,得到图6-22中右侧图像。通过结果可以看出,腐蚀运算能够消除连通域因膨胀运算带来的面积增长,但是图像中依然存在较大的面积增长,主要是因为连通域膨胀后,有较大区域在图像的边缘区域,而图像边缘区域的形态学操作结果与图像的边缘外推方法有着密切的关系,因此采用默认外推方法时,边缘的连通域不会被腐蚀掉,从而产生图6-23右侧的结果。
图6-23 图像闭运算三个阶段

闭运算是对图像膨胀和腐蚀的组合,OpenCV 4提供的**morphologyEx()**函数可以选择闭运算参数MORPH_CLOSE实现图像的闭运算,该函数的函数原型已经在代码清单6-15中给出,函数的使用方式将在介绍该函数涉及到的所有形态学操作相关概念后在代码清单6-16中给出。

形态学梯度

形态学梯度能够描述目标的边界,根据图像腐蚀和膨胀与原图之间的关系计算得到,形态学梯度可以分为基本梯度内部梯度外部梯度基本梯度是原图像膨胀后图像和腐蚀后图像间的差值图像内部梯度图像是原图像和腐蚀后图像间的差值图像外部梯度是膨胀后图像和原图像间的差值图像。图6-24给出了计算形态学基本梯度的三个阶段,图6-24中左侧图像是原图像利用3×3矩形结构元素进行膨胀后的图像,中间的图像是原图像利用3×3矩形结构元素进行腐蚀后的图像,右侧图像是左侧图像和中间图像的差值。
图6-24 形态学梯度计算的三个阶段

OpenCV 4提供的morphologyEx()函数可以选择闭运算参数MORPH_GRADIENT实现图像的基本梯度,如果需要计算图像的内部梯度或者外部梯度,需要自己通过程序实现。morphologyEx()函数的函数原型已经在代码清单6-15中给出,函数的使用方式将在介绍该函数涉及到的所有形态学操作相关概念后在代码清单6-16中给出。

顶帽运算

图像顶帽运算是原图像与开运算结果之间的差值往往用来分离比邻近点亮一些的斑块,因为开运算带来的结果是放大了裂缝或者局部低亮度的区域,因此,从原图中减去开运算后的图,得到的效果图突出了比原图轮廓周围的区域更明亮的区域。顶帽运算先对图像进行开运算,之后从原图像中减去开运算计算的结果,在图6-25中给出了计算顶帽运算的三个阶段,图6-25中左侧图像是原图像,中间的图像是利用3×3矩形结构元素对原图像进行开运算后的图像,右侧图像是左侧原与中间开运算结果图像之间的差值,即为原图像顶帽运算的结果。
图6-25 图像顶帽运算的三个阶段

OpenCV 4提供的morphologyEx()函数可以选择顶帽运算的参数MORPH_TOPHAT实现图像的闭运算,该函数的函数原型已经在代码清单6-15中给出,函数的使用方式将在介绍该函数涉及到的所有形态学操作相关概念后在代码清单6-16中给出。

黑帽运算

图像黑帽运算是与图像顶帽运算相对应的形态学操作,与顶帽运算相反,黑帽运算是原图像与闭运算结果之间的差值,往往用来分离比邻近点暗一些的斑块。顶帽运算先对图像进行开运算,之后从原图像中减去开运算计算的结果,在图6-26中给出了计算顶帽运算的三个阶段,图6-26中左侧图像是利用3×3矩形结构元素对原图像进行闭运算后的图像,中间的图像是原图像,右侧图像是左侧闭运算结果图像与中间原图像之间的差值,即为原图像黑帽运算的结果。
图6-26 图像黑帽运算的三个阶段

OpenCV 4提供的morphologyEx()函数可以选择黑帽运算的参数MORPH_BLACKHAT实现图像的闭运算,该函数的函数原型已经在代码清单6-15中给出,函数的使用方式将在介绍该函数涉及到的所有形态学操作相关概念后在代码清单6-16中给出。

击中击不中变换

击中击不中变换是比图像腐蚀要求更加苛刻的一种形态学操作,图像腐蚀只需要图像能够将结构元素中所有非0元素包含即可,但是击中击不中变换要求原图像中需要存在与结构元素一模一样的结构,即结构元素中非0元素也需要同时被考虑。如图6-27中所示,如果用中间的结构元素对左侧图像进行腐蚀,那么将会得到图6-24中所示的腐蚀计算结果,而用中间结构元素对左侧图像进行击中击不中变换结果为图6-27右侧所示,因为结构元素的中心位置为0,而在原图像中符合这种结构的位置只在图像中的中心处,因此击中击不中变换的结果与图像腐蚀结果具有极大的差异。但是在使用矩形结构元素时,击中击不中变换与图像的腐蚀结果相同。
图6-27 图像击中击不中变换结果

OpenCV 4提供的morphologyEx()函数可以选择击中击不中变换参数MORPH_HITMISS实现图像的击中击不中变换,该函数的函数原型已经在代码清单6-15中给出,函数的使用方式在介绍该函数涉及到的所有形态学操作相关概念后在代码清单6-16中给出。程序中构建了用于介绍形态学多种操作原理的原图像,之后用3×3矩形结构元素分别对原图像进行开运算、闭运算、形态学梯度、顶帽运算、黑帽运算以及击中击不中变换等操作,验证morphologyEx()函数处理结果与理论处理结果是否相同,处理结果在图6-28给出。其中需要注意的是由于进行击中击不中变换使用的结构元素与原理介绍时不同,因此程序中的击中击不中变换结果与图6-27不同。此外,为了验证多种形态学操作处理图像的效果,程序中读取一张灰度图像,对图像进行二值化后分别进行多种形态学操作,灰度图像和二值化后的图像如图6-29所示,形态学操作处理后的图像在图6-30给出。

#include <opencv2\opencv.hpp>
#include <iostream>
#include <vector>

using namespace cv;
using namespace std;

int main()
{
//用于验证形态学应用的二值化矩阵
Mat src = (Mat_<uchar>(9, 12) << 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 255, 255, 255, 255, 255, 255, 255, 0, 0, 255, 0,
0, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0,
0, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0,
0, 255, 255, 255, 0, 255, 255, 255, 0, 0, 0, 0,
0, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0,
0, 255, 255, 255, 255, 255, 255, 255, 0, 0, 255, 0,
0, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
namedWindow("src", WINDOW_NORMAL);  //可以自由调节显示图像的尺寸
imshow("src", src);
//3×3矩形结构元素
Mat kernel = getStructuringElement(0, Size(3, 3));

//对二值化矩阵进行形态学操作
Mat open, close,gradient,tophat,blackhat,hitmiss;

//对二值化矩阵进行开运算
morphologyEx(src, open, MORPH_OPEN, kernel);
namedWindow("open", WINDOW_NORMAL);  //可以自由调节显示图像的尺寸
imshow("open", open);

//对二值化矩阵进行闭运算
morphologyEx(src, close, MORPH_CLOSE, kernel);
namedWindow("close", WINDOW_NORMAL);  //可以自由调节显示图像的尺寸
imshow("close", close);

//对二值化矩阵进行梯度运算
morphologyEx(src, gradient, MORPH_GRADIENT, kernel);
namedWindow("gradient", WINDOW_NORMAL);  //可以自由调节显示图像的尺寸
imshow("gradient", gradient);

//对二值化矩阵进行顶帽运算
morphologyEx(src, tophat, MORPH_TOPHAT, kernel);
namedWindow("tophat", WINDOW_NORMAL);  //可以自由调节显示图像的尺寸
imshow("tophat", tophat);

//对二值化矩阵进行黑帽运算
morphologyEx(src, blackhat, MORPH_BLACKHAT, kernel);
namedWindow("blackhat", WINDOW_NORMAL);   //可以自由调节显示图像的尺寸
imshow("blackhat", blackhat);

//对二值化矩阵进行击中击不中变换
morphologyEx(src, hitmiss, MORPH_HITMISS, kernel);
namedWindow("hitmiss", WINDOW_NORMAL);  //可以自由调节显示图像的尺寸
imshow("hitmiss", hitmiss);
  
//用图像验证形态学操作效果
Mat coin = imread("coin.png",IMREAD_GRAYSCALE);
imshow("coin", coin);
threshold(coin, coin, 130, 255, THRESH_BINARY);
imshow("二值化后的coin", coin);

//5×5矩形结构元素
Mat kernel_coin = getStructuringElement(0, Size(5, 5));
Mat open_coin, close_coin, gradient_coin;
Mat tophat_coin, blackhat_coin, hitmiss_coin;

//对图像进行开运算
morphologyEx(coin, open_coin, MORPH_OPEN, kernel_coin);
imshow("open_coin", open_coin);

//对图像进行闭运算
morphologyEx(coin, close_coin, MORPH_CLOSE, kernel_coin);
imshow("close_coin", close_coin);

//对图像进行梯度运算
morphologyEx(coin, gradient_coin, MORPH_GRADIENT, kernel_coin);
imshow("gradient_coin", gradient_coin);

//对图像进行顶帽运算
morphologyEx(coin, tophat_coin, MORPH_TOPHAT, kernel_coin);
imshow("tophat_coin", tophat_coin);

//对图像进行黑帽运算
morphologyEx(coin, blackhat_coin, MORPH_BLACKHAT, kernel_coin);
imshow("blackhat_coin", blackhat_coin);

//对图像进行击中击不中变换
morphologyEx(coin, hitmiss_coin, MORPH_HITMISS, kernel_coin);
imshow("hitmiss_coin", hitmiss_coin);

waitKey(0);
return 0;
}

图6-28 myMorphologyApp.cpp程序中验证形态学操作的处理结果
图6-29 myMorphologyApp.cpp程序中灰度图像及二值化后的图像
图6-30 myMorphologyApp.cpp程序中图像形态学操作后的结果

C++版本OpenCv教程(四十二)霍夫变换原理及直线检测

Excerpt

霍夫变换(Hough Transform)是图像处理中检测是否存在直线的重要算法,该算法是由Paul Hough在1962年首次提出,最开始只能检测图像中的直线,但是霍夫变换经过不断的扩展和完善已经可以检测多种规则形状,例如圆形、椭圆等。霍夫变换通过将图像中的像素在一个空间坐标系中变换到另一个坐标空间坐标系中,使得在原空间中具有形同特性的曲线或者直线映射到另一个空间中形成峰值,从而把检测任意形状的问题转化为统计峰值的问题。霍夫变换通过构建检测形状的数学解析式将图像中像素点映射到参数空间中,例如我们想检测两


霍夫变换(Hough Transform)是图像处理中检测是否存在直线的重要算法,该算法是由Paul Hough在1962年首次提出,最开始只能检测图像中的直线,但是霍夫变换经过不断的扩展和完善已经可以检测多种规则形状,例如圆形、椭圆等。霍夫变换通过将图像中的像素在一个空间坐标系中变换到另一个坐标空间坐标系中,使得在原空间中具有形同特性的曲线或者直线映射到另一个空间中形成峰值,从而把检测任意形状的问题转化为统计峰值的问题。

霍夫变换通过构建检测形状的数学解析式将图像中像素点映射到参数空间中,例如我们想检测两个像素点所在的直线,需要构建直线的数学解析式。在图像空间x-y直角坐标系中,对于直线可以用式(7.1)所示的解析式来表示。
在这里插入图片描述

其中k是直线的斜率,b是直线的截距。假设图像中存在一像素点A(x0,y0),所有经过这个像素点直线可以用式(7.2)表示。
在这里插入图片描述

在图像空间x-y直角坐标系中,由于变量是x和y,因此式(7.2)表示的是经过点像素点A(x0,y0)的直线,但是经过一点的直线有无数条,因此式(7.2)中的 和 具有无数个可以选择的值,如果将x0和y0看作是变量, k和 b表示定值,那么式(7.2)可以表示在k-b空间的一条直线,映射过程示意图如图7-1所示。用式(7.1)的形式表示映射的结果如式(7.3)所示,即霍夫变换将x-y直角坐标系中经过一点的所有直线映射成了k-b空间中的一条直线,直线上的每个点都对应着x-y直角坐标系中的一条直线。
在这里插入图片描述

当图像中存在另一个像素点B(x1,y1)时,在图像空间x-y直角坐标系中所有经过像素点B(x1,y1)的直线也会在参数空间中映射出一条直线。由于参数空间中每一个点都表示图像空间x-y直角坐标系中直线的斜率和截距,因此如果有一条直线经过像素点A(x0,y0)和像素点B(x1,y1)时,这条直线所映射在参数空间中的坐标点应该既在像素点A(x0,y0)映射的直线上又在像素点B(x1,y1)映射的直线上。在平面内一个点同时在两条直线上,那么这个点一定是两条直线的交点,因此这条同时经过A(x0,y0)和B(x1,y1)的直线所对应的斜率和截距就是参数空间中两条直线的交点。
霍夫变换空间映射

根据前面的分析可以得到霍夫变换中存在两个重要的结论:(1)图像空间中的每条直线在参数空间中都对应着单独一个点来表示;(2)图像空间中的直线上任何像素点在参数空间对应的直线相交于同一个点。图7-2给出了第二条结论的示意图。因此通过霍夫变换寻找图像中的直线就是寻找参数空间中大量直线相交的一点。
霍夫变换中同一直线上不同点在参数空间中对应的直线交于一点示意图
利用式(7.1)形式进行霍夫变换可以寻找到图像中绝大多数直线,但是当图像中存在垂直直线时,即所有的像素点的x坐标相同时,直线上的像素点利用上述霍夫变换方法得到的参数空间中多条直线互相平行,无法相交于一点。例如在图像上存在3个像素点(2,1)、(2,2)和(2,3) ,利用式(7.3)可以求得参数空间中3条直线解析式如式中所示,这些直线具有相同的斜率,因此无法交于一点,具体形式如图7-3所示。
垂直直线霍夫变换映射示意图

为了解决垂直直线在参数空间没有交点的问题,一般采用极坐标方式表示图像空间x-y直角坐标系中的直线,具体形式如式(7.5)所示。
在这里插入图片描述

其中 r为坐标原点到直线的距离, Θ为坐标原点到直线的垂线与x轴的夹角,这两个参数的含义如图7-4所示。
图像空间中极坐标表示直线示意图

根据霍夫变换原理,利用极坐标形式表示直线时,在图像空间中经过某一点的所有直线映射到参数空间中是一个正弦曲线。图像空间中直线上的两个点在参数空间中映射的两条正弦曲线相交于一点,图7-5中给出了用极坐标形式表示直线的霍夫变换的示意图。
极坐标表示直线的霍夫变换示意图

通过上述的变换过程,将图像中的直线检测转换成了在参数空间中寻找某个点 通过的正线曲线最多的问题。由于在参数空间内的曲线是连续的,而在实际情况中图像的像素是离散的,因此我们需要将参数空间的r轴和Θ轴进行离散化,用离散后的方格表示每一条正弦曲线。首先寻找符合条件的网格,之后寻找该网格对应的图像空间中所有的点,这些点共同组成了原图像中的直线。

总结上面所有的原理和步骤,霍夫变换算法检测图像中的直线主要分为4个步骤:
在这里插入图片描述

霍夫检测具有抗干扰能力强,对图像中直线的残缺部分、噪声以及其它共存的非直线结构不敏感,能容忍特征边界描述中的间隙,并且相对不受图像噪声影响等优点,但是霍夫变换的时间复杂度和空间复杂度都很高,并且检测精度受参数离散间隔制约。离散间隔较大时会降低检测精度,离散间隔较小时虽然能提高精度,但是会增加计算负担,导致计算时间边长。

OpenCV 4提供了两种用于检测图像中直线的相关函数,分别是标准霍夫变换和多尺度霍夫变换函数HoughLins()渐进概率式霍夫变换函数HoughLinesP()。首先将介绍标准霍夫变换函数HoughLins(),该函数的函数原型在代码清单7-1中给出。

void cv::HoughLines(InputArray  image,
                    OutputArray  lines,
                    double  rho,
                    double  theta,
                    int  threshold,
                    double  srn = 0,
                    double  stn = 0,
                    double  min_theta = 0,
                    double  max_theta = CV_PI 
                    )
  • image:待检测直线的原图像,必须是CV_8U的单通道二值图像。
  • lines:霍夫变换检测到的直线输出量,每一条直线都由两个参数表示,分别表示直线距离坐标原点的距离 和坐标原点到直线的垂线与x轴的夹角 。
  • rho:以像素为单位的距离分辨率,即距离 离散化时的单位长度。
  • theta:以弧度为单位的角度分辨率,即夹角 离散化时的单位角度。
  • threshold:累加器的阈值,即参数空间中离散化后每个方格被通过的累计次数大于该阈值时将被识别为直线,否则不被识别为直线。
  • srn:对于多尺度霍夫变换算法中,该参数表示距离分辨率的除数,粗略的累加器距离分辨率是第三个参数rho,精确的累加器分辨率是rho/srn。这个参数必须是非负数,默认参数为0。
  • stn:对于多尺度霍夫变换算法中,该参数表示角度分辨率的除数,粗略的累加器距离分辨率是第四个参数rho,精确的累加器分辨率是rho/stn。这个参数必须是非负数,默认参数为0。当这个参数与第六个参数srn同时为0时,此函数表示的是标准霍夫变换。
  • min_theta:检测直线的最小角度,默认参数为0。
  • max_theta:检测直线的最大角度,默认参数为CV_PI,是OpenCV
    4中的默认数值具体为3.1415926535897932384626433832795。

该函数用于寻找图像中的直线,并以极坐标的形式将图像中直线的极坐标参数输出。该函数的第一个参数为输入图像,必须是CV_8U的单通道二值图像,如果需要检测彩色图像或者灰度图像中是否存在直线,可以通过Canny()函数计算图像的边缘,并将边缘检测结果二值化后的图像作为输入图像赋值给该参数。函数的第二个参数是霍夫变换检测到的图像中直线极坐标描述的系数,是一个N×2的vector矩阵,每一行中的第一个元素是直线距离坐标原点的距离,第二个元素是该直线过坐标原点的垂线与x轴的夹角,这里需要注意的是图像中的坐标原点在图像的左上角。函数第三个和第四个参数是霍夫变换中对参数空间坐标轴进行离散化后单位长度,这两个参数的大小直接影响到检测图像中直线的精度,数值越小精度越高。第三个参数表示参数空间 轴的单位长度,单位为像素,该参数常设置为1;第四个参数表示参数空间 轴的单位长度,单位为弧度,该函数常设置为CV_PI/180。函数第五个参数是累加器的阈值,表示参数空间中某个方格是否被认定为直线的判定标准,这个数值越大,对应在原图像中构成直线的像素点越多,反之则越少。第六个和第七个参数起到选择标准霍夫变换和多尺度霍夫变换的作用,当两个参数全为0时,该函数使用标准霍夫变换算法,否则该函数使用多尺度霍夫变换算法,当函数使用多尺度霍夫变换算法时,这两个函数分别表示第三个参数单位距离长度的除数和第四个参数角度单位角度的除数。函数最后两个参数是检测直线的最小角度和最大角度,两个参数必须大于等于0小于等于CV_PI(3.1415926535897932384626433832795),并且最小角度的数值要小于最大角度的数值。

该函数只能输出直线的极坐标表示形式的参数,如果想在图像中绘制该直线需要进一步得到直线两端的坐标,通过line()函数在原图像中绘制直线,由于该函数只能判断图像中是否有直线,而不能判断直线的起始位置,因此使用line()函数绘制直线时常绘制尽可能长的直线。在代码清单7-2中给出了利用HoughLines()函数检测图像中直线的示例程序,程序中根据直线的参数计算出直线与经过坐标原点的垂线的交点的坐标,之后利用直线的线性关系计算出直线两端尽可能远的端点坐标,最后利用line()函数在原图像中绘制直线。程序首先利用Canny()函数对灰度图像进行边缘提取,然后对边缘经过进行二值化处理,之后检测图像中的直线,为了验证第五个参数累加器阈值对检测直线长短的影响,分别设置较小和较大的两个累加器,程序运行结果在图7-6、图7-7给出,通过结果可以看出累加器较小时较短的直线也可以被检测出来,累加器较大时只能检测出图像中较长的直线。

#include <opencv2/opencv.hpp>
#include <opencv2/highgui/highgui.hpp>
using namespace cv;
using namespace std;

void drawLIne(Mat &img,//要标记的图像
              vector<Vec2f>lines,//检测的直线数据
              double rows,//原图像的行数
              double cols,//原图像的列数
              Scalar scalar,//绘制直线的颜色
              int n//绘制直线的线宽
              )
{
    Point pt1,pt2;
    for(size_t i=0;i<lines.size();++i){
        float rho=lines[i][0];//直线距离坐标原点的距离
        float theta=lines[i][1];//直线过坐标原点垂线与x轴夹角
        double a=cos(theta);//夹角的余弦值
        double b=sin(theta);//夹角的正弦值
        double x0=a*rho,y0=b*rho;//直线与坐标原点垂线的交点
        double length=max(rows,cols);//图像高宽的最大值

        //计算直线上的一点
        pt1.x=cvRound(x0+length*(-b));
        pt1.y=cvRound(y0+length*(a));
        //计算直线上另一点
        pt2.x=cvRound(x0-length*(-b));
        pt2.y=cvRound(y0-length*(a));
        //两点绘制一条直线
        line(img,pt1,pt2,scalar,n);
    }
}

int main(){
    Mat img=imread("HoughLines.jpg");
    if(img.empty()){
        cout<<"请确认输入的图片路径是否正确"<<endl;
        return -1;
    }
    Mat edge;

    //检测边缘图像,病二值化
    Canny(img,edge,80,100,3, false);
    threshold(edge,edge,170,255,THRESH_BINARY);

    //用不同的累加器进行检测直线
    vector<Vec2f>lines1,lines2;
    HoughLines(edge,lines1,1,CV_PI/100,50,0,0);
    HoughLines(edge,lines2,1,CV_PI/100,150,0,0);

    //在原图像中绘制直线
    Mat img1,img2;
    img.copyTo(img1);
    img.copyTo(img2);
    drawLIne(img1,lines1,edge.rows,edge.cols,Scalar(255),2);
    drawLIne(img2,lines2,edge.rows,edge.cols,Scalar(255),2);

    //显示图像
    imshow("edge",edge);
    imshow("img",img);
    imshow("img1",img1);
    imshow("img2",img2);
    waitKey(0);
    return 0;
}

myHoughLines.cpp程序中原图像和边缘检测结果myHoughLines.cpp程序中累加器较小阈值和较大阈值的直线检测结果
使用标准霍夫变换和多尺度霍夫变换函数HoughLins()提取直线时无法准确知道图像中直线或者线段的长度,只能得到图像中是否存在符合要求的直线以及直线的极坐标解析式。如果需要准确的定位图像中线段的位置,HoughLins()函数便无法满足需求,但是OpenCV 4提供的渐进概率式霍夫变换函数HoughLinesP()可以得到图像中满足条件的直线或者线段两个端点的坐标,进而确定直线或者线段的位置,该函数的函数原型在代码清单7-3中给出。

void cv::HoughLinesP(InputArray  image,
                     OutputArray  lines,
                     double  rho,
                     double  theta,
                     int  threshold,
                     double  minLineLength = 0,
                     double  maxLineGap = 0 
                     )
  • image:待检测直线的原图像,必须是CV_8C的单通道二值图像。
  • lines:霍夫变换检测到的直线输出量,每一条直线都由4个参数进行描述,分别是直线两个端点的坐标
  • rho:以像素为单位的距离分辨率,即距离 离散化时的单位长度。
  • theta:以弧度为单位的角度分辨率,即夹角 离散化时的单位角度。
  • threshold:累加器的阈值,即参数空间中离散化后每个方格被通过的累计次数大于阈值时则被识别为直线,否则不被识别为直线。
  • minLineLength:直线的最小长度,当检测直线的长度小于该数值时将会被剔除。
  • maxLineGap:允许将同一行两个点连接起来的最大距离。

该函数用于寻找图像中满足条件的直线或者线段两个端点的坐标。该函数的第一个参数为输入图像,必须是CV_8U的单通道二值图像,如果需要检测彩色图像或者灰度图像中是否存在直线,可以通过Canny()函数计算图像的边缘,并将边缘检测结果二值化后的图像作为输入图像赋值给该参数。函数的第二个参数是图像中直线或者线段两个端点的坐标,是一个N×4的vector矩阵。Vec4i中前两个元素分别是直线或者线段一个端点的x坐标和y坐标,后两个元素分别是直线或者线段另一个端点的x坐标和y坐标。函数第三个和第四个参数含义与HoughLines()函数的参数含义相同,都是霍夫变换中对参数空间坐标轴进行离散化后的单位长度,这两个参数的大小直接影响到检测图像中直线的精度,数值越小精度越高。第三个参数表示参数空间 轴的单位长度,单位为像素,该参数常设置为1;第四个参数表示参数空间 轴的单位角度,单位为弧度,该函数常设置为CV_PI/180。函数第五个参数是累加器的阈值,表示参数空间中某个方格是否被认定为直线的判定标准,这个数值越大,对应在原图像中的直线越长,反之则越短。第六个参数是检测直线或者线段的长度,如果图像中直线的长度小于这个阈值,即使是直线也不会作为最终结果输出。函数最后一个参数是邻近两个点连接的最大距离,这个参数主要能够控制倾斜直线的检测长度,当提取较长的倾斜直线时该参数应该具有较大取值。

该函数的最大特点是能够直接给出图像中直线或者线段两个端点的像素坐标,因此可较精确的定位到图像中直线的位置。为了了解该函数的使用方式,在代码清单7-4中给出了利用HoughLinesP()函数提取图像直线的示例程序,程序中使用的原图像与代码清单7-2中相同,程序的输出结果在图7-8给出,程序结果说明HoughLinesP()函数确实可以实现图像中直线或者线段的定位任务,并且结果也说明函数最后一个参数较大时倾斜直线检测的完整度较高。

#include <opencv2/opencv.hpp>
#include <opencv2/highgui/highgui.hpp>
using namespace cv;
using namespace std;

int main(){
    Mat img=imread("HoughLines.jpg");
    if(img.empty()){
        cout<<"请确认输入的图片路径是否正确"<<endl;
        return -1;
    }
    Mat edge;

    //检测边缘图像,病二值化
    Canny(img,edge,80,100,3, false);
    threshold(edge,edge,170,255,THRESH_BINARY);

    //利用渐进概率式霍夫变换提取直线
    vector<Vec2f>linesP1,linesP2;
    HoughLinesP(edge,linesP1,1,CV_PI/100,150,30,10);
    HoughLinesP(edge,linesP2,1,CV_PI/100,150,30,30);

    //在原图像中绘制直线
    Mat img1,img2;
    img.copyTo(img1);
    for(size_t i=0;i<linesP1.size();++i){
        line(img1,Point(linesP1[i][0],linesP1[i][1]),
             Point(linesP1[i][2],linesP1[i][3]),Scalar(255),3);
    }
    
    img.copyTo(img2);
    for(size_t i=0;i<linesP1.size();++i){
        line(img2,Point(linesP2[i][0],linesP2[i][1]),
             Point(linesP2[i][2],linesP2[i][3]),Scalar(255),3);
    }

    //显示图像
    imshow("img1",img1);
    imshow("img2",img2);
    waitKey(0);
    return 0;
}

在这里插入图片描述
前面两个函数都是检测图像中是否存在直线,但是在实际工程或者任务需求中我们可能得到的是图像中一些点的坐标而不是一副完整的图像,因此OpenCV 4中提供了能够在含有坐标的众多点中寻找是否存在直线的**HoughLinesPointSet()**函数,该函数的函数原型在代码清单7-5中给出。

void cv::HoughLinesPointSet(InputArray  _point,
                            OutputArray  _lines,
                            int  lines_max,
                            int  threshold,
                            double  min_rho,
                            double  max_rho,
                            double  rho_step,
                            double  min_theta,
                            double  max_theta,
                            double  theta_step 
                            )
  • _point:输入点的集合,必须是平面内的2D坐标,数据类型必须是CV_32FC2或CV_32SC2。
  • _lines:在输入点集合中可能存在的直线,每一条直线都具有三个参数,分别是权重、直线距离坐标原点的距离 和坐标原点到直线的垂线与x轴的夹角 。
  • lines_max:检测直线的最大数目。
  • threshold:累加器的阈值,即参数空间中离散化后每个方格被通过的累计次数大于阈值时则被识别为直线,否则不被识别为直线。
  • min_rho:检测直线长度的最小距离,以像素为单位。
  • max_rho:检测直线长度的最大距离,以像素为单位。
  • rho_step::以像素为单位的距离分辨率,即距离 离散化时的单位长度。
  • min_theta:检测直线的最小角度值,以弧度为单位。
  • max_theta:检测直线的最大角度值,以弧度为单位。
  • theta_step:以弧度为单位的角度分辨率,即夹角 离散化时的单位角度。

该函数用于在含有坐标的2D点的集合中寻找直线,函数检测直线使用的方法是标准霍夫变换法。函数第一个参数是2D点集合中每个点的坐标,由于坐标必须是CV_32F或者CV_32S类型,因此可以将点集定义成vector< Point2f>或者vector< Point2f>类型。函数的第二个参数是检测到的输入点集合中可能存在的直线,是一个1×N的矩阵,数据类型为CV_64FC3,其中第1个数据表示该直线的权重,权重越大表示是直线的可靠性越高,第2个数据和第3个数据分别表示直线距离坐标原点的距离 和坐标原点到直线的垂线与x轴的夹角 ,矩阵中数据的顺序是按照权重由大到小依次存放。函数第三个参数是检测直线的数目,如果数目过大,检测到的直线可能存在权重较小的情况。函数第四个参数是累加器的阈值,表示参数空间中某个方格是否被认定为直线的判定标准,这个数值越大,表示检测的直线需要通过的点的数目越多。函数第五个、第六个参数是检测直线长度的取值范围,单位为像素。函数第七个参数是霍夫变换算法中离散化时距离分辨率的大小,单位为像素。函数第八个、第九个参数是检测直线经过坐标原点的垂线与x轴夹角的范围,单位为弧度。函数第七个参数是霍夫变换算法中离散化时角度分辨率的大小,单位为弧度。

为了了解该函数的使用方法,在代码清单7-6中给出了利用该函数检测2D点集合中直线的示例程序。程序中首先生成2D点集,之后利用HoughLinesPointSet()函数检测其中可能存在的直线,并将检测的直线权重和距离坐标原点的距离 和坐标原点到直线的垂线与x轴的夹角 输出,程序的输出结果在图7-9给出。

#include <opencv2/opencv.hpp>
#include <iostream>
#include <vector>

using namespace cv;
using namespace std;

int main()
{
    system("color F0");  //更改输出界面颜色
    Mat lines;  //存放检测直线结果的矩阵
    vector<Vec3d> line3d;  //换一种结果存放形式
    vector<Point2f> point;  //待检测是否存在直线的所有点
    const static float Points[20][2] = {
            { 0.0f,   369.0f },{ 10.0f,  364.0f },{ 20.0f,  358.0f },{ 30.0f,  352.0f },
            { 40.0f,  346.0f },{ 50.0f,  341.0f },{ 60.0f,  335.0f },{ 70.0f,  329.0f },
            { 80.0f,  323.0f },{ 90.0f,  318.0f },{ 100.0f, 312.0f },{ 110.0f, 306.0f },
            { 120.0f, 300.0f },{ 130.0f, 295.0f },{ 140.0f, 289.0f },{ 150.0f, 284.0f },
            { 160.0f, 277.0f },{ 170.0f, 271.0f },{ 180.0f, 266.0f },{ 190.0f, 260.0f }
    };
    //将所有点存放在vector中,用于输入函数中
    for (int i = 0; i < 20; i++)
    {
        point.push_back(Point2f(Points[i][0], Points[i][1]));
    }
    //参数设置
    double rhoMin = 0.0f;  //最小长度
    double rhoMax = 360.0f;  //最大长度
    double rhoStep = 1;  //离散化单位距离长度
    double thetaMin = 0.0f;  //最小角度
    double thetaMax = CV_PI / 2.0f;  //最大角度
    double thetaStep = CV_PI / 180.0f;  离散化单位角度弧度
    HoughLinesPointSet(point, lines, 20, 1, rhoMin, rhoMax, rhoStep,
                       thetaMin, thetaMax, thetaStep);
    lines.copyTo(line3d);

    //输出结果
    for (int i = 0; i < line3d.size(); i++)
    {
        cout << "votes:" << (int)line3d.at(i).val[0] << ", "
             << "rho:" << line3d.at(i).val[1] << ", "
             << "theta:" << line3d.at(i).val[2] << endl;
    }
    return 0;
}

在这里插入图片描述

C++版本OpenCv教程(四十三)直线拟合

Excerpt

前面介绍的函数都是寻找图像或者点集中是否存在直线,而有时我们明确已知获取到的数据在一条直线上,此时需要将所有数据拟合出一条直线,但是由于噪声的存在,这条直线可能不会通过大多数的数据,因此需要保证所有的数据点距离直线的距离最小,如图7-10所示。相比于直线检测,直线拟合的最大特点是将所有数据只拟合出一条直线。OpenCV 4中提供了利用最小二乘M-estimator方法拟合直线的**fitLine()**函数,该函数的函数原型在代码清单7-7中给出。void cv::fitLine(InputArray


前面介绍的函数都是寻找图像或者点集中是否存在直线,而有时我们明确已知获取到的数据在一条直线上,此时需要将所有数据拟合出一条直线,但是由于噪声的存在,这条直线可能不会通过大多数的数据,因此需要保证所有的数据点距离直线的距离最小,如图7-10所示。相比于直线检测,直线拟合的最大特点是将所有数据只拟合出一条直线。
在这里插入图片描述

OpenCV 4中提供了利用最小二乘M-estimator方法拟合直线的**fitLine()**函数,该函数的函数原型在代码清单7-7中给出。

void cv::fitLine(InputArray  points,
             OutputArray  line,
             int  distType,
             double  param,
             double  reps,
             double  aeps 
             )
  • points:输入待拟合直线的2D或者3D点集。
  • line:输出描述直线的参数,2D点集描述参数为Vec4f类型,3D点集描述参数为Vec6f类型。
  • distType:M-estimator算法使用的距离类型标志,可以选择的距离类型在表7-1中给出。
  • param:某些类型距离的数值参数(C)。如果数值为0,则自动选择最佳值。
  • reps:坐标原点与直线之间的距离精度,数值0表示选择自适应参数,一般常选择0.01。
  • aeps:直线角度精度,数值0表示选择自适应参数,一般常选择0.01。

该函数利用最小二乘法拟合出距离所有点距离最小的直线,直线的描述形式可以转化成点斜式。函数第一个参数是待拟合直线的2D或者3D点集,可以存放在vector<>或者Mat类型的变量中赋值给参数。函数第二个参数是拟合直线的描述参数,如果是2D点集,输出量为Vec4f类型的(vx vy x0 y0),其中**(vx vy)是与直线共线的归一化向量**,(x0 y0)是拟合直线上的随意一点,根据这四个量可以计算得到2维平面直线的点斜式解析式,表示形式如式(7.6)所示。
在这里插入图片描述

如果输入参数是3D点集,输出量为Vec6f类型的(vx vy vz x0 y0 z0),其中(vx vy vz)是与直线共线的归一化向量,(x0 y0 z0)是拟合直线上的随意一点。函数第三个参数是M-estimator算法使用的距离类型标志,可以选择的距离类型在表7-1中给出。函数第四个参数是某些距离类型中的数值参数C,如果数值0表示选择最佳值。函数第五个参数表示坐标原点与拟合直线之间的距离精度,数值0表示选择自适应参数;函数第六个参数表示拟合直线的角度精度,数值0表示选择自适应参数。第五个参数和第六个参数一般取值0.01。
在这里插入图片描述
为了了解该函数的使用方法,在代码清单7-8中给出了利用fitLine()函数拟合直线的示例程序。程序中给出了 直线上的坐标点,为了模拟采集数据过程中产生的噪声,在部分坐标中添加了噪声。程序拟合出的直线很好的逼近了真实的直线,程序运行的结果在图7-11给出。

#include <opencv2/opencv.hpp>
#include <iostream>
#include <vector>

using namespace cv;
using namespace std;

int main()
{
    system("color F0");  //更改输出界面颜色
    Vec4f lines;//存放拟合后的直线
    vector<Point2f>point;//待检测是否存在直线的所有点
    const static float Points[20][2]={
            {0.0f,0.0f},{10.0f,11.0f},{21.0f,20.0f},{30.0f,30.0f},
            {40.0f,42.0f},{50.0f,50.0f},{60.0f,60.0f},{70.0f,70.0f},
            {80.0f,80.0f},{90.0f,92.0f},{100.0f,100.0f},{110.0f,110.0f},
            {120.f,120.0f},{136.0f,130.0f},{138.0f,140.0f},{150.0f,150.0f},
            {160.0f,163.0f},{175.0f,170.0f},{181.0f,180.0f},{200.0f,190.0f}
    };
    //将所有点存放在vector中,用于输入函数中
    for(int i=0;i<20;++i){
        point.push_back(Point2f(Points[i][0],Points[i][1]));
    }
    //参数设置
    double param=0;//距离模型中的数值参数C
    double reps=0.01;//坐标原点与直线之间的距离
    double aeps=0.01;//角度精度
    fitLine(point,lines,DIST_L1,0,0.01,0.01);
    double k=lines[1]/lines[0];//直线斜率
    cout<<"直线斜率: "<<k<<endl;
    cout<<"直线上一点坐标x: "<<lines[2]<<",y: "<<lines[3]<<endl;
    cout<<"直线解析式: y="<<k<<"(x-"<<lines[2]<<")+"<<lines[3]<<endl;
    return 0;
}

运行结果:

直线斜率: 0.999955
直线上一点坐标x: 76.7907,y: 76.7907
直线解析式: y=0.999955(x-76.7907)+76.7907

C++版本OpenCv教程(四十四)轮廓发现与绘制

Excerpt

图像的轮廓不仅能够提供物体的边缘,而且还能提供物体边缘之间的层次关系以及拓扑关系。我们可以将图像轮廓发现简单理解为带有结构关系的边缘检测,这种结构关系可以表明图像中连通域或者某些区域之间的关系。图7-14为一个具有4个不连通边缘的二值化图像,由外到内依次为0号、1号、2号、3号条边缘。为了描述不同轮廓之间的结构关系,定义由外到内的轮廓级别越来越低,也就是高一层级的轮廓包围着较低层级的轮廓,被同一个轮廓包围的多个不互相包含的轮廓是同一层级轮廓。例如在图7-14中,0号轮廓层级比1号和第2号轮廓的层及都要高,2


图像的轮廓不仅能够提供物体的边缘,而且还能提供物体边缘之间的层次关系以及拓扑关系。我们可以将图像轮廓发现简单理解为带有结构关系的边缘检测,这种结构关系可以表明图像中连通域或者某些区域之间的关系。图7-14为一个具有4个不连通边缘的二值化图像,由外到内依次为0号、1号、2号、3号条边缘。为了描述不同轮廓之间的结构关系,定义由外到内的轮廓级别越来越低,也就是高一层级的轮廓包围着较低层级的轮廓,被同一个轮廓包围的多个不互相包含的轮廓是同一层级轮廓。例如在图7-14中,0号轮廓层级比1号和第2号轮廓的层及都要高,2号轮廓包围着3号轮廓,因此2号轮廓的层级要高于3号轮廓。
图7-14 图像轮廓序号
为了更够更好的表明各个轮廓之间的层级关系,常用4个参数来描述不同层级之间的结构关系,这4个参数分别是:同层下一个轮廓索引同层上一个轮廓索引下一层第一个子轮廓索引上层父轮廓索引。根据这种描述方式,图7-14中0号轮廓没有同级轮廓和父轮廓需要用-1表示,其第一个子轮廓为1号轮廓,因此可以用描述该轮廓的结构。1号轮廓的下一个同级轮廓为2号轮廓但是没有上一个同级轮廓用-1表示,父轮廓为0号轮廓,第一个子轮廓为3号轮廓,因此可以用描述该轮廓结构。2号轮廓和3号轮廓同样可以用这样的方式构建结构关系描述子。图7-14中不同轮廓之间的层级关系可以用图7-15表示。
图7-15 图7-14中不同轮廓之间的结构关系

OpenCV 4提供了可以在二值图像中检测图像中所有轮廓并生成不同轮廓结构关系描述子的findContours()函数,该函数的函数原型在代码清单7-11中给出。

void cv::findContours(InputArray  image,
                      OutputArrayOfArrays  contours,
                      OutputArray  hierarchy,
                  int  mode,
                  int  method,
                      Point  offset = Point() 
                      )
  • image:输入图像,数据类型为CV_8U的单通道灰度图像或者二值化图像。
  • contours:检测到的轮廓,每个轮廓中存放着像素的坐标。
  • hierarchy:轮廓结构关系描述向量。
  • mode:轮廓检测模式标志,可以选择的参数在表7-2给出。
  • method:轮廓逼近方法标志,可以选择的参数在表7-3给出。
  • offset:每个轮廓点移动的可选偏移量。这个参数主要用在从ROI图像中找出轮廓并基于整个图像分析轮廓的场景中。

该函数主要用于检测图像中的轮廓信息,并输出各个轮廓之间的结构信息。
函数的第一个参数是待检测轮廓的输入图像,从理论上讲检测图像轮廓需要是二值化图像,但是该函数会对非0像素视为1,0像素保持不变,因此该参数能够接受非二值化的灰度图像。由于该函数默认二值化操作不能保持图像主要的内容,因此常需要对图像进行预处理,利用threshold()函数或者adaptiveThreshold()函数根据需求进行二值化。
第二个参数用于存放检测到的轮廓,数据类型为vector,每个轮廓中存放着属于该轮廓的像素坐标。
函数的第三个参数用于存放各个轮廓之间的结构信息,数据类型为vector,数据的尺寸与检测到的轮廓数目相同,每个轮廓结构信息中第1个数据表示同层下一个轮廓索引、第2个数据表示同层上一个轮廓索引、第3个数据表示下一层第一个子轮廓索引、第4个数据表示上层父轮廓索引。
函数第四个参数是轮廓检测模式的标志,可以选择的参数及含义在表7-2给出。
函数第五个参数是选择轮廓逼近方法的标志,可以选择的参数及含义在表7-3给出。
函数最后一个参数是每个轮廓点移动的可选偏移量。这个函数主要用在从ROI图像中找出轮廓并基于整个图像分析轮廓的场景中。
在这里插入图片描述
有时我们只需要检测图像的轮廓,并不关心轮廓之间的结构关系信息,此时轮廓之间的结构关系变量会造成内存资源的浪费,因此OpenCV 4提供了findContours()函数的另一种函数原型,可以不输出轮廓之间的结构关系信息,该种函数原型在代码清单7-12中给出。

void cv::findContours(InputArray  image,
                  OutputArrayOfArrays  contours,
                  int  mode,
                  int  method,
                  Point  offset = Point() 
                  )
  • image:输入图像,数据类型为CV_8U的单通道灰度图像或者二值化图像。
  • contours:检测到的轮廓,每个轮廓中存放着像素的坐标。
  • mode:轮廓检测模式标志,可以选择的参数在表7-2给出。
  • method:轮廓逼近方法标志,可以选择的参数在表7-3给出。
  • offset:每个轮廓点移动的可选偏移量。这个函数主要用在从ROI图像中找出轮廓并基于整个图像分析轮廓的场景中。

提取了图像轮廓后,为了能够直观的查看轮廓检测的结果,OpenCV 4提供了显示轮廓的drawContours()函数,该函数的函数原型在代码清单7-13中给出。

void cv::drawContours(InputOutputArray  image,
                      InputArrayOfArrays  contours,
                      int   contourIdx,
                      const Scalar &  color,
                      int  thickness = 1,
                      int  lineType = LINE_8,
                      InputArray  hierarchy = noArray(),
                      int  maxLevel = INT_MAX,
                      Point  offset = Point() 
                      )
  • image:绘制轮廓的目标图像。
  • contours:所有将要绘制的轮廓
  • contourIdx:要绘制的轮廓的数目,如果是负数,则绘制所有的轮廓。
  • color:绘制轮廓的颜色。
  • thickness:绘制轮廓的线条粗细,如果参数为负数,则绘制轮廓的内部,默认参数值为1.
  • lineType:边界线连接的类型,可以选择参数在表7-4给出,默认参数值为LINE_8。
  • hierarchy:可选的结构关系信息,默认值为noArray()。
  • maxLevel:表示用于绘制轮廓的最大等级,默认值为INT_MAX。
  • offset:可选的轮廓偏移参数,按指定的移动距离绘制所有的轮廓。

该函数用于绘制findContours()函数检测到的图像轮廓。
函数的第一个参数为绘制轮廓的图像,根据需求该参数可以是单通道的灰度图像或者三通道的彩色图像。
第二个参数是所有将要绘制的轮廓,数据类型为vector。
第三个参数是要绘制的轮廓数目,该参数的数值与第二个参数相对应,应小于所有轮廓的数目,如果该参数值为负数,则绘制所有的轮廓。
第四个参数是绘制轮廓的颜色,对于单通道的灰度图像用Scalar(x)赋值,对于三通道的彩色图像用Scalar(x,y,z)赋值。
第五个参数是边界线的连接类型,可以选择的参数在表7-4给出,默认参数值为LINE_8。
第六个参数是可选的结构关系信息,默认值为noArray()。
第七个参数表示绘制轮廓的最大等级,参数值如果为0,则仅绘制指定的轮廓;如果为1,则该函数绘制轮廓和所有嵌套轮廓;如果为2,则该函数绘制轮廓以及所有嵌套轮廓和所有嵌套到嵌套轮廓的轮廓,以此类推,默认值为INT_MAX。函数最后一个参数是可选的轮廓偏移参数,按指定的移动距离绘制所有的轮廓。
在这里插入图片描述

为了了解图像轮廓检测和绘制相关函数的使用,在代码清单7-14中给出了检测图像中的轮廓和绘制轮廓的示例程序。程序中不仅绘制了物体的轮廓,还输出了图像所有轮廓的结构关系信息。程序绘制的轮廓信息在图7-16给出,所有轮廓结构关系信息在图7-17给出,同时根据结果绘制了直观的结构关系。

#include <opencv2/opencv.hpp>
#include <iostream>
#include <vector>

using namespace cv;
using namespace std;

int main()
{
    system("color F0");  //更改输出界面颜色
    Mat img=imread("coins.jpeg");
    if(img.empty()){
        cout<<"请确认输入烦人图片路径是否正确"<<endl;
        return -1;
    }
    imshow("原图",img);
    Mat gray,binary;
    cvtColor(img,gray,COLOR_BGR2GRAY);
    GaussianBlur(gray,gray,Size(9,9),2,2);//平滑滤波
    threshold(gray,binary,170,255,THRESH_BINARY|THRESH_OTSU);//自适应二值化

    //轮廓发现与绘制
    vector<vector<Point>>contours;//轮廓
    vector<Vec4i>hierachy;//存放轮廓结构变量
    findContours(binary,contours,hierachy,RETR_TREE,CHAIN_APPROX_SIMPLE);
    //绘制轮廓
    for(int i=0;i<contours.size();++i){
        drawContours(img,contours,i,Scalar(0,0,255),2,8);
    }
    //输出轮廓结构描述子
    for(int i=0;i<hierachy.size();++i){
        cout<<hierachy[i]<<endl;
    }
    //显示结果
    imshow("轮廓检测结果",img);
    waitKey(0);
    return 0;
}

运行结果:
在这里插入图片描述

[1, -1, -1, -1]
[2, 0, -1, -1]
[3, 1, -1, -1]
[4, 2, -1, -1]
[5, 3, -1, -1]
[6, 4, -1, -1]
[7, 5, -1, -1]
[8, 6, -1, -1]
[9, 7, -1, -1]
[10, 8, -1, -1]
[11, 9, -1, -1]
[12, 10, -1, -1]
[13, 11, -1, -1]
[14, 12, -1, -1]
[15, 13, -1, -1]
[16, 14, -1, -1]
[17, 15, -1, -1]
[-1, 16, -1, -1]

C++版本OpenCv教程(四十五)计算轮廓面积与长度

Excerpt

轮廓面积轮廓面积是轮廓重要的统计特性之一,通过轮廓面积的大小可以进一步分析每个轮廓隐含的信息,例如通过轮廓面积区分物体大小识别不同的物体。轮廓面积是指每个轮廓中所有的像素点围成区域的面积,单位为像素。OpenCV 4提供了检测轮廓面积的**contourArea()**函数,该函数的函数原型在代码清单7-15中给出。double cv::contourArea(InputArray contour,bool oriented = false )contour:轮廓的像素点oriented:区


轮廓面积

轮廓面积是轮廓重要的统计特性之一,通过轮廓面积的大小可以进一步分析每个轮廓隐含的信息,例如通过轮廓面积区分物体大小识别不同的物体。轮廓面积是指每个轮廓中所有的像素点围成区域的面积,单位为像素。OpenCV 4提供了检测轮廓面积的**contourArea()**函数,该函数的函数原型在代码清单7-15中给出。

double cv::contourArea(InputArray  contour,bool  oriented = false )
  • contour:轮廓的像素点
  • oriented:区域面积是否具有方向的标志,true表示面积具有方向性,false表示不具有方向性,默认值为不具有方向性的false。

该函数用于统计轮廓像素点围成区域的面积,函数的返回值是统计轮廓面积的结果,数据类型为double。函数第一个参数表示轮廓的像素点,数据类型为vector或者Mat,相邻的两个像素点之间逐一相连构成的多边形区域即为轮廓面积的统计区域。连续的三个像素点之间的连线有可能在同一条直线上,因此为了减少输入轮廓像素点的数目,可以只输入轮廓的顶点像素点,例如一个三角形的轮廓,轮廓中可能具有每一条边上的所有像素点,但是在统计面积时可以只输入三角形的三个顶点。函数第二个参数是区域面积是否具有方向的标志,参数为true时表示统计的面积具有方向性,轮廓顶点顺时针给出和逆时针给出时统计的面积互为相反数;参数为false时表示统计的面积不具有方向性,输出轮廓面积的绝对值。

为了了解该函数的使用方法,在代码清单7-16中给出了统计轮廓面积的示例程序。程序中给出一个直角三角形轮廓的三个顶点以及斜边的中点,统计出的轮廓面积与三角形的面积相等,同时统计图7-16中每个轮廓的面积,程序的运行结果在图7-18给出。

#include <opencv2\opencv.hpp>
#include <iostream>
#include <vector>

using namespace cv;
using namespace std;

int main()
{
system("color F0");  //更改输出界面颜色
//用四个点表示三角形轮廓
vector<Point> contour;
contour.push_back(Point2f(0, 0));
contour.push_back(Point2f(10, 0));
contour.push_back(Point2f(10, 10));
contour.push_back(Point2f(5, 5));
double area = contourArea(contour);
cout << "area =" << area << endl;

Mat img = imread("coins.jpg");
if (img.empty())
{
cout << "请确认图像文件名称是否正确" << endl;
return -1;
}
imshow("原图", img);
Mat gray, binary;
cvtColor(img, gray, COLOR_BGR2GRAY);  //转化成灰度图
GaussianBlur(gray, gray, Size(9, 9), 2, 2);  //平滑滤波
threshold(gray, binary, 170, 255, THRESH_BINARY | THRESH_OTSU);  //自适应二值化
// 轮廓检测
vector<vector<Point>> contours;  //轮廓
vector<Vec4i> hierarchy;  //存放轮廓结构变量
findContours(binary, contours, hierarchy,RETR_TREE,CHAIN_APPROX_SIMPLE, Point());

//输出轮廓面积
for (int t = 0; t < contours.size(); t++)
{
double area1 = contourArea(contours[t]);
  cout << "第" << t << "轮廓面积=" << area1 << endl;
}
return 0;
}

在这里插入图片描述

轮廓长度

轮廓的周长也是轮廓重要的统计特性之一,轮廓的周长虽然无法直接反应轮廓区域的大小和形状,但是可以与轮廓面积结合得到关于轮廓区域的更多信息,例如某个区域的面积与周长平方的比值为十六分之一时该区域为正方形。OpenCV 4提供了用于检测轮廓周长或者曲线长度的**arcLength()**函数,该函数的函数原型在代码清单7-17中给出。

double cv::arcLength(InputArray  curve,bool  closed )
  • curve:轮廓或者曲线的2D像素点。
  • closed:轮廓或者曲线是否闭合标志,true表示闭合。

该函数能够统计轮廓或者曲线的长度,函数返回值为统计长度,单位为像素,数据类型为double。函数的第一个参数是轮廓或者曲线的2D像素点,数据类型为vector或者Mat。函数的第二个参数是轮廓或者曲线是否闭合的标志,true表示闭合。

函数统计的长度是轮廓或者曲线相邻两个像素点之间连线的距离,例如计算三角形三个顶点A、B和C构成的轮廓长度时,并且函数第二个参数为true时,统计的长度是三角形三个边AB、BC和CA的长度之和;当参数为false时,统计的长度是由A到C三个点之间依次连线的距离长度之和,即AB和BC的长度之和。

为了了解该函数的使用方法,在代码清单7-18中给出统计轮廓长度的示例程序。程序中给出一个直角三角形轮廓的三个顶点以及斜边的中点,分别利用arcLength()函数统计轮廓闭合情况下的尺度和非闭合情况下的长度,同时统计图7-16中每个轮廓的长度,程序的运行结果在图7-19给出。

#include <opencv2\opencv.hpp>
#include <iostream>
#include <vector>

using namespace cv;
using namespace std;

int main()
{
system("color F0");  //更改输出界面颜色
//用四个点表示三角形轮廓
vector<Point> contour;
contour.push_back(Point2f(0, 0));
contour.push_back(Point2f(10, 0));
contour.push_back(Point2f(10, 10));
contour.push_back(Point2f(5, 5));

double length0 = arcLength(contour, true);
double length1 = arcLength(contour, false);
cout << "length0 =" << length0 << endl;
cout << "length1 =" << length1 << endl;

Mat img = imread("coins.jpg");
if (img.empty())
{
cout << "请确认图像文件名称是否正确" << endl;
  return -1;
}
imshow("原图", img);
Mat gray, binary;
cvtColor(img, gray, COLOR_BGR2GRAY);  //转化成灰度图
GaussianBlur(gray, gray, Size(9, 9), 2, 2);  //平滑滤波
threshold(gray, binary, 170, 255, THRESH_BINARY | THRESH_OTSU);  //自适应二值化

// 轮廓检测
vector<vector<Point>> contours;  //轮廓
vector<Vec4i> hierarchy;  //存放轮廓结构变量
findContours(binary, contours, hierarchy,RETR_TREE,CHAIN_APPROX_SIMPLE, Point());

//输出轮廓长度
for (int t = 0; t < contours.size(); t++)
{
double length2 = arcLength(contours[t], true);
cout << "第" << t << "个轮廓长度=" << length2 << endl;
}
return 0;
}

在这里插入图片描述

C++版本OpenCv教程(四十六)轮廓外接多边形

Excerpt

由于噪声和光照的影响,物体的轮廓会出现不规则的形状,根据不规则的轮廓形状不利于对图像内容进行分析,此时需要将物体的轮廓拟合成规则的几何形状,根据需求可以将图像轮廓拟合成矩形、多边形等。本小节将介绍OpenCV 4中提供的轮廓外接多边形函数,实现图像中轮廓的形状拟合。矩形是常见的几何形状,矩形的处理和分析方法也较为简单,OpenCV 4提供了两个函数求取轮廓外接矩形,分别是**求取轮廓最大外接矩形的boundingRect()函数和求取轮廓最小外接矩形的minAreaRect()**函数。寻找轮廓外接最大


由于噪声和光照的影响,物体的轮廓会出现不规则的形状,根据不规则的轮廓形状不利于对图像内容进行分析,此时需要将物体的轮廓拟合成规则的几何形状,根据需求可以将图像轮廓拟合成矩形、多边形等。本小节将介绍OpenCV 4中提供的轮廓外接多边形函数,实现图像中轮廓的形状拟合。

矩形是常见的几何形状,矩形的处理和分析方法也较为简单,OpenCV 4提供了两个函数求取轮廓外接矩形,分别是**求取轮廓最大外接矩形的boundingRect()函数和求取轮廓最小外接矩形的minAreaRect()**函数。

寻找轮廓外接最大矩形就是寻找轮廓X方向和Y方向两端的像素,该矩形长和宽分别与图像的两个轴平行。boundingRect()函数可以实现这个功能,该函数的函数原型在代码清单7-19中给出。

Rect cv::boundingRect(InputArray  array)
  • array:输入的灰度图像或者2D点集,数据类型为vector或者Mat。

该函数可以求取包含输入图像中物体轮廓或者2D点集的最大外接矩形,函数只有一个参数,可以是灰度图像或者2D点集,灰度图像的参数类型为Mat,2D点集的参数类型为vector或者Mat。该函数的返回值是一个Rect类型的变量,该变量可以直接用rectangle()函数绘制矩形。返回值共有四个参数,前两个参数是最大外接矩形左上角第一个像素的坐标,后两个参数分别表示最大外接矩形的宽和高。

最小外接矩形的四个边都与轮廓相交,该矩形的旋转角度与轮廓的形状有关,多数情况下矩形的四个边不与图像的两个轴平行。minAreaRect()函数可以求取轮廓的最小外接矩形,该函数的函数原型在代码清单7-20中给出。

RotatedRect cv::minAreaRect(InputArray  points)
  • points:输入的2D点集合

该函数可以根据输入的2D点集合计算最小的外接矩形,函数的返回值是RotatedRect类型的变量,含有矩形的中心位置、矩形的宽和高和矩形旋转的角度RotatedRect类具有两个重要的方法和属性,可以输出矩形的四个顶点和中心坐标。输出四个顶点坐标的方法是points(),假设RotatedRect类的变量为rrect,可以通过rrect.points(points)命令进行读取,其中坐标存放的变量是Point2f类型的数组。输出矩形中心坐标的属性是center,假设RotatedRect类的变量为rrect,可以通过opt=rrect.center命令进行读取,其中坐标存放的变量是Point2f类型的变量。

为了了解两个外接矩形函数的使用方法,代码清单7-21中给出了提取轮廓外接矩形的示例程序。程序中首先利用Canny算法提取图像边缘,之后通过膨胀算法将邻近的边缘连接成一个连通域,然后提取图像的轮廓,并提取每一个轮廓的最大外接矩形和最小外接矩形,最后在图像中绘制出矩形轮廓,程序的运行结果在图7-20给出。

#include <opencv2/opencv.hpp>
#include <iostream>
#include <vector>

using namespace cv;
using namespace std;

int main()
{
Mat img = imread("stuff.jpg");
if (img.empty())
{
cout << "请确认图像文件名称是否正确" << endl;
return -1;
}
Mat img1, img2;
img.copyTo(img1);  //深拷贝用来绘制最大外接矩形
img.copyTo(img2);  //深拷贝迎来绘制最小外接矩形
imshow("img", img);

// 去噪声与二值化
Mat canny;
Canny(img, canny, 80, 160, 3, false);
imshow("", canny);

//膨胀运算,将细小缝隙填补上
Mat kernel = getStructuringElement(0, Size(3, 3));
dilate(canny, canny, kernel);

// 轮廓发现与绘制
vector<vector<Point>> contours;
vector<Vec4i> hierarchy;
findContours(canny, contours, hierarchy, 0, 2, Point());

//寻找轮廓的外接矩形
for (int n = 0; n < contours.size(); n++)
{
// 最大外接矩形
Rect rect = boundingRect(contours[n]);
rectangle(img1, rect, Scalar(0, 0, 255), 2, 8, 0);

// 最小外接矩形
RotatedRect rrect = minAreaRect(contours[n]);
Point2f points[4];
rrect.points(points);  //读取最小外接矩形的四个顶点
Point2f cpt = rrect.center;  //最小外接矩形的中心

// 绘制旋转矩形与中心位置
for (int i = 0; i < 4; i++)
{
if (i == 3)
{
line(img2, points[i], points[0], Scalar(0, 255, 0), 2, 8, 0);
break;
}
line(img2, points[i], points[i + 1], Scalar(0, 255, 0), 2, 8, 0);
}
//绘制矩形的中心
circle(img, cpt, 2, Scalar(255, 0, 0), 2, 8, 0);
}

//输出绘制外接矩形的结果
imshow("max", img1);
imshow("min", img2);
waitKey(0);
return 0;
}

在这里插入图片描述

有时候用矩形逼近轮廓会造成较大的误差,例如图7-20中对于圆形轮廓的逼近矩形围成的面积比真实轮廓面积大,如果寻找逼近轮廓的多边形,那么多边形围成的面积会更加接近真实的圆形轮廓面积。OpenCV 4提供了**approxPolyDP()**函数用于寻找逼近轮廓的多边形,该函数的函数原型在代码清单7-22中给出。

void cv::approxPolyDP(InputArray  curve,
                      OutputArray  approxCurve,
                      double  epsilon,
                      bool  closed 
                      )
  • curve:输入轮廓像素点。
  • approxCurve:多边形逼近结果,以多边形顶点坐标的形式给出。
  • epsilon:逼近的精度,即原始曲线和逼近曲线之间的最大距离。
  • closed:逼近曲线是否为封闭曲线的标志, true表示曲线封闭,即最后一个顶点与第一个顶点相连。

该函数根据输入的轮廓得到最佳的逼近多边形。
函数的第一个参数是输入的轮廓2D像素点,数据类型是vector或者Mat。
第二个参数是多边形的逼近结果,以多边形顶点坐标的形式输出,是CV_32SC2类型的N×1的Mat类矩阵,可以通过输出结果的顶点数目初步判断轮廓的几何形状。
第三个参数是多边形逼近时的精度,即原始曲线和逼近曲线之间的最大距离。
第四个参数是逼近曲线是否为封闭曲线的标志, true表示曲线封闭,即最后一个顶点与第一个顶点相连。

为了了解该函数用法,在代码清单7-23中给出了对多个轮廓进行多边形逼近的示例程序。程序中首先提取了图像的边缘,然后对边缘进行腐蚀运算将靠近的边缘变成一个连通域,之后对边缘结果进行轮廓检测,并对每个轮廓进行多边形逼近,将逼近结果绘制在原图像中,并通过判断逼近多边形的顶点数目识别轮廓的形状,程序运行结果在图7-21给出。

#include <opencv2/opencv.hpp>
#include <iostream>
#include <vector>

using namespace cv;
using namespace std;

//绘制轮廓函数
void drawapp(Mat result, Mat img2)
{
for (int i = 0; i < result.rows; i++)
{
//最后一个坐标点与第一个坐标点连接
if (i == result.rows - 1)
{
Vec2i point1 = result.at<Vec2i>(i);
Vec2i point2 = result.at<Vec2i>(0);
line(img2, point1, point2, Scalar(0, 0, 255), 2, 8, 0);
break;
}
Vec2i point1 = result.at<Vec2i>(i);
Vec2i point2 = result.at<Vec2i>(i + 1);
line(img2, point1, point2, Scalar(0, 0, 255), 2, 8, 0);
}
}

int main(int argc, const char *argv[])
{
Mat img = imread("approx.png");
if (img.empty())
{
cout << "请确认图像文件名称是否正确" << endl;
return -1;
}
// 边缘检测
Mat canny;
Canny(img, canny, 80, 160, 3, false);
//膨胀运算
Mat kernel = getStructuringElement(0, Size(3, 3));
dilate(canny, canny, kernel);

// 轮廓发现与绘制
vector<vector<Point>> contours;
vector<Vec4i> hierarchy;
findContours(canny, contours, hierarchy, 0, 2, Point());

//绘制多边形
for (int t = 0; t < contours.size(); t++)
{
//用最小外接矩形求取轮廓中心
RotatedRect rrect = minAreaRect(contours[t]);
Point2f center = rrect.center;
circle(img, center, 2, Scalar(0, 255, 0), 2, 8, 0);

Mat result;
approxPolyDP(contours[t], result, 4, true);  //多边形拟合
drawapp(result, img);
cout << "corners : " << result.rows << endl;

//判断形状和绘制轮廓
if (result.rows == 3)
{
putText(img, "triangle", center, 0, 1, Scalar(0, 255, 0), 1, 8);
}
if (result.rows == 4)
{
putText(img, "rectangle", center, 0, 1, Scalar(0, 255, 0), 1, 8);
}
if (result.rows == 8)
{
putText(img, "poly-8", center, 0, 1, Scalar(0, 255, 0), 1, 8);
}
if (result.rows > 12)
{
putText(img, "circle", center, 0, 1, Scalar(0, 255, 0), 1, 8);
}
}
imshow("result", img);
waitKey(0);
return 0;
}

在这里插入图片描述

C++版本OpenCv教程(四十七)图像矩的计算与应用

Excerpt

矩是描述图像特征的算子,被广泛用于图像检索和识别、图像匹配、图像重建、图像压缩以及运动图像序列分析等领域。本节中将介绍几何矩与Hu矩的计算方法以及应用Hu矩实现图像轮廓的匹配。几何矩与中心矩图像几何矩的计算方式如式(7.8)所示:其中I(x,y)是像素(x,y)处的像素值。当x和y同时取值0时称为零阶矩,零阶矩可以用于计算某个形状的质心,当x和y分别取值0和1时被称为一阶矩,以此类推。图像质心的计算公式如(7.9)所示:图像中心距计算方式如式(7.10)所示:图像归一化几何矩计算方式如式所示


矩是描述图像特征的算子,被广泛用于图像检索和识别、图像匹配、图像重建、图像压缩以及运动图像序列分析等领域。本节中将介绍几何矩与Hu矩的计算方法以及应用Hu矩实现图像轮廓的匹配。

几何矩与中心矩

图像几何矩的计算方式如式(7.8)所示:
在这里插入图片描述

其中I(x,y)是像素(x,y)处的像素值。当x和y同时取值0时称为零阶矩,零阶矩可以用于计算某个形状的质心,当x和y分别取值0和1时被称为一阶矩,以此类推。图像质心的计算公式如(7.9)所示:
在这里插入图片描述

图像中心距计算方式如式(7.10)所示:
在这里插入图片描述

图像归一化几何矩计算方式如式所示:
在这里插入图片描述

OpenCV 4提供了计算图像矩的moments()函数,该函数的函数原型在代码清单7-28中给出。

Moments cv::moments(InputArray  array,bool  binaryImage = false )
  • array:计算矩的区域2D像素坐标集合或者单通道的CV_8U图像
  • binaryImage:是否将所有非0像素值视为1的标志。

该函数用于计算图像连通域的几何矩和中心距以及归一化的几何矩。函数第一个参数是待计算矩的输入图像或者2D坐标集合。函数第二个参数为是否将所有非0像素值视为1的标志,该标志只在第一个参数输入为图像类型的数据时才会有作用。函数会返回一个Moments类的变量,Moments类中含有几何矩、中心距以及归一化的几何矩的数值属性,例如Moments.m00是零阶矩,Moments.m01和Moments.m10是一阶矩。Moments类中所有的属性在表7-5给出。
在这里插入图片描述

为了了解函数的使用方法,在代码清单7-29中给出了计算图像矩和读取每一种矩数值方法的示例程序,程序的部分运行结果如图7-24所示。

#include <opencv2/opencv.hpp>
#include <iostream>
#include <vector>

using namespace cv;
using namespace std;

int main()
{
Mat img = imread("approx.png");

// 二值化
Mat gray, binary;
cvtColor(img, gray, COLOR_BGR2GRAY);
threshold(gray, binary, 105, 255, THRESH_BINARY);

//开运算消除细小区域
Mat k = getStructuringElement(MORPH_RECT, Size(3, 3), Point(-1, -1));
morphologyEx(binary, binary, MORPH_OPEN, k);

// 轮廓发现
vector<vector<Point>> contours;
vector<Vec4i> hierarchy;
findContours(binary, contours, hierarchy, 0, 2, Point());
for (int n = 0; n < contours.size(); n++) 
{
Moments M;
M = moments(contours[n], true);
cout << "spatial moments:" << endl
<< "m00:" << M.m00 << " m01:" << M.m01 << " m10:" << M.m10 << endl
<< "m11:" << M.m11 << " m02:" << M.m02 << " m20:" << M.m20 << endl
<< "m12:" << M.m12 << " m21:" << M.m21 << " m03:" << M.m03 << " m30:"<< M.m30 << endl;

cout << "central moments:" << endl
<< "mu20:" << M.mu20 << " mu02:" << M.mu02 << " mu11:" << M.mu11 << endl
<< "mu30:" << M.mu30 << " mu21:" << M.mu21 << " mu12:" << M.mu12 << " mu03:" << M.mu03 << endl;

cout << "central normalized moments:" << endl
<< "nu20:" << M.nu20 << " nu02:" << M.nu02 << " nu11:" << M.nu11 << endl
<< "nu30:" << M.nu30 << " nu21:" << M.nu21 << " nu12:" << M.nu12 << " nu03:" << M.nu03 << endl;
}
return 0;
}

在这里插入图片描述

Hu矩

Hu矩具有旋转、平移和缩放不变性,因此在图像具有旋转和放缩的情况下Hu矩具有更广泛的应用领域。Hu矩是由二阶和三阶中心距计算得到七个不变矩,具体计算公式如式(7.12)所示:
在这里插入图片描述
OpenCV 4提供了用于计算Hu矩的**HuMoments()**函数,根据参数类型的不同该函数具有两种原型。在代码清单7-30中给出这两种函数原型。

void cv::HuMoments(const Moments &  moments,
                   double  hu[7] 
                   )
void cv::HuMoments(const Moments &  m,
                   OutputArray  hu 
                   )
  • moments:输入的图像矩
  • hu[7]:输出Hu矩的七个值
  • m:输入的图像矩
  • hu:输出Hu矩的矩阵

该函数可以根据图像的中心距计算图像的Hu矩。两个函数原型只有第二个参数的数据类型不同,第一个参数是输入图的Moments类的图像矩,第二个参数是输出的Hu矩,第一种函数原型输出值存放在长度为7的double类型数组中,第二种函数原型输出值为Mat类型。

为了了解函数的使用方法,在代码清单7-31中给出了计算图像Hu的示例程序,程序的部分运行结果如图7-25所示。

#include <opencv2/opencv.hpp>
#include <iostream>
#include <vector>

using namespace cv;
using namespace std;

int main()
{
system("color F0");  //更改输出界面颜色
Mat img = imread("approx.png");
if (img.empty())
{
cout << "请确认图像文件名称是否正确" << endl;
return -1;
}
// 二值化
Mat gray, binary;
cvtColor(img, gray, COLOR_BGR2GRAY);
threshold(gray, binary, 105, 255, THRESH_BINARY);

//开运算消除细小区域
Mat k = getStructuringElement(MORPH_RECT, Size(3, 3), Point(-1, -1));
morphologyEx(binary, binary, MORPH_OPEN, k);

// 轮廓发现
vector<vector<Point>> contours;
vector<Vec4i> hierarchy;
findContours(binary, contours, hierarchy, 0, 2, Point());
for (int n = 0; n < contours.size(); n++)
{
Moments M;
M = moments(contours[n], true);
Mat hu;
HuMoments(M, hu);  //计算Hu矩
cout << hu << endl;
}
return 0;
}

在这里插入图片描述

基于Hu矩的轮廓匹配

Hu矩具有旋转、平移和比例不变性,因此可以通过Hu实现图像轮廓的匹配。OpenCV 4提供了利用Hu矩进行轮廓匹配的**matchShapes()**函数,该函数的函数原型在代码清单7-32中给出。

double cv::matchShapes(InputArray  contour1,
                       InputArray  contour2,
                       int  method,
                       double  parameter 
                       )
  • contour1:原灰度图像或者轮廓
  • contour2:模板图像或者轮廓
  • method:匹配方法的标志,可以选择的参数及含义在表7-6给出。
  • parameter:特定于方法的参数(现在不支持)

该函数用于实现在图像或者轮廓中寻找与模板图像或者轮廓像素匹配的区域。函数的第一个参数是原灰度图像或者轮廓,第二个参数是模板图像或者轮廓。函数第三个参数是两个轮廓Hu矩匹配的计算方法标志,可以选择的参数和每种方法相似性计算公式在表7-6给出。函数最后一个参数在目前的OpenCV 4版本中没有意义,可以将参数设置为0。
在这里插入图片描述

为了了解函数的用法,在代码清单7-33中给出了利用Hu矩实现模板与原图像或者轮廓之间匹配的示例程序。程序中原图像有三个字母,模板图像有一个字母,并且模板图像中字母的尺寸小于原图像中字母的尺寸。通过对两张图像提取轮廓并计算每个轮廓的Hu矩,之后寻找原图像和模板图像中Hu矩最相似的两个轮廓,并在原图像中绘制出相似轮廓,程序运行结果在图7-26给出。

#include <opencv2/opencv.hpp>
#include <iostream>
#include <vector>

using namespace cv;
using namespace std;

void findcontours(Mat &image, vector<vector<Point>> &contours)
{
Mat gray, binary;
vector<Vec4i> hierarchy;
//图像灰度化
cvtColor(image, gray, COLOR_BGR2GRAY);
//图像二值化
threshold(gray, binary, 0, 255, THRESH_BINARY | THRESH_OTSU);
//寻找轮廓
findContours(binary, contours, hierarchy, 0, 2);
}

int main()
{
Mat img = imread("ABC.png");
Mat img_B = imread("B.png");
if (img.empty() || img_B.empty())
{
cout << "请确认图像文件名称是否正确" << endl;
return -1;
}

resize(img_B, img_B, Size(), 0.5, 0.5);
imwrite("B.png", img_B);
imshow("B", img_B);

// 轮廓提取
vector<vector<Point>> contours1;
vector<vector<Point>> contours2;
findcontours(img, contours1);
findcontours(img_B, contours2);
// hu矩计算
Moments mm2 = moments(contours2[0]);
Mat hu2;
HuMoments(mm2, hu2);
// 轮廓匹配
for (int n = 0; n < contours1.size(); n++)
{
Moments mm = moments(contours1[n]);
Mat hum;
HuMoments(mm, hum);
//Hu矩匹配
double dist;
dist = matchShapes(hum, hu2, CONTOURS_MATCH_I1, 0);
if (dist < 1)
{
drawContours(img, contours1, n, Scalar(0, 0, 255), 3, 8);
}
}
imshow("match result", img);
waitKey(0);
return 0;
}

在这里插入图片描述

C++版本OpenCv教程(四十八)图像修复

Excerpt

在实际应用或者工程中,图像常常会收到噪声的干扰,例如在拍照时镜头上存在灰尘或者飞行的小动物,这些干扰会导致拍摄到的图像出现部分内容被遮挡的情况。对于较为久远的图像,可能只有实体图像而没有数字存储形式的底板,因此相片在保存和运输过程中可能产生划痕,导致图像中信息的损坏和丢失。图像修复技术就是利用图像中损坏区域边缘的像素,根据像素值的大小以及像素间的结构关系,估计出损坏区域可能的像素排列,从而去除图像中受污染的区域。图像修复不仅可以去除图像中得“划痕”,还可以去除图像中得水印、日期等。OpenCV 4提供了


在实际应用或者工程中,图像常常会收到噪声的干扰,例如在拍照时镜头上存在灰尘或者飞行的小动物,这些干扰会导致拍摄到的图像出现部分内容被遮挡的情况。对于较为久远的图像,可能只有实体图像而没有数字存储形式的底板,因此相片在保存和运输过程中可能产生划痕,导致图像中信息的损坏和丢失。

图像修复技术就是利用图像中损坏区域边缘的像素,根据像素值的大小以及像素间的结构关系,估计出损坏区域可能的像素排列,从而去除图像中受污染的区域。图像修复不仅可以去除图像中得“划痕”,还可以去除图像中得水印、日期等。

OpenCV 4提供了能够对含有较少污染或者水印的图像进行修复的inpaint()函数,该函数的函数原型在代码清单8-26中给出。

void cv::inpaint(InputArray  src,
                 InputArray  inpaintMask,
                 OutputArray  dst,
                 double  inpaintRadius,
                 int  flags 
                 )
  • src:输入待修复图像,当图像为单通道时,数据类型可以是CV_8U、CV_16U或者CV_32F,当图像为三通道时数据类型必须是CV_8U。
  • inpaintMask:修复掩模,数据类型为CV_8U的单通道图像,与待修复图像具有相同的尺寸。
  • dst:修复后输出图像,与输入图像具有相同的大小和数据类型。
  • inpaintRadius:算法考虑的每个像素点的圆形邻域半径。
  • flags:修复方法标志,可以选择的参数及含义在表8-7给出

该函数利用图像修复算法对图像中指定的区域进行修复,函数无法判定哪些区域需要修复,因此在使用过程中需要明确指出需要修复的区域。
函数的第一个参数是需要修复的图像,该函数可以对灰度图像和彩色图像进行修复。修复灰度图像时,图像的数据类型可以为CV_8U、CV_16U或者CV_32F;修复彩色图像时,图像的数据类型只能为CV_8U。
第二个参数是修复掩码,即指定图像中需要修复的区域,该参数输入量是一个与图像具有相同尺寸的数据类型为CV_8U的单通道图像,图像中非0像素表示需要修复的区域。
函数的第三个参数是修复后的输出图像,与输入图像具有相同的大小和数据类型。第四个参数表示修复算法考虑的每个像素点的圆形邻域半径。最后一个参数表示修复图像方法标志,可以选择的参数及含义在表8-7给出。

该函数虽然可以对图像受污染区域进行修复,但是需要借助污染边缘区域的像素信息,离边缘区域越远的像素估计出的准确性越低,因此如果受污染区域较大,修复的效果就会降低。
在这里插入图片描述

为了了解函数的使用方法以及图像修复的效果,在代码清单8-27中给出了图像修复的示例程序。程序中分别对污染较轻和较严重的两张图像进行修复,首先计算每张图像需要修复的掩码图像,之后利用inpaint()函数对图像进行修复,程序输出结果如图8-18和图8-19所示,通过结果可以看出污染区域较细并且较为稀疏的情况下图像修复效果较好,污染区域较为密集时修复效果较差。

#include <opencv2\opencv.hpp>
#include <iostream>

using namespace cv;
using namespace std;

int main()
{
Mat img1 = imread("inpaint1.png");
Mat img2 = imread("inpaint2.png");
if (img1.empty() || img2.empty())
{
cout << "请确认图像文件名称是否正确" << endl;
return -1;
}
imshow("img1", img1);
imshow("img2", img2);

//转换为灰度图
Mat img1Gray, img2Gray;
cvtColor(img1, img1Gray, COLOR_RGB2GRAY, 0);
cvtColor(img2, img2Gray, COLOR_RGB2GRAY, 0);

//通过阈值处理生成Mask掩模
Mat img1Mask, img2Mask;
threshold(img1Gray, img1Mask, 245, 255, THRESH_BINARY);
threshold(img2Gray, img2Mask, 245, 255, THRESH_BINARY);

//对Mask膨胀处理,增加Mask面积
Mat Kernel = getStructuringElement(MORPH_RECT, Size(3, 3));
dilate(img1Mask, img1Mask, Kernel);
dilate(img2Mask, img2Mask, Kernel);

//图像修复
Mat img1Inpaint, img2Inpaint;
inpaint(img1, img1Mask, img1Inpaint, 5, INPAINT_NS);
inpaint(img2, img2Mask, img2Inpaint, 5, INPAINT_NS);

//显示处理结果
imshow("img1Mask", img1Mask);
imshow("img1修复后", img1Inpaint);
imshow("img2Mask", img2Mask);
imshow("img2修复后", img2Inpaint);
waitKey();
return 0;
}

在这里插入图片描述在这里插入图片描述

  • 33
    点赞
  • 227
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
### 回答1: 要使用 C 和 OpenCV 写车牌识别,需要先安装 OpenCV 库。安装完成后,可以使用 OpenCV 提供的函数进行图像处理和特征提取。具体的步骤包括: 1. 读入图像:使用 OpenCV 的 imread() 函数读取图像数据。 2. 图像预处理:对图像进行灰度处理、降噪、二值化等处理,使得图像中的车牌更易于检测。 3. 车牌定位:使用 OpenCV 的模板匹配、Canny边缘检测等方法来定位车牌区域。 4. 字符分割:在定位到的车牌区域中,将车牌分割为单个字符。 5. 字符识别:对分割出的每个字符进行识别,识别可以使用机器学习算法如KNN,SVM,CNN等。 6. 输出识别结果:将识别出的车牌号码输出。 需要注意的是,车牌识别是一个非常复杂的问题,上述步骤中的每一步都可能需要经过大量调试和优化才能得到理想的结果。 ### 回答2: 使用c和OpenCV编写车牌识别可以通过以下步骤进行: 1. 导入OpenCV库和所需的其他库。 2. 读取图像:使用OpenCV的imread函数读取要识别的图像。 3. 图像预处理:首先,可以对图像进行灰度化处理,将彩色图像转换为灰度图像。然后,可以通过应用高斯模糊或其他滤波器来减少图像中的噪声。还可以使用阈值化方法对图像进行二值化处理,以便更好地区分车牌的区域。 4. 文本检测:可以使用OpenCV中的文本检测算法(例如,EAST算法)来检测图像中的文本区域。该算法可以帮助我们找到图像中可能包含车牌的区域。 5. 车牌区域提取:基于文本检测结果,可以根据车牌的特征(例如,颜色和形状)进一步提取可能的车牌区域。使用OpenCV的形态学操作和轮廓检测技术,可以提取出包含车牌的图像区域。 6. 字符分割:通过将车牌区域划分为多个字符区域,可以使用OpenCV的字符分割技术将车牌中的字符分离开来。 7. 字符识别:对于每个字符区域,可以使用OpenCV中的光学字符识别(OCR)技术,或者使用机器学习算法(如卷积神经网络)进行字符识别。 8. 结果显示:最后,可以在原始图像上绘制车牌区域和识别的字符,并将结果显示出来。 通过以上步骤,就可以使用c和OpenCV编写一个简单的车牌识别系统。这只是一个基本的思路,具体的实现可能会涉及更多的细节和算法。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值