图像处理中的卷积运算
所谓对图像进行卷积运算就是使用卷积核(卷积模板,一般为n*n的奇数方框)在数字图像中进行移动,如图所示,图像中的卷积核大小为3*3的方阵,在图像的滑动过程中卷积核中对应的数值与图像中的数值相乘并对九个数字求和得出中心像素的数值,然后继续滑动生成新的中心像素值(注意:已经计算出的中心像素值并不参与下一次的卷积运算),滑动的距离我们称为步长通常为1.当整幅图像都进行这样的操作后将会生成一幅根据卷积运算得出的新的图像.
边界填充问题
图像处理中二维离散卷积的运算比较直观也很好理解,仔细观察上述描述可能会发现一个问题,卷积运算后的新图像将会比原图像的大小要小,具体小多少和卷积核大小有关,卷积核尺寸越大新生成的图像越小,为了避免上述问题我们通常在卷积操作之前会扩充原图像的大小,或者说填充图像的边界,使其在经历卷积运算后尺寸任然和原图像大小一样.具体的填充策略有很多,举几个例子.
零填充:用0填充图像的边界,卷积处理之后图像容易出现黑边
0 | 0 | 0 | 0 | 0 |
0 | 1 | 2 | 3 | 0 |
0 | 4 | 5 | 6 | 0 |
0 | 7 | 8 | 9 | 0 |
0 | 0 | 0 | 0 | 0 |
常数填充:选定常数进行填充
2 | 2 | 2 | 2 | 2 |
2 | 1 | 2 | 3 | 2 |
2 | 4 | 5 | 6 | 2 |
2 | 7 | 8 | 9 | 2 |
2 | 0 | 0 | 0 | 2 |
夹取填充:复制或夹取图像边缘
1 | 1 | 2 | 3 | 3 |
1 | 1 | 2 | 3 | 3 |
4 | 4 | 5 | 6 | 6 |
7 | 7 | 8 | 9 | 9 |
7 | 7 | 8 | 9 | 9 |
此外还有很多种填充方式在此就不一一列举了.
卷积核
应用不同的卷积核对图像进行卷积运算会得出不同的效果.以下为一些常用的卷积核
均值滤波
1/9 | 1/9 | 1/9 |
1/9 | 1/9 | 1/9 |
1/9 | 1/9 | 1/9 |
原图 滤波后
图像锐化
-1 | -1 | -1 |
-1 | 9 | -1 |
-1 | -1 | -1 |
原图 滤波后
Sobel
-1 | 0 | 1 |
-2 | 0 | 2 |
-1 | 0 | 1 |
原图 滤波后
高斯核
高斯核用于图像的模糊处理,和均值滤波的作用类似.但不同的是高斯滤波进行的是一种加权平均,越靠近核中心的数值权重越大
例如下表所示3*3大小σ为10的一个高斯核
| | |
| | |
| | |
原图 处理后
其中σ越大模糊效果越显著
获取高斯核
一维高斯函数数学表达式,σ为1为标准正态分布
二维高斯函数表达式:
根据二维高斯函数可以写出获取核函数的相关代码
//size核大小,越大平滑效果越明显
//sigma越大平滑效果越明显
Mat GetGaussianKernel( const int size,const double sigma)
{
double **gaus=new double *[size];
for(int i=0;i<size;i++)
{
gaus[i]=new double[size]; //动态生成矩阵
}
Mat Kernel(size,size,CV_64FC1,Scalar(0));
const double PI=4.0*atan(1.0); //圆周率π赋值
int center=size/2;
double sum=0;
for(int i=0;i<size;i++)
{
for(int j=0;j<size;j++)
{
gaus[i][j]=(1/(2*PI*sigma*sigma))*exp(-((i-center)*(i-center)+(j-center)*(j-center))/(2*sigma*sigma));//二维高斯函数
sum+=gaus[i][j];
}
}
for(int i=0;i<size;i++)
{
for(int j=0;j<size;j++)
{
gaus[i][j]/=sum;
cout<<gaus[i][j]<<", ";
Kernel.at<double>(i,j) = gaus[i][j];//将数组转换为Mat
}
cout<<endl<<endl;
}
cout << "kernel[0][0] = " << dec << Kernel.at<double>(0,1) << endl;
imshow("kernel", Kernel);
return Kernel;
}
卷积运算的C++代码实现
void Convlution(Mat &input_img,Mat OutputImage, Mat Kernel)
{
int border_x = Kernel.rows/2;
int border_y = Kernel.cols/2;
for(int img_y = 0; img_y < input_img.cols - 2*border_y; img_y++)
{
for(int img_x = 0; img_x < input_img.rows - 2*border_x; img_x++)
{
int end_value = 0;
for(int kernel_y = 0; kernel_y < Kernel.cols; kernel_y++)
{
for(int kernel_x = 0; kernel_x < Kernel.rows; kernel_x++)
{
int img_value = input_img.at<uchar>(img_y+kernel_y, img_x+kernel_x);
double Kernel_value = Kernel.at<double>(kernel_y, kernel_x);
end_value += img_value*Kernel_value;
}
}
if(end_value > 256)
end_value = 255;
else if (end_value < 0)
end_value = 0;
OutputImage.at<char>(img_y+border_y, img_x+border_x) = (uchar)end_value;
// OutputImage.at<uchar>(img_y+border_y, img_x+border_x) = saturate_cast<uchar>((int)end_value);
}
}
input_img = OutputImage;
}
关于卷积运算部分讲解
老实说这代码我是参考了chaibubble的博客https://blog.csdn.net/chaipp0607/article/details/78277395修改了一点得出的
代码的主要思路是通过四次嵌套的循环完成卷积运算,最外的两层循环完成遍历图像的操作,内层两个循环完成对卷积核的遍历.
其中
int img_value = input_img.at<uchar>(img_y+kernel_y, img_x+kernel_x);
double Kernel_value = Kernel.at<double>(kernel_y, kernel_x);
end_value += img_value*Kernel_value;
img_value为图像中mask所对应的像素值,通过与图像坐标+核函数坐标(通过内层循环可获取一次卷积运算中所需要的所有的对应像素值)实现获取对应与核函数的像素值,Kernel_value显然就是核函数的数值,end_value为最后中心像素值.
if(end_value > 256)
end_value = 255;
else if (end_value < 0)
end_value = 0;
OutputImage.at<char>(img_y+border_y, img_x+border_x) = (uchar)end_value;
// OutputImage.at<uchar>(img_y+border_y, img_x+border_x) = saturate_cast<uchar>((int)end_value);
此部分为最终像素值截取,防止溢出(超出255或小于0),注释掉的代码可取代以上操作,效果相同
所有测试代码
#include<opencv.hpp>
#include<iostream>
using namespace cv;
using namespace std ;
Mat src,gray,dst;
Mat GetGaussianKernel( const int size,const double sigma);
void Convlution(Mat &input_img,Mat OutputImage, Mat Kernel);
Mat Kernel_test_3_3 = (Mat_<double>(3,3) <<
0.110741, 0.111296, 0.110741,
0.111296, 0.111854, 0.111296,
0.110741, 0.111296, 0.110741);
Mat Kernel_average_3_3 = (Mat_<double>(3,3) <<
0.111111, 0.111111, 0.111111,
0.111111, 0.111111, 0.111111,
0.111111, 0.111111, 0.111111);
Mat Kernel_ruihua_3_3 = (Mat_<double>(3,3) <<
-1, -1, -1,
-1, 9, -1,
-1, -1, -1);
Mat Kernel_Sobel = (Mat_<double>(3,3) <<
-1, 0, 1,
-2, 0, 2,
-1, 0, 1);
int main(int argc, char *argv[])
{
cout.setf(ios::fixed,ios::floatfield);
src =imread("/home/qinzihang/opencv-4.2.0/samples/data/lena.jpg");
cvtColor(src,src,COLOR_BGR2GRAY);
Mat dstImage(src.rows,src.cols,CV_8UC1,Scalar(0));
imshow("GG",src);
int size=5; //定义卷积核大小
double **gaus=new double *[size];
for(int i=0;i<size;i++)
{
gaus[i]=new double[size]; //动态生成矩阵
}
Mat Gauss_Kernel = GetGaussianKernel(3,10);
Convlution(src,dstImage,Gauss_Kernel);
imshow("after", src);
waitKey(0);
}
Mat GetGaussianKernel( const int size,const double sigma)
{
double **gaus=new double *[size];
for(int i=0;i<size;i++)
{
gaus[i]=new double[size]; //动态生成矩阵
}
Mat Kernel(size,size,CV_64FC1,Scalar(0));
const double PI=4.0*atan(1.0); //圆周率π赋值
int center=size/2;
double sum=0;
for(int i=0;i<size;i++)
{
for(int j=0;j<size;j++)
{
gaus[i][j]=(1/(2*PI*sigma*sigma))*exp(-((i-center)*(i-center)+(j-center)*(j-center))/(2*sigma*sigma));
sum+=gaus[i][j];
}
}
for(int i=0;i<size;i++)
{
for(int j=0;j<size;j++)
{
gaus[i][j]/=sum;
cout<<gaus[i][j]<<", ";
Kernel.at<double>(i,j) = gaus[i][j];
}
cout<<endl<<endl;
}
cout << "kernel[0][0] = " << dec << Kernel.at<double>(0,1) << endl;
imshow("kernel", Kernel);
return Kernel;
}
void Convlution(Mat &input_img,Mat OutputImage, Mat Kernel)
{
int border_x = Kernel.rows/2;
int border_y = Kernel.cols/2;
for(int img_y = 0; img_y < input_img.cols - 2*border_y; img_y++)
{
for(int img_x = 0; img_x < input_img.rows - 2*border_x; img_x++)
{
int end_value = 0;
for(int kernel_y = 0; kernel_y < Kernel.cols; kernel_y++)
{
for(int kernel_x = 0; kernel_x < Kernel.rows; kernel_x++)
{
int img_value = input_img.at<uchar>(img_y+kernel_y, img_x+kernel_x);
double Kernel_value = Kernel.at<double>(kernel_y, kernel_x);
end_value += img_value*Kernel_value;
}
}
if(end_value > 256)
end_value = 255;
else if (end_value < 0)
end_value = 0;
OutputImage.at<char>(img_y+border_y, img_x+border_x) = (uchar)end_value;
// OutputImage.at<uchar>(img_y+border_y, img_x+border_x) = saturate_cast<uchar>((int)end_value);
}
}
input_img = OutputImage;
}
以上便是本文的所有内容,如有错误疏漏欢迎大家指出.