opencv环境下有自带的filter2D()函数可以实现图像的卷积,自己写一个卷积函数函数貌似是没事找事。。。。好吧,事实是这是我们计算机视觉课程上的一项作业。我们很多算法过程仅仅只调用别人写好的接口,即使原理我们已经清楚,但是真正编写代码的时候很多细节我们可能还是没有意识到,也许自己再实现一遍是一种深入学习的途径吧。
本文对图像卷积操作的原理不作详细讨论,博客https://blog.csdn.net/chaipp0607/article/details/72236892?locationNum=9&fps=1已经写得非常详细,所以这里只对opencv环境下的C++实现进行讨论。
OK,先上代码为敬
这里定义了一个My_Convolution的类,以下是头文件和源文件
#ifndef MY_CONVOLUTION #define MY_CONVOLUTION #include <opencv2/opencv.hpp> class My_Convolution{ public: My_Convolution(); ~My_Convolution(); bool load_kernal(cv::Mat kernal);//加载卷积核 void convolute(const cv::Mat &image,cv::Mat &dst);//卷积操作 private: bool kernal_loaded;//是否已经加载卷积核 cv::Mat curr_kernal;//当前卷积核 int bios_x,bios_y;//记录偏移量 void compute_sum_of_product(int i,int j,int chan,cv::Mat &complete_image,cv::Mat & dst);//计算每一个像素的掩模乘积之和 void complete_image_transform(const cv::Mat &image,cv::Mat &dst);//将原图像转换成边框补全的图像 }; #endif // MY_CONVOLUTION
#include "my_convolution.h" using namespace std; using namespace cv; My_Convolution::My_Convolution(){ kernal_loaded=false; } My_Convolution::~My_Convolution(){} //加载卷积核 bool My_Convolution::load_kernal(Mat kernal){ if(kernal.cols%2==1&&kernal.rows%2==1){ curr_kernal=kernal.clone(); bios_x=(kernal.cols-1)/2; bios_y=(kernal.rows-1)/2; kernal_loaded=true; return true; } else{ cout<<"The size of kernal is not suitable!"<<endl; return false; } } //卷积操作 void My_Convolution::convolute(const Mat &image, Mat &dst){ if(!kernal_loaded){ cout<<"kernal is empty!Please load the kernal first!"<<endl;return; } Mat complete_image; complete_image_transform(image,complete_image); dst=Mat::zeros(image.rows,image.cols,image.type()); int channels=image.channels();//获取图像的通道数 if(channels==3){ for(int chan=0;chan<channels;chan++){ for(int i=0;i<dst.rows;i++){ for(int j=0;j<dst.cols;j++){ compute_sum_of_product(i,j,chan,complete_image,dst); } } } return ; } if(channels==1){ for(int i=0;i<dst.rows;i++){ for(int j=0;j<dst.cols;j++){ compute_sum_of_product(i,j,0,complete_image,dst); } } } } //计算掩模乘积之和 void My_Convolution::compute_sum_of_product(int i, int j,int chan,Mat &complete_image, Mat &dst){ if(complete_image.channels()==3){ float sum=0; int bios_rows=i; int bios_cols=j; for(int curr_rows=0;curr_rows<curr_kernal.rows;curr_rows++){ for(int curr_cols=0;curr_cols<curr_kernal.cols;curr_cols++){ float a=curr_kernal.at<float>(curr_rows,curr_cols)*complete_image.at<Vec3b>(curr_rows+bios_rows,curr_cols+bios_cols)[chan]; sum+=a; } } dst.at<Vec3b>(i,j)[chan]=(int)sum; } else{ if(complete_image.channels()==1){ float sum=0; int bios_rows=i; int bios_cols=j; for(int curr_rows=0;curr_rows<curr_kernal.rows;curr_rows++){ for(int curr_cols=0;curr_cols<curr_kernal.cols;curr_cols++){ float a=curr_kernal.at<float>(curr_rows,curr_cols)*complete_image.at<uchar>(curr_rows+bios_rows,curr_cols+bios_cols); sum+=a; } } dst.at<uchar>(i,j)=(int)sum; } else{ cout<<"the type of image is not suitable!"<<endl;return ; } } } //边框像素补全 void My_Convolution::complete_image_transform(const Mat &image,Mat &dst){ if(!kernal_loaded){ cout<<"kernal is empty!"<<endl; return ; } dst=Mat::zeros(2*bios_y+image.rows,2*bios_x+image.cols,image.type());//初始化一个补全图像的大小。 Rect real_roi_of_image=Rect(bios_x,bios_y,image.cols,image.rows); Mat real_mat_of_image=dst(real_roi_of_image); image.copyTo(dst(real_roi_of_image)); }
My_Convolution类对外提供了load_kernal()和convolute()两个函数接口,分别用来加载卷积核和进行卷机操作。若在convolute操作之前没有加载卷积核,则会给出提醒。
当卷积核以边缘像素为基准点进行卷积操作的时候,会出现卷积核内部分的值无法在原图像上找到相互对应的像素点,因为原图必然有边界。这里采取的办法是,在加载卷积核之后,获取卷积核的尺寸,根据卷积核的尺寸适当地在原图像的边缘增加bios_x列和bios_y行的像素,因此补全之后带有原图信息的新的图像的尺寸为(2*bios_x+image.cols)*(2*bios_y+image.rows)。
得到补全的图像之后,使用一个2层的for循环来对原图像进行卷积操作,新产生的图像的尺寸要和原图像保持一致。
main函数里的测试代码:
#include <iostream> #include <opencv2/opencv.hpp> #include "my_convolution.h" using namespace std; using namespace cv; //高斯核构造函数 Mat Gaussian_kernal(int kernal_size, int sigma) { const double PI = 3.14159265358979323846; int m = kernal_size / 2; Mat kernal(kernal_size, kernal_size, CV_32FC1); float s = 2 * sigma*sigma; for (int i = 0; i < kernal_size; i++) { for (int j = 0; j < kernal_size; j++) { int x = i - m, y=j - m; kernal.ptr<float>(i)[j] = exp(-(x*x + y*y) / s) / (PI*s); } } return kernal; } //3*3均值卷积核 cv::Mat average_kernal_3 = (Mat_<float>(3,3) << 0.111, 0.111 ,0.111, 0.111, 0.111, 0.111, 0.111, 0.111, 0.111); //5*5均值卷积核 cv::Mat average_kernal_5 = (Mat_<float>(3,3) << 0.04, 0.04 ,0.04, 0.04, 0.04, 0.04, 0.04 ,0.04, 0.04, 0.04, 0.04, 0.04 ,0.04, 0.04, 0.04, 0.04, 0.04 ,0.04, 0.04, 0.04, 0.04, 0.04 ,0.04, 0.04, 0.04); //sobel边缘检测算子 cv::Mat sobel_y_kernal= (Mat_<float>(3,3) << -1, -2 ,-1, 0, 0 , 0, 1, 2 , 1); cv::Mat sobel_x_kernal= (Mat_<float>(3,3) << -1, 0 , 1, -2, 0 , 2, -1, 0 , 1); //prewitt边缘检测算子 cv::Mat prewitt_y_kernal= (Mat_<float>(3,3) << -1, -1 ,-1, 0, 0 , 0, 1, 1 , 1); cv::Mat prewitt_x_kernal= (Mat_<float>(3,3) << -1, 0 , 1, -1, 0 , 1, -1, 0 , 1); int main(){ My_Convolution myconvolution; Mat image=imread("lena.jpg"); imshow("src",image); Mat dst_prewitt; //高斯卷积 Mat dst_gaussian; myconvolution.load_kernal(Gaussian_kernal(7,2)); myconvolution.convolute(image,dst_gaussian); imshow("dst_gaussian",dst_gaussian); //均值3*3 Mat dst_aver_3; myconvolution.load_kernal(average_kernal_3); myconvolution.convolute(image,dst_aver_3); imshow("dst_aver_3",dst_aver_3); //均值5*5 Mat dst_aver_5; myconvolution.load_kernal(average_kernal_5); myconvolution.convolute(image,dst_aver_5); imshow("dst_aver_5",dst_aver_5); //sobel操作 Mat dst_sobel_x; Mat dst_sobel_y; myconvolution.load_kernal(sobel_x_kernal); myconvolution.convolute(image,dst_sobel_x); imshow("dst_sobel_x",dst_sobel_x); myconvolution.load_kernal(sobel_y_kernal); myconvolution.convolute(image,dst_sobel_y); imshow("dst_sobel_y",dst_sobel_y); //prewitt操作 Mat dst_prewitt_x; Mat dst_prewitt_y; myconvolution.load_kernal(prewitt_x_kernal); myconvolution.convolute(image,dst_prewitt_x); imshow("dst_prewitt_x",dst_prewitt_x); myconvolution.load_kernal(prewitt_y_kernal); myconvolution.convolute(image,dst_prewitt_y); imshow("dst_prewitt_y",dst_prewitt_y); waitKey(0); return 0; }
main函数里分别对两个尺寸的均值卷积和高斯卷积进行了测试,高斯卷积的尺寸和其中的参数可以使用Guassian_kernal()函数生成。这里因为程序中只给出了sobel和prewitt两个方向上的卷积核并只分别单纯地做了卷积运算,并没有根据sobel和prewitt算子的完整计算过程算出其卷积图像,如果想知道sobel边缘检测和prewitt边缘检测是什么效果的朋友可以自己试一试。
最后给出效果图:
src原图
dst_guassian——高斯
dst_aver_3——3*3均值
dst_aver_5——5*5均值
dst_prewitt_x——x方向prewitt算子
dst_prewitt_y——y方向prewitt算子
dst_sobel_x——x方向sobel算子
dst_sobel_y——y方向sobel算子