OpenCV算法学习笔记之初识OpenCV
前言
从这篇开始写一系列关于OpenCV算法的笔记,主要目录为基础知识、几何变换、对比度增强、平滑算法、阈值分割、形态学处理、边缘检测以及形状检测。
在大概2018年底首次接触到图像识别这个领域,自然而然也就接触到了OpenCV,虽然使用了一些函数,但是对于底层的算法并不清楚,于是在开学之后买了一本关于OpenCV算法介绍的书,感觉收获还是挺多的,就萌生了写一系列博客的想法,也借此整理一下学到的东西。
OpenCV是什么
OpenCV是一个基于BSD许可(开源)发行的跨平台计算机视觉库,可以运行在Linux、Windows、Android和Mac OS操作系统上。它轻量级而且高效——由一系列 C 函数和少量 C++ 类构成,同时提供了Python、Ruby、MATLAB等语言的接口,实现了图像处理和计算机视觉方面的很多通用算法。
OpenCV用C++语言编写,它的主要接口也是C++语言,但是依然保留了大量的C语言接口。该库也有大量的Python、Java 和 MATLAB/OCTAVE(版本2.5)的接口。这些语言的API接口函数可以通过在线文档获得。如今也提供对于C#、Ch、Ruby,GO的支持。
OpenCV的安装
简述一下Python与VS中C++的安装:
Python利用命令行直接输入pip install opencv-python
即可,不过这样下载的是最新版本的OpenCV,如果想下载其他版本,可以指定版本号,如opencv-python==3.4.0.12
;
在VS中使用麻烦一些,这里以OpenCV 4.0.1为例。首先将OpenCV下载下来,可以发现在其路径下有以下文件:
|--build
|----bin
|----etc
|----include
|----java
|----python
|----x64
|------vc14
|------vc15
|--sources //源文件
|----....
- 首先要将\build\x64\vc15\bin添加到系统环境变量中(如果用的是VS2015之前的版本则添加vc14,在OpenCV 3.X中只有vc14);
- 创建一个项目,进入VS界面后,打开项目属性面板,在“VC++目录(VC++ Directories)”—“包含目录(Include Directories)”中添加\build\include路径、\build\include\opencv路径以及\build\include\opencv2路径;
- 在“库目录(Library Directories)”中添加\build\x64\vc15\lib路径;
- 点击“链接器(Linker)”—“输入(Input)”—“附加依赖项(Additional Dependencies)”,将opencv_world401.lib(此文件在\build\x64\vc15\lib文件夹内)添加进去,只需添加文件名即可,如果是Debug模式则添加opencv_world401d.lib。
至此,项目关于OpenCV的配置完成。
OpenCV基础知识
在OpenCV中,可以认为图像是以矩阵的形式存储的,利用函数imread(filename)
即可将图片读取到内存中(在C++中还要指定图像读取格式),参数filename是图片名称,可以包含路径,下面将介绍在Python和C++中如何操作图像。
Python
numpy的使用
在Python中,对于图像的操作要借用第三方库numpy
,如果电脑没有安装的话在安装OpenCV时会自动添加,在使用OpenCV时需要import cv2
与import numpy
。
在使用numpy
创建矩阵时,有以下几种方式:
import numpy as np
m = np.zeros((3, 3), np.uint8) # 创建3行3列且元素全都为0的矩阵
n = np.ones((2, 4), np.uint8) # 创建2行三列且元素全都为1的矩阵
z = np.array([[1, 2, 3], [2, 3, 4]], np.float32) # 创建2行3列的矩阵
打印结果:
m = array([[0, 0, 0],
[0, 0, 0],
[0, 0, 0]])
n = array([[1, 1, 1, 1],
[1, 1, 1, 1]])
z = array([[1, 2, 3],
[2, 3, 4]])
其中在构造时第二个参数代表数据类型。构造三维矩阵与二维矩阵类似,这里创建2×2×4的三维矩阵为例可以理解为2个2×4的二维矩阵,如:
t = np.array(
[
[[1, 1, 1, 1], [2, 2, 2, 2]],
[[3, 3, 3, 3], [4, 4, 4, 4]]], np.float32)
打印结果:
array([
[[1, 1, 1, 1],
[2, 2, 2, 2]],
[[3, 3, 3, 3],
[4, 4, 4, 4]]
], dtype=float32)
可以利用成员变量shape
获取矩阵的行和列,返回一个tuple;利用成员变量dtype
获得矩阵数据类型,如:
>>print(t.shape)
(2, 2, 4)
>>print(t.dtype)
float32
访问矩阵的某个位置也比较简单,直接用切片操作符即可。如:
>>print(t[0][0][0])
1.0
>>print(t[0, 0, 0])
1.0
>>print(t[1, :, :])
[[3. 3. 3. 3.]
[4. 4. 4. 4.]]
>>print(t[:, 1, :])
[[2. 2. 2. 2.]
[4. 4. 4. 4.]]
>>print(t[:, :, 0])
[[1. 2.]
[3. 4.]]
C++
Mat类的构建
Mat类是OpenCV中最核心的类,该类的声明在头文件opencv2\core\core.hpp中,所以要使用该类需要引入头文件;Mat的构造函数为Mat(int rows, int cols, int type)
,也可以用Mat(Size(int cols, int rows), int type)
,其中Size类一般用来存储矩阵的列数和行数,需要注意的是Size类的元素顺序与第一种构造方式相反。也可以用Mat::zeros(rows, cols, type)
和Mat::ones(rows, cols, type)
构造元素全为零或一的矩阵,例如:
#include<opencv2/core/core.hpp>
using namespace cv;
int main(){
Mat m1 = Mat(1, 3, CV_32FC1); //构造1行3列矩阵
Mat m2 = Mat(Size(3, 1), CV_32FC1); //构造1行3列矩阵
Mat o = Mat::zeros(2, 2, CV_32FC1); //构造2行2列全为0的矩阵
//也可以用以下方式构建矩阵
Mat m;
m.create(1, 3, CV_32FC1); //m.create(Size(3, 1), CV_32FC1)
return 0;
}
关于矩阵的初始化可以用以下简单的方式:
Mat x = (Mat_<int>(2, 3) << 1, 2, 3, 4, 5, 6);
Mat类的信息获取
- 通过成员变量
rows
和cols
可以获取矩阵的行数和列数; - 利用成员函数
size()
可以获取矩阵的尺寸; - 利用成员函数
channels()
获取矩阵的通道数; - 用成员函数
total()
获取矩阵行数×列数,与通道数无关; - 通过成员函数
dims()
获取矩阵维数;
以上面的矩阵x为例:
cout << x.rows; //输出 2
cout << x.cols; //输出 3
cout << x.size(); //输出 [2 × 3]
cout << x.channels(); //输出 1
矩阵某个位置元素的访问可以利用成员函数at<type>(r, c)
,其中type为矩阵的数据类型,如x.at<int>(0, 0)
,则会打印位于第一行第一列的元素。此外可以利用指针与两个重要的成员变量step
和data
来获取。
矩阵的每一行的值存储在连续的内存区域中,如果行与行之间有间隔,则间隔也是想等的,而step[0]
则代表每一行所占的字节数,如果有间隔的话,间隔也作为字节数被计算在内,step[1]
代表每一个数值所占的字节数,data
指向第一个数值的地址,类型为uchar。所以如果想要得到第r行第c列的值,可以用以下代码实现,并且速度比at
更快:
*((int*)(x.data + x.step[0]*r + x.step[1]*c))
如果矩阵数据类型为CV_32F类型,将int换为float即可。
此外还有成员函数ptr
、isContinuous
,在此就不作介绍。
向量类Vec
OpenCV提供了一种向量的构造方式Vec<type, rows>
,默认为列向量,type为数据类型,rows代表行数,可以在创建时直接初始化:Vec<int, 3> v(1, 2, 3)
。利用切片操作符“[ ]”或“( )”即可获取向量中的值,OpenCV为向量类的声明起了别名:
typedef Vec<uchar, 3> Vec3b;
typedef Vec<int,2> Vec2i;
typedef Vec<float, 4> Vec4f;
typedef Vec<double, 3> Vec3d;
更多声明可以查看头文件“opencv2/core/core.hpp”。
构造多通道Mat对象即用Vec类,如构造2×2的三通道矩阵:
Mat m = Mat_<Vec3f>(2, 2) << Vec3f(1, 2, 3), Vec3f(4, 5, 6),
Vec3f(7, 8, 9), Vec3f(10, 11, 12);
同样可以用成员函数at
、ptr
成员变量data
和step
等获取矩阵值。
分离通道与合并通道
通过OpenCV提供的函数void split(const Mat& src, Mat* mvbegin)
分离多通道,分离后的单通道矩阵被存放到vector中;通过void merge(const Mat *mv, size_t count, OutputArray dst)
可以合并多通道:
//分离多通道,m为多通道矩阵
vector<Mat> d;
spilt(m, d);
//合并通道,将要合并的矩阵放到数组中
Mat p1;
Mat p2;
Mat p3;
Mat p[] = {p1, p2, p3};
Mat dst;
merge(p, 3, dst);
merge()
的重载函数:void merge(InputArrayOfArrays mv, OutputArray dst)
可以合并存储在vector中的矩阵:
vector<Mat> p;
p.push_back(p1);
p.push_back(p2);
p.push_back(p3);
Mat dst;
merge(p, dst);
获取某个区域的值
- 利用成员函数
row(i)
和col(j)
获取矩阵的第i行和第j列,返回的仍是Mat类型; - 利用成员函数
rowRange()
和colRange()
获取连续的行或列; - 使用成员函数
clone()
和copyTo()
; - 使用Rect类;
对于第二种方法,以rowRange()
函数为例,函数形式为rowRange(int _start, int _end)
,是一个左闭右开的序列,即rowRange(2, 5)
会返回第2、3、4的行;
clone
函数和copyTo
类似,都是将矩阵克隆或复制一份(关于克隆与复制的不同可以参考OpenCV官方手册),如Mat r_range = m.rowRange(1, 3).clone();
使用Rect类相对于第二种方法更简单。Rect的构造有多种方法,除了最简单的Rect(int _x, int _y, int _width, int _higtht)
(_x,_y为左上角坐标,_width,_hight为矩形的宽度和高度),也可以将_width,_hight存在Size中,构造函数为Rect(int _x, int _y, Size size)
,知道左上角和右下角坐标也可以构造,此时就变为了Rect(Point2i &pt1, Point2i &pt2)
,Point2i为Point<int, 2>
,与Vec类类似,表示一个点。
矩阵的运算
加法
C++中可以直接用重载运算符“+”,规则为对应位置相加,且要求相加的双方数据类型相同;如果数据类型为uchar,相加后和大于255会截断为255;Python的Numpy同样用“+”,参与运算的双方数据类型不要求相同,结果的数据类型与范围大的一方相同,如果类型为uchar,结果大于255的会与255进行取模运算;
OpenCV提供函数void add(InputArray src1, InputArray src2, OutputArray dst, InputArray mask = noArray(), int dtype = -1)
,只有src1与src2数据类型相同时才可以令dtype等于-1,否则需要自行指定输出的数据类型;
减法
与加法类似,不同的是C++中对于小于0的会直接取0,而ndarray中则会与255取模后加1;
OpenCV提供函数void subtract(InputArray src1, InputArray src2, OutputArray dst, InputArray mask = noArray(), int dtype = -1)
,用法和加法类似;
点乘
点乘即对应位置相乘
利用Mat对象成员函数src1.mul(src2)
可以实现点乘,要求双方数据类型相同,对于大于255的数据也会进行截断处理;ndarray的点乘可以利用“*”运算符,与ndarray的“+”类似;
OpenCV提供函数void multiply(InputArray src1, InputArray src2, OutputArray dst, InputArray mask = noArray(), int dtype = -1)
,用法和加法类似;
点除
C++与numpy都可以通过“/”运算符运算,不同的是C++针对除数为0的情况结果会是0,而numpy当两个ndarray都是uint8类型时结果是0,其他情况返回inf;
矩阵的乘法
矩阵乘法定义:设为矩阵
与
相乘后的第r行第c列的元素的值,
与
为对应矩阵对应位置的值,
为m×p,
为p×n,则:
C++中利用“*”可以实现矩阵的乘法,需要注意的是参与运算的双方只能同时是float或者double类型,其他类型会报错;如果是双通道矩阵进行乘法运算,则每个双通道的元素被当做了复数,第一通道存放实部,第二通道存放虚部。Numpy中矩阵的乘法可以使用函数dot()
,返回数据类型与参与运算的数据类型范围大的相同。
其他运算
指数与对数运算:OpenCV中提供函数exp()
和log()
分别实现了矩阵的指数和对数运算(这里的log
是以e为底的)(事实上是通过循环分别对矩阵的每个元素进行运算,OpenCV封装了该操作),要求输入的数据类型必须为CV_32F或CV_64F,否则会报错。Numpy中同样提供了exp()
和log()
函数,并且对于输入的数据类型没有要求,返回的ndarray类型为float或double类型;
幂运算和开平方运算:同样是对矩阵进行运算,OpenCV提供函数pow(InputArray src, int series, OutputArray dst)
和sqrt()
函数分别实现了幂运算与开方运算,需要注意的是开方运算的输入数据类型必须为CV_32F或CV_64F,而幂运算则没有限制,输出的数据类型与输入的相同。Numpy同样提供函数power()
,但是幂指数的数据类型对于返回的ndarray影响很大,所以为了不影响精度将其设置为浮点类型即可:power(src, 2.0)
;
图像数字化
通过前面的知识,我们了解了对于矩阵的一系列操作,借此我们就可以实现对于图像的操作。图像在OpenCV中以矩阵的形式存储,对于8bit位深的灰度图像,对应的矩阵每个元素是uchar类型,范围则是[0, 255]共256种取值;若是多通道的彩色图像,通常每个元素则是一个1×3或3×1的矩阵,而小矩阵中的元素类型也是uchar,如下所示:
若是16bit位深的图像,每个元素的范围则是[0, 216]。在OpenCV中,三通道的彩色图像通常是BGR的顺序,即蓝色(Blue),绿色(Green),红色(Red)。对于灰度图像,0代表黑色,255代表白色;对于多通道图像,(0, 0, 0)代表黑色,(255, 255, 255)代表白色。除了比较常用的BGR模式,还有HSV、HLS等色彩空间,在此暂不作过多介绍。通过函数cvtColor()
可以实现彩色图像向灰度或其他颜色空间的转换,对于转为灰度图像来说,每个像素点的转换公式为:
OpenCV提供函数Mat imread(const string\& filename, int flags=1)
进行图像的读取,其中filename参数代表图像名称,可以包含路径,参数flags代表读入的图像类型,有以下几种类型:
enum ImreadModes {
IMREAD_UNCHANGED = -1, //原样返回加载的图像(使用alpha通道,否则将被剪切)
IMREAD_GRAYSCALE = 0, //单通道的灰度图像
IMREAD_COLOR = 1, //三通道的BGR图像
IMREAD_ANYDEPTH = 2, //任意位深
IMREAD_ANYCOLOR = 4, //任意图像
IMREAD_LOAD_GDAL = 8, //使用GDAL驱动读取图像
IMREAD_REDUCED_GRAYSCALE_2 = 16, //单通道的灰度图像且图像尺寸变为原来的1/2
IMREAD_REDUCED_COLOR_2 = 17, //三通道的彩色图像且图像尺寸变为原来的1/2
IMREAD_REDUCED_GRAYSCALE_4 = 32, //单通道的灰度图像且图像尺寸变为原来的1/4
IMREAD_REDUCED_COLOR_4 = 33, //三通道的彩色图像且图像尺寸变为原来的1/4
IMREAD_REDUCED_GRAYSCALE_8 = 64, //单通道的灰度图像且图像尺寸变为原来的1/8
IMREAD_REDUCED_COLOR_8 = 65, //三通道的彩色图像且图像尺寸变为原来的1/8
};
在Python中可以不指定flags,如src = cv2.imread("C:/test.png")
OpenCV提供函数void imshow(const string\& filename, InputArray mat)
进行图像显示,其中filename是窗口的名称(由于OpenCV是GBK编码,不支持中文),mat则是将要显示的图像,运行函数后会自动出现一个与图像相同尺寸的窗口并显示图像。
OpenCV提供函数imwrite( const String& filename, InputArray img, const std::vector<int>& params = std::vector<int>())
实现图像写入硬盘,同样不支持中文,否则文件名会乱码。
将多通道的彩色图像转换为单通道的图像可以利用前面讲过的split()
函数,在彩色图像处理中,我们通常先分离通道,对于每个通道单独处理后再合并,分离通道代码为:
#include<opencv2/core.hpp>
#include<opencv2/highgui/highgui.hpp>
using namespace cv;
int main(){
Mat src = imread("test.png", IMREAD_COLOR);
vector<Mat> p;
split(src, p);
//显示通道
imshow("B", p[0]);
imshow("G", p[1]);
imshow("R", p[2]);
waitKey(0);
return 0;
}
至此,关于OpenCV的基础知识便基本完成。