一. Canny基本思想
1. 边缘检测
解析:边缘是对象和背景之间的边界,还能表示重叠对象之间的边界。边缘检测是图像分割的一部分,图像分割的目的是识别出图像中的区域。边缘检测是定位边缘像素的过程,而边缘增强是增加边缘和背景之间的对比度以便能够更清楚地看清边缘的过程。边缘跟踪是沿着边缘进行跟踪的过程,这个过程通常会把边缘像素采集到一个列表中,链码算法是边缘跟踪算法的一个特例。
2. 最优边缘准则 [1]
Canny边缘检测算子是John F. Canny于1986年开发出来的一个多级边缘检测算法。Canny的目标是找到一个最优的边缘检测算法,最优边缘检测的含义,如下所示:
[1]最优检测:算法能够尽可能多地标识出图像中的实际边缘,漏检真实边缘的概率和误检非边缘的概率都尽可能小;
[2]最优定位准则:检测到的边缘点的位置距离实际边缘点的位置最近,或者是由于噪声影响引起检测出的边缘偏离物体的真实边缘的程度最小;
[3]检测点与边缘点一一对应:算子检测的边缘点与实际边缘点应该是一一对应。
二. Canny算法实现
1. 高斯滤波图像去噪
垃圾进,垃圾出,数据预处理工作是非常重要的,图像处理也不例外。这里使用高斯滤波进行图像去噪,比如blur = cv2.GaussianBlur(img, (5,5), 0),处理后的图像与原始图像相比稍微有些模糊。这样单独的一个像素噪声在经过高斯滤波的图像上变得几乎没有影响。
2. 计算图像梯度
Canny算法的基本思想是找寻一幅图相中灰度强度变化最强的位置。所谓变化最强,即指梯度方向。对平滑后的图像使用Sobel算子计算水平方向和竖直方向的一阶导数[图像梯度][和]。根据得到的这两幅梯度图(和)找到边界的梯度和方向。如下所示:
梯度的方向一般总是与边界垂直。梯度方向被归为四类:垂直,水平,和两个对角线。
3. 非极大值抑制
在获得梯度的方向和大小之后,应该对整幅图像做一个扫描,去除那些非边界上的点。对每一个像素进行检查,看这个点的梯度是不是周围具有相同梯度方向的点中最大的。如下所示:
上图中的数字代表了像素点的梯度强度,箭头方向代表了梯度方向。以第二排第三个像素点为例,由于梯度方向向上,则将这一点的强度[7]与其上下两个像素点的强度[5和4]比较,由于这一点强度最大,则保留。
4. 滞后阈值
现在要确定那些边界才是真正的边界。这时我们需要设置两个阈值:minVal和maxVal。当图像的灰度梯度高于maxVal时被认为是真的边界,那些低于minVal的边界会被抛弃。如果介于两者之间的话,就要看这个点是否与某个被确定为真正的边界点相连,如果是就认为它也是边界点,如果不是就抛弃。如下所示:
A高于阈值maxVal所以是真正的边界点,C虽然低于maxVal但高于minVal并且与A相连,所以也被认为是真正的边界点。而B就会被抛弃,因为它不仅低于maxVal而且不与真正的边界点相连。所以选择合适的maxVal和minVal对于能否得到好的结果非常重要。在这一步一些小的噪声点也会被除去,因为我们假设边界都是一些长的线段。
三. Canny代码应用
import cv2
import numpy as np
if __name__ == '__main__':
def nothing(*arg):
pass
cv2.namedWindow('edge')
cv2.createTrackbar('thrs1', 'edge', 2000, 5000, nothing)
cv2.createTrackbar('thrs2', 'edge', 4000, 5000, nothing)
cap = cv2.VideoCapture(0)
while True:
flag, img = cap.read()
img = cv2.GaussianBlur(img, (3,3), 0)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
thrs1 = cv2.getTrackbarPos('thrs1', 'edge')
thrs2 = cv2.getTrackbarPos('thrs2', 'edge')
edge = cv2.Canny(gray, thrs1, thrs2, apertureSize=5)
vis = img.copy()
vis = np.uint8(vis/2.)
vis[edge != 0] = (0, 255, 0)
cv2.imshow('edge', vis)
ch = cv2.waitKey(5) & 0xFF
if ch == 27:
break
cv2.destroyAllWindows()
解析:
1. cv2.Canny(image, threshold1, threshold2[, edges[, apertureSize[, L2gradient]]]) → edges
[1]其中较大的threshold2用于检测图像中明显的边缘,但一般情况下检测的效果不会那么完美,边缘检测出来是断断续续的,所以这时候用较小的threshold1用于将这些间断的边缘连接起来。
[2]可选参数apertureSize是Sobel算子的大小[默认值为3],而参数L2gradient是一个布尔值,如果为真,则使用更精确的L2范数进行计算[即两个方向的倒数的平方和再开方],否则使用L1范数[直接将两个方向导数的绝对值相加]。
2. cv2.Sobel(src, ddepth, dx, dy[, dst[, ksize[, scale[, delta[, borderType]]]]]) → dst
[1] ddepth表示图像的深度,-1表示采用的是与原图像相同的深度。目标图像的深度必须大于等于原图像的深度;
[2] dx和dy表示求导的阶数,0表示这个方向上没有求导,一般为0、1、2。
[3] ksize表示Sobel算子的大小,必须为1、3、5、7。
[4] scale:optional scale factor for the computed derivative values; by default, no scaling is applied。
[5] delta:optional delta value that is added to the results prior to storing them in dst.
[6] borderType:pixel extrapolation method.
3. cv2.convertScaleAbs(src[, dst[, alpha[, beta]]]) → dst
[1] Scales, calculates absolute values, and converts the result to 8-bit, namely dst = src*alpha + beta.
[2] alpha:optional scale factor.
[3] beta:optional delta added to the scaled values.
4. cv2.addWeighted(src1, alpha, src2, beta, gamma[, dst[, dtype]]) → dst
[1] alpha表示src1的权重。
[2] beta表示src2的权重。
[3] gamma:scalar added to each sum.
[4] dtype:optional depth of the output array; when both input arrays have the same depth, dtype can be set to -1, which will be equivalent to src1.depth().
cv2.addWeighted表示计算两个数组的权重和,即dst = src1alpha + src2beta + gamma。
说明:边缘检测常用方法:Canny算子,Sobel算子,Laplace算子,Scharr滤波器。
参考文献:
[1] OpenCV-Python教程:OpenCV-Python教程(6、Sobel算子)_Daetalus的博客-CSDN博客