一:卷积:
卷积的数学原理
因为卷积的概念还是比较容易理解的,所以在这里,我们来详细讲一下卷积的数学原理。
首先我们先来看一下卷积的定义:
在泛函分析中,卷积、旋积或摺积(英语:Convolution)是通过两个函数f 和g 生成第三个函数的一种数学算子,表征函数f 与g经过翻转和平移的重叠部分函数值乘积对重叠长度的积分。
通过定义,我们再来理解一下卷积这个名词(注:只是为了帮助大家理解,这是我的个人理解,不一定具有严格的准确性,初学者可以借用这个理解方式快速了解卷积,如果有更完整,更准确的理解方式,还望大家能够评论一起交流):
卷:两个函数的反转和平移,可以理解为两个函数通过运算纠缠到了一起,卷到了一起。
积:积分(本质就是运算的求和)
理解了这两个字,对于卷积操作,我们就可以更好地理解他们的公式了。
首先我们先来看连续数据:
设:f(x),g(x)是R1上的两个可积函数,作积分:
这个积分就定义了一个新函数h(x),称为函数f与g的卷积,记为h(x)=(f*g)(x)。
接下来我们看离散数据。
我们知道,积分就是连续的无限的求和运算。讲了这么多,可能大家还是对具体的不太清楚,那我们通过离散来理解一下卷积的过程。通过一个具体的例子来说明一下:
左边是一个图像,后面是经过卷积操作之后的图像,中间的3×3的二维矩阵就是一个卷积核。具体计算流程如下:
31 = (151 + 171 + 191 + 561 + 181 + 201 + 971 + 191 + 20*1) / 9
我们在掩膜操作之中没有平均,而且我们的掩膜操作和卷积操作的核实不一样的,但是计算过程非常类似,大家不要弄混。
定义图像为I(x,y),核为G(i,j),其中0<i<Mi-1和0<j<Mj-1,锚点位于相应核的(ai,aj)坐标上。所以对于上面这个我们能得到计算公式如下:
二:自定义线性滤波
通过上面的讲解我们知道了卷积,现在我们来讲一下自定义线性滤波,所谓自定义,就是我们自己定义线性滤波的卷积核。
而我们经常定义的卷积核就是全一的卷积核。
#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的全一卷积核,这个全一卷积核中的全一是不考虑最后要除以的核大小的。如下图,我们在最外面还是要除以核大小
我们的输出结果如下:
三:自定义线性滤波所用到的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。
四:代码:
#include<iostream>
#include<opencv2/opencv.hpp>
#include<highgui/highgui_c.h>
using namespace std;
using namespace cv;
int main()
{
Mat src,dst;
src = imread("F:/pic/lw.png");
if (src.empty())
{
cout << "Could not load the image!";
return -1;
}
namedWindow("input image", CV_WINDOW_AUTOSIZE);
imshow("input image", src);
//sobel X方向
Mat dst_x;
Mat kernel_x = (Mat_<int>(3, 3) << -1, 0, 1, -2,0,2,-1,0,1);
filter2D(src, dst_x,-1, kernel_x, Point(-1, -1), 0.0);
//Sobel Y方向
Mat dst_y;
Mat kernel_y= (Mat_<int>(3, 3) << -1,-2,-1,0,0,0,1,2,1);
filter2D(src, dst_y, -1, kernel_y,Point(-1, -1), 0.0);
imshow("sobel_x", dst_x);
imshow("sobel_y", dst_y);
//拉普拉斯算子
Mat kernel_L = (Mat_<int>(3, 3) << 0, -1, 0, -1, 4, -1, 0, -1, 0);
filter2D(src, dst, -1, kernel_L, Point(-1, -1), 0.0);
imshow("laplance", dst);
waitKey(0);
return 0;
}
实验现象:
五:卷积边缘处理:
卷积边界问题:
图像进行卷积操作时,图像的边界像素并不能被卷积操作到,原因在于边界像素没有完全跟 kernel 重叠,例如当33滤波时有1个像素的边缘没有被处理,55滤波时有2个像素的边缘没有被处理。
边缘问题解决方法:
在卷积操作开始之前,先增加边缘像素,比如3*3滤波时,在图像四周各填充1个像素的边缘,这样就确保图像的边缘能被处理,在卷积处理之后再去掉这些边缘。
常用方法有:
BORDER_DEFAULT - 对称法,也就是以最边缘像素为轴对称复制(通常为默认方法)。
BORDER_CONSTANT - 常量法就是以一个常量像素值(由参数 value给定)填充扩充的边界值,这种方式在仿射变换,透视变换中非常常见。
BORDER_REPLICATE - 复制法,也就是复制最边缘像素。
BORDER_WRAP - 用另外一边的像素补偿填充。
相关API
copyMakeBorder()
函数原型:
void copyMakeBorder( const Mat& src, Mat& dst,int top, int bottom, int left, int right,int borderType, const Scalar& value=Scalar() );
src:输入的数组。
dst:输出的拓展边界后的数组。
top:在src上边界向上拓展的行数。
bottom:在src下边界向下拓展的行数。
left:在src的左边界向左拓展的列数。
right:在src的右边界向右拓展的列数。
borderType:上一节中的边界拓展策略中的一个。
value:当你的边界策略使用的是BORDER_CONSTANT的时候,此处是指边界处填写的常数值。
代码如下:
#include<iostream>
#include<opencv2/opencv.hpp>
#include<highgui/highgui_c.h>
using namespace std;
using namespace cv;
int main()
{
Mat src, dst;
src = imread("F:/pic/lw.png");
if (src.empty())
{
cout << "Could not load the image!";
return -1;
}
namedWindow("input image", CV_WINDOW_AUTOSIZE);
imshow("input image", src);
int top = (int)(0.05 * src.rows);
int buttom = (int)(0.05 * src.rows);
int left = (int)(0.05 * src.cols);
int right = (int)(0.05 * src.cols);
RNG rng(12345);
int borderType = BORDER_DEFAULT;
int c = 0;
while (true)
{
c = waitKey(500);
if ((char)c == 27)
{
break;
}
if ((char)c == 'r') {
borderType = BORDER_REPLICATE;
}
else if ((char)c == 'v') {
borderType = BORDER_WRAP;
}
else if ((char)c == 'c') {
borderType = BORDER_CONSTANT;
}
Scalar color = Scalar(rng.uniform(0, 255), rng.uniform(0, 255), rng.uniform(0, 255));
//
copyMakeBorder(src, dst, top, buttom, left, right, borderType, color);
imshow("output", dst);
}
//GaussianBlur(src, dst, Size(5, 5), 0, 0, BORDER_WRAP);
//imshow("output", dst);
waitKey(0);
return 0;
}
实验结果:
1:默认(BORDER_DEFAULT)
2:BORDER_REPLICATE 结果
3:BORDER_WRAP;
4:BORDER_CONSTANT
应用:
例如在图像模糊时
blur(src, dst, Size(5, 5), Point(-1, -1),BORDER_DEFAULT);