数字图像处理第三章边缘检测(Sobel算子、Laplace算子)

边缘

边缘(edge)是指图像局部强度变化最显著的部分。主要存在于目标与目标、目标与背景、区域与区域(包括不同色彩)之间,是图像分割、纹理特征和形状特征等图像分析的重要基础。

边缘检测概念

1、边缘检测的目的:检测识别出图像中亮度变化剧烈的像素点构成的集合,边缘检测大大减少了源图像的数据量,剔除了与目标不相干的信息,保留了图像重要的结构属性。
2、目标物体形成边缘存在的情形:
(1)目标物呈现在图像的不同物体平面上,深度不连续。
(2)目标物本身平面不同,表面方向不连续
(3)目标物材料不均匀,表面反射光不同
(4)目标物受外部场景光影响不一
3、边缘检测的原理:利用图像边缘的突变性质来检测边缘。
4、边缘检测类型
(1)一阶微分为基础的边缘检测,通过计算图像的梯度值来检测图像边缘,如Sobel算子,Prewitt算子,Roberts算子及差分边缘检测。
(2)二阶微分为基础的边缘检测,通过寻求二阶导数中的过零点来检测边缘,如拉普拉斯算子,Canny算子边缘检测。
(3)混合一阶与二阶微分为基础的边缘检测,综合利用一阶微分与二阶微分特征,如Marr-Hildreth边缘检测算子。

图像强度的显著变化可分为:

阶跃变化函数,即图像强度在不连续处的两边的像素灰度值有着显著的差异;
线条(屋顶)变化函数,即图像强度突然从一个值变化到另一个值,保持一较小行程后又回到原来的值。
图像的边缘有方向和幅度两个属性,沿边缘方向像素变化平缓,垂直于边缘方向像素变化剧烈.边缘上的这种变化可以用微分算子检测出来,通常用一阶或二阶导数来检测边缘。

在这里插入图片描述

(a)(b)分别是阶跃函数和屋顶函数的二维图像;(c)(d)是阶跃和屋顶函数的函数图象;(e)(f)对应一阶倒数;(g)(h)是二阶倒数。

3.7.1梯度算子(Sobel算子)

1、梯度算子

1、处理图像时常用差分代替微分运算;对于数字图像简单一阶微分运算,由于其具有固定的方向性,只能检测特定的某一方向的边缘,所以不具有的普遍性。为了克服这种缺点,我们定义图像的梯度为梯度算子,它是图像处理中最常用的一阶微分算法。图像中不连续的灰度值会产生边缘。
2、图像梯度最重要的性质是–梯度的方向在图像灰度最大变化率上,它恰好可以反映图像边缘上的灰度变化。
3、图像中不连续的灰度值会产生边缘,图像梯度将在边缘处取得极大值的特性进行检测。
4、梯度算子总是指向变换最剧烈的方向,在图像处理中,梯度向量总是与边缘正交。
5、梯度是一个矢量,具有方向和大小模:
1、方向提供了边缘的趋势信息,梯度方向始终与边缘正交。
2、梯度模值大小提供边缘的强度信息。
在这里插入图片描述
在这里插入图片描述

2.非极大值抑制

1.目的
非极大值抑制可以剔除伪边缘信息:如果图像梯度矩阵中的元素值越大,说明图像中该点的梯度值越大,但这个点有可能不是极大值,被误判为边缘。
2.原理
通过像素领域的局部最优值,将非极大值点多对应的灰度值设置为背景像素点,如果像素领域区域满足梯度值局部最优解则判断为该像素的边缘,对其他的非极大值的相关信息进行抑制,利用该准则可以提出大部分非边缘点。
3.例子
如下图判断中心点p0的梯度是否为极大值,则需要在计算其它点的梯度进行比较。PN,PM是其梯度线在像素领域的交点,其中PNPM是非8领域点,故采用线性插值进行计算梯度值,,同时考虑梯度方向变换。最后进行比较,判断P0是不是最优点,如果是其他点则视之为背景点。
在这里插入图片描述

3.sobel算子

1、数学原理
Sobel算子是离散微分算子之一,计算图像灰度函数的近似梯度;
1.原理
利用像素点Sobel算子计算出相应的梯度向量及向量的范数,基于图像卷积来实现在水平方向和垂直方向检测对应方向上的边缘。
2.步骤
对于x、y方向:
1、首先对 源图像和奇数Sobel算子水平核Gx、垂直核Gy进行卷积可计算水平与垂直变换,当内核为3x3时,Gx、Gy的卷积内核为:
在这里插入图片描述

