Opencv 笔记5 边缘处理-canny、sobel、Laplacian、Prewitt

一、边缘检测概述

       边缘检测是计算视觉中的基本问题,边缘检测的目的是标识图像中亮度变换明显的点。边缘检测大幅度的减少了图像的数据量(分为两种:灰度图像边缘检测和彩色图像边缘检测),并且剔除了不相关的信息,保留了重要的结构属性。总之,图像的边缘检测是图像分割、目标区域识别和区域形状提取等图像分析的基石,也是图像中特征提取的很重要的方法。如何来实现?可以分为大的两步,一是图像边缘和背景的分离,二是 辨别出轮廓。实际的图像不像我们说的那么简单,往往是各种类型的边缘和他们模糊之后结果的结合,并且实际图像中存在着噪声,而噪声和边缘都属于高频信号。

    为了得到图像轮廓清晰的图像,我们一般要进行锐化,要说锐化,就要从锐度说起,锐度其实就是边缘的对比度,锐度的提高是在不增加图像的像素的基础上造成的提高图像清晰度的假象。锐化的目的是使图像边缘、轮廓线、细节变的清晰,因为当我们去除噪声的时候源图像经过了一系列的平滑处理之后图像和边缘变的模糊,因此可以对平滑后的图像进行逆运算。锐化的方法有两种:1、高通滤波;2、空域微分法。

      常见的噪声:加性噪声、乘性噪声、量化噪声

边缘的特征和分类:边缘有方向和幅度两个特征,沿着边缘方向走像素值逐渐平稳,垂直于边缘方向,像素值变化剧烈。

             分类:1、阶跃性边缘,它两边的像素值有明显的差距,方向导数在边缘处是零交叉;

                       2、屋顶状边缘,它是从增加到减少的转折点,二阶方向导数在这里取得极致

常见边缘检测类型:

                       1、一阶微分边缘检测,通过计算图像的梯度值来检测图像边缘,常见的算子有Sobel  、Prewitt 、Roberts、差分算子,canny(在一阶的基础上进行改进)

                         2、二阶微分边缘检测,求二阶导数的过零点来检测边缘,如拉普拉斯、高斯拉普拉斯

二、梯度以及边缘检测思想

如上图所示,边缘一般是以一阶导和二阶导数来检测。

术语介绍:

(1)、边缘点:灰度值显著变化的点

 (2)、边缘段:边缘点坐标和方向的总和,边缘的方向可以是梯度角

 (3)、轮廓:边缘列表

二阶偏导数-拉普拉斯算子数学原理

 梯度以及Roberts 、Sobel 数学原理

 

旋转不变性证明如下 

 导数总结如下:

a、一阶导数产生比较粗劣的边缘,二阶导数则比较精致,对细节的把控比较好,如细线,孤立的亮点等。

b、二阶导在灰度斜坡和台阶出会产生双边边缘响应,二阶的符号可以判断亮暗走势,和halcon 里面的双边算子是一样的。

设计到梯度的东西,后续会在PCA中详细介绍。下面是常见的几种边缘检测的详解

三、经典图像边缘检测算法

   差分边缘检测

   Roberts

   Sobel

  Prewitt

  Log

 Dog

 Canny

Laplacian

 Scharr、Kirsch、Robinson (了解)

1、  差分边缘检测

   当我们处理图像的时候,可以用一阶差分来代替图像的导数,在x和y方向上各种差分得到一个x和y方向上的矩阵,还有一个是对角线的矩阵,他和x和有方向上的差分的推导过程是一样的,矩阵如下:

 如何实现的先不写了,重点放在后面的几个算子上

2、   Roberts

从上面可以知道Roberts 算子的矩阵推导,Roberts 算子又被称为交叉微分算子,基于交叉差分的梯度算法(一阶导数),矩阵如下:

 

