我的OpenCV学习笔记(18):使用Sobel变化和拉普拉斯变换检测图像边沿

时间:2021-09-05 20:05:40

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


我们选择一个小的区域重点观测:

我的OpenCV学习笔记(18):使用Sobel变化和拉普拉斯变换检测图像边沿

先看看主函数:

	//使用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仅仅因为计算的结果太大,如果正常显示,无法对齐。但这并不影响过零点(因为他只用考虑正负号)

而过零点是:

我的OpenCV学习笔记(18):使用Sobel变化和拉普拉斯变换检测图像边沿

这恰恰就是那个小窗口中的塔楼的边沿!