2、然后对最终的每一点的卷积结果求出图像灰度函数的近似梯度,得到最终的图像边缘:
在这里插入图片描述
在这里插入图片描述

对于其他角度,采用Scharr滤波器,其水平与垂直核因子如下:
在这里插入图片描述

3.c++代码实现
第一种opencv自带函数
void Sobel(InputArray src, OutputArray dst, int ddpth, int dx, int dy, int ksize=3, double scale=1, double delta=0,int borderType=BORDER_DEFAULT);
v参数详解:

第一个参数:输入图像,Mat类型即可;
第二个参数:输出,需要和源图像有一样的大小和类型;
第三个参数:int类型的ddepth,输入图像的深度,支持ruxiasrc.depth()和ddepth的组合。
若sec.depth()=CV_8U,取ddepth=-1/CV_16S/CV_32F/CV_64F;
若sec.depth()=CV_16U/CV_16S,取ddepth=-1/CV_16S/CV_32F/CV_64F;
若sec.depth()=CV_32F,取ddepth=-1/CV_16S/CV_32F/CV_64F;
若sec.depth()=CV_64F,取ddepth=-1/CV_16S/CV_64F;
第四个参数:int类型的dx,x方向上的差分阶数。
第五个参数:it类型dy,y方向上的差分阶数。
第六个参数:int类型的ksize,默认值是3,卷积核的大小,只能取1,3,5,7.
第七个参数:double类型的scale,计算导数时的可选尺度因子,默认值是1,表示默认情况下是没有应用缩放的。
第八个参数:double类型的delta,表示在结果存入目标图之前可选的delta值,默认值是0.
第九个参数:int类型的borderType,边界模式,默认值是BORDER_DEFALULT。
一般情况下都是用ksizeksize的内核来计算导数的,但是一种特殊情况是,当ksize=1时,往往会使用31或1*3的内核,且这种情况下,并没有进行高斯平滑操作。

#include <iostream>
#include <opencv2/opencv.hpp>
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
 
using namespace std;
using namespace cv;
 
int main()
{
//    初始化x和y方向的梯度
    Mat grad_x, grad_y;
    Mat abs_grad_x, abs_grad_y;
 
    Mat srcImage;
    srcImage = imread("/Users/dwz/Desktop/cpp/1.jpg");
    cvtColor(srcImage, srcImage, COLOR_BGR2GRAY);
//    计算x方向的梯度
    Sobel(srcImage, grad_x,CV_16S, 1, 0, 3, 1, 0, BORDER_DEFAULT);
    convertScaleAbs(grad_x, abs_grad_x);  //实现图像增强
 
//    计算y方向的梯度
    Sobel(srcImage, grad_y, CV_16S, 0, 1, 3, 1, 0, BORDER_DEFAULT);
    convertScaleAbs(grad_y, abs_grad_y);
 
//    合并梯度
    Mat dstImage;
    addWeighted(abs_grad_x, 0.5, abs_grad_y, 0.5, 0, dstImage);
    //cvAddWeighted( const CvArr* src1, double alpha,const CvArr* src2, double beta,double gamma, CvArr* dst );
    /*参数1:src1,第一个原数组.
	参数2:alpha,第一个数组元素权重

	参数3:src2第二个原数组
	参数4:beta,第二个数组元素权重
	参数5:gamma,图1与图2作和后添加的数值。不要太大,不然图片一片白。总和等于255以上就是纯白色了。

	参数6:dst,输出图片*/
    imwrite("sobel.jpg", dstImage);
}

第二种.图像直接卷积实现sobel:
实现步骤:
1.首先定义水平或垂直方向的Sobel核因子,直接对源图像进行窗遍历,计算窗内的领域梯度幅值;
2.然后根据梯度模长进行二值化操作,完成图像水平或垂直方向的边缘检测。