这个是用opencv已经集成的算子得出的结果(为了找一个能说明的图片我费了好长的时间)


	src = imread("C:\\Users\\19473\\Desktop\\opencv_images\\305.jpg");
	if (!src.data)
	{
		printf("could not  load  image....\n");
	}



	cvtColor(src, src_gray,COLOR_BGR2GRAY);
	imshow("原图", src_gray);
	//  Robert  X方向上的算子
	Mat  kernel = (Mat_<int>(2, 2) << -1, 0, 0, 1);
	filter2D(src_gray, dest1, -1, kernel, Point(-1, -1), 0.0);
	imshow("45度", dest1);

	//  Robert  Y方向上的算子
	Mat  kernel_y = (Mat_<int>(2, 2) << 0, -1, 1, 0);
	filter2D(src_gray, dest2, -1, kernel_y, Point(-1, -1), 0.0);
	imshow("135度", dest2);

	// 图像融合
	convertScaleAbs(dest2, dest2);
	convertScaleAbs(dest1, dest1);
	Mat  RobertImage;
	addWeighted(dest1, 0.5, dest2, 0.5, 0, RobertImage);
	imshow("RobertImage", RobertImage);



 下面是自定义并实现一个Roberts 算子:

从效果上看我拟合的没有opencv集成的好,但是原理是一样的。

代码实现:

Mat   Robertsoperation(Mat &src,int type);
Mat   Robertsoperation(Mat& src, int type) 
{
	//type =1,2,3 分别表示 135 35 和合并后的结果
	Mat dstImage = src.clone();
	int nrows = src.rows;
	int ncols = src.cols;
	for (int i = 0; i < nrows-1; i++)
	{
		for (int j = 0; j < ncols - 1; j++)
		{
			// 135度
			int t1 = ((src.at<uchar>(i, j) - (src.at<uchar>(i + 1, j + 1)))* (src.at<uchar>(i, j) - (src.at<uchar>(i + 1, j + 1))));
			//  45 度
			int t2 = ((src.at<uchar>(i+1, j) - (src.at<uchar>(i , j + 1 ))) * (src.at<uchar>(i + 1, j) - (src.at<uchar>(i, j + 1))));
			if ( type == 1 )
			{
				dstImage.at<uchar>(i, j) = (uchar)t1;
			}
			else if (type == 2)
			{
				dstImage.at<uchar>(i, j) = (uchar)t2;
			}
			else 
			{
				dstImage.at<uchar>(i, j) = (uchar)sqrt(t1+t2);
			}

		}
	}
	return dstImage;
	
}

 3、   Sobel

具体步骤:

     a:分解矩阵,将矩阵分解成一个二项式系数*差分算子的过程

     b: 先求出二项式系数,然后再用这个系数计算差分算子

     c:卷积运算 :

算子解释:

Sobel(inputArray,outputArray,int ddepth,int dx,int dy,int ksize=3,double scale=1,double delta=0,int borderType=BORDER_DEFAULT) 
*第一个参数,输入图像。 
*第二个参数,输出图像。 
*第三个参数,输出图像深度。 
*第四个参数,x方向上的差分阶数。 
*第五个参数,y方向上的差分阶数。 
*第六个参数,Sobel核的大小,必须是奇数。 
*第七个参数,计算导数值时可选的缩放因子,默认值1,表示默认情况下没用应用缩放。 
*第八个参数,表示在结果存入输出图像之前可选的delta值 (这个参数默认是0.0)
*第九个参数,扩充边界模式。

效果如下:

 代码显示:

