opencv 学习之 Sobel算子及cvSobel

时间:2021-02-10 09:52:12

    由于项目里要用到边缘检测,所以今天研究了一下最简单的梯度的方法。

    首先,我们来开一下计算机是如何检测边缘的。以灰度图像为例,它的理论基础是这样的,如果出现一个边缘,那么图像的灰度就会有一定的变化,为了方便假设由黑渐变为白代表一个边界,那么对其灰度分析,在边缘的灰度函数就是一个一次函数y=kx,对其求一阶导数就是其斜率k,就是说边缘的一阶导数是一个常数,而由于非边缘的一阶导数为零,这样通过求一阶导数就能初步判断图像的边缘了。通常是X方向和Y方向的导数,也就是梯度。理论上计算机就是通过这种方式来获得图像的边缘。

    但是,具体应用到图像中你会发现这个导数是求不了的,因为没一个准确的函数让你去求导,而且计算机在求解析解要比求数值解麻烦得多,所以就想到了一种替代的方式来求导数。就是用一个3×3的窗口来对图像进行近似求导。拿对X方向求导为例,某一点的导数为第三列的元素之和减去第一列元素之和,这样就求得了某一点的近似导数。其实也很好理解为什么它就近似代表导数,导数就代表一个变化率,从第一列变为第三列,灰度值相减,当然就是一个变化率了。这就是所谓的Prewitt算子。这样近似X方向导数就求出来了。Y方向导数与X方向导数求法相似,只不过是用第三行元素之和减去第一行元素之和。X方向和Y方向导数有了,那么梯度也就出来了。这样就可以找出一幅图中的边缘了。

    还有一个问题,由于求的是3×3中心点的导数,所以给第二列加了一个权重,它的权重为2,第一列和第三列的权重为1,好了,这就是Sobel算子了。相比Prewitt算子,Sobel的抗噪能力更强举个例子吧。

    X点以Sobel方式求导数ΔX=1×50+2×30+1×50-(1×50+2×30+1×50)=0。这样可以看出这个点不是边界。

   好了,了解了基本理论之后,我们看看OpenCv下的Sobel函数吧,void cvSobel( const CvArr* src, CvArr* dst, int xorder, int yorder, int aperture_size=3 );src:输入图像;dst:输出图像;xorder:x 方向上的差分阶数;yorder:y 方向上的差分阶数;aperture_size 扩展 Sobel 核的大小(既窗口阶数),必须是 1(注意这是一个3×1或1×3向量而不是一个方阵), 3, 5 或 7。

    下面编写一个Sobel边缘检测的程序吧,平台是VS08,建立Win32控制台应用程序。


#include <cv.h>
#include <highgui.h>

void main()
{
	IplImage *frame,*gray,*sobel;
	frame=cvLoadImage("lena.jpg");//加载图像
	gray=cvCreateImage(cvGetSize(frame),frame->depth,1);//分配图像空间
	sobel=cvCreateImage(cvGetSize(frame),frame->depth,1);
	cvNamedWindow("frame");
	cvNamedWindow("gray");
	cvNamedWindow("sobel");
	cvCvtColor(frame,gray,CV_BGR2GRAY);//转为灰度
	cvSobel(gray,sobel,1,0,3);
	cvShowImage("frame",frame);//显示图像
	cvShowImage("gray",gray);
	cvShowImage("sobel",sobel);
	cvWaitKey(0);//等待
	cvReleaseImage(&frame);//释放空间(对视频处理很重要,不释放会造成内存泄露)
	cvReleaseImage(&gray);
	cvReleaseImage(&sobel);
	cvDestroyWindow("frame");
	cvDestroyWindow("gray");
	cvDestroyWindow("sobel");
}

运行,你会发现出错,仔细看看没有问题啊。其实,这里是问题的,因为以Sobel方式求完导数后会有负值,还有会大于255的值而你建的Sobel的图像是 IPL_DEPTH_8U,也就是8位无符号数,所以Sobel建立的图像位数不够,要16位有符号的,也就是 IPL_DEPTH_16S。把建立图像这句改为

sobel=cvCreateImage(cvGetSize(frame),IPL_DEPTH_16S,1);运行,发现不报错了,但是Sobel图像显示不出来,这是什么原因呢?原来图像显示是以8位无符号显示的,现在是16位有符号,当然显示会出问题了。所以还要将Sobel转为8位无符号。OpenCv里提供了一个函数,就是cvConvertScaleAbs( const CvArr* src, CvArr* dst, double scale=1, double shift=0 );src:源图像;dst:目标图像;scale:转化前乘的系数;shift转化前加的系数。这样新建一个无符号图像再转换就可以实现了。

