目录
一、前言
继续填坑。
如果想看其他有关于OpenCV学习方法介绍、学习教程、代码实战、常见报错及解决方案等相关内容,可以直接看我的OpenCV分类:
【OpenCV系列】:https://blog.csdn.net/shuiyixin/article/category/7581855
如果你想了解更多有关于计算机视觉、OpenCV、机器学习、深度学习等相关技术的内容,想与更多大佬一起沟通,那就扫描下方二维码加入我们吧!
二、卷积
1、啥是卷积
我们经常说卷积,那啥是卷积呢?
卷积本身是一个数学概念,但是更多的,我们经常会在深度学习听到:卷积神经网络。但真正研究核心神经网络的人会发现,其实研究的就是数学。除了在深度学习中,在计算机视觉中,卷积也会经常见到,它常用于图像处理。
想要更加深入的理解卷积,我们还是通过数学了解一下,卷积到底是个什么鬼。
2、卷积的数学原理
因为卷积的概念还是比较容易理解的,所以在这里,我们来详细讲一下卷积的数学原理。
首先我们先来看一下卷积的定义:
在泛函分析中,卷积、旋积或摺积(英语:Convolution)是通过两个函数f 和g 生成第三个函数的一种数学算子,表征函数f 与g经过翻转和平移的重叠部分函数值乘积对重叠长度的积分。
通过定义,我们再来理解一下卷积这个名词(注:只是为了帮助大家理解,这是我的个人理解,不一定具有严格的准确性,初学者可以借用这个理解方式快速了解卷积,如果有更完整,更准确的理解方式,还望大家能够评论一起交流):
卷:两个函数的反转和平移,可以理解为两个函数通过运算纠缠到了一起,卷到了一起。
积:积分(本质就是运算的求和)
理解了这两个字,对于卷积操作,我们就可以更好地理解他们的公式了。
首先我们先来看连续数据:
设:f(x),g(x)是R1上的两个可积函数,作积分:
这个积分就定义了一个新函数h(x),称为函数f与g的卷积,记为h(x)=(f*g)(x)。
接下来我们看离散数据。
我们知道,积分就是连续的无限的求和运算。讲了这么多,可能大家还是对具体的不太清楚,那我们通过离散来理解一下卷积的过程。通过一个具体的例子来说明一下:
左边是一个图像,后面是经过卷积操作之后的图像,中间的3×3的二维矩阵就是一个卷积核。具体计算流程如下:
31 = (15*1 + 17*1 + 19*1 + 56*1 + 18*1 + 20*1 + 97*1 + 19*1 + 20*1) / 9
大家对这个图相对比较熟悉了,因为我们在之前也见到过,就是我们之前学的图像的掩膜操作,掩膜操作实现图像对比度调整。
掩膜操作的计算过程如下:
1 * 0 + 2 * (-1) + 3 * 0 + 2 * (-1) + 3 * 5 + 4 * (-1) + 3 * 0 + 4 * (-1) + 5 * 0 = 3
我们在掩膜操作之中没有平均,而且我们的掩膜操作和卷积操作的核实不一样的,但是计算过程非常类似,大家不要弄混。
定义图像为I(x,y),核为G(i,j),其中0<i<Mi-1和0<j<Mj-1,锚点位于相应核的(ai,aj)坐标上。所以对于上面这个我们能得到计算公式如下:
三、自定义线性滤波
1、讲解
通过上面的讲解我们知道了卷积,现在我们来讲一下自定义线性滤波,所谓自定义,就是我们自己定义线性滤波的卷积核。
而我们经常定义的卷积核就是全一的卷积核。
#include<iostream>
#include<opencv2/opencv.hpp>
using namespace std;
using namespace cv;
int main()
{
Mat kernel = Mat::ones(Size(3, 3),CV_32F)/9;
cout << kernel << endl;
waitKey(0);
return 0;
}
我们可以定义一个3×3的全一卷积核,这个全一卷积核中的全一是不考虑最后要除以的核大小的。如下图,我们在最外面还是要除以核大小。
我们的输出结果如下:
2、API
因为我们和掩膜操作过程是一样的,只不过是核不同,所以我们自定义线性滤波的API也是:filter2D,具体如下:
void filter2D(
InputArray src,
OutputArray dst,
int ddepth,
InputArray kernel,
Point anchor = Point(-1,-1),
double delta = 0,
int borderType = BORDER_DEFAULT
);
函数参数含义如下:
(1)InputArray类型的src ,输入图像。
(2)OutputArray类型的dst ,输出图像,图像的大小、通道数和输入图像相同。
(3)int类型的ddepth,目标图像的所需深度。
(4)InputArray类型的kernel,卷积核(或者更确切地说是相关核)是一种单通道浮点矩阵;如果要将不同的核应用于不同的通道,请使用split将图像分割成不同的颜色平面,并分别对其进行处理。。
(5)Point类型的anchor,表示锚点(即被平滑的那个点),注意他有默认值Point(-1,-1)。如果这个点坐标是负值的话,就表示取核的中心为锚点,所以默认值Point(-1,-1)表示这个锚点在核的中心。。
(6)double类型的delta,在将筛选的像素存储到dst中之前添加到这些像素的可选值。说的有点专业了其实就是给所选的像素值添加一个值delta。
(7)int类型的borderType,用于推断图像外部像素的某种边界模式。有默认值BORDER_DEFAULT。
3、代码展示
#include<iostream>
#include<opencv2/opencv.hpp>
using namespace std;
using namespace cv;
int main()
{
Mat img, src, kernel;
img = imread("E:/image/girl2.png");
if (!img.data)
{
cout << "could not load image !";
return -1;
}
imshow("【输入图像】", img);
kernel = Mat::ones(Size(3, 3),CV_32F)/9;
filter2D(img, src, -1, kernel, Point(-1, -1),0);
cout << kernel << endl;
imshow("【输出图像】", src);
waitKey(0);
return 0;
}
4、执行结果
四、borderType
1、为啥要处理边缘
我们在做卷积操作的时候,我想大家都意识到了一个问题,就是边缘问题:
对于卷积示例中,3×3的卷积核,图像的最外层一个像素宽度的边缘是没有数据的,因为我们通过简单地卷积操作,会在行列分别丢失两个像素。
也就是说,我们要单独处理一下边缘。
2、borderType
这个就涉及到我们的最后一个参数,borderType。opencv中常用的边界类型如下:
enum BorderTypes {
BORDER_CONSTANT = 0, //!< `iiiiii|abcdefgh|iiiiiii` with some specified `i`
BORDER_REPLICATE = 1, //!< `aaaaaa|abcdefgh|hhhhhhh`
BORDER_REFLECT = 2, //!< `fedcba|abcdefgh|hgfedcb`
BORDER_WRAP = 3, //!< `cdefgh|abcdefgh|abcdefg`
BORDER_REFLECT_101 = 4, //!< `gfedcb|abcdefgh|gfedcba`
BORDER_TRANSPARENT = 5, //!< `uvwxyz|absdefgh|ijklmno`
BORDER_REFLECT101 = BORDER_REFLECT_101, //!< same as BORDER_REFLECT_101
BORDER_DEFAULT = BORDER_REFLECT_101, //!< same as BORDER_REFLECT_101
BORDER_ISOLATED = 16 //!< do not look outside of ROI
};
除了默认的BORDER_DEFAULT。最常用的还有:
- BORDER_CONSTANT – 填充边缘用指定像素值
- BORDER_REPLICATE – 填充边缘像素用已知的边缘像素值。
- BORDER_WRAP – 用另外一边的像素来补偿填充
3、代码实战
我们尝试使用不同的边界类型,看一下它的结果。为了更加明显,我们将卷积核设的更大一些。
大家也可以自己尝试一下呀,一定要多做练习!