0%

Python实现Canny算子边缘检测

Canny边缘检测算子是John F.Canny于1986年开发出来的一个多级边缘检测算法。Canny的目标是找到一个最优的边缘检测算法。

这里附上我手写的代码,不保证有bug。这里图像偷懒直接裁剪而没有做padding。以及代码大多没用numpy写,所以运行速度较慢(实际就是不会)

其三个主要评价标准:

  1. 低错误率: 标识出尽可能多的实际边缘,同时尽可能的减少噪声产生的误报。
  2. 高定位性: 标识出的边缘要与图像中的实际边缘尽可能接近。
  3. 最小响应: 图像中的边缘只能标识一次,并且可能存在的图像噪声不应标识为边缘。

Canny算子求边缘点具体算法步骤如下:

1. 用高斯滤波器平滑图像

概念

高斯滤波器(kernel)是将高斯函数离散化,将滤波器中对应的横纵坐标索引代入高斯函数,即可得到对应的值。

(2k+1)x(2k+1) 滤波器的计算公式如下:

$$ H[i, j] = \frac{1}{2 \pi \sigma ^ 2} e ^ {- \frac{(i-k-1)^2 + (j-k-1)^2}{2 \sigma ^ 2}} $$

常见的高斯滤波器为size=5,其近似值为:

$$
K = \frac{1}{159}
\left[
\begin{matrix}
2 & 4 & 5 & 4 & 2 \\
4 & 9 & 12 & 9 & 4 \\
5 & 12 & 15 & 12 & 5 \\
4 & 9 & 12 & 9 & 4 \\
2 & 4 & 5 & 4 & 2
\end{matrix}
\right]
$$

代码

代码首先根据length和σ计算出高斯滤波器,然后对图片进行平滑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def smooth(image, sigma = 1.4, length = 5):
# Compute gaussian filter
k = length // 2
gaussian = np.zeros([length, length])
for i in range(length):
for j in range(length):
gaussian[i, j] = np.exp(-((i-k) ** 2 + (j-k) ** 2) / (2 * sigma ** 2))
gaussian /= 2 * np.pi * sigma ** 2
# Batch Normalization
gaussian = gaussian / np.sum(gaussian)

# Use Gaussian Filter
W, H = image.shape
new_image = np.zeros([W - k * 2, H - k * 2])

for i in range(W - 2 * k):
for j in range(H - 2 * k):
new_image[i, j] = np.sum(image[i:i+length, j:j+length] * gaussian)

new_image = np.uint8(new_image)

2. 计算梯度幅值和方向

概念

常见方法采用Sobel滤波器在计算梯度和方向。

采用以下卷积阵列计算x和y的差分:

$$
G_x =
\left[
\begin{matrix}
-1 & 0 & +1 \\
-2 & 0 & +2 \\
-1 & 0 & +1 \\
\end{matrix}
\right]
$$

$$
G_y =
\left[
\begin{matrix}
-1 & -2 & -1 \\
0 & 0 & 0 \\
+1 & +2 & +1 \\
\end{matrix}
\right]
$$

采用下列公式计算梯度幅值和方向:

$$
G = \sqrt{(G_x^2 + G_y^2)}
$$

$$
\theta = \arctan{\frac{G_y}{G_x}}
$$

梯度角度θ范围从弧度-π到π,然后把它近似到四个方向,分别代表水平,垂直和两个对角线方向(0°,45°,90°,135°)。可以以 $±\frac{iπ}{8}$(i=1,3,5,7)分割,落在每个区域的梯度角给一个特定值,代表四个方向之一。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def get_gradient_and_direction(image):
Gx = np.array([[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]])
Gy = np.array([[-1, -2, -1], [0, 0, 0], [1, 2, 1]])

W, H = image.shape
gradients = np.zeros([W - 2, H - 2])
direction = np.zeros([W - 2, H - 2])

for i in range(W - 2):
for j in range(H - 2):
dx = np.sum(image[i:i+3, j:j+3] * Gx)
dy = np.sum(image[i:i+3, j:j+3] * Gy)
gradients[i, j] = np.sqrt(dx ** 2 + dy ** 2)
if dx == 0:
direction[i, j] = np.pi / 2
else:
direction[i, j] = np.arctan(dy / dx)

