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)
*第九个参数,扩充边界模式。

效果如下:

 代码显示:

int  factorial(int  n)
{
	int fact = 1;
	if (n==0)
	{
	 return 	fact = 1;

	}
	for (int i = 1; i < n; i++)
	{
		fact *= i;
	}
	return  fact;
}
Mat   CreateDiff(int  n);
void  my_sobel(Mat& src, Mat  dst, int  x_flag, int y_flag, int winsize, int borderType);
// 得到一个平滑算子  n为 窗口的大小
Mat  Getsmooth(int  n) 
{
	Mat smooth = Mat::zeros(Size(n,1),CV_32FC1);
	for (int i = 0; i < n; i++)
	{
		smooth.at<float>(0, i) = factorial(n - 1) / (factorial(i) * (factorial(n - i - 1)));
	}

	//当窗口 3     二项式展开的系数是   1 2 1       差分算子是       1  0 -1 
	//当窗口 5     二项式展开的系数是   1 4 6 4  1       差分算子是       1 2  0 -2 -1
	return  smooth;
}


//从上面的的二项式系数中 得到 差分算子
Mat   CreateDiff(int  n)
{
	Mat Diff = Mat::zeros(Size(n, 1), CV_32FC1);
	Mat Diff_pres = Getsmooth(n-1);// n=5 是返回的是1 3 3 1
	for (int i = 0; i < n; i++)
	{
		if (i == 0)
		{
			Diff.at <float>(0, i) = 1;
		}
		else if (i == n - 1)
		{
			Diff.at <float>(0, i) = -1;
		}
		else
		{
			Diff.at <float>(0, i) = Diff_pres.at<float>(0, i) - Diff_pres.at<float>(0, i - 1);
		}
	}
		//当n=3 时候,Diff_pres= 1, 1     Diff_pres.at<float>(0, i)=1,  Diff_pres.at<float>(0, i - 1)=1 
		//  Diff  =1 0  -1
		//当n=5  时候,Diff_pres= 1 3,3 1    
		//  Diff  =1 2 0  -2  -1
		return  Diff; 

}
// 是用sobel 算子,对图像完成卷积  ,当 x-flag=0,-->x的
void  my_sobel(Mat& src, Mat  dst, int  x_flag, int y_flag, int winsize, int borderType)
{
	// winsize 应该是》=3的奇数
	CV_Assert(winsize>=3&& winsize%2==1);
	// 得到 二项式的系数
	Mat  smooth = Getsmooth(winsize);
	//  得到差分系数
	Mat  smoothdiff = CreateDiff(winsize);

	//当x_flag!=0 是, 返回图像与水平方向上的卷积
	if (x_flag != 0)
	{
		// smooth.t() 表示矩阵的转置
		sepFilter2D(src, dst, CV_32FC1, smooth.t(), smoothdiff, Point(-1,-1), borderType);

	}
	// 当x_flag==0  && y_flag!=0 的时候 垂直方向的
	if (x_flag == 0 && y_flag != 0)
	{
		sepFilter2D(src, dst, CV_32FC1, smooth, smoothdiff.t(), Point(-1, -1), borderType);
	}

}


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);

	//  sobel   X方向上的算子
	Mat  sobel_kernel_x = (Mat_<int>(3, 3) << 1, 0, -1, 2, 0, -2, 1, 0, -1);
	filter2D(src_gray, dest3, -1, sobel_kernel_x, Point(-1, -1), 0.0);
	imshow("Opencv sobel_X", dest3);

	//  sobel   Y 方向上的算子
	Mat  sobel_kernel_y = (Mat_<int>(3, 3) << 1, 2, 1 , 0, 0, 0, -1, -2, -1);
	filter2D(src_gray, dest4, -1, sobel_kernel_y, Point(-1, -1), 0.0);
	imshow("Opencv sobel_Y", dest4);

	//================================================================
	
	// 自定义sobel算法
	my_sobel(src_gray, dest3, 1, 0, 3, BORDER_DEFAULT);
	imshow("自定义-水平方向", dest3);
	my_sobel(src_gray, dest3, 0, 1, 3, BORDER_DEFAULT);
	imshow("自定义-垂直方向", dest4);


	waitKey(0);
	return -1;
}

 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。

