引言
Sobel算子、Scharr算子、Laplacian算子和Canny算子都是常用的图像边缘检测算法。它们可以用来识别图像中物体之间的边界,从而对物体进行定位、跟踪、分割、识别等处理。
Sobel算子和Scharr算子都是基于卷积运算实现的边缘检测算法。Sobel算子使用两个3×3的矩阵对原始图像进行卷积运算,分别用于检测水平方向和垂直方向上的边缘。而Scharr算子的卷积核比Sobel算子更加精细,能够提供更加准确的边缘检测结果。这两种算子在实现方式上类似,都能够有效地检测出图像中的边缘信息。
Laplacian算子则是一种基于二阶微分的边缘检测算法。它通过对原始图像进行拉普拉斯变换来检测图像中的边缘。这种算法对噪声比较敏感,但是能够提供更加清晰锐利的边缘效果。与Sobel算子和Scharr算子不同,Laplacian算子可以同时检测出图像中的正向和负向边缘。
Canny算子则是一种常用的基于梯度的边缘检测算法。它通过计算图像中每个像素点处的梯度值和方向,从而检测出图像中的边缘。Canny算子处理结果具有良好的连续性、准确性和低误检率,因此在实际应用中被广泛应用。
Sobel算子
Sobel算子的具体实现方法是使用两个3×3的矩阵分别对图像进行卷积操作,其中一个矩阵用于检测水平方向的边缘,另一个矩阵用于检测垂直方向的边缘。通过将这两个卷积结果合并,可以得到最终的边缘检测结果。
上式中A为卷积核中所有的元素,Gx为x方向上的差异,Gy为y方向的差异,此时的乘法并不是矩阵相乘,是对应元素相乘,最后取和,得到x方向和y方向的分量。Gx矩阵中所有元素代表卷积核A中所有元素的权。最终每个像素点的值是加权求和。那么对于Gx来说,他就只考虑水平方向的差异,而对于Gy是一样的。
在opencv中的方法为cv2.Sobel(src, ddepth, dx, dy, dst, ksize, scale, delta, borderType)
其中src为传入图像
ddepth为图像深度
dx,dy指求水平方向还是竖直方向
ksize指卷积核大小
borderType:是判断图像边界的模式,这个参数默认值为cv2.BORDER_DEFAULT
实际操作
通过实际操作理解
现有下图circle,可以用自带的画图软件画一个也可以网上找
计算水平方向的边缘
import cv2#展示图像的方法def cv_show(title,img):cv2.imshow(title,img)cv2.waitKey(0) cv2.destroyAllWindows()return读取图像img = cv2.imread('circle.png')sobelx = cv2.Sobel(img,cv2.CV_64F,1,0,ksize = 3)cv_show('sobelx',sobelx)
解释代码
sobelx = cv2.Sobel(img,cv2.CV_64F,1,0,ksize = 3)
这行代码使用了OpenCV中的Sobel函数,对输入的图像进行水平方向上的Sobel边缘检测,并将结果赋值给变量sobelx。
具体地,该函数的第一个参数img是输入的待处理图像;第二个参数cv2.CV_64F指定了处理后图像的数据类型为64位浮点数;第三个参数1表示对输入图像进行水平方向上的Sobel算子运算;第四个参数0表示不对输入图像进行垂直方向上的Sobel算子运算;第五个参数ksize=3表示使用3×3大小的Sobel算子进行卷积运算
注意在OpenCV中,如果要进行Sobel算子等卷积运算,则需要指定输入图像和输出图像的数据类型。由于Sobel算子运算过程中涉及到浮点型数据的运算,因此需要使用cv2.CV_64F数据类型。
可以看到结果:
此时我们发现边缘检测只有一半,原因在于之前的计算矩阵,
本质上是右边减左边。对于图像左侧边缘的像素来说,这些像素的右边是白色(255)左边是黑色(0),那么右减左就是255(白色),但是对于图像左边来说,右边是黑色(0),左边是白色(255),右减左等于-255,然而由于OpenCV中图像矩阵的dtype是uint8类型的,取值范围是0-255,那么对于-255他会默认赋予0(黑色)。当然这边是由于我们的例子的问题,实际上在计算灰度图矩阵的梯度时候,Sobel的矩阵不会误差这么明显,但是仍然会出现运算结果为负数从而导致有些边缘识别不出来的情况。
此时我们需要cv2.convertScaleAbs()方法,取个绝对值,代码如下
import cv2def cv_show(title,img):cv2.imshow(title,img)cv2.waitKey(0) cv2.destroyAllWindows()returnimg = cv2.imread('circle.png')sobelx = cv2.Sobel(img,cv2.CV_64F,1,0,ksize = 3)sobelx = cv2.convertScaleAbs(sobelx)#为应对sobel算子计算为负的情况,取绝对值求正梯度。cv_show('sobelx',sobelx)
运算为负的值都取绝对值就行。最后得到结果:
对于y轴我们也试一下,y轴的本质就是下减上
import cv2import numpy as npdef cv_show(title,img):cv2.imshow(title,img)cv2.waitKey(0) cv2.destroyAllWindows()returnimg = cv2.imread('circle.png')sobely = cv2.Sobel(img,cv2.CV_64F,0,1,ksize = 3)#y轴就是y取1,x取0sobely = cv2.convertScaleAbs(sobely)#为应对sobel算子计算为负的情况,取绝对值求正梯度。cv_show('sobely',sobely)
结果如下:注意代码中我们已经取绝对值了 ,没取绝对值的话只有一半
那么算出x和y方向上的边缘之后,我们还要求一个总和。结合x方向和y方向计算总的边缘。还记得前文讲图像叠加时所教的addWeighted方法吗,此时我们就用的这个方法:
import cv2import numpy as npdef cv_show(title,img):cv2.imshow(title,img)cv2.waitKey(0) cv2.destroyAllWindows()returnimg = cv2.imread('circle.png')sobelx = cv2.Sobel(img,cv2.CV_64F,1,0,ksize = 3)#x方向sobelx = cv2.convertScaleAbs(sobelx)#取绝对值求正梯度。sobely = cv2.Sobel(img,cv2.CV_64F,0,1,ksize = 3)#y方向sobely = cv2.convertScaleAbs(sobely)#取绝对值求正梯度。sobelxy = cv2.addWeighted(sobelx,0.5,sobely,0.5,0)cv_show('xy',sobelxy)
最后结果:
可以发现x方向和y方向边缘检测后的两组数据相互叠加之后的边缘更全面。
不建议使用sobel算子同时求x轴和y轴,效果不好。
测试一下同时求XY的效果:
import cv2def cv_show(title,img):cv2.imshow(title,img)cv2.waitKey(0) cv2.destroyAllWindows()returnimg = cv2.imread('circle.png')sobelxy = cv2.Sobel(img,cv2.CV_64F,1,1,ksize = 3)#xy方向sobelxy = cv2.convertScaleAbs(sobelxy)#取绝对值求正梯度。cv_show('xy',sobelxy)
结果:
会发现同时求x,y,的效果不如分别求完x和y方向后叠加效果,会出现边缘缺失和重影。
之前是计算二值图,那么实际情况很少有计算二值图的,一般都是灰度图。接下来用第一章时讲的灰度图示范一下。
import cv2def cv_show(title,img):cv2.imshow(title,img)cv2.waitKey(0) cv2.destroyAllWindows()returnimg = cv2.imread('bird.jpg',cv2.IMREAD_GRAYSCALE)#复习一下第一张读取灰度图的方式cv2.IMREAD_GRAYSCALEsobelx = cv2.Sobel(img,cv2.CV_64F,1,0,ksize = 3)#x方向sobelx = cv2.convertScaleAbs(sobelx)sobely = cv2.Sobel(img,cv2.CV_64F,0,1,ksize = 3)#y方向sobely = cv2.convertScaleAbs(sobely)sobelxy = cv2.addWeighted(sobelx,0.5,sobely,0.5,0)#xy方向sobelxy = cv2.convertScaleAbs(sobelxy)cv_show('xy',sobelxy)
效果如下:
Scharr算子
scharr算子和sobel的原理一致,就是Gx和Gy参数的大小不同,也就是卷积核中各元素的权不同,其他都一样,scharr算子对于边界的梯度计算效果更精确。
用bird图试一下:cv2.Scharr()
import cv2def cv_show(title,img):cv2.imshow(title,img)cv2.waitKey(0) cv2.destroyAllWindows()returnimg = cv2.imread('bird.jpg',cv2.IMREAD_GRAYSCALE)scharrx = cv2.Scharr(img,cv2.CV_64F,1,0)#x方向,ksize是可选参数,可以不写scharrx = cv2.convertScaleAbs(scharrx)scharry = cv2.Scharr(img,cv2.CV_64F,0,1)#y方向scharry = cv2.convertScaleAbs(scharry)scharrxy = cv2.addWeighted(scharrx,0.5,scharry,0.5,0)#xy方向scharrxy = cv2.convertScaleAbs(scharrxy)cv_show('xy',scharrxy)
从权重矩阵可以看出,Scharr算子的卷积核相对于Sobel算子的卷积核来说,权重较大的位置更加集中在中心的位置上,且权值之间的差异更加明显。因此,Scharr算子比Sobel算子对图像边缘的响应更加敏感,在边缘检测的效率和精度上更优秀一些。当然也更容易被噪声影响。
在实际应用中,Sobel算子的计算速度较快,适合用于大规模的边缘检测处理;而Scharr算子的检测效果更加准确,适合用于对较小目标、边缘特别细致的图像进行处理。
laplacian算子
这个算子和之前的Sobel算子Scharr算子不同的是这个算子不分xy,权重矩阵如下:
之前的算子是像素点上下或左右进行比较,而这个算子是比较中心像素点和周围的像素点的关系。
实际操作:cv2.Laplacian()
import cv2def cv_show(title,img):cv2.imshow(title,img)cv2.waitKey(0) cv2.destroyAllWindows()returnimg = cv2.imread('bird.jpg',cv2.IMREAD_GRAYSCALE)laplacian = cv2.Laplacian(img,cv2.CV_64F)#这里就不需要穿x或者y的参数了cv_show('lap',laplacian)
结果:
由于Laplacian算子是对图像灰度函数进行二阶微分操作(Laplacian算子的卷积核是基于二阶微分计算而来的。权重矩阵卷积后得到的结果实际上代表了原始图像的二阶导数),因此其处理结果对噪声非常敏感。为了避免噪声对边缘检测结果产生干扰,通常需要采用一些预处理、平滑滤波等方式来对原始图像进行预处理。此外,在实际应用中,还可以根据实际需求选择不同的阈值化和后处理等方式来优化Laplacian算子的边缘检测结果。
Canny算子
Canny算子是一个边缘检测的完整过程:
1)使用高斯滤波,去除噪声
2)计算图像中每个像素点的梯度强度和方向
3)应用非极大值抑制NMS,只保留明显边缘
4)应用双阈值检测来确定真实的边缘和潜在的边缘
5)通过抑制弱边缘最终完成边缘检测
详解:
1.之前讲的边缘检测方法都会受到噪声的干扰,所以在进行边缘检测前,就会先降噪,一般都是用高斯滤波进行降噪处理。可以复习一下前几张讲的高斯滤波OpenCV(4):图像平滑处理,四种滤波_浪浪山猪的博客-CSDN博客
2.降噪完成之后,利用Sobel算子将各方向梯度算出来,分别算出Gx和Gy后,我们便可以知道梯度强度G 和梯度方向θ
3.接着进行非极大值抑制,逐一遍历所有的像素点,如果该点是梯度方向(可能是正梯度方向,可能是负梯度方向)上局部的最大值,那么就保留该点,如果不是局部最大值,那么就认为不是边缘,抑制该点(置零)。
如下图中黑点被认为是边缘上的像素点,箭头表示该点的梯度方向,由Gx和Gy确定。
通过这一处理之后,同一方向上只保留一个点被视为边缘点。这一做法就被称为非极大值抑制,一些效果不明显的边缘就被剔除了。
4. 当完成非极大值抑制后,只保留了效果明显的边缘,然而,对于这些边缘,有些确实是图像的边缘,而有些却是由噪声产生的。之前用高斯滤波只是降噪,并不能消除噪声。而这些噪声造成的边缘被称为虚边缘,我们需要通过双值域将其消除。
双值域法的具体方法是首先设置两个阈值:高阈值和低阈值 对于所有的像素点,逐一遍历, 大于高阈值定为强边缘, 低于高阈值但是高于低阈值被定为虚边缘, 低于低阈值的像素点(抑制)置零。
所有的强边缘保留,而对于虚边缘,我们要做个判断。如果这些虚边缘是连接着强边缘的,那么保留,如果不是,那么抑制(置零)
以上就是Canny边缘检测的全部流程,虽然很复杂,但是在OpenCV中我们直接用cv2.Canny()来实现 Canny 边缘检测:
cv.Canny( image, threshold1, threshold2[, apertureSize[, L2gradient]])
image: 输入图像。
threshold1: 处理过程中的第一个阈值。
threshold2:处理过程中的第二个阈值。
apertureSize: 表示 Sobel 卷积核大小。
L2gradient:为计算图像梯度幅度的标识。默认值为 False。如果为 True,则使用更精确的L2范数进行计算(即两个方向的导数的平方和再开方),否则使用 L1 范数(直接将两个方向导数的绝对值相加)。
import cv2def cv_show(title,img):cv2.imshow(title,img)cv2.waitKey(0) cv2.destroyAllWindows()returnimg = cv2.imread('bird.jpg',cv2.IMREAD_GRAYSCALE)p1 = cv2.Canny(img,threshold1 = 50,threshold2 = 200)#设置上下阈值为50和200,也可任意设置cv_show('p1',p1)
返回结果:
总结
这些边缘检测算法各有优缺点,在实际应用中需要根据具体场景选取合适的算法。同时,为了提高处理效率,还可以采用一些预处理、阈值化、后处理等方式对边缘检测结果进行优化。