gradients = np.uint8(gradients)

3. 对梯度幅值进行非极大值抑制

概念

非极大值抑制是进行边缘检测的一个重要步骤,通俗意义上是指寻找像素点局部最大值。沿着梯度方向,比较它前面和后面的梯度值,如果它不是局部最大值,则去除。

这里借用知乎@戴馨乐的图片,由于图片是之前记录下的,原文地址没有保存

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
def NMS(gradients, direction):
W, H = gradients.shape
nms = np.copy(gradients[1:-1, 1:-1])

for i in range(1, W - 1):
for j in range(1, H - 1):
theta = direction[i, j]
weight = np.tan(theta)
if theta > np.pi / 4:
d1 = [0, 1]
d2 = [1, 1]
weight = 1 / weight
elif theta >= 0:
d1 = [1, 0]
d2 = [1, 1]
elif theta >= - np.pi / 4:
d1 = [1, 0]
d2 = [1, -1]
weight *= -1
else:
d1 = [0, -1]
d2 = [1, -1]
weight = -1 / weight

g1 = gradients[i + d1[0], j + d1[1]]
g2 = gradients[i + d2[0], j + d2[1]]
g3 = gradients[i - d1[0], j - d1[1]]
g4 = gradients[i - d2[0], j - d2[1]]

grade_count1 = g1 * weight + g2 * (1 - weight)
grade_count2 = g3 * weight + g4 * (1 - weight)

if grade_count1 > gradients[i, j] or grade_count2 > gradients[i, j]:
nms[i - 1, j - 1] = 0

4. 用双阈值算法检测

概念

设置两个阈值,minVal和maxVal。梯度大于maxVal的任何边缘是真边缘,而minVal以下的边缘是非边缘。位于这两个阈值之间的边缘会基于其连通性而分类为边缘或非边缘,如果它们连接到“可靠边缘”像素,则它们被视为边缘的一部分;否则,不是边缘。

这里采用dfs搜索的方法,对于所有真边缘开始dfs搜索,直至搜索完成。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
def double_threshold(nms, threshold1, threshold2):
visited = np.zeros_like(nms)
output_image = nms.copy()
W, H = output_image.shape

def dfs(i, j):
if i >= W or i < 0 or j >= H or j < 0 or visited[i, j] == 1:
return
visited[i, j] = 1
if output_image[i, j] > threshold1:
output_image[i, j] = 255
dfs(i-1, j-1)
dfs(i-1, j)
dfs(i-1, j+1)
dfs(i, j-1)
dfs(i, j+1)
dfs(i+1, j-1)
dfs(i+1, j)
dfs(i+1, j+1)
else:
output_image[i, j] = 0

for w in range(W):
for h in range(H):
if visited[w, h] == 1:
continue
if output_image[w, h] >= threshold2:
dfs(w, h)
elif output_image[w, h] <= threshold1:
output_image[w, h] = 0
visited[w, h] = 1

for w in range(W):
for h in range(H):
if visited[w, h] == 0:
output_image[w, h] = 0

OpenCV自带函数

OpenCV有自带函数可以进行高斯平滑和canny算子。

1
2
image = cv2.GaussianBlur(image, (5,5), 0)
canny = cv2.Canny(image, 50, 160)

全部手写代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
# -*- coding: utf-8 -*-
import numpy as np
import cv2

def smooth(image, sigma = 1.4, length = 5):
""" Smooth the image
Compute a gaussian filter with sigma = sigma and kernal_length = length.
Each element in the kernal can be computed as below:
G[i, j] = (1/(2*pi*sigma**2))*exp(-((i-k-1)**2 + (j-k-1)**2)/2*sigma**2)
Then, use the gaussian filter to smooth the input image.

Args:
image: array of grey image
sigma: the sigma of gaussian filter, default to be 1.4
length: the kernal length, default to be 5

Returns:
the smoothed image
"""
# Compute gaussian filter
k = length // 2
gaussian = np.zeros([length, length])
for i in range(length):
for j in range(length):
gaussian[i, j] = np.exp(-((i-k) ** 2 + (j-k) ** 2) / (2 * sigma ** 2))
gaussian /= 2 * np.pi * sigma ** 2
# Batch Normalization
gaussian = gaussian / np.sum(gaussian)