//******************Sobel算子计算X、Y方向梯度 以及  梯度方向角********************
//第一个参数imageSourc原始灰度图像;
//第二个参数imageSobelX是X方向梯度图像;
//第三个参数imageSobelY是Y方向梯度图像;
//第四个参数   theta  是梯度方向角数组指针  下一步很重要 就是要用这个值来计算
//*************************************************************
void  SobelGradDirction(const Mat imageSource, Mat& imageX, Mat& imageY, Mat& gradXY, Mat& theta)
{
	imageX = Mat::zeros(imageSource.size(), CV_32SC1);
	imageY = Mat::zeros(imageSource.size(), CV_32SC1);
	gradXY = Mat::zeros(imageSource.size(), CV_32SC1);
	theta = Mat::zeros(imageSource.size(), CV_32SC1);

	int rows = imageSource.rows;
	int cols = imageSource.cols;

	int stepXY = imageX.step;
	int step = imageSource.step;
	/*
	Mat.step参数指图像的一行实际占用的内存长度,
	因为opencv中的图像会对每行的长度自动补齐(8的倍数),
	编程时尽量使用指针,指针读写像素是速度最快的,使用at函数最慢。
	*/
	uchar* PX = imageX.data;
	uchar* PY = imageY.data;
	uchar* P = imageSource.data;
	uchar* XY = gradXY.data;

	for (int i = 1; i < rows - 1; i++)
	{
		for (int j = 1; j < cols - 1; j++)
		{
			int a00 = P[(i - 1) * step + j - 1];
			int a01 = P[(i - 1) * step + j];
			int a02 = P[(i - 1) * step + j + 1];

			int a10 = P[i * step + j - 1];
			int a11 = P[i * step + j];
			int a12 = P[i * step + j + 1];

			int a20 = P[(i + 1) * step + j - 1];
			int a21 = P[(i + 1) * step + j];
			int a22 = P[(i + 1) * step + j + 1];

			double gradY = double(a02 + 2 * a12 + a22 - a00 - 2 * a10 - a20);
			double gradX = double(a00 + 2 * a01 + a02 - a20 - 2 * a21 - a22);


			imageX.at<int>(i, j) = abs(gradX);
			imageY.at<int>(i, j) = abs(gradY);
			if (gradX == 0)
			{
				gradX = 0.000000000001;
			}
			// 计算梯度的方向  用角度来显示
			theta.at<int>(i, j) = atan(gradY / gradX) * 57.3;
			theta.at<int>(i, j) = (theta.at<int>(i, j) + 360) % 360;
			//  用模来计算梯度    方向就是theta 
			gradXY.at<int>(i, j) = sqrt(gradX * gradX + gradY * gradY);
			//XY[i*stepXY + j*(stepXY / step)] = sqrt(gradX*gradX + gradY*gradY);
		}

	}
	// 将所有的图像都变成0-255 的图像
	convertScaleAbs(imageX, imageX);
	convertScaleAbs(imageY, imageY);
	convertScaleAbs(gradXY, gradXY);
}

 4、  Prewitt  边缘检测

代码如下:


// 分离卷积运算

void conv2d(InputArray  src, InputArray  kernel, OutputArray dst, int  ddepth,
	Point achor = Point(-1, -1),
	int borderType = BORDER_DEFAULT)
{
	// 卷积运算的第一步,将卷积核逆时针旋转180度  。。 主要是为了计算方便
	Mat  kernelFlip;
	flip(kernel, kernelFlip,-1);
    // 第二步才开始计算

	filter2D(src,dst, ddepth, kernelFlip, achor,0.0, borderType);
	//InputArray src, OutputArray dst, int ddepth,
	//	InputArray kernel, Point anchor = Point(-1, -1),
	//	double delta = 0, int borderType = BORDER_DEFAULT
}
// 卷积的顺序不一样


void Mysepfilter2D_YX_order( InputArray src, OutputArray src_xy, int ddepth, InputArray kernel_y,
	InputArray kernel_x, Point achor = Point(-1, -1), int  border_type = BORDER_DEFAULT)
{
	Mat tempk;
	conv2d(src, kernel_y, tempk, ddepth, achor, border_type);
	conv2d(tempk, kernel_x, src_xy, ddepth, achor, border_type);
}


void Mysepfilter2D_XY_order(InputArray src, OutputArray src_xy, int ddepth, InputArray kernel_x,
	InputArray kernel_y, Point achor = Point(-1, -1), int  border_type = BORDER_DEFAULT)
{
	Mat tempk;
	conv2d(src, kernel_x, tempk, ddepth, achor, border_type); // 垂直
	conv2d(tempk, kernel_y, src_xy, ddepth, achor, border_type);// 水平
}