IplImage *sobel8u=cvCreateImage(cvGetSize(sobel),IPL_DEPTH_8U,1);

再在显示图像前加上cvConvertScaleAbs(sobel,sobel8u,1,0);这样就可以看到cvSobel的效果了。可以看X方向或Y方向求导是什么效果。

为了方便大家,我把改好后的程序也放上来了。


#include <cv.h>
#include <highgui.h>

void main()
{
	IplImage *frame,*gray,*sobel;
	frame=cvLoadImage("d:\\picture\\01.bmp");//加载图像
	gray=cvCreateImage(cvGetSize(frame),frame->depth,1);//分配图像空间
	sobel=cvCreateImage(cvGetSize(frame),IPL_DEPTH_16S,1);
	cvNamedWindow("frame");
	cvNamedWindow("gray");
	cvNamedWindow("sobel");
	cvCvtColor(frame,gray,CV_BGR2GRAY);//转为灰度
	cvSobel(gray,sobel,1,0,3);
 
	IplImage *sobel8u=cvCreateImage(cvGetSize(sobel),IPL_DEPTH_8U,1);
	cvConvertScaleAbs(sobel,sobel8u,1,0);
	cvShowImage("frame",frame);//显示图像
	cvShowImage("gray",gray);
	cvShowImage("sobel",sobel8u);
	cvWaitKey(0);//等待
	cvReleaseImage(&frame);//释放空间(对视频处理很重要,不释放会造成内存泄露)
	cvReleaseImage(&gray);
	cvReleaseImage(&sobel);
	cvDestroyWindow("frame");
	cvDestroyWindow("gray");
	cvDestroyWindow("sobel");
}



下面一段代码是对 sobel 的优化,忘记从哪拷贝的

#define SOBEL_HORZ  1
#define SOBEL_VERT  2
#define SOBEL_BOTH  4

bool dzSobel(const Mat& image, Mat& result, int TYPE)
{
	if(image.channels()!=1) 	return false;
	
	int kx(0), ky(0);		// 系数设置
	if( TYPE==SOBEL_HORZ )
	{
		kx=0;ky=1;
	}
	else if( TYPE==SOBEL_VERT )
	{
		kx=1;ky=0;
	}
	else if( TYPE==SOBEL_BOTH )
	{
		kx=1;ky=1;
	}
	else	return false;

	// 设置mask
	float mask[3][3]={{1,2,1},{0,0,0},{-1,-2,-1}};
	Mat y_mask=Mat(3,3,CV_32F,mask)/8;
	Mat x_mask=y_mask.t(); // 转置

	// 计算x方向和y方向上的滤波
	Mat sobelX,sobelY;
	filter2D(image,sobelX,CV_32F,x_mask);
	filter2D(image,sobelY,CV_32F,y_mask);
	sobelX=abs(sobelX);
	sobelY=abs(sobelY);
	// 梯度图
	Mat gradient=kx*sobelX.mul(sobelX)+ky*sobelY.mul(sobelY);

	// 计算阈值
	int scale=1;
	double cutoff=scale*mean(gradient)[0];

	result.create(image.size(),image.type());
	result.setTo(0);
	for(int i=1;i<image.rows-1;i++)
	{
		float* sbxPtr=sobelX.ptr<float>(i);
		float* sbyPtr=sobelY.ptr<float>(i);
		float* prePtr=gradient.ptr<float>(i-1);
		float* curPtr=gradient.ptr<float>(i);
		float* lstPtr=gradient.ptr<float>(i+1);
		uchar* rstPtr=result.ptr<uchar>(i);
		
		for(int j=1;j<image.cols-1;j++)		// 阈值化和极大值抑制
		{
			if( curPtr[j]>cutoff && (
				(sbxPtr[j]>kx*sbyPtr[j] && curPtr[j]>curPtr[j-1] && curPtr[j]>curPtr[j+1]) ||
				(sbyPtr[j]>ky*sbxPtr[j] && curPtr[j]>prePtr[j] && curPtr[j]>lstPtr[j]) ))
				rstPtr[j]=255;
		}
	}
	return true;
}


opencv 学习之 Sobel算子及cvSobel