【OpenCV图像处理】二十二、图像边缘检测(上)

→图像边缘检测的目的是检测邻域内灰度明显变化的像素,常用一阶差分和二阶差分来进行边缘检测

→数字图像中的边缘是由邻域内灰度值明显变化的像素构成,边缘检测主要是图像灰度的度量检测和定位

→图像的边缘有方向幅值两个属性,沿边缘方向像素灰度值变化平缓或不发生变化,而垂直于边缘方向像素灰度值变化剧烈

→需要理解的是,边缘是灰度值变化的产物,可以利用差分来检测这种不连续性,边缘检测方法大致可以分为两类:

(1)基于一阶差分的方法

(2)基于二阶差分的方法

→一阶差分算子通过寻找一阶差分中的最大值来检测边缘,将边缘定位在一阶差分最大的方向

→二阶差分算子通过寻找二阶差分过零点来定位边缘,常用的有Laplace过零点等


在实际图像中,图像的边缘部分通常具有一定的斜坡面,斜坡部分与边缘模糊程度成比例

→边缘的宽度取决于其是灰度值到终止灰度值的斜面长度,而斜面长度取决于模糊程度


灰度不连续检测(间断检测)是最普遍的边缘检测方法:

在边缘检测中,梯度算子是常用的一阶差分算子,用于检测图像中边缘的存在和强度,Laplace算子是常用的二阶差分算子,二阶差分在边缘亮的一边符号为负,在边缘暗的一边符号为正。而在边缘处,二阶差分穿过零点


应该注意的是,差分操作对噪声十分敏感

→对于有噪边缘,利用差分检测图像边缘将放大噪声的影响,因此在利用差分进行边缘检测时,应该谨慎的考虑噪声的影响,通常在进行边缘检测之前对图像进行去噪或降噪处理

→利用一阶差分模板检测图像边缘实际上是利用一种局部处理的方法,当某一像素邻域内的灰度值的一阶差分大于阈设值时,则判定该像素为边缘像素。

→利用一阶差分模板提取的边缘图像是由许多不连续的边缘组成,这些边缘像素勾画出各个物体的轮廓,但是不能形成图像分割所需的闭合且连通的边界。

边缘连接:

是指将邻近的边缘像素连接起来,从而产生一条闭合且连通边缘的过程。根据事先定义的链接准则,对这样的边缘像素进行边缘连接处理,天不由于噪声和阴影造成的间断。

→利用二阶差分模板检测图像边缘实际上是寻找二阶差分过零点,二阶差分模板产生连续的边缘,但是不能保证检测的边缘是准确的,只能检测边缘的大致形状。


下面首先介绍一阶差分算子,大致可以分为两类,分别是梯度算子方向算子

(1)梯度算子

梯度算子定义在二维一阶导数的基础上,在数字图像中,由于像素是离散的,因此常用差分来近似偏导数

→对于一幅数字图像f(x,y),在像素(x,y)处梯度定义为:

向量


其中,Gx(x, y)和Gy(x, y)分别表示x(垂直)和y(水平)方向上的一阶差分

在边缘检测中,一个重要的量是梯度的幅度,用公式可以表示为


上式中,mag{·}表示求幅度的函数,但是为了降低复杂度,避免进行平方和开放运算,所以求幅度的公式通常写为如下形式:


使用上面这种绝对和计算简单而且保持了灰度的相对变化

→但是,也导致了梯度算子不具备各向同性→也就是不具备旋转不变性,梯度的方向指向像素值f(x, y)在(x,y)处增加最快的方向。

梯度关于x轴的角度为:


上式表示的是像素(x,y)处梯度关于x轴的方向角,这一点的梯度方向和该点边缘方向垂直


→梯度算子检测灰度值变化的两个属性是灰度值的变化率和方向,分别用幅度和方向来表示

→梯度的计算需要在每一个像素位置计算两个方向的一阶差分,常用的一阶差分算子有Robberts,Prewitt和Sobel算子等

在介绍各个算子之前,先给出一个3x3像素领域的定义,如下图所示:

【OpenCV图像处理】二十二、图像边缘检测(上)_第1张图片


首先简单介绍一下Robberts交叉算子,这是最简单的梯度算子,

在中心像素x(垂直)方向上,一阶差分计算式为

Gx = z9 - z5

在中心像素y(水平)方向上,一阶差分计算式为

Gy = z8 - z6

需要说明的是,Robbert交叉算子没有固定的中心点,因此不能使用2x2的模板卷积来实现

对于原始图像f(x,y),Roberts边缘检测输出图像为g(x,y),图像的Roberts边缘检测可以用下式来表示:


具体实现程序如下所示:

//实现Roberts边缘检测
#include 
#include 
#include 
#include 

using namespace cv;
using namespace std;

//函数:Roberts算子实现
Mat Roberts(Mat srcImage);

int main()
{
	Mat srcImage = imread("2345.jpg",0);
	if (!srcImage.data)
	{
		cout << "读入图片错误!" << endl;
		system("pause");
		return -1;
	}
	imshow("原图像", srcImage);
	Mat dstImage = Roberts(srcImage);
	imshow("Roberts边缘检测图像", dstImage);
	waitKey();
	return 0;
}

//函数:Roberts算子实现
Mat Roberts(Mat srcImage)
{
	Mat dstImage = srcImage.clone();
	int rowsNum = srcImage.rows;
	int colsNum = srcImage.cols;
	for (int i = 0; i < rowsNum - 1; i++)
	{
		for (int j = 0; j < colsNum - 1; j++)
		{
			//根据公式进行计算
			int t1 = (srcImage.at(i, j) -
				srcImage.at(i + 1, j + 1))*
				(srcImage.at(i, j) -
				srcImage.at(i + 1, j + 1));
			int t2 = (srcImage.at(i + 1, j) -
				srcImage.at(i, j + 1))*
				(srcImage.at(i + 1, j) -
				srcImage.at(i, j + 1));
			//计算对角线像素差
			dstImage.at(i, j) = (uchar)sqrt(t1 + t2);
		}
	}
	return dstImage;
}

程序执行后的结果如下所示:

【OpenCV图像处理】二十二、图像边缘检测(上)_第2张图片


→在实际使用中,常常使用Prewitt算子和Sobel算子来当做梯度算子。


下面首先介绍利用图像差分运算进行边缘检测,然后再分别介绍各种边缘检测算子,包括一阶算子和二阶算子


(1)图像差分运算:

对于原函数f(u),积分运算使计算f(u)映射到 f(u+a) - f(u+b)的值,差分运算分为前向差分和后向差分,一阶前向差分是指Δf = f(u+1) - f(u),一阶逆向差分是指Δf = f(u) - f(u-1)

二维离散图像f(x,y)在x方向的一阶差分定义为Δfx = f(x+1, y) - f(x, y),y方向的一阶差分定义为Δfy = f(x, y+1) - f(x, y)

差分运算通过求图像灰度变化剧烈处的一阶微分算子的极值来检测奇异点,通过奇异点的值进一步设定阈值就可以得到边缘二值化图像。

差分边缘检测中差分的水平或垂直方向都与边缘方向正交,因此在实际应用场景中,常常将边缘检测分为水平边缘,垂直边缘和对角线边缘,差分边缘检测定义方向模板如下所示:

【OpenCV图像处理】二十二、图像边缘检测(上)_第3张图片         【OpenCV图像处理】二十二、图像边缘检测(上)_第4张图片       【OpenCV图像处理】二十二、图像边缘检测(上)_第5张图片

垂直边缘 水平边缘 对角线边缘


利用OpenCV实现差分边缘检测实例如下所示:

//实现差分边缘检测
#include 
#include 
#include 
#include 

using namespace cv;
using namespace std;

//图像差分函数
void diffOperation(const Mat srcImage, Mat &edgeXImage, Mat &edgeYImage);

int main()
{
	Mat srcImage = imread("2345.jpg", 0);
	if (!srcImage.data)
	{
		cout << "读入图片错误!" << endl;
		system("pause");
		return -1;
	}
	imshow("原图像", srcImage);
	Mat edgeXImage(srcImage.size(), srcImage.type());
	Mat edgeYImage(srcImage.size(), srcImage.type());

	//计算差分图像
	diffOperation(srcImage, edgeXImage, edgeYImage);
	imshow("垂直方向差分图像", edgeXImage);
	imshow("水平方向差分图像", edgeYImage);
	Mat edgeImage(srcImage.size(), srcImage.type());
	//将水平与垂直边缘进行叠加
	addWeighted(edgeXImage, 0.5, edgeYImage, 0.5, 0.0, edgeImage);
	imshow("水平和垂直方向边缘", edgeImage);
	waitKey();
	return 0;
}

void diffOperation(const Mat srcImage, Mat &edgeXImage, Mat &edgeYImage)
{ 
	Mat tempImage = srcImage.clone();
	int rowsNum = tempImage.rows;
	int colsNum = tempImage.cols;
	for (int i = 0; i < rowsNum - 1; i++)
	{
		for (int j = 0; j < colsNum - 1; j++)
		{
			//计算垂直边缘
			edgeXImage.at(i, j) =
				abs(tempImage.at(i + 1, j) - tempImage.at(i, j));
			//计算水平边缘
			edgeYImage.at(i, j) =
				abs(tempImage.at(i, j + 1) - tempImage.at(i, j));
		}
	}
}