void myPrewitt( InputArray src, OutputArray dst, int ddepth, int x, int y, Point achor = Point(-1, -1), int  border_type = BORDER_DEFAULT)
{
	Mat  prewitt_xy = (Mat_<float>(3, 1) << 1, 1, 1);
	Mat  prewitt_xx = (Mat_<float>(1, 3) << 1, 1, 1);


	Mat  prewitt_yx = (Mat_<float>(3, 1) << 1, 1, 1);
	Mat  prewitt_yy = (Mat_<float>(1, 3) << 1, 1, 1);

	if (x!=0&&y==0)
	{
		Mysepfilter2D_YX_order(src,dst, ddepth, prewitt_xy, prewitt_xx);
	}


	if (y != 0 && x == 0)
	{
		Mysepfilter2D_YX_order(src, dst, ddepth, prewitt_yx, prewitt_yy);
	}

}
int main(int args, char* arg)
{
	Mat src, src_gray, dest1, dest2, dest3, dest4, dest5, dest6;
	// point  
	src = imread("C:\\Users\\19473\\Desktop\\opencv_images\\305.jpg");
	if (!src.data)
	{
		printf("could not  load  image....\n");
	}
	cvtColor(src, src_gray, COLOR_BGR2GRAY);
	imshow("原图", src_gray);
	// prewitt 卷积
	Mat p_x;
	myPrewitt(src_gray, p_x,CV_32FC1,1,0);


	Mat p_y;
	myPrewitt(src_gray, p_y, CV_32FC1, 0, 1);
	// 水平方向和垂直方向的 边缘强度
	// 数据类型转换。边缘强度的灰度显示
	Mat  abs_image_prewitt_x, abs_image_prewitt_y;
	convertScaleAbs(p_x, abs_image_prewitt_x,1,0);
	convertScaleAbs(p_y, abs_image_prewitt_y,1,0);
	imshow("垂直方向边缘",abs_image_prewitt_x);
	imshow("水平方向边缘", abs_image_prewitt_y);


	// 通过上面求得的两个方向上的边缘求得最终的边缘强度

	//Mat  abs_image_prewitt_x2, abs_image_prewitt_y2;


	//pow(abs_image_prewitt_x, 2.0, abs_image_prewitt_x2);
	//pow(abs_image_prewitt_y, 2.0, abs_image_prewitt_y2);
	//Mat edge=Mat::zeros(src.size(),CV_32F);
	//sqrt((abs_image_prewitt_x2 + abs_image_prewitt_y2), edge);
	edge.convertTo(edge,CV_8UC1);
	imshow("边缘强度", edge);
	waitKey(0);
	return -1;
}

5、   Canny(最常用的算子)

基于卷积运算的边缘检测算法,比如sobel prewitt算子有几个缺点:

a、没有充分的利用梯度的方向

b、最后输出的边缘二值图只是简单的利用了阈值进行处理,当阈值很大的时候就会损失很多的边缘,反之亦然

那么canny 是这这个基础上进行了一部分的优化:(1)  基于边缘梯度方向的非极大值抑制。
(2)  双阈值的滞后阈值处理。

Canny 边缘检测的近似算法的步骤如下

第一步:图像矩阵I 分别与水平方向上的卷积核和垂直方向上的卷积核卷积得到dx和dy ,然后利用平方和的开方magnitude=sqrt(dx^2+dy^2)得到边缘强度。举例如下:

 sobel 在边缘检测的过程中将值大于255的截断为255然后得到一个二值图像。

第二步:利用第一步计算出的dx和dy ,计算出梯度方向angle=arctan 2(dy ,dx), 即对每一个位置(r,c),angle(r,c)=arctan 2(dy (r,c),dx(r,c))代表该 位置的梯度方向,一般用角度表示,即angle(r,c)∈[0,180]∪[-180,0]。得到一个关于角度的矩阵angle:

以上图133中心点为例 

水平方向:dx=(154-133)

垂直方向:dx=(175-133)

 第三步:对每一个位置进行非极大值抑制的处理  ,magnitude是从第一步里面得到的及边缘矩阵,在这里要进行一个非极大值抑制的处理,所以在边缘扩充的时候用的是补零的方式。

 上图右边的图像中,左边为nonMaxSup(1,1)的值为912,那么现在需要做的是严重这条梯度方向的线走,经过邻域区域(我们目前默认是3x3的邻域)的值和(1,1)的值(912)做对比,若nonMaxSup(1,1)>是由于的这条线上邻域的值,那么(1,1)就是极大值,则nonMaxSup(1,1)=magnitude(1,1)=912,若nonMaxSup(1,1)不全大于是由于的这条线上邻域的值,则nonMaxSup(1,1)=0。用同样的方法计算nonMaxSup(1,2)的值得到nonMaxSup(1,2)=0。