代码显示:

Mat src, src_gray, dst,dx,dy;
int  t1_value = 50;
int  max_value = 255;
const  char* OUTPUT_TITLE = "Canny Reult";

void  CannyCallback(int, void*);
void  CannyCallback(int, void*)
{
	Mat  edge_output;
	// 2 模糊之后的图像    8位的灰度图图像
	blur(src_gray, src_gray, Size(3, 3), Point(-1, -1), BORDER_DEFAULT);
	// 3 使用canny 算子  阈值越低 细节越多
	Canny(src_gray, edge_output, t1_value, t1_value * 2, 3, false);


	dst.create(src.size(), src.type());
	src.copyTo(dst, edge_output);
	imshow(OUTPUT_TITLE, ~dst);//  ~dst
}



Mat non_max_supprusion(Mat  dx ,Mat dy)  //传进来的是两个方向上的差分矩阵  3*3 的掩膜
{
	//边缘强度=sqrt(dx 的平方+dy 的平方) 
	Mat  edge;
	magnitude(dx, dy, edge);// 计算幅度值
	int  rows = dx.rows;
	int  cols = dy.cols;
	//边缘强度的非极大值抑制
	Mat edgemag_nonMaxSup = Mat::zeros(dx.size(),dx.type());
	// 用两个循序计算出  和梯度方向 并且转换成angleMatrix
	for (int row = 1; row < rows-1; row++)
	{
		for (int col = 1; col < cols-1; col++)
		{
			float x = dx.at<float>(row, col);
			float y = dx.at<float>(row, col);
			// 梯度的方向---atan2f

			float  angle = atan2f(y, x) / CV_PI * 180;
			// 当前位置的边缘强度
			float  mag = edge.at<float>(row, col);
			// 找到左右两个方向
			if (abs(angle)<22.5||abs(angle)>157.5) 
			{
				float  left = edge.at<float>(row, col - 1);
				float  right = edge.at<float>(row, col + 1);
				// 判断两个方向上的
				if (mag>left&&mag>right) {
					edgemag_nonMaxSup.at<float>(row, col) = mag;
				}

			}
			// 左上和右下两个方向
			if ((abs(angle) >= 22.5 && abs(angle) < 67.5)|| (abs(angle) <-112.5 && abs(angle) > 157.5))
			{
				float  lefttop = edge.at<float>(row-1, col - 1);
				float  rightbottom = edge.at<float>(row+1, col + 1);
				// 判断两个方向上的
				if (mag > lefttop && mag > rightbottom) {
					edgemag_nonMaxSup.at<float>(row, col) = mag;
				}
			}

			//  上  下 方向
			if ((abs(angle) >= 67.5 && abs(angle) <= 112.5) || (abs(angle) >= -112.5 && abs(angle) <= -67.5))
			{
				float  top = edge.at<float>(row - 1, col);
				float  down = edge.at<float>(row + 1, col);
				// 判断两个方向上的
				if (mag > top && mag > down) {
					edgemag_nonMaxSup.at<float>(row, col) = mag;
				}
			}




			//  右上  左下 方向
			if ((abs(angle) > 122.5 && abs(angle) < 157.5) || (abs(angle) > -67.5 && abs(angle) <=-22.5))
			{
				float  leftdown = edge.at<float>(row - 1, col+1);
				float  rightup = edge.at<float>(row + 1, col-1);
				// 判断两个方向上的
				if (mag > leftdown && mag > rightup) {
					edgemag_nonMaxSup.at<float>(row, col) = mag;
				}
			}
		}
	}
	return  edgemag_nonMaxSup;
}


