边缘检测
在图像中,边缘就是图像强度快速变化的地方。而我们求边缘实际上就是求图像的梯度方向,因为沿着梯度向量方向,函数值增加最快。
由于在实际表现中,图像是离散的,是有一个个像素组成的,所以,我们可以近似认为,图像的导数是:
当我们求出导数时,我们就可以求出梯度了
sobel算子
由于图像中存在噪声,比如下图
这时候我们就很难得到边缘的位置,所以首先进行一次滤波就是一件十分必要的事情了,我们使用高斯核对图像进行卷积后,在进行微分,就可以得到相对准确的边缘,由于卷积的微分满足:
这时只需要对高斯核求导数就好了,于是,就产生了Sobel算子,他是高斯导数的一般近似。
然后将Sobel算子与原图像卷积。我们就得到了在x方向和在y方向的梯度, 然后通过公式就可以得出梯度的幅值和方向
一般来说Sobel算子的标准定义忽略了1/8,因为这不会对边缘检测产生影响,但如果想要得到正确的梯度幅值,1/8是必须的。
Opencv实现
cv.Sobel()
是Opencv提供来处理Sobel算子的函数
cv.Sobel(src, ddepth, dx, dy, ksize, scale, delta, borderType)
参数:
- src: 原始图像
- dx: 导数x的阶数
- dy: 导数y的阶数
- ksize: sobel算子的尺寸,必须为奇数和正数
- scale: 计算得出的导数值的可选比例因子;默认情况下,不应用
- delta: 在将结果存储到dst之前将其添加到结果中的可选增量值。
- borderType:像素外推法。
一般来说,当(dx=1, dy=0, ksieze=3)是就是Sx:
而(dx=0, dy=1, ksize=3)就是Sy:
实例说明
import cv2 as cv
import numpy as np
def sobel_by_own(img):
kernel = np.array([
[-1, 0, 1],
[-2, 0, 2],
[-1, 0, 1]]
)
img = cv.filter2D(img, -1, kernel)
return img
def sobel_by_opencv(img):
sobelx = cv.Sobel(img, -1, 1, 0, ksize=3)
return sobelx
def sobel_by_test(img):
# 重新滤波一次会有更好的效果...原来sobel算子说或多或少的滤波效果还真是或多或少啊
img = cv.GaussianBlur(img, (3, 3), 1, 0)
soeblx = cv.Sobel(img, -1, 1, 0,ksize=3)
return soeblx
if __name__ == '__main__':
img = cv.imread('img/edge.png', 0)
dst1 = sobel_by_opencv(img)
dst2 = sobel_by_own(img)
dst3 = sobel_by_test(img)
cv.namedWindow('sobel1')
cv.namedWindow('Own2')
cv.namedWindow('gaussian')
cv.imshow('sobel1', dst1)
cv.imshow('Own2', dst2)
cv.imshow('gaussian', dst3)
cv.waitKey(0)
cv.destroyAllWindows()
canny算子
canny算子被认为是当今最优的边缘检测算法。他一般包括四个部分
- 消除噪声
- 计算梯度和幅值
- 非最大抑制
- 滞后阈值
步骤详细
消除噪声
虽然Sobel算子可以或多或少的消除噪声,但是这个作用在某些方面微乎其微(见上例),所以首先使用一次高斯滤波消除部分噪声对于边缘检测有非常大的作用.
计算梯度和幅值
接下来就应该计算图像中每一个像素的梯度,这时,我们可以使用Sobel算子近似计算其在X和Y方向的梯度(见上)。当然也可以使用其他算法。
然后我们就可以得到这一个像素的梯度幅值和方向了。
非最大抑制
通过第二个步骤,我们得到了一个图像的梯度矩阵。如果梯度中的元素值越大,那么说明图像中该点的梯度值越大。但是,这仅仅只能说明这属于图像增强的过程,而不一定是边缘。因为只有像素变化最快的地方才是边缘。
所以非最大抑制就是为了寻找这样的点。
为了得到更为准确的图像边缘,我们对梯度矩阵进行全面扫描,检查每一个梯度是否为其所代表的像素在梯度方向的局部最大值。如果不是,我们就可以将其置为0。
我们取一个像素点C,如图
则,像素点C附近有八个像素,我们只需要判断C的梯度是否为其八值临域的最大值即可。同时,因为梯度最大值一定分布在其梯度方向上,所以,我们只需要判断梯度方向上的三个点的大小即可。
但是梯度方向不一定是水平,竖直,或者两个个对角线。他可能是各种方向,如图,蓝色就是梯度方向,而蓝色这条线并没有没有经过C的附近的八个点。这时我们插入两个假想的像素dTmp1和dTmp2,并通过某种算式得出dTmp1(通过g1和g2)和dTmp2(通过g3和g4)的值。
然后,将C点的梯度幅值与dTmp1和dTmp2相比较,如果C点值是最大的,则保留,进行下一步操作。如果不是,说明他不是局部最大值,就将其置为0。
滞后阈值
尽管如此,我们还是有可能存在一些虚假的边缘(如噪声), 所以这一步可以得到真正的边缘,为此,我们设置了两个阈值。minval和maxval。
强度大于maxval的任何边缘必定是边缘,而小于minval的那些强边缘必定是非边缘。而介于两者之间的,则根据其联通性被分配,如果连接到强边缘则一定是边缘,否则, 被丢弃。
Opencv实现
cv.Canny()
是Opencv提供用来进行Canny边缘检测的函数
edges = cv.Canny(image, threshold1, threshold2, edges, apertureSize, L2gradient)
参数:
- image: 原图像
- threshold1: minval
- threshold2: maxval
- apertureSize: Sobel算子的尺寸
- L2gradient:一个flag,如果为真,那么就使用具体的计算。如果为假,那么就近似为
L1=|dx|+|dy|