bool _tsobelEdge(const cv::Mat& srcImage, cv::Mat& dstImage, uchar threshold)
{
	CV_Assert(srcImage.channels() == 1);CV_Assert()若括号中的表达式值为false,则返回一个错误信息。,确认是灰度图
	//初始化水平核因子
	Mat sobelx = (Mat_<double>(3, 3) <<
		1, 0, -1,
		2, 0, -2,
		1, 0, -1);
	//初始化垂直因子
	Mat sobely = (Mat_<double>(3, 3) <<
		1, 2, 1,
		0, 0, 0,
		-1, -2, -1);
	
	dstImage = Mat::zeros(srcImage.rows - 2, srcImage.cols - 2, srcImage.type());
	double edgesx = 0;
	double edgesy = 0;
	for (int k = 1; k < srcImage.rows - 1; k++)
	{
		for (int n = 1; n < srcImage.cols - 1; n++)
		{
			edgesx = 0;
			edgesy = 0;
			//遍历计算水平与垂直梯度
			for (int i = -1; i <= 1; i++)
			{
				for (int j = -1; j <= 1; j++)
				{
					edgesx += srcImage.at<uchar>(k + i, n + j) * sobelx.at<double>(1 + i, 1 + j);  //水平方向梯度模值
					edgesy += srcImage.at<uchar>(k + i, n + j) * sobely.at<double>(1 + i, 1 + j); //垂直方向梯度模值
				}
			}
			//计算梯度模长
			double graMage = 0;
			graMage = sqrt(pow(edgesy, 2) + pow(edgesx, 2)); //计算总的模值,sqrt是求根号,pow是求二次幂

			//二值化
			dstImage.at<uchar>(k - 1, n - 1) = ((graMage > threshold) ? 255 : 0);
		}
	}
	return true;
}

第二种:非极大值抑制Sobel检测
实现步骤:
1.将图像转换为32位浮点型数据,定义水平或垂直方向的Sobel算子。
2.利用filter2D完成图像与算子的卷积操作,计算卷积结果的梯度幅值‘
3.自适应计算出梯度幅值阈值,阈值设置不梯度幅值的均乘以4,根据阈值对水平或垂直的领域区域梯度进行比较。
4.判断当前领域梯度是否大于水平或者垂直领域梯度,自适应完成边缘检测出二值图像的操作。
实现代码:

#include <opencv2/opencv.hpp>
#include <iostream>
#include <math.h>
#include <vector>
#include <opencv2/imgproc/imgproc.hpp>
#include<cmath>
using namespace std;
using namespace cv;
bool SobelVerEdge(cv::Mat& srcImage, cv::Mat& resultImage)
{
	//1.将图像转换为32位浮点型数据
	CV_Assert(srcImage.channels == 1);//CV_Assert()若括号中的表达式值为false,则返回一个错误信息。,确认是灰度图
	srcImage.convertTo(srcImage, CV_32FC1);
	//水平方向的Sobel算子
	cv::Mat sobelx = (cv::Mat_<float>(3, 3) <<
		-0.125, 0, 0.125,
		-0.25, 0, 0.25,
		-0.125, 0, 0.125); //构建3x3的浮点矩阵
	cv::Mat ConResMat;
	//卷积运算
	cv::filter2D(srcImage, ConResMat, srcImage.type(), sobelx);
	//自适应计算梯度的幅值
	cv::Mat graMagMat;
	cv::multiply(ConResMat, ConResMat, graMagMat);// src1:作为被乘数的图像数组;src2:作为乘数的图像数组,大小和类型与src1相同;dst:可选参数,输出结果保存的变量,默认值为None,

	//根据梯度幅值及参数设置阀值
	int scaleVal = 4;
	//阈值设置不梯度幅值的均乘以4
	double thresh = scaleVal * cv::mean(graMagMat).val[0];//cv::mean该函数会得到graMagMatt中各个通道的均值,.val[0]表示第一个通道的均值
	cv::Mat resultTempMat = cv::Mat::zeros(graMagMat.size(), graMagMat.type());
	float* pDateMag = (float*)graMagMat.data;
	float* pDateRes = (float*)resultTempMat.data;
	const int nRows = ConResMat.rows;
	const int ncols = ConResMat.cols;
	for (int i = 1; i != nRows - 1; i++)
	{
		for (int j = 1; j != ncols - 1; j++)
		{
			 //计算该点梯度与水平或垂直梯度值的大小并比较结果
			bool b1 = (pDateMag[i * ncols + j] > pDateMag[i * ncols + j - 1]);
			bool b2= (pDateMag[i * ncols + j] > pDateMag[i * ncols + j + 1]);
			bool b3 = (pDateMag[i * ncols + j] > pDateMag[(i + 1) * ncols + j ]);
			bool b4 = (pDateMag[i * ncols + j] > pDateMag[(i - 1) * ncols + j ]);

			//判断领域领域梯度是否满足大于水平或垂直梯度的条件
			//并根据自适应阈值参数进行二值化
			pDateRes[i * ncols + j] = 255 * ((pDateMag[i * ncols + j] > thresh) && ((b1 & b2) || (b3 & b4)));

		}
	}
	resultTempMat.convertTo(resultTempMat, CV_8UC1);
	resultImage = resultTempMat.clone;
	return true;
}