总结上述非极大值抑制的过程:如果magnitude(r,c)在沿着梯度方向angle(r, c)上的邻域内是最大的则为极大值;否则,设置为0。

 非极大值抑制的实现: 

非极大值抑制的第二种方式:用插值法拟 合梯度方向上的边缘强度,这样会更加准确地衡量梯度方向上的边缘强度

假设 M=

 插值方式:L1::L2=M:(1-M),那么可以近似的算出左上角的插值 P1= M*292+(1-M)*720,同理可以算出右下角:P1= M*276+(1-M)*560,然后比较P1 和P2再和920比较大小,若920>P1&&920>P2,则为极大值,否则不是极大值。一般将梯度方向离散化为以下四种情况:
· angle(r,c)∈(45,90]∪(-135,-90]       · angle(r,c)∈(90,135]∪(-90,-45] ·     angle(r,c)∈[0,45]∪[-180,-135] ·        angle(r,c)∈(135,180]∪(-45,0)

 第四步:双阈值的滞后阈值处理。经过上一步的极大值抑制处理之后,一般需要阈值化处理(threshold  和 adaptiveThreshold),常用阈值滞后方法:高阈值  低阈值,具体方法如下:(1)  边缘强度大于高阈值的那些点作为确定边缘点
(2)  边缘强度比低阈值小的那些点立即被剔除
(3) 边缘强度在低阈值和高阈值之间的那些点,可以理解为,首先选定边缘强度大于高阈 值的所有确定边缘点,然后在边缘强度大于低阈值的情况下尽可能延长边缘(该像素仅仅在连接到一个高于高阈值的像素时被保留)。

上述所说的高阈值一般是低阈值的2~3倍。

Canny算子边缘检测流程

算子解释:

void Canny(inputArray,outputArray,double threshold1,double threshold2,int apertureSize=3,bool L2gradient=false) 
*第一个参数,输入图像,且需为单通道8位图像。 
*第二个参数,输出的边缘图。 
*第三个参数,第一个滞后性阈值。用于边缘连接。 
*第四个参数,第二个滞后性阈值。用于控制强边缘的初始段,高低阈值比在2:1到3:1之间。 
*第五个参数,表明应用sobel算子的孔径大小,默认值为3。 
*第六个参数,bool类型L2gradient,一个计算图像梯度幅值的标识,默认值false。

代码显示:

opencv c++ canny 实现 以及与halcon canny的对比_c++ opencv canny-CSDN博客

6、拉普拉斯算子(二阶算子)

仔细上圈出来的图像,拉普拉斯算子不可以分解为水平方向和竖直方向的分量来计算,最常用的任然是上面4和-4的核,所以计算的时候也是直接用卷积来计算,也就是fliter2D这个算子,后面补一期filter2d的源码,这里不多做赘述。

注意:Laplacian边缘检测算子不像Sobel和Prewitt算子那样对图像进行了平滑处理,所以它 会对噪声产生较大的响应,误将噪声作为边缘,并且得不到有方向的边缘,所以在做拉普拉斯卷积之前要进行高斯模糊

算子解释:

Laplacian( src_gray, dst, ddepth, kernel_size, scale, delta, BORDER_DEFAULT );
src_gray: 输入图像的深度是 CV_8U 
dst: 输出图像
ddepth: 输出图像的深度。 因为输入图像的深度是 CV_8U ,这里我们必须定义 ddepth = CV_16S 以避免外溢
kernel_size: 从上面的推导公式中可以看出默认是3*3的核,如果是5*5的那么推导公式就很复杂,但是原理一样。
scale,这个的值是-1或者1,如果是-1 则便是上面中心位置的值是4 ,如果是1,则表示为-4
delta,
BORDER_DEFAULT: 使用默认值。
CV_8U : convertScaleAbs( dst, abs_dst );主要是将灰度值放到0~255之间,是一个截断值

