Canny边缘检测原理和逐行代码详解
Canny边缘检测原理和逐行代码详解
理论基础
我们希望最后的图像是一个二值图,就是说只有两种情况,要么是边缘要么不是边缘。并且最后的边缘是一个很细的线,甚至只有一个像素点。
卷积算子
我们在进行边缘检测前需要执行一个去噪的步骤,去噪我们一般使用高斯核。
梯度大小和方向

观察图片我们可以发现,边一般的特征就是在边的两边(垂直方向)亮度变化比较大,这就类似与连续曲线的求导,最后我们再选取导数比较大的像素(即亮度变化比较大)即可。但是图像是一个一个像素点,是离散的数据,所以我们没法求导数,我们回顾一下导数的定义:
f
′
(
x
)
=
lim
h
→
0
f
(
x
+
h
)
−
f
(
x
−
h
)
2
h
\ f^{\prime}(x)=\lim_{h\to 0} \frac{f(x+h)-f(x-h)}{2h}
f′(x)=h→0lim2hf(x+h)−f(x−h)
上面是中心差分,就是连续函数求导的意思。由于图像是离散数据,我们可以想象一下,如果我们令
h
=
1
h=1
h=1,就是一个像素点,那么这个时候在图像中就是类似于
h
→
0
h \to 0
h→0,因为一个像素点的差距就是最小的了,那么我们让某个像素点旁边两个像素相减就类似于对这个像素点求导了。由于一个像素点有两个方向,即水平方向和竖直方向,那我们就有一下两个求导算子,分别对水平方向和数值方向求导。

根据上面两个算子
G
x
Gx
Gx,
G
y
Gy
Gy,算出所有像素点两个方向的导数后,然后再根据公式求出所有像素点的梯度,公式为:
g
r
a
d
i
e
n
t
=
G
x
2
+
G
y
2
gradient=\sqrt{{Gx}^2+{Gy}^2}
gradient=Gx2+Gy2
求梯度方向公式为:
a
n
g
l
e
(
θ
)
=
t
a
n
−
1
(
G
y
G
x
)
angle(\theta)=tan^{-1}(\frac{Gy}{Gx})
angle(θ)=tan−1(GxGy)
梯度大小很好理解,就是求出一个像素点的变化程度大小,因为对于变化程度,我们不仅要考虑水平方向,还要考虑垂直方向,所以我们要把它俩加起来,因为导数可能有正有负,最后我们想要一个正数,所以先平方再加再开根号。
为什么还要求方向呢?上面我们说了我们希望边缘就是确定边缘的位置在哪,我们把它标记出来就行,所以对于最后的图像的边缘点要是非常细的,只有一个像素点。为了完成这个目标,我们通过找到某个像素点变化最快的方向,在这个方向如果没有比这个点更大的梯度像素点,那么这个像素点就是边缘。如果有比这个点梯度更大的点,那么我们就认为这个点不是边缘,把它的大小置为0。
非极大值抑制
由于图像数据是离散的,我们求出梯度某个像素点的梯度方向后,可能出现改梯度方向没有像素点的情况,那这样就没有像素点和该像素点比较了,我们不希望这样的情况发生。

如上图,点C的梯度方向落在了g1和g2两点之间,g3和g4两点之间,那么我们就求g1和g2的加权和,权重是多少呢?离那个点(dTmp1,dTmp2)越近权重就越大,而且重要程度随着距离线性变化,知道了
θ
\theta
θ,就很容易确定权重了。所以我们有公式:
d
T
m
p
1
=
t
a
n
θ
g
2
+
(
1
−
t
a
n
θ
)
g
1
dTmp1=tan\theta g_2+(1-tan\theta)g_1
dTmp1=tanθg2+(1−tanθ)g1
边缘的连接
首先我们使用双阈值进行处理,设置一个最大值,一个最小值。大于最大值的像素点,我们认为该像素点一定是边缘。小于最小值的像素点我们认为它一定不是边缘,我们把它置为0. 一系列操作后,边缘点并不是连续的,可能断断续续的,造成不太美观,所以我们要把连续的点连再一起。怎么连呢?由于我们已经确定了一定为边缘的点。那么我们就从这些点开始进行连接,我们利用栈的做法,把这些点压栈,然后判断栈首的八领域内有没有可能为边缘的点,如果有就把这两个点连起来(赋值为1),然后将栈首元素弹出,直至栈空为止。
代码逐行详解
首先使用python实现我们的思路,后续会考虑使用c++实现。
第一步实现卷积,先使用本方法,三重循环实现卷积,下面再使用im2col的方法实现更加高效的卷积。
import numpy as np
import matplotlib.pyplot as plt
def convolve(filter, img, padding, stride):
result = None
img_size = img.shape
filter_size = filter.shape
if len(img_size) == 3:
result = []
# 遍历通道维度
for i in range(0, img_size[-1]):
channel = []
padded_img = np.pad(img[:, :, i], ((padding[0], padding[1]), (padding[2], padding[3])), 'constant')
# 遍历行维度,添加一个列表到channel尾部,用来存放生成数据
for j in range(0, img_size[0], stride):
channel.append([])
# 遍历列维度,将val存放在channel内部最后一个列表的最后位置
for k in range(0, img_size[1], stride):
val = (filter * padded_img[j:j + filter_size[0], k:k + filter_size[1]]).sum()
channel[-1].append(val)
result.append(channel)
elif len(img_size) == 2:
channel = []
padded_img = np.pad(img, ((padding[0], padding[1]), (padding[2], padding[3])), 'constant')
# 遍历行维度
for j in range(0, img_size[0], stride):
channel.append([])
# 遍历列维度
for k in range(0, img_size[-1], stride):
val = (filter * padded_img[k, k + filter_size[0]:j, j + filter_size[1]]).sum()
channel[-1][-1].append(val)
result = channel
return result

浙公网安备 33010602011771号