OpenCV-Python教程:边沿检测(Canny)

原文链接:http://www.juzicode.com/opencv-python-detect-edge

返回Opencv-Python教程

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方向的梯度图像,使用起来更繁杂些。

扩展阅读:

  1. OpenCV-Python教程
  2. 论如何把自己变成卡通人物(OpenCV制作卡通化头像)

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注