图像显示:

 代码显示:


int main(int args, char* arg)
{
	Mat src, src_gray, edge_laplance, laplance, edge_mohu;
	// point  
	src = imread("C:\\Users\\19473\\Desktop\\opencv_images\\88.jpg");
	if (!src.data)
	{
		printf("could not  load  image....\n");
	}
	namedWindow("input_demo", CV_WINDOW_AUTOSIZE);
	imshow("input_demo", src);
	//0  高斯模糊
	GaussianBlur(src, edge_mohu,Size(3,3),0.0,0);
	//1. 将这个图像转换成灰度图像
	cvtColor(edge_mohu, src_gray, CV_BGR2GRAY);
	namedWindow("Gray_demo", CV_WINDOW_AUTOSIZE);
	imshow("Gray_demo", src_gray);

    //2 laplace 
	Mat scharr_x, scharr_y, scharr;
	Laplacian(src_gray, edge_laplance,CV_16S,3);
    // 这里一定是CV_16S
    // 3 去绝对值
	convertScaleAbs(edge_laplance, laplance);
	namedWindow("Output_Lapalce_demo", CV_WINDOW_AUTOSIZE);
	// 4  显示
	imshow("Output_Lapalce_demo", laplance);


	waitKey(0);
	return 0;
}

 7、Scharr、Kirsch、Robinson (了解)

Scharr:标准的Scharr边缘检测算子与Prewitt边缘检测算子和3阶的Sobel边缘检测算子类似, 由以下两个卷积核:

 Scharr边缘检测算子也可以扩展到其他方向,比如:

 Kirsch算子[6]由以下8个卷积核:

 Robinson算子[4]也是由8个卷积核:

 8、Log-高斯拉普拉斯边缘检测

          拉普拉斯边缘检测算子没有对图像做平滑处理,会对噪声产生明显的响应,所以在 用拉普拉斯核进行边缘检测时,首先要对图像进行高斯平滑处理,然后再与拉普拉斯核 进行卷积运算,有二维高斯函数如下:

 

 高斯边缘检测的步骤如下:

        第一步:构建窗口大小为H×W、标准差为σ的LoG卷积核,如上所示,

        第二步:图像矩阵与LoGH×W 核卷积,结果记为I_Cov_LoG

        第三步:边缘二值化显示(有白色显示和黑色显示两种)

       

 c++实现:在实现的过程中还是用到了高斯核分离的性质(Opencv 笔记3 图像平滑_Σίσυφος1900的博客-CSDN博客),二阶分离是一个比较麻烦推导,由于推导和实现都比较复杂,因此Opencv提出了一个一阶近似模型(Dog),可以简化计算,这里知道原理就可以了。

实现步骤:第一步:构建高斯核

                  第二步 :图像矩阵先与水平方向上的卷积核卷积,然后再与垂直方向上的卷积核卷 积。
                  第三步:与第一步相反,图像矩阵先与垂直方向上的卷积核卷积,然后再与水平方 向上的卷积核卷积。
                  第四步:将第一步和第二步得到的卷积结果相加,其中对于分离卷积仍然使用“图像 平滑”,具体代码如下:

代码实现


void conv2d(InputArray  src, InputArray  kernel, OutputArray dst, int  ddepth,
	Point achor = Point(-1, -1),
	int borderType = BORDER_DEFAULT)
{
	// 卷积运算的第一步,将卷积核逆时针旋转180度  。。 主要是为了计算方便
	Mat  kernelFlip;
	flip(kernel, kernelFlip, -1);
	// 第二步才开始计算

	filter2D(src, dst, ddepth, kernelFlip, achor, 0.0, borderType);
	//InputArray src, OutputArray dst, int ddepth,
	//	InputArray kernel, Point anchor = Point(-1, -1),
	//	double delta = 0, int borderType = BORDER_DEFAULT
}

// 卷积的顺序不一样
void sepConv2D_Y_X(InputArray src, OutputArray src_xy, int ddepth, InputArray kernel_y,
	InputArray kernel_x, Point achor = Point(-1, -1), int  border_type = BORDER_DEFAULT)
{
	Mat tempk;
	conv2d(src, kernel_y, tempk, ddepth, achor, border_type);
	conv2d(tempk, kernel_x, src_xy, ddepth, achor, border_type);
}


