OpenCV算法学习笔记之初识OpenCV
in OpenCV with 0 comment

OpenCV算法学习笔记之初识OpenCV

in OpenCV with 0 comment

前言

从这篇开始写一系列关于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  //源文件
|----....
  1. 首先要将\build\x64\vc15\bin添加到系统环境变量中(如果用的是VS2015之前的版本则添加vc14,在OpenCV 3.X中只有vc14);
  2. 创建一个项目,进入VS界面后,打开项目属性面板,在“VC++目录(VC++ Directories)”—“包含目录(Include Directories)”中添加\build\include路径、\build\include\opencv路径以及\build\include\opencv2路径;
  3. 在“库目录(Library Directories)”中添加\build\x64\vc15\lib路径;
  4. 点击“链接器(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 cv2import 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中最核心的类,该类的声明在头文件opencv2corecore.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类的信息获取

  1. 通过成员变量rowscols可以获取矩阵的行数和列数;
  2. 利用成员函数size()可以获取矩阵的尺寸;
  3. 利用成员函数channels()获取矩阵的通道数;
  4. 用成员函数total()获取矩阵行数×列数,与通道数无关
  5. 通过成员函数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),则会打印位于第一行第一列的元素。此外可以利用指针与两个重要的成员变量stepdata来获取。

矩阵的每一行的值存储在连续的内存区域中,如果行与行之间有间隔,则间隔也是想等的,而step[0]则代表每一行所占的字节数,如果有间隔的话,间隔也作为字节数被计算在内,step[1]代表每一个数值所占的字节数,data指向第一个数值的地址,类型为uchar。所以如果想要得到第r行第c列的值,可以用以下代码实现,并且速度比at更快:

*((int*)(x.data + x.step[0]*r + x.step[1]*c))

如果矩阵数据类型为CV_32F类型,将int换为float即可。

此外还有成员函数ptrisContinuous,在此就不作介绍。

向量类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);

同样可以用成员函数atptr成员变量datastep等获取矩阵值。

分离通道与合并通道

通过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);

获取某个区域的值

  1. 利用成员函数row(i)col(j)获取矩阵的第i行和第j列,返回的仍是Mat类型;
  2. 利用成员函数rowRange()colRange()获取连续的行或列;
  3. 使用成员函数clone()copyTo()
  4. 使用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;

矩阵的乘法

矩阵乘法定义:设$f(r,c)$为矩阵$G_1$与$G_2$相乘后的第r行第c列的元素的值,$g_1(r,c)$与$g_2(r,c)$为对应矩阵对应位置的值,$G_1$为m×p,$G_2$为p×n,则有:

$$f(r,c)=\sum_{k=0}^{p}{g_1(r,k)g_2(k,c)}$$

C++中利用“*”可以实现矩阵的乘法,需要注意的是参与运算的双方只能同时是float或者double类型,其他类型会报错;如果是双通道矩阵进行乘法运算,则每个双通道的元素被当做了复数,第一通道存放实部,第二通道存放虚部。Numpy中矩阵的乘法可以使用函数dot(),返回数据类型与参与运算的数据类型范围大的相同。

其他运算

指数与对数运算:OpenCV中提供函数explog分别实现了矩阵的指数和对数运算(这里的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,如下所示:

$$ \left[ \begin{matrix} \begin{matrix}[1&142&13]\end{matrix} \begin{matrix}[31&111&15]\end{matrix}...\begin{matrix}[20&12&213]\end{matrix}\begin{matrix}[121&111&0]\end{matrix}\\ \begin{matrix}[10&42&113]\end{matrix} \begin{matrix}[221&11&15]\end{matrix}...\begin{matrix}[20&122&13]\end{matrix}\begin{matrix}[60&21&50]\end{matrix}\\ \begin{matrix}[66&142&13]\end{matrix} \begin{matrix}[71&22&99]\end{matrix}...\begin{matrix}[200&42&255]\end{matrix}\begin{matrix}[121&11&0]\end{matrix}\\ \begin{matrix}...\end{matrix} \end{matrix} \right] $$

若是16bit位深的图像,每个元素的范围则是[0, 216]。在OpenCV中,三通道的彩色图像通常是BGR的顺序,即蓝色(Blue),绿色(Green),红色(Red)。对于灰度图像,0代表黑色,255代表白色;对于多通道图像,(0, 0, 0)代表黑色,(255, 255, 255)代表白色。除了比较常用的BGR模式,还有HSV、HLS等色彩空间,在此暂不作过多介绍。通过函数cvtColor()可以实现彩色图像向灰度或其他颜色空间的转换,对于转为灰度图像来说,每个像素点的转换公式为:

$$ gray=\left(\begin{matrix}0.114&0.587&0.299\end{matrix}\right)\left(\begin{matrix}B\\G\\R\end{matrix}\right) $$

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的基础知识便基本完成。

参考

《OpenCV算法精解——基于Python和C++》(张平)第一、二章

Responses