OpenCV-Python教程:边沿检测(Canny)
原文链接:http://www.juzicode.com/opencv-python-detect-edge
Canny
图像梯度 反映的是图像像素值的变化过程,不管变化大小都考虑在内,所以Sobel,Laplacian变换得到的是一个多级灰度图。边沿检测也可以看做是图像梯度的一种延伸,不过边沿检测更注意图像的“边沿”部分,图像梯度变化较小的部分会被忽略,只有较大变化的部分保留下来。
今天要介绍的canny边沿检测有低错误率、很好地定位边缘点、单一的边缘点响应等优点。canny边沿检测算法由以下几个步骤组成:
- 1)高斯滤波器平滑输入图像;
- 2)计算梯度幅值图像和角度方向;
- 3)对梯度幅值图像应用非最大值抑制;
- 4)用双阈值处理和连接分析检测和连接边沿。
接口
第1种接口形式:
edges=cv2.Canny(image, threshold1, threshold1[, edges[, apertureSize[, L2gradient]]])
- 参数含义:
- image:8bit源图像,可以是单通道或多通道;
- threshold1:迟滞阈值1;
- threshold2:迟滞阈值2,和threshold1没有大小要求,函数内部会调整交换;
- edges:目标图像,二值图像;
- apertureSize:kernel尺寸,默认为3;
- L2gradient:是否使用L2范式,如果设置为True,计算梯度时使用的是2个方向梯度的平方和开平方,如果设置为False,则使用2个方向梯度的绝对值的和;
第2种接口形式如下:
edges=cv2.Canny(dx, dy, threshold1, threshold2[, edges[, L2gradient]])
- 参数含义:
- dx:源图像的16bit(CV_16SC1 or CV_16SC3) x方向梯度图像;
- dy:源图像的16bit(CV_16SC1 or CV_16SC3) y方向梯度图像;
- threshold1:迟滞阈值1;
- threshold2:迟滞阈值2;
- edges:目标图像;
- L2gradient:是否使用L2范式,如果设置为True,计算梯度时使用的是2个方向梯度的平方和开平方,如果设置为False,则使用2个方向梯度的绝对值的和;
第2种接口形式和第1种实现边沿检测的效果是一样的,第2种形式需要先计算图像的x和y方向的梯度,所以计算梯度的kernel尺寸在第2种接口中就不需要了。
ksize差异
下面这个例子是取threshold为20和100、ksize分别为3,5,7时计算边沿的例子:
import matplotlib.pyplot as plt
import cv2
print('VX公众号: 桔子code / juzicode.com')
print('cv2.__version__:',cv2.__version__)
plt.rc('font',family='Youyuan',size='9')
img_src = cv2.imread('..\\samples\\data\\sudoku.png' ,cv2.IMREAD_GRAYSCALE)
img_edge3 = cv2.Canny(img_src,20,100,apertureSize=3)
img_edge5 = cv2.Canny(img_src,20,100,apertureSize=5)
img_edge7 = cv2.Canny(img_src,20,100,apertureSize=7)
#显示
fig,ax = plt.subplots(2,2)
ax[0,0].set_title('原图(juzicode.com)')
ax[0,0].imshow(img_src ,cmap = 'gray')
ax[0,1].set_title('img_edge3')
ax[0,1].imshow(img_edge3,cmap = 'gray')
ax[1,0].set_title('img_edge5')
ax[1,0].imshow(img_edge5,cmap = 'gray')
ax[1,1].set_title('img_edge7')
ax[1,1].imshow(img_edge7,cmap = 'gray')
ax[0,0].axis('off');ax[0,1].axis('off');ax[1,0].axis('off');ax[1,1].axis('off')#关闭坐标轴显示
plt.show()
运行结果:
从这个例子可以看到,相同的threshold值,ksize越大边沿细节越多,这点和Sobel(),Scharr(),Laplacian()计算图像梯度效果是一样的。
迟滞阈值threshold1/2
2个迟滞阈值在函数内部会进行比较,较小者存入low_thresh,在源码中有如下的比较过程:
if (low_thresh > high_thresh)
std::swap(low_thresh, high_thresh);
所以前面的例子中按照下面的方式交换threshold1和threshold2参数实现的效果是一样的:
img_edge3 = cv2.Canny(img_src,100,20,apertureSize=3) #交换threshold1和threshold2
img_edge5 = cv2.Canny(img_src,100,20,apertureSize=5)
img_edge7 = cv2.Canny(img_src,100,20,apertureSize=7)
阈值宽度
下面的例子展示了相同的threshold1,不同的threshold2得到不同的“阈值宽度”生成边沿图像的差异:
import matplotlib.pyplot as plt
import cv2
print('VX公众号: 桔子code / juzicode.com')
print('cv2.__version__:',cv2.__version__)
plt.rc('font',family='Youyuan',size='9')
img_src = cv2.imread('..\\samples\\data\\sudoku.png' ,cv2.IMREAD_GRAYSCALE)
img_edge3 = cv2.Canny(img_src,50,60,apertureSize=3)
img_edge5 = cv2.Canny(img_src,50,120,apertureSize=3)
img_edge7 = cv2.Canny(img_src,50,240,apertureSize=3)
fig,ax = plt.subplots(2,2)
ax[0,0].set_title('原图(juzicode.com)')
ax[0,0].imshow(img_src ,cmap = 'gray')
ax[0,1].set_title('img_edge thresh 50-60')
ax[0,1].imshow(img_edge3,cmap = 'gray')
ax[1,0].set_title('img_edge thresh 50-120')
ax[1,0].imshow(img_edge5,cmap = 'gray')
ax[1,1].set_title('img_edge thresh 50-240')
ax[1,1].imshow(img_edge7,cmap = 'gray')
ax[0,0].axis('off');ax[0,1].axis('off');ax[1,0].axis('off');ax[1,1].axis('off')#关闭坐标轴显示
plt.show()
运行结果:
可以看到相同的ksize时,threshold1和threshold2的差值越小,边沿细节越多。
像素值
将canny()变换后的边沿图像用直方图显示,可以看到变换后的图像是一个二值图像,像素的取值为0或者255,这点和Sobel()、Laplacian()等梯度变换得到的是一个灰度图是不一样的:
plt.hist(img_edge3.ravel( ),255)
第2种接口形式
这个例子中我们使用第2种接口形式,先求出x和y方向的图像梯度,再用这2个梯度图像计算canny边沿,并和第1种接口形式做对比:
import matplotlib.pyplot as plt
import cv2
print('VX公众号: 桔子code / juzicode.com')
print('cv2.__version__:',cv2.__version__)
plt.rc('font',family='Youyuan',size='9')
img_src = cv2.imread('..\\samples\\data\\sudoku.png' ,cv2.IMREAD_GRAYSCALE)
#第1种接口形式
img_edge = cv2.Canny(img_src,20,100,apertureSize=3)
#第2种接口形式
grad_x = cv2.Sobel(img_src, cv2.CV_16S, 1, 0, ksize=3)
grad_y = cv2.Sobel(img_src, cv2.CV_16S, 0, 1, ksize=3)
img_edge2 = cv2.Canny(grad_x,grad_y,20,100 )
img_diff = cv2.absdiff(img_edge2,img_edge)
print('差异像素点个数:',cv2.countNonZero(img_diff))
print('图像像素点个数:',img_edge.shape[0]*img_edge.shape[1])
#显示
fig,ax = plt.subplots(2,2)
ax[0,0].set_title('原图(juzicode.com)')
ax[0,0].imshow(img_src ,cmap = 'gray')
ax[0,1].set_title('img_edge' )
ax[0,1].imshow(img_edge,cmap = 'gray')
ax[1,0].set_title('img_edge API2')
ax[1,0].imshow(img_edge2,cmap = 'gray')
ax[1,1].set_title('img_diff')
ax[1,1].imshow(img_diff,cmap = 'gray')
ax[0,0].axis('off');ax[0,1].axis('off');ax[1,0].axis('off');ax[1,1].axis('off')#关闭坐标轴显示
plt.show()
运行结果:
差异像素点个数: 208
图像像素点个数: 314154
从图像显示效果和统计的2幅图像差异,差异图像几乎是全黑色,存在差异的像素点数量也不到千分之一(208/314154),2种方式计算出来的边沿是一样的。
小结:Canny()变换中相同的threshold值,ksize越大边沿细节越多;threshold1和threshold2的差值越小,边沿细节越多;Canny()变换后得到的是一个二值图像;第2种接口形式得到的图像效果和第1种相比几乎没有差别,因为需要先计算图像x和y方向的梯度图像,使用起来更繁杂些。