void sepConv2D_X_Y(InputArray src, OutputArray src_xy, int ddepth, InputArray kernel_x,
	InputArray kernel_y, Point achor = Point(-1, -1), int  border_type = BORDER_DEFAULT)
{
	Mat tempk;
	conv2d(src, kernel_x, tempk, ddepth, achor, border_type); // 垂直
	conv2d(tempk, kernel_y, src_xy, ddepth, achor, border_type);// 水平
}



void getSepLoGKernel(Mat &kx,Mat &ky,float sigma,int ksize)  // 构建高斯核
{
	kx.create(Size(ksize, 1),CV_32FC1);
	ky.create(Size(1,ksize), CV_32FC1);


	int center = (ksize - 1) / 2;
	double sigma2 = pow(sigma, 2.0);
	// 构建可分离的高斯拉普拉斯核 
	for (int i = 0; i < ksize; i++)
	{
		float  dist2 = pow(i - center, 2.0);// 
		ky.at<float>(i, 0) = exp(-dist2 / (sigma2 * 2));
		kx.at<float>(0, i) = (dist2 / sigma2 - 1.0) * ky.at<float>(i, 0);
	}
}
// 开始卷积
Mat  Log(InputArray  image, float sigma, int ksize)
{
	Mat  kx, ky;

	// 1,先得到两个卷积分离核
	getSepLoGKernel(kx,ky, sigma, ksize);
	// 2、水平---垂直
	Mat  conXY;
	sepConv2D_X_Y(image, conXY,CV_32FC1,kx,ky); 

	//3、垂直 -水平  ,如果是一阶的话就没有这一步了
	Mat  kx_t, ky_t;
	kx_t = kx.t();
	ky_t = ky.t(); // 矩阵的转置

	Mat  conYX;
	sepConv2D_Y_X(image, conYX, CV_32FC1, kx_t, ky_t);

	Mat  logCov;
	add(conXY, conYX, logCov);

	return logCov;
}

 9、Gog-高斯差分

二维高斯函数对σ的一阶偏导数如下

 

 

 第一步:构建窗口大小为H×W 的高斯差分卷积核。
第二步:图像矩阵与DoGH×W 核卷积,结果记为I_Cov_DoG。高斯拉普拉斯与高斯差分
第三步:与拉普拉斯边缘检测相同的二值化的显示。
高斯差分核是两个非归一化的高斯核的差,已知高斯核又是可分离的,所以真正在 用程序实现时,为了减少计算量,可以不用创建高斯差分核,而是根据卷积的加法分配 率和结合律的性质,图像矩阵分别与两个高斯核卷积,然后做差,用来代替第一步和第 二步操作。

效果显示

 代码显示


void conv2d(InputArray  src, InputArray  kernel, OutputArray dst, int  ddepth,
	Point achor = Point(-1, -1),
	int borderType = BORDER_DEFAULT)
{
	// 卷积运算的第一步,将卷积核逆时针旋转180度  。。 主要是为了计算方便
	Mat  kernelFlip;
	flip(kernel, kernelFlip, -1);
	// 第二步才开始计算

	filter2D(src, dst, ddepth, kernelFlip, achor, 0.0, borderType);
	//InputArray src, OutputArray dst, int ddepth,
	//	InputArray kernel, Point anchor = Point(-1, -1),
	//	double delta = 0, int borderType = BORDER_DEFAULT
}

// 卷积的顺序不一样
void sepConv2D_Y_X(InputArray src, OutputArray src_xy, int ddepth, InputArray kernel_y,
	InputArray kernel_x, Point achor = Point(-1, -1), int  border_type = BORDER_DEFAULT)
{
	Mat tempk;
	conv2d(src, kernel_y, tempk, ddepth, achor, border_type);
	conv2d(tempk, kernel_x, src_xy, ddepth, achor, border_type);
}


