Sobel变换和拉普拉斯变换都是高通滤波器。什么是高通滤波器呢?就是保留图像的高频分量(变化剧烈的部分),抑制图像的低频分量(变化缓慢的部分)。而图像变化剧烈的部分,往往反应的就是图像的边沿信息了。
在OpenCV中,调用sobel函数很简单:
Mat image = imread("D:/picture/images/boldt.jpg",0); if(!image.data) return -1; imshow("源图像",image); Mat sobelX; //参数为:源图像,结果图像,图像深度,x方向阶数,y方向阶数,核的大小,尺度因子,增加的值 Sobel(image,sobelX,CV_8U,1,0,3,0.4,128); imshow("X方向Sobel结果",sobelX); Mat sobelY; Sobel(image,sobelY,CV_8U,0,1,3,0.4,128); imshow("Y方向Sobel结果",sobelY);
注意到,这里对sobel的结果进行了一些尺度变换来更好的显示。
有一点需要特别注意:
由于是对X方向求导,sobelX保留了很多垂直方向的信息,所以垂直的轮廓“看起来更加清楚”;y方向同理。
按照数学推倒,应该是把两个方向的值平方以后相加在开方(2范数),得到梯度。而实际上,为了简化运算,我们直接把他们的绝对值相加(1范数)得出梯度:
//合并结果 Mat sobel; Sobel(image,sobelX,CV_32F,1,0); Sobel(image,sobelY,CV_32F,0,1); //计算1范数 sobel= abs(sobelX)+abs(sobelY); double sobmin,sobmax; minMaxLoc(sobel,&sobmin,&sobmax); //转换为8比特,进行尺度变换 Mat sobelImage; sobel.convertTo(sobelImage,CV_8U,-255./sobmax,255); imshow("结果",sobelImage); threshold(sobelImage, sobelImage, 190, 255, cv::THRESH_BINARY); imshow("最终结果",sobelImage);
注意:由于运算结果有正有负,所以这里没有使用CV_8U类型,而是CV_32F类型。
如果你想精确的计算梯度,不但有大小,还有方向,你可以这样做:
Mat norm,dir; //计算L2范数和方向 cartToPolar(sobelX,sobelY,norm,dir);
拉普拉斯变换是对x和y方向求2阶偏导数,然后加起来。
他当在图像边沿作用时(例如,从暗到亮)我们可以观察到灰度值的上升必然意味着从正曲度(强度升高)到负曲度(强度达到瓶颈)的变化。因此,拉普拉斯变换结果从正到负(或者相反)组成了一个图像边沿的很好的指示器。另一种方法表达这个事实是说,边沿出现在拉普拉斯变换的过零点处。
OpenCV中计算拉普拉斯变换也比较容易:
//直接计算 Mat laplace; //变换的结果*1+128 Laplacian(image,laplace,CV_32F,7,1,128); imshow(" 直接使用的结果",laplace); //计算一个小窗口内的拉普拉斯变换的值 for(int i = 0; i < 12;i++) { for(int j = 0; j < 12; j++) { //由于前面的变换中加了128,所以这里要减去128 cout<<setw(5)<<static_cast<int>(laplace.at<float>(i+135,j+362))-128<<" "; } cout<<endl; } cout<<endl; cout<<endl; cout<<endl;
为了更好的说明拉普拉斯变换的作用,我们先定义1个类:
#if !defined LAPLACEZC #define LAPLACEZC #include <opencv2/core/core.hpp> #include <opencv2/highgui/highgui.hpp> #include <opencv2/imgproc/imgproc.hpp> using namespace cv; class LaplacianZC { private: //源图像 Mat img; //拉普拉斯变换的结果,32比特 Mat laplace; //拉普拉斯核的大小 int aperture; public: //构造函数 LaplacianZC():aperture(3){} //设置核的大小 void setAperture(int a) { aperture = a; } //计算拉普拉斯变换 Mat computeLaplacian(const Mat &image) { //计算拉普拉斯变换 Laplacian(image,laplace,CV_32F,aperture,1,0); //保留副本 img = image.clone(); return laplace; } //获取变换后的图像 Mat getLaplacianImage(double scale = -1.0) { double lapmin, lapmax; if(scale < 0) { //获取变换的最大值和最小值 minMaxLoc(laplace,&lapmin,&lapmax); scale = 127/ std::max(-lapmin,lapmax); } Mat laplaceImage; laplace.convertTo(laplaceImage,CV_8U,scale,128); return laplaceImage; } //获得过零点的2值图像 Mat getZeroCrossings(float threshold = 1.0) { //第二行第一个元素 Mat_<float>::const_iterator it = laplace.begin<float>()+laplace.step1(); //最后一个元素 Mat_<float>::const_iterator itend = laplace.end<float>(); //第一行第一个元素 Mat_<float>::const_iterator itup = laplace.begin<float>(); //2值图像初始化为白色 Mat binary(laplace.size(),CV_8U,Scalar(255)); Mat_<uchar>::iterator itout = binary.begin<uchar>()+binary.step1(); //使得门限无效 threshold *= -1.0; for(;it != itend; ++it,++itup,++itout) { //如果相邻像个像素的积为负,这里的符号就发生了变化 if(*it * *(it-1) < threshold) *itout = 0; else if(*it * *(itup) < threshold ) *itout = 0; } return binary; } }; #endif
我们选择一个小的区域重点观测:
先看看主函数:
//使用LaplacianZC类计算拉普拉斯变换 LaplacianZC laplacian; laplacian.setAperture(7); //变换后的结果有正有负,小于0的设为0,大于255的设为255,效果很差 Mat flap = laplacian.computeLaplacian(image); double lpmin,lpmax; //获取变换后的最大值和最小值 minMaxLoc(flap,&lpmin,&lpmax); cout<<"拉普拉斯变换后的范围为:"<<"["<<lpmin<<" ,"<<lpmax<<"]"<<endl; cout<<endl; cout<<endl; cout<<endl; laplace = laplacian.getLaplacianImage(); imshow("Laplacian Image",laplace); //打印窗口内拉普拉斯变换的值 for(int i = 0; i < 12; i++) { for(int j = 0; j < 12; j++) { cout<<setw(5)<<static_cast<int>(flap.at<float>(i+135,j+362))/100 << " "; } cout<<endl; } cout<<endl; //计算和显示过零点 Mat zeros = laplacian.getZeroCrossings(lpmax); imshow("过零点",zeros);
打印消息中对每个点的拉普拉斯变换除以100仅仅因为计算的结果太大,如果正常显示,无法对齐。但这并不影响过零点(因为他只用考虑正负号)
而过零点是:
这恰恰就是那个小窗口中的塔楼的边沿!