Mat non_max_supprusion_inter(Mat  dx, Mat dy)  //non_max_supprusion的改进,使得非极大值抑制更加完整
{
	//边缘强度=sqrt(dx 的平方+dy 的平方) 
	Mat  edge;
	dx.convertTo(dx,CV_32F);// CV_32FC1
	dy.convertTo(dy, CV_32F);// CV_32FC1
	magnitude(dx, dy, edge);// 计算幅度值
	int  rows = dx.rows;
	int  cols = dy.cols;
	//边缘强度的非极大值抑制
	Mat edgemag_nonMaxSup = Mat::zeros(dx.size(), dx.type());
	// 用两个循序计算出  和梯度方向 并且转换成angleMatrix
	for (int row = 1; row < rows - 1; row++)
	{
		for (int col = 1; col < cols - 1; col++)
		{
			float x = dx.at<float>(row, col);
			float y = dx.at<float>(row, col);
			// 梯度的方向---atan2f

			if (x==0 && y==0)
			{
				continue;
			}

			float  angle = atan2f(y, x) / CV_PI * 180;
			// 邻域内8个方向上的边缘处理
			float  left = edge.at<float>(row, col - 1); // 左
			float  right = edge.at<float>(row, col + 1); // 右

			float  lefttop = edge.at<float>(row - 1, col - 1); // 左上
			float  rightbottom = edge.at<float>(row + 1, col + 1); //右下


			float  top = edge.at<float>(row - 1, col);  // 上
			float  bottom = edge.at<float>(row + 1, col);  // 下

			float  leftdown = edge.at<float>(row - 1, col + 1);  // 做下
			float  rightup = edge.at<float>(row + 1, col - 1);// 右上




			// 当前位置的边缘强度
			float  mag = edge.at<float>(row, col);
			// 找到左上 和上面的方向  以及 右下  下面
			if ((abs(angle) >45 || abs(angle) <=90)|| (abs(angle) > -135 || abs(angle) <= -90))
			{
				float  ratio = x / y;
				float  top= edge.at<float>(row - 1, col);
				// c插值
				float  lefttop_top = ratio * lefttop + (1 - ratio) * top;
				float  rightdown_down = ratio * rightbottom + (1 - ratio) * bottom;
				// 判断两个方向上的
				if (mag > lefttop_top && mag > rightdown_down) {
					edgemag_nonMaxSup.at<float>(row, col) = mag;
				}

			}



			// 找到右上 和上面的方向  以及 左下  下面
			if ((abs(angle) > 90 || abs(angle) <= 135) || (abs(angle) > -90 || abs(angle) <= -45))
			{
				float  ratio =abs( x / y);
				float  top = edge.at<float>(row - 1, col);
				// c插值
				float  rithttop_top = ratio * rightup + (1 - ratio) * top;
				float  leftdown_down = ratio * leftdown + (1 - ratio) * bottom;
				// 判断两个方向上的
				if (mag > rithttop_top && mag > leftdown_down) {
					edgemag_nonMaxSup.at<float>(row, col) = mag;
				}

			}



			// 找到左上 和左的方向  以及 右下  右面
			if ((abs(angle) > 0 || abs(angle) <= 45) || (abs(angle) > -180 || abs(angle) <= -135))
			{
				float  ratio = x / y ;
				float  top = edge.at<float>(row - 1, col);
				// c插值
				float  rithtbottom_right = ratio * rightbottom + (1 - ratio) * right;
				float  leftup_left = ratio * lefttop + (1 - ratio) * left;
				// 判断两个方向上的
				if (mag > rithtbottom_right && mag > leftup_left) {
					edgemag_nonMaxSup.at<float>(row, col) = mag;
				}

			}



			// 找到右上 和右 的方向  以及 左下  左面
			if ((abs(angle) > 135 || abs(angle) <= 180) || (abs(angle) > -45 || abs(angle) <= -0))
			{
				float  ratio = abs(x / y);
				float  top = edge.at<float>(row - 1, col);
				// c插值
				float  rithttop_right = ratio * rightup + (1 - ratio) * right;
				float  leftdown_left = ratio * leftdown + (1 - ratio) * bottom;
				// 判断两个方向上的
				if (mag > rithttop_right && mag > leftdown_left) {
					edgemag_nonMaxSup.at<float>(row, col) = mag;
				}
			}
		}
	}
	printf("edgemag_nonMaxSup  is over");
	return  edgemag_nonMaxSup;
}

bool  CheckInRange(int r,int c,int  rows,int cols) {  // 确定一个点是不是在给出的图像范围里面
	 
	if (r >= 0 && r < rows && c>= 0 && c < cols)
	{
		return  true;
	}
	else
	{
		return  false;
	}
}