void sepConv2D_X_Y(InputArray src, OutputArray src_xy, int ddepth, InputArray kernel_x,
	InputArray kernel_y, Point achor = Point(-1, -1), int  border_type = BORDER_DEFAULT)
{
	Mat tempk;
	conv2d(src, kernel_x, tempk, ddepth, achor, border_type); // 垂直
	conv2d(tempk, kernel_y, src_xy, ddepth, achor, border_type);// 水平
}



Mat  getSepDoGKernel(Mat image, float sigma, int ksize)  // 构建高斯核 非归一化的
{
    // 1、构建一个水平方向的
	Mat  xk = Mat::zeros(1, ksize, CV_32FC1);
	int center = (ksize - 1) / 2;
	float  sg2 = pow(sigma, 2.0); // 方差
	for (int  i = 0; i < ksize; i++)
	{
		float n2 = pow(i-center,2.0);
		xk.at<float>(0,i) = exp(-n2 / (2 * sg2));
	}
	// 因为高斯核是对称的,所以可以可以用转置来计算y方向的
	Mat yk = xk.t();
	Mat gausscore;
	sepConv2D_X_Y(image, gausscore, CV_32FC1, xk, yk);
	gausscore.convertTo(gausscore, CV_32F,1.0/sg2);
	return gausscore;
}
// 开始卷积
Mat  DoG(Mat  image, float sigma, int ksize,float k=1.1)
{

	// 1,得到高斯核
	Mat  kernel = getSepDoGKernel(image,sigma,ksize);
	// 2、与标准差k*sigma 的非归一化高斯卷积
	Mat  kernelK = getSepDoGKernel(image, k*sigma, ksize);
	// 3 两个做差分
	Mat  dogCov= kernelK- kernel;
	return dogCov;
}
Mat src, src_gray, dest1, dest2, dest3, dest4, dest5, dest6;
const char* win = "自定义Dog";


int main(int args, char* arg)
{

	// point  
	src = imread("C:\\Users\\19473\\Desktop\\opencv_images\\88.jpg");
	if (!src.data)
	{
		printf("could not  load  image....\n");
	}

	//1 显示灰色图像
	namedWindow("input_demo", CV_WINDOW_AUTOSIZE);
	cvtColor(src, src_gray, CV_BGR2GRAY);
	imshow("input_demo", src_gray);


	Mat dogCov = DoG(src_gray, 2, 13,1.05);
	Mat  edge;
	threshold(dogCov, edge, 0, 255, THRESH_BINARY);
	imshow(win, edge);
	waitKey(0);
	return 0;
}

遗留问题;canny 自定义计算的时候有问题

  • 14
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
OpenCV是一个开源的计算机视觉库,提供了丰富的图像处理计算机视觉算法。其中,图像边缘检测是OpenCV中的一个重要功能,用于检测图像中物体的边缘。 在OpenCV中,常用的图像边缘检测算法有以下几种: 1. Canny边缘检测算法:Canny算法是一种经典的边缘检测算法,它通过多阶段的处理来提取图像中的边缘。首先,对图像进行高斯滤波以降低噪声;然后,计算图像的梯度,并根据梯度的方向和幅值来确定边缘;最后,使用非极大值抑制和双阈值处理来提取最终的边缘。 2. Sobel算子:Sobel算子是一种基于梯度的边缘检测算子,它通过计算图像的一阶或二阶导数来检测边缘Sobel算子可以分别计算图像在水平和垂直方向上的梯度,并将两个方向上的梯度合并得到最终的边缘。 3. Laplacian算子:Laplacian算子是一种基于二阶导数的边缘检测算子,它可以检测出图像中的高频变化区域,即边缘Laplacian算子对图像进行二阶导数计算,并通过零交叉点来确定边缘。 使用OpenCV进行图像边缘检测的步骤如下: 1. 读取图像:使用OpenCV的函数读取图像文件。 2. 灰度化:将彩色图像转换为灰度图像,可以使用OpenCV的函数将图像转换为灰度模式。 3. 滤波处理:对灰度图像进行滤波处理,常用的滤波方法有高斯滤波。 4. 边缘检测:使用OpenCV提供的边缘检测函数,如CannySobelLaplacian等。 5. 显示结果:将检测到的边缘结果显示出来,可以使用OpenCV的函数将图像显示在窗口中。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值