1. Canny算法概述

Canny边缘检测算子是John F. Canny于 1986 年开发出来的一个多级边缘检测算法。更为重要的是 Canny 创立了边缘检测计算理论(Computational theory of edge detection)解释这项技术如何工作。
通常情况下边缘检测的目的是在保留原有图像属性的情况下,显著减少图像的数据规模。有多种算法可以进行边缘检测,虽然Canny算法年代久远,但可以说它是边缘检测的一种标准算法,而且仍在研究中广泛使用。

2.最优边缘准则

Canny 的目标是找到一个最优的边缘检测算法,最优边缘检测的含义是:
(1)最优检测:算法能够尽可能多地标识出图像中的实际边缘,漏检真实边缘的概率和误检非边缘的概率都尽可能小;
(2)最优定位准则:检测到的边缘点的位置距离实际边缘点的位置最近,或者是由于噪声影响引起检测出的边缘偏离物体的真实边缘的程度最小;
(3)检测点与边缘点一一对应:算子检测的边缘点与实际边缘点应该是一一对应。
为了满足这些要求 Canny 使用了变分法(calculus of variations),这是一种寻找优化特定功能的函数的方法。最优检测使用四个指数函数项表示,但是它非常近似于高斯函数的一阶导数。

2. Canny算法实现步骤

第一步:灰度化

第二步:高斯滤波
任何边缘检测算法都不可能在未经处理的原始数据上很好地工作,所以第一步是对原始数据与高斯 mask 作卷积,得到的图像与原始图像相比有些轻微的模糊(blurred)。这样,单独的一个像素噪声在经过高斯平滑的图像上变得几乎没有影响。

首先生成二维高斯分布矩阵:

pytorch PIL 边缘检测 canny边缘检测算法python_d3


然后与灰度图像进行卷积实现滤波:

pytorch PIL 边缘检测 canny边缘检测算法python_d3_02


第三步:计算梯度值和方向

求变化率时,对于一元函数,即求导;对于二元函数,求偏导。 数字图像处理中,用一阶有限差分近似求取灰度值的梯度值(变化率)。
(即:使差商(Δf/Δx)近似取代微商(∂f/∂x)。求灰度的变化率,分别取x和y方向上相邻像素做差,代替求取x和y 方向一阶偏导) 。

pytorch PIL 边缘检测 canny边缘检测算法python_d3_03

其中f为图像灰度值,P代表X方向梯度幅值,Q代表Y方向 梯度幅值,M是该点幅值,Θ是梯度方向,也就是角度。

注:图像梯度方向与边缘方向互相垂直:

pytorch PIL 边缘检测 canny边缘检测算法python_pytorch PIL 边缘检测_04

第四步:非极大值抑制(NMS)

这一步的目的是将模糊(blurred)的边界变得清晰(sharp)。通俗的讲,就是保留了每个像素点上梯度强度的极大值,而删掉其他的值。对于每个像素点,进行如下操作:

沿着梯度方向,比较它前面和后面的梯度值。在沿其方向上邻域的梯度幅值最大,则保留;否则,抑制。

pytorch PIL 边缘检测 canny边缘检测算法python_像素点_05


图中的数字代表了像素点的梯度强度,箭头方向代表了梯度方向。以第二排第三个像素点为例,由于梯度方向向上,则将这一点的强度(7)与其上下两个像素点的强度(5和4)比较,由于这一点强度最大,则保留。处理后效果如下图所示。

pytorch PIL 边缘检测 canny边缘检测算法python_像素点_06


上图中,可以想象,边界处的梯度方向总是指向垂直于边界的方向,即最后会保留一条边界处最亮的一条细线。

5.双阈值(Double Thresholding),边缘连接

经过非极大抑制后图像中仍然有很多噪声点。Canny算法中应用了一种叫双阈值的技术。即设定一个阈值上界和阈值下界(opencv中通常由人为指定的),图像中的像素点如果大于阈值上界则认为必然是边界(称为强边界,strong edge),小于阈值下界则认为必然不是边界,两者之间的则认为是候选项(称为弱边界,weak edge),需进行进一步处理。

选取高阈值T H 和低阈值T L ,比率为2:1或3:1。(一般取TH =0.3 或 0.2, TL =0.1 )

取出非极大值抑制后的图像中的最大梯度幅值,重新定义高低阈值。即:T H ×Max,T L ×Max 。(当然可以自己给定)

将小于TL 的点抛弃,赋0;将大于T H 的点立即标记(这些点就是边缘点),赋1。

将大于TL ,小于TH 的点使用8连通区域确定(即:只有与TH 像素连接时才会被接受,成为边缘点,赋1) 。

import matplotlib.pyplot as plt
import numpy as np
import math
import cv2

img = cv2.imread('./Images/lena.jpg')
img=cv2.cvtColor(img,cv2.COLOR_BGR2RGB)