3.7.2拉普拉斯变换

1.数学原理

拉普拉斯算子是基于二阶微分的图像增强;
首先定义一个二阶微分离散公式,然后构造一个基于此公式的各向同性滤波器----拉普拉斯算子。具有旋转不变性,该点像素值的二阶微分为0的点为边缘点;
拉普拉斯算子利用二次微分特性与峰值间的过零点来确定边缘的位置,对奇异点或边界点更见敏感,常见应用于图像锐化处理中。
图像锐化操作的目的是突出图像的细节或增强被模糊的图像细节,可实现灰度反差增强,同时使得图像变得清晰,微分运算可以实现图像细节的突出,积分运算或者加权平均可使图像模糊。
一个二元函数f(x,y)的拉普拉斯变化定义为
在这里插入图片描述
为了更适合数字图像处理,需要表示为离散形式:
在这里插入图片描述
在这里插入图片描述

合起来3x3模板: = 【f(x+1,y)+f(x-1,y)+f(x,y+1)+f(x,y-1)】- 4f(x,y)
常用的3*3掩模:要求定义数字形式的拉普拉斯要求系数之和必为0
在这里插入图片描述
在做完拉普拉斯算子后,为了保护拉普拉斯锐化效果,复原背景信息,采用讲原始图像和拉普拉斯图像叠加在一起的方法:
g(x,y) = f(x,y)- 拉普拉斯算子 中心为负数的掩膜
g(x,y) = f(x,y)-+拉普拉斯算子 中心为正数的掩膜

2.代码实现

1.采用opencv自带函数
void Laplacian(
InputArray src,
OutputArray dst,
int ddepth,
int ksize=1,
double scale=1,
double delta=0,
int borderType=BORDER_DEFAULT );

参数一:InputArray类型的image,一般是cv::Mat;
参数二:OutputArray类型的dst,与输入的图像大小和通道相同;
参数三:int类型的ddepth,输出图像深度;
参数四:int类型的ksize,高斯核大小,默认值为1,必须为1或3;
参数五:double类型的scale(比例的意思),缩放计算的导数值的可选比例因子;默认值为1,表示没有进行缩放;
参数六:double类型的detal,delta在将结果存储到dst前添加到结果中;
参数七:int类型的borderType,一般情况下无需关注;

需要注意的是
cvConvertScaleAbs
该函数是OpenCV中的函数。   
函数作用:使用线性变换转换输入数组元素成8位无符号整型
函数原型:void cvConvertScaleAbs( const CvArr* src, CvArr*dst, double scale=1, double shift=0 );
#define cvCvtScaleAbs cvConvertScaleAbs
其中参数含义:
src : 原数组
dst :输出数组 (深度为 8u).
scale :比例因子.
shift :原数组元素按比例缩放后添加的值。
函数 cvConvertScaleAbs 与前一函数是相同的,但它是存贮变换结果的绝对值:
dst(I)=abs(src(I)*scale + (shift,shift,…))
函数只支持目标数数组的深度为 8u (8-bit 无符号) , 对于别的类型函数仿效于cvConvertScale 和 cvAbs 函数的联合 。

#include <opencv2/opencv.hpp>
#include <iostream>
#include <math.h>
#include <vector>
#include <opencv2/imgproc/imgproc.hpp>
#include<cmath>
using namespace std;
using namespace cv;
int main()
{
	Mat srcImage = imread("c:/users/征途/desktop/vs-cpp/project3/04.jpg");
	if (!srcImage.data) {
		cout << "falied to read" << endl;
		system("pause");
		return -1;
	}
	Mat srcGray;
	cvtColor(srcImage, srcGray, CV_BGR2GRAY);
	//高斯滤波
	GaussianBlur(srcGray, srcGray, Size(3, 3),
		0, 0, BORDER_DEFAULT);
	//拉普拉斯变换
	Mat laplace_result;
	Laplacian(srcGray, laplace_result, CV_16S, 3);
	convertScaleAbs(laplace_result, laplace_result); //经过处理后,需要用convertScaleAbs()函数将其转回原来的uint8形式,否则将无法显示图像,而只是一副灰色的窗口
	imshow("src", srcImage);
	imshow("Laplace_result", laplace_result);
		
	waitKey(0);
	return 0;
}