void   trace(Mat  edge_nonMaxSup, Mat& edge, float  lower, int  r, int c, int rows, int cols) {
	// 从确定边缘点出发,延长边缘
	if (edge.at<uchar>(r,c)==0)
	{
		edge.at<uchar>(r, c) = 255;
		// 3*3  的掩膜
		for (int i = -1; i <= 1; i++)
		{
			for (int j = -1; j <= 1; j++)
			{
				float  mag = edge_nonMaxSup.at<float>(r + i, c + j);
				if (CheckInRange(r + i, c + j,rows,cols)&&mag>=lower)
				{
					//printf("trace");
					trace(edge_nonMaxSup,edge, lower,r+i,c+j,rows,cols); // 用递归来找边缘的点
				}
			}
		}
	}
}

Mat  hysteresisThreshold(Mat  edge_mag_momMaxSup,float  lower,float hight)
{
	int  rows = edge_mag_momMaxSup.rows;
	int  cols = edge_mag_momMaxSup.cols;
	// 最后输出的图像
	Mat edgeImage = Mat::zeros(Size(rows,cols),CV_8UC1);
	// 滞后阈值处理
	for (int i = 1; i < rows-1; i++)
	{


		for (int  j = 1; j < cols - 1; j++)
		{
			if (i == 349&&j>=354)
				printf("baocuo %d\n",j);
			float  mag = edge_mag_momMaxSup.at<float>(i, j);
			// mag >hight --->肯定是边缘点,并将这个点做为起始点的延长点,点集合依次扩充
			if (mag >= hight)
			{
				trace(edge_mag_momMaxSup, edgeImage, lower, i, j, rows, cols);
			}
		
			// 小于低阈值的肯定不是
			if (mag < lower) 
			{
				edgeImage.at<uchar>(i, j) = 0;
			}
		}
	}
	printf("hysteresisThreshold");
	return  edgeImage;
}


int main(int args, char* arg)
{
	// point  
	src = imread("C:\\Users\\19473\\Desktop\\opencv_images\\lover.jpg");
	if (!src.data)
	{
		printf("could not  load  image....\n");
	}
	namedWindow("input_demo", CV_WINDOW_AUTOSIZE);
	namedWindow(OUTPUT_TITLE, CV_WINDOW_AUTOSIZE);
	imshow("input_demo", src);

	cvtColor(src, src_gray, CV_BGR2GRAY);



	//createTrackbar("Thread_Value:", OUTPUT_TITLE, &t1_value, max_value, CannyCallback);
	//CannyCallback(0, 0);
	// 自定义cannY算子


	//==================================================================
	// 1 , sobel   X方向上的算子
		//  sobel   X方向上的算子
	Mat  sobel_kernel_x = (Mat_<int>(3, 3) << 1, 0, -1, 2, 0, -2, 1, 0, -1);
	filter2D(src_gray, dx, -1, sobel_kernel_x, Point(-1, -1), 0.0);
	imshow("solel_dx", dx);
	//  sobel   Y 方向上的算子
	Mat  sobel_kernel_y = (Mat_<int>(3, 3) << 1, 2, 1, 0, 0, 0, -1, -2, -1);
	filter2D(src_gray, dy, -1, sobel_kernel_y, Point(-1, -1), 0.0);
	imshow("solel_dy", dy);
	// 2 生产 非极大值抑制滞后的矩阵
	Mat edge_mag_momMaxSup,edgeImage;
	edge_mag_momMaxSup=non_max_supprusion_inter(dx,dy);
	// 调用这个阈值滞后处理函数
	edgeImage=hysteresisThreshold(edge_mag_momMaxSup,100,200);
	imshow("自定义Canny", edgeImage);
	waitKey(0);
	return 0;
}

上面的代码中hysteresisThreshold中有点问题,找了好几个小时没有找出问题,为了不耽搁后面的学习,这问题暂时留下,后续更新

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 自定义计算的时候有问题

猜你喜欢

转载自blog.csdn.net/weixin_39354845/article/details/121715932
今日推荐