边缘检测可以说是一种定向滤波,图像分为垂直频率(垂直方向的变化)和水平频率(水平方向的变化)两个方向,总体来说就是用后一个像素点的值减去前一个像素点的值得到的结果,我们称之为梯度,就是我们最后要求得边缘。也就是说用下图中第3列减去第二列得值,但是我们怎么实现这种操作呢,我们就用图像滤波方式来实现,区别在于滤波器得值我们在这里指定了大小,正负。
以一个3∗3大小的为例,中心点为 f(x,y) ,如图:
Robert算子
我们首先定义Robert算子得卷积核,如下图:
得到的X,Y方向的梯度就是:
上述公式是怎么计算出来的呢?我们看一幅图:
我们计算X方向的梯度:
Y方向同理。
卷积核要翻转180°之后再参与计算的。
Robert算子没有相应的函数,其实在实际中也不怎么用。
#coding=utf-8
import cv2
# robert 算子[[-1,-1],[1,1]]
def robert_suanzi(img):
r, c = img.shape
r_sunnzi = [[-1, -1], [1, 1]]
for x in range(r):
for y in range(c):
if (y + 2 <= c) and (x + 2 <= r):
imgChild = img[x:x + 2, y:y + 2]
list_robert = r_sunnzi * imgChild
img[x, y] = abs(list_robert.sum()) # 求和加绝对值
return img
img = cv2.imread('images/robert.png',0)
dst = robert_suanzi(img)
cv2.imshow('dst',dst)
cv2.waitKey()
Sobel算子
原理与上述Robert
算子一致,我们分别计算X和Y方向的梯度,但是整幅图像的梯度是什么呢?
sobel算子opencv已经给我们写好了函数,我们直接调用即可。
函数API:
dst = cv2.Sobel(src, ddepth, dx, dy[, dst[, ksize[, scale[, delta[, borderType]]]]])
参数解释:
src:原始图像
ddepth:处理结果图像的深度
dx:X方向的梯度
dy:Y方向的梯度
ksize:滤波器大小
我们来具体讲解一下depth参数,通常情况来说我们会设为-1,代表处理结果与原始图像保持一致;但是一般不用,因为会存在溢出问题。
下面有这样一幅图:
B这条边,我们用右面白色部分减去黑色部分,得到的是一个正数
A这条边,我们用右面黑色部分减去白色部分,得到的是一个负数
但是在灰度图中,像素值的范围是0-255,那么小于0的数值,我们就会自动赋值为0,称为截断。那么这条边就不会显示了,所以我们就需要把depth参数的深度调大一点,即cv2.CV_64F,代表64位的浮点数,之后再转换到正常图像的类型为cv2.CV_8U,也就是np.uint8类型,8位整型。
转换方法就是我们取A计算出来的绝对值,用到的函数API:
dst = cv2.convertScaleAbs(src[, dst[, alpha[, beta]]])
虽然这个函数有很多参数,但是我们用的时候,只填第一个参数即可。
目标函数 = cv2.convertScaleAbs(原始图像)
在灰度图像中,也就是256色位图中,白色的像素值位255,黑色为0。
垂直边界的与水平原理一样。
当我们计算最后的结果时,我们需要把两个方向的梯度都加起来,有两种方式:
sobel_xY = cv2.Sobel(img, -1, 1, 1, ksize =3)
但是这样的效果并不是很好,同时求导对于精度的要求太高,使得有些边缘并没有计算出来。
单独对X方向和Y方向求导,再相加,需要用到cv2.addWeighted函数
# Sobel算子边缘检测,分别对x,和y方向计算梯度
sobel_x = cv2.Sobel(img, cv2.CV_64F, 1, 0, ksize =3)
sobel_y = cv2.Sobel(img, cv2.CV_64F, 0, 1, ksize =3)
#经过卷积之后,会出现溢出的问题,需要转换
abs_x = cv2.convertScaleAbs(sobel_x)
abs_y = cv2.convertScaleAbs(sobel_y)
#第一个参数:第一幅图像
#第二个参数:第一幅图像的权重
#第三个参数:第二幅图像
#第四个参数:第二幅图像的权重
#第五个参数:修正值,一般为0
sobel_show = cv2.addWeighted(abs_x, 0.5, abs_y, 0.5, 0)
拉普拉斯算子
其实原理都是一样的,拉普拉斯就是需要减两次就可以了。
减几次就对应的求函数的几阶导数,因为图像也是一个函数嘛。
一阶导:sobel算子 |右-左| + |下-上|
二阶导:laplacian算子 |右-左| + |下-上| + |右-左| + |下-上|
它的卷积核长这个样子:
函数API:
dst = Laplacian(src, ddepth[, dst[, ksize[, scale[, delta[, borderType]]]]])
src:原始图像
ddepth:处理结果图像的深度,与Sobel算子的一样
ksize:滤波器大小
代码演示:
laplacian = cv2.Laplacian(img, cv2.CV_64F)
lap_show = cv2.convertScaleAbs(laplacian)
Canny算子
canny算子的计算步骤:
1.去除噪声
边缘检测容易受到噪声的影响,所以先使用高斯滤波去除噪声
2.梯度计算
canny算子的梯度不仅有值还有方向,梯度计算上面已经说过了,一个像素点的梯度方向计算方式:
,因为这个方向是任意的,所以我们归为4个大类:水平,垂直和两个对角线。
3.非极大值抑制
得到梯度值和方向后,我们去除不是边界的点。
因为一个梯度方向上,有好多点,我们就判断当前像素点的梯度值是否是周围像素点中最大值,最大值就保留下来,否则就赋值为0。
4.双阈值
双阈值就是判断边缘的两个阈值,一个高阈值,一个低阈值,边缘点大于高阈值我们就认为他肯定是边缘,叫做强边缘点,小于低阈值,肯定不是边缘,我们可以忽略这部分边缘点;重要的是处于高低阈值中间的点的处理:需要判断这个点的8邻域中,是否有强边缘点,有就留下,没有就被忽略。
Canny算子的函数API:
edges = Canny(image, threshold1, threshold2[, edges[, apertureSize[, L2gradient]]])
image:原始图像
threshold1:阈值1
threshold2:阈值2
实验表明:高低阈值的比率最好为 2:1或者3:1。
例如:
canny = cv2.Canny(img, 50, 150)
如果想要图像边缘的细节信息多一些,那么阈值就设置小一些。