# Use Gaussian Filter
W, H = image.shape
new_image = np.zeros([W - k * 2, H - k * 2])

for i in range(W - 2 * k):
for j in range(H - 2 * k):
new_image[i, j] = np.sum(image[i:i+length, j:j+length] * gaussian)

new_image = np.uint8(new_image)

return new_image


def get_gradient_and_direction(image):
""" Compute gradients and its direction
Use Sobel filter to compute gradients and direction.
-1 0 1 -1 -2 -1
Gx = -2 0 2 Gy = 0 0 0
-1 0 1 1 2 1

Args:
image: array of grey image

Returns:
gradients: the gradients of each pixel
direction: the direction of the gradients of each pixel
"""
Gx = np.array([[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]])
Gy = np.array([[-1, -2, -1], [0, 0, 0], [1, 2, 1]])

W, H = image.shape
gradients = np.zeros([W - 2, H - 2])
direction = np.zeros([W - 2, H - 2])

for i in range(W - 2):
for j in range(H - 2):
dx = np.sum(image[i:i+3, j:j+3] * Gx)
dy = np.sum(image[i:i+3, j:j+3] * Gy)
gradients[i, j] = np.sqrt(dx ** 2 + dy ** 2)
if dx == 0:
direction[i, j] = np.pi / 2
else:
direction[i, j] = np.arctan(dy / dx)

gradients = np.uint8(gradients)

return gradients, direction


def NMS(gradients, direction):
""" Non-maxima suppression

Args:
gradients: the gradients of each pixel
direction: the direction of the gradients of each pixel

Returns:
the output image
"""
W, H = gradients.shape
nms = np.copy(gradients[1:-1, 1:-1])

for i in range(1, W - 1):
for j in range(1, H - 1):
theta = direction[i, j]
weight = np.tan(theta)
if theta > np.pi / 4:
d1 = [0, 1]
d2 = [1, 1]
weight = 1 / weight
elif theta >= 0:
d1 = [1, 0]
d2 = [1, 1]
elif theta >= - np.pi / 4:
d1 = [1, 0]
d2 = [1, -1]
weight *= -1
else:
d1 = [0, -1]
d2 = [1, -1]
weight = -1 / weight

g1 = gradients[i + d1[0], j + d1[1]]
g2 = gradients[i + d2[0], j + d2[1]]
g3 = gradients[i - d1[0], j - d1[1]]
g4 = gradients[i - d2[0], j - d2[1]]

grade_count1 = g1 * weight + g2 * (1 - weight)
grade_count2 = g3 * weight + g4 * (1 - weight)

if grade_count1 > gradients[i, j] or grade_count2 > gradients[i, j]:
nms[i - 1, j - 1] = 0

return nms


def double_threshold(nms, threshold1, threshold2):
""" Double Threshold
Use two thresholds to compute the edge.

Args:
nms: the input image
threshold1: the low threshold
threshold2: the high threshold

Returns:
The binary image.
"""
visited = np.zeros_like(nms)
output_image = nms.copy()
W, H = output_image.shape

def dfs(i, j):
if i >= W or i < 0 or j >= H or j < 0 or visited[i, j] == 1:
return
visited[i, j] = 1
if output_image[i, j] > threshold1:
output_image[i, j] = 255
dfs(i-1, j-1)
dfs(i-1, j)
dfs(i-1, j+1)
dfs(i, j-1)
dfs(i, j+1)
dfs(i+1, j-1)
dfs(i+1, j)
dfs(i+1, j+1)
else:
output_image[i, j] = 0


for w in range(W):
for h in range(H):
if visited[w, h] == 1:
continue
if output_image[w, h] >= threshold2:
dfs(w, h)
elif output_image[w, h] <= threshold1:
output_image[w, h] = 0
visited[w, h] = 1

for w in range(W):
for h in range(H):
if visited[w, h] == 0:
output_image[w, h] = 0

return output_image


if __name__ == "__main__":
# code to read image
smoothed_image = smooth(image)
gradients, direction = get_gradient_and_direction(smoothed_image)
nms = NMS(gradients, direction)
output_image = double_threshold(nms, 40, 100)

效果