blur = cv2.GaussianBlur(img, (5, 5), 0)  # 用高斯滤波处理原图像降噪
canny = cv2.Canny(blur, 50, 150)  # 50是最小阈值,150是最大阈值

sigma1 = sigma2 = 1
sum = 0

gaussian = np.zeros([5, 5])
for i in range(5):
    for j in range(5):
        gaussian[i,j] = math.exp(-1/2 * (np.square(i-3)/np.square(sigma1)           #生成二维高斯分布矩阵
                        + (np.square(j-3)/np.square(sigma2)))) / (2*math.pi*sigma1*sigma2)
        sum = sum + gaussian[i, j]
        
gaussian = gaussian/sum
# print(gaussian)

def rgb2gray(rgb):
    return np.dot(rgb[...,:3], [0.299, 0.587, 0.114])


# step1.高斯滤波
gray = rgb2gray(img)
W, H = gray.shape
new_gray = np.zeros([W-5, H-5])
for i in range(W-5):
    for j in range(H-5):
        new_gray[i,j] = np.sum(gray[i:i+5,j:j+5]*gaussian)   # 与高斯矩阵卷积实现滤波 

# plt.imshow(new_gray, cmap="gray")
     
        
# step2.增强 通过求梯度幅值
W1, H1 = new_gray.shape
dx = np.zeros([W1-1, H1-1])
dy = np.zeros([W1-1, H1-1])
d = np.zeros([W1-1, H1-1])
for i in range(W1-1):
    for j in range(H1-1):   
        dx[i,j] = new_gray[i, j+1] - new_gray[i, j]
        dy[i,j] = new_gray[i+1, j] - new_gray[i, j]        
        d[i, j] = np.sqrt(np.square(dx[i,j]) + np.square(dy[i,j]))   # 图像梯度幅值作为图像强度值
         
# plt.imshow(d, cmap="gray")
      
        
# setp3.非极大值抑制 NMS
W2, H2 = d.shape
NMS = np.copy(d)
NMS[0,:] = NMS[W2-1,:] = NMS[:,0] = NMS[:, H2-1] = 0
for i in range(1, W2-1):
    for j in range(1, H2-1):
        
        if d[i, j] == 0:
            NMS[i, j] = 0
        else:
            gradX = dx[i, j]
            gradY = dy[i, j]
            gradTemp = d[i, j]
            
            # 如果Y方向幅度值较大
            if np.abs(gradY) > np.abs(gradX):
                weight = np.abs(gradX) / np.abs(gradY)
                grad2 = d[i-1, j]
                grad4 = d[i+1, j]
                # 如果x,y方向梯度符号相同
                if gradX * gradY > 0:
                    grad1 = d[i-1, j-1]
                    grad3 = d[i+1, j+1]
                # 如果x,y方向梯度符号相反
                else:
                    grad1 = d[i-1, j+1]
                    grad3 = d[i+1, j-1]
                    
            # 如果X方向幅度值较大
            else:
                weight = np.abs(gradY) / np.abs(gradX)
                grad2 = d[i, j-1]
                grad4 = d[i, j+1]
                # 如果x,y方向梯度符号相同
                if gradX * gradY > 0:
                    grad1 = d[i+1, j-1]
                    grad3 = d[i-1, j+1]
                # 如果x,y方向梯度符号相反
                else:
                    grad1 = d[i-1, j-1]
                    grad3 = d[i+1, j+1]
        
            gradTemp1 = weight * grad1 + (1-weight) * grad2
            gradTemp2 = weight * grad3 + (1-weight) * grad4
            if gradTemp >= gradTemp1 and gradTemp >= gradTemp2:
                NMS[i, j] = gradTemp
            else:
                NMS[i, j] = 0
        
# plt.imshow(NMS, cmap = "gray")


# step4. 双阈值算法检测、连接边缘
W3, H3 = NMS.shape
DT = np.zeros([W3, H3])               
# 定义高低阈值
TL = 0.2 * np.max(NMS)
TH = 0.3 * np.max(NMS)
for i in range(1, W3-1):
    for j in range(1, H3-1):
        if (NMS[i, j] < TL):
            DT[i, j] = 0
        elif (NMS[i, j] > TH):
            DT[i, j] = 1
        elif ((NMS[i-1, j-1:j+1] < TH).any() or (NMS[i+1, j-1:j+1]).any() 
              or (NMS[i, [j-1, j+1]] < TH).any()):
            DT[i, j] = 1
        
plt.figure(1)
#第一行第一列图形
ax1 = plt.subplot(1,3,1)
plt.sca(ax1)
plt.imshow(img)
plt.title("artwork")
#第一行第二列图形
ax2 = plt.subplot(1,3,2)
plt.sca(ax2)
plt.imshow(canny,cmap="gray")
plt.title("opencv Canny")

ax3 = plt.subplot(1,3,3)
plt.sca(ax3)
plt.imshow(DT,cmap="gray")
plt.title("my Canny")

pytorch PIL 边缘检测 canny边缘检测算法python_像素点_07