在这里插入图片描述
2.c++自编写代码实现
注意,在源码中要用到—cvFilter2D函数,这是是openCV中的一个库函数,实现图像的卷积变换。函数cvFilter2D对图像进行线性滤波,支持 In-place 操作。当核运算部分超出输入图像时,函数从最近邻的图像内部象素差值得到边界外面的象素值。
函数格式:
void cvFilter2D( const CvArr* src, CvArr* dst, const CvMat* kernel, CvPoint anchor=cvPoint(-1,-1));
参数:
src输入图像。
dst输出图像。
kernel卷积核,单通道浮点矩阵。如果想要应用不同的核于不同的通道,先用cvSplit函数分解图像到单个色彩通道上,然后单独处理。
anchor核的锚点表示一个被滤波的点在核内的位置。 锚点应该处于核内部。缺省值 (-1,-1) 表示锚点在核中心。
下面代码中:
getMat()是一种获取矩阵的Mat的常用方法,不用额外的复制矩阵的数据。
InputArray或者OutputArray型是函数的接口
InputArray这个接口类可以是Mat、Mat_、Mat_<T, m, n>、vector、vector<vector>、vector
这个类只能作为函数的形参参数使用,不要试图声明一个InputArray类型的变量
注意,在函数的内部可以使用_InputArray::getMat()函数将传入的参数转换为Mat的结构,方便你函数内的操作,如Mat src = _src.getMat();

#include <opencv2/opencv.hpp>
#include <iostream>
#include <math.h>
#include <vector>
#include <opencv2/imgproc/imgproc.hpp>
#include<cmath>
using namespace std;
using namespace cv;

//首先自定义laplacian函数 源码文件opencv\sources\modules\imgproc\src文件夹下的deriv.cpp文件
void myLaplacian(InputArray _src, OutputArray _dst, int ddepth, int ksize,
	double scale, double delta, int borderType)
{
	//首先对前三个参数定义
	Mat src = _src.getMat();//在函数的内部可以使用_InputArray::getMat()函数将传入的参数转换为Mat的结构,方便你函数内的操作
	if (ddepth < 0)
		ddepth = src.depth();
	_dst.create(src.size(), CV_MAKETYPE(ddepth, src.channels()));
	Mat dst = _dst.getMat();

	if (ksize == 1 || ksize == 3)
	{
		float K[2][9] =
		{ {0, 1, 0, 1, -4, 1, 0, 1, 0},//这是ksize=1的卷积核
		 {2, 0, 2, 0, -8, 0, 2, 0, 2} };//这是ksize=3的卷积核
		Mat kernel(3, 3, CV_32F, K[ksize == 3]); //如果kernel size等于3,就用第二行的{ 0,1,0,... }这个kernel,如果等于1,就用的二行的{ 2,0,2,... }
		if (scale != 1)
			kernel *= scale;
		filter2D(src, dst, ddepth, kernel, Point(-1, -1), delta, borderType);//该函数是cv中是实现卷积运算
	}

}

int main()
{
	 Mat srcImage = imread("c:/users/征途/desktop/vs-cpp/project3/04.jpg");
	if (!srcImage.data) {
		cout << "falied to read" << endl;
		system("pause");
		return -1;
	}
	Mat srcGray;
	cvtColor(srcImage, srcGray, CV_BGR2GRAY);
	//高斯滤波
	GaussianBlur(srcGray, srcGray, Size(3, 3),
		0, 0, BORDER_DEFAULT);
	//拉普拉斯变换
	Mat laplace_result;
	double delta;

	myLaplacian(srcGray, laplace_result, srcImage.depth(), CV_16S, 3,1,  delta );
	convertScaleAbs(laplace_result, laplace_result); //经过处理后,需要用convertScaleAbs()函数将其转回原来的uint8形式,否则将无法显示图像,而只是一副灰色的窗口
	imshow("src", srcImage);
	imshow("Laplace_result", laplace_result);
		
	waitKey(0);
	return 0;
}
  • 2
    点赞
  • 48
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值