执行程序后,效果如下图所示:

【OpenCV图像处理】二十二、图像边缘检测(上)_第6张图片

【OpenCV图像处理】二十二、图像边缘检测(上)_第7张图片


(2)非极大值抑制

在介绍其余的边缘检测算子之前,首先简单介绍一下图像中的非极大值抑制。

图像梯度矩阵中的元素值越大,说明图像中该点的梯度值越大,但是这并不能将它判断为该点处的边缘。

非极大值抑制操作可以提出为伪边缘信息,被广泛应用于图像边缘检测中,其原理是通过像素邻域的局部最优值,将非极大值点所对应的灰度值设置为背景像素点,如像素邻域区域满足梯度值局部最优值则判断为该像素的边缘,对其余非极大值的相关信息进行抑制,利用这个准则可以剔除大部分非边缘点。


(3)Sobel边缘检测算子

Sobel算子是应用广泛的离散微分算子之一,常常用于图像处理中的边缘检测,计算图像灰度函数的近似梯度。利用图像像素点Sobel算子计算出相应的地图向量及向量的范数,基于图像卷积来实现在水平方向与垂直方向检测对应方向的边缘。对于原图像与奇数Sobel水平核Gx,垂直核Gy进行卷积可计算水平与垂直变换,当内核大小为3x3时,Gx与Gy为下式:


对图像中没一点结合卷积后的结果求出近似梯度幅度G:

G= √(Gx^2 + Gy^2)

Sobel算子在进行边缘检测时效率较高,当对精度要求不是很高时,是一种比较常用的边缘检测方法。Sobel算子对于沿着x轴和y轴的排列的边缘表示的较好,但是对与其他角度的表示取不够精确,这时候可以使用Scharr滤波器。这种滤波器的水平与垂直的核因子如下:


首先给出使用OpenCV中的自带函数库实现Sobel边缘检测,

在OpenCV中实现相关功能使用函数Sobel(),函数的声明和说明如下所示:

void Sobel( InputArray src, OutputArray dst, int ddepth,int dx, int dy, int ksize=3,
                         double scale=1, double delta=0, int borderType=BORDER_DEFAULT );

第一个和第二个参数分别为源图像和输出图像

第三个参数为int类型的ddepth,为输出图像的深度,支持src.depth()和ddepth的组合:

(·若src.depth() = CV_8U,则取ddepth = -1 / CV_16S / CV_32F / CV_64F

(·若src.depth() = CV_16U / CV_16S ,则ddepth = -1 /CV_32F / CV_64F

(·若src.depth() = CV_32F,取ddepth = -1 /CV_32F / CV_64F

(·若src.depth() = CV_64F,取ddepth = -1 / CV_64F

第四个参数为int类型的dx,为x方向上的差分阶数

第五个参数为int类型的dy,为y方向上的差分阶数

第六个参数为int类型的ksize,有默认值3,表示Sobel核的大小,需要注意的是,Sobel算子的内核大小必须取 1,3,5,7。

第七个参数是double类型的scale,计算导数值是可选的缩放因子,默认值为1,表示默认的情况下不进行放缩操作。可以查阅getDeriveKernels查看相关内容

第八个参数是double类型的delta,表示在结果存入目标图(dst)之前可选的delta值,有默认值0

第九个参数是int类型的bordertype,有默认值BORDER_DEFAULT,在之前的文章中有过这个参数的相关介绍,在这里不再赘述。



具体程序如下:

//使用OpenCV自带函数实现Sobel边缘检测
#include 
#include 
#include 
#include 

using namespace cv;
using namespace std;

int main()
{
	Mat srcImage = imread("2345.jpg");
	if (!srcImage.data)
	{
		cout << "读入图片错误!" << endl;
		system("pause");
		return -1;
	}
	Mat srcGray;
	cvtColor(srcImage, srcGray, CV_BGR2GRAY);
	imshow("原图像灰度图", srcGray);
	//定义边缘图,水平和垂直
	Mat edgeImage, edgeImageX, edgeImageY;
	//求x方向Sobel边缘
	Sobel(srcGray, edgeImageX, CV_16S, 1, 0, 3, 1, 0);
	//求y方向的Sobel算子
	Sobel(srcGray, edgeImageY, CV_16S, 0, 1, 3, 1);
	//线性变换,转换输入数组元素为8位无符号整型
	convertScaleAbs(edgeImageX, edgeImageX);
	convertScaleAbs(edgeImageY, edgeImageY);
	//x与y方向进行叠加
	addWeighted(edgeImageX, 0.5, edgeImageY, 0.5, 0, edgeImage);
	imshow("Sobel边缘图像", edgeImage);

	//定义scharr边缘图像
	Mat edgeXImageS, edgeYImageS, edgeImageS;
	//计算X方向的Scharr边缘
	Scharr(srcGray, edgeXImageS, CV_16S, 1, 0, 1, 0);
	convertScaleAbs(edgeXImageS, edgeXImageS);
	//计算Y方向的Scharr边缘
	Scharr(srcGray, edgeYImageS, CV_16S, 0, 1, 1, 0);
	convertScaleAbs(edgeYImageS, edgeYImageS);
	//x与y方向进行边缘叠加
	addWeighted(edgeXImageS, 0.5, edgeYImageS, 0.5,0,edgeImageS);
	imshow("Scharr边缘图像", edgeImageS);
	waitKey();
	return 0;
}

执行后的结果如下所示:

【OpenCV图像处理】二十二、图像边缘检测(上)_第8张图片【OpenCV图像处理】二十二、图像边缘检测(上)_第9张图片

可以看出,Scharr能够保持更多更好的图像边缘部分。

                                                                                                                                                                

下面介绍不使用OpenCV中的函数库实现Sobel边缘检测

(a.非极大值抑制Sobel边缘检测

→非极大值抑制Sobel边缘检测实现步骤主要如下:

(①.将图像转换为32位浮点型数据,定义水平或垂直方向的Sobel算子

(②.利用filter2D完成图像与算子的卷积操作,计算卷积结果的梯度幅值

(③.自适应计算出梯度幅度阈值,阈值设置不大于梯度幅值的均值乘以4,根据与之对水平或垂直的邻域区域梯度进行比较。

(④.判断当前邻域梯度是否大于水平或垂直邻域梯度,自适应完成边缘检测处二值图像的操作。

对上面这个过程进行程序实现如下所示:

//图像非极大值抑制实现Sobel竖直细化边缘
bool SobelVerEdge(Mat srcImage, Mat &dstImage)
{
	CV_Assert(srcImage.channels() == 1);
	srcImage.convertTo(srcImage, CV_32FC1);
	//水平方向的Sobel算子
	Mat sobelX = (Mat_(3, 3) << -0.125, 0, 0.125,
		-0.25, 0, 0.25,
		-0.125, 0, 0.125);
	Mat ConResMat;
	//卷积运算
	filter2D(srcImage, ConResMat, srcImage.type(), sobelX);
	//计算梯度的幅度
	Mat gradMagMat;
	multiply(ConResMat, ConResMat, gradMagMat);
	//根据梯度幅度及参数设置阈值
	int scaleVal = 4;
	double thresh = scaleVal*mean(gradMagMat).val[0];
	Mat resultTempMat = Mat::zeros(gradMagMat.size(), gradMagMat.type());
	float *pDataMag = (float*)gradMagMat.data;
	float *pDataRes = (float*)resultTempMat.data;
	const int rowsNum = ConResMat.rows;
	const int colsNum = ConResMat.cols;
	for (int i = 1; i != rowsNum - 1; ++i)
	{
		for (int j = 1; j != colsNum - 1; ++j)
		{
			//计算这一点的梯度与水平或垂直梯度值的大小并比较结果
			bool b1 = (pDataMag[i*colsNum + j] > pDataMag[i*colsNum + j - 1]);
			bool b2 = (pDataMag[i*colsNum + j] > pDataMag[i*colsNum + j + 1]);
			bool b3 = (pDataMag[i*colsNum + j] > pDataMag[(i - 1)*colsNum + j]);
			bool b4 = (pDataMag[i*colsNum + j] > pDataMag[(i + 1)*colsNum + j]);
			//判断邻域梯度是否满足大于水平或垂直梯度的条件
			//并根据自适应阈值参数进行二值化
			pDataRes[i*colsNum + j] = 255 * ((pDataMag[i*colsNum + j] > thresh)
				&& ((b1 && b2) || (b3 && b4)));
		}
	}
	resultTempMat.convertTo(resultTempMat, CV_8UC1);
	dstImage = resultTempMat.clone();
	return true;
}
(b.图像直接卷积实现Sobel

图像直接卷积Sobel边缘检测实现比较简单,首先定义水平或垂直方向的Sobel核因子,直接对源图像进行窗遍历,计算窗口内的邻域梯度幅值;然后根据梯度模长进行二值化操作,完成图像水平或垂直方向的边缘检测

图像直接卷积Sobel边缘实现代码如下所示:

//图像直接卷积实现Sobel
bool SobelEdge(const Mat &srcImage, Mat &dstImage, uchar threshold)
{
	CV_Assert(srcImage.channels() == 1);
	//初始化水平核因子
	Mat sobelX = (Mat_(3, 3) << 1, 0, -1,
		2, 0, -2,
		1, 0, -1);
	//初始化垂直核因子
	Mat sobelY = (Mat_(3, 3) << 1, 2, 1,
		0, 0, 0,
		-1, -2, -1);
	dstImage = Mat::zeros(srcImage.rows - 2, srcImage.cols - 2, srcImage.type());
	double edgeX = 0;
	double edgeY = 0;
	double graMag = 0;
	for (int k = 1; k < srcImage.rows - 1; ++k)
	{
		for (int n = 1; n < srcImage.cols - 1; ++n)
		{
			edgeX = 0;
			edgeY = 0;
			//遍历计算水平与垂直梯度
			for (int i = -1; i <= 1; i++)
			{
				for (int j = -1; j <= 1; j++)
				{
					edgeX += srcImage.at(k + i, n + j)*
						sobelX.at(1 + i, 1 + j);
					edgeY += srcImage.at(k + i, n + j)*
						sobelY.at(1 + i, 1 + j);
				}
			}
			//计算梯度模长
			graMag = sqrt(pow(edgeY, 2) + pow(edgeX, 2));
			//进行二值化
			dstImage.at(k - 1, n - 1) =
				((graMag > threshold) ? 255 : 0);
		}
	}
	return true;
}


(c.图像卷积下非极大值抑制Sobel

图像卷积下非极大值抑制Sobel边缘检测的实现过程与之前的a部分比较类似。但是需要说明的是,非极大值抑制虽然能够较好的剔除虚假边缘点,但是对于某些特定场景下的边缘检测并不起作用,例如无损文本字符识别

相关的实现代码如下所示:


//图像卷积实现Sobel非极大值抑制
bool sobelOptaEdge(const Mat &srcImage, Mat &dstImage, int flag)
{
	CV_Assert(srcImage.channels() == 1);
	//初始化Sobel水平核因子
	Mat sobelX = (Mat_(3, 3) << 1, 0, -1,
		2, 0, -2,
		1, 0, -1);
	//初始化Sobel垂直核因子
	Mat sobelY = (Mat_(3, 3) << 1, 2, 1,
		0, 0, 0,
		-1, -2, -1);
	//计算水平与垂直卷积
	Mat edgeX, edgeY;
	filter2D(srcImage, edgeX, CV_32F, sobelX);
	filter2D(srcImage, edgeY, CV_32F, sobelY);
	//根据传入的参数确定计算水平或垂直方向的边缘
	int paraX = 0;
	int paraY = 0;
	switch (flag)
	{
	case 0:	
		paraX = 1;
		paraY = 0;
		break;
	case 1:
		paraX = 0;
		paraY = 1;
		break;
	case 2:
		paraX = 1;
		paraY = 1;
		break;
	default:
		break;
	}
	edgeX = abs(edgeX);
	edgeY = abs(edgeY);
	Mat graMagMat = paraX*edgeX.mul(edgeX) +
		paraY*edgeY.mul(edgeY);
	//计算阈值
	int scaleVal = 4;
	double thresh = scaleVal*mean(graMagMat).val[0];
	dstImage = Mat::zeros(srcImage.size(), srcImage.type());
	for (int i = 1; i < srcImage.rows - 1; i++)
	{
		float *pDataEdgeX = edgeX.ptr(i);
		float *pDataEdgeY = edgeY.ptr(i);
		float *pDataGraMag = graMagMat.ptr(i);
		//阈值化和极大值抑制
		for (int j = 1; j < srcImage.cols - 1; j++)
		{
			//判断当前邻域梯度是否大于阈值与大于水平或垂直梯度
			if (pDataGraMag[j]>thresh &&
				(pDataEdgeX[j]>paraX*pDataEdgeY[j] &&
				pDataGraMag[j] > pDataGraMag[j - 1] &&
				pDataGraMag[j] > pDataGraMag[j + 1] ||
				(pDataEdgeY[j] > paraY*pDataEdgeX[j] &&
				pDataGraMag[j] > pDataGraMag[j - 1] &&
				pDataGraMag[j] > pDataGraMag[j + 1])))
				dstImage.at = 255;
		}
	}
	return true;
}

你可能感兴趣的:(OpenCV,图像处理)