从0开始用Python实现图像边缘检测

  • 自上次使用感知哈希进行图像检测以后,我又开始造轮子了。这次是从0开始的使用Python的图像边缘检测

本次针对图像的边缘检测一口气使用了5个算子,包括Prewitt算子,Sobel算子,laplace算子,拓展laplace算子,canny算子。一个一个慢慢说

首先

既然我们要做图像边缘检测,那就要首先弄清楚什么是图像的边缘,边缘有什么特征,这样才能对特征加以识别

现在一幅常规的图像都是彩色的,但是色彩的存在会增加我们判断的难度,所以我们既要消除色彩对我们的影响。所以我们要对图像进行灰度化处理

得到灰度图像以后那么边缘两侧的图像灰度值肯定相差很大,但是图像的像素之间都会有多多少少的灰度差异,因此我们需要做的不仅是找到灰度差异大的部分,而是需要找到灰度差异最大的部分,这样这个地方是边缘的可能性就越强。

那么接下来的问题就转化成如何知道那里的灰度变化值最大。

说到变化值或者变化率很容易就想到需要用到导数的知识来进行解决,假设图像的灰度像素是连续的,那么就可以分别对X,Y两个方向求导。得到X和Y上的方向导数。

而方向导数则隐含了图像的灰度变化信息,自然也就隐含了边缘信息。接下里我们只需要利用梯度将X,Y两个方向的边缘信息进行合并,这样就可以得到图像在X,Y两个方向上的边缘信息

然而可惜的是在实际中图像的灰度像素并不连续,即图像矩阵是离散的。既然不是连续的那上面所说的基于导数的自然是要做出一些变更。也就是求离散函数的变化率,这里需要用到有限差分法来近似代表X,Y方向上的导数。

所谓差分实际上就是相邻两个数的差。即用相邻两个数的差来表示变化率。实际上在后面Canny算子的实现中使用到了差分的概念,在那里我们使用的是前向差分,当然你可以使用逆向差分,实际上只是编程问题

而在实际的操作中,我们一般的是使用某一算子和图像矩阵进行卷积代替上述某些或全部操作步骤,因此下面的Prewitt算子,Sobel算子,laplace算子,拓展laplace算子实际上都是算子直接和图像矩阵进行卷积,如果算子分X,Y方向,那么在卷积后需要对卷积结果求梯度,如果算子不分X,Y方向那么则不需要求梯度

卷积

前面提到了一个概念叫卷积,其实这个概念在深度学习中是一个很常见的概念,例如在CNN网络中需要对layer进行卷积和池化,卷积其实是分析数学中一个重要的运算,但不代表很难懂

上图为维基百科对于卷积的图解,可以看到卷积需要具备一个卷积核,也就是Sliding window,学过网络原理的同学这里可以类比Sliding window protocol,差别就在于Sliding window protocol中的Sliding window是可变的,在卷积中Sliding window在一次卷积结束前并不会发生改变,没有学过Sliding window protocol也看不懂上面的图,其实不要紧,其实这都是我为了装逼,下面用一个更清晰明了的图来说明卷积

首先看第一个图:投影部分是一个4 * 4的区域,这个投影就是卷积核,步长就是每次移动的距离,卷积核负责把投影对应区域*卷积核然后求和。可能还不是很好理解,那么就看第二个图,是一个卷积核为

[1,0,1]

[0,1,0]

[1,0,1]步长1的卷积过程,可以看到,在卷积过程中,首先将卷积核经过的对应位置的元素相乘,然后再将与卷积核相乘得到的九个数值相加变成一个数值,这样就是卷积的一步,当以上述方式便利完整个图像数组后,一次卷积就完成了

Prewitt算子

Prewitt算子在实际使用是分为X,Y两个方向,因此在求得结果后需要对最后的结果求梯度,下面给出的是Prewitt算子的X方向,Y方向是X方向的转置

[-1,0,1]
[-1,0,1]
[-1,0,1]

第一张图为原图,第二张为Prewitt处理后的结果(后面的都是这个顺序我就不说了)、

可以看到Prewitt算子基本能选中图像的边缘,但是结果很粗糙,而且噪声略大

Sobel算子

Sobel算子和Prewitt算子一样,分X,Y方向。下面给出的是Sobel算子的X方向,Y方向是X方向的转置

[-1,0,1]
[-2,0,2]
[-1,0,1]

可以看到对于这个图像来说Sobel的效果实际上并没有Prewitt好,噪声比Prewitt还要多一些

Laplace算子

Laplace算子区别于上面两个算子,他是二阶导数的算子也就是说,他同时包含了X,Y两个方向,实际上是由Sobel算子导出的,所以这个只需要一步卷积就可以实现目的,下面给出的是laplace算子

[0,1,0]
[1,-4,1]
[0,1,0]

可以看到laplace算子计算出的结果有一部分比上述两个算子要好,对于周围的附属物,就显得处理能力差一点了

拓展Laplace算子

拓展Laplace算子

[1,1,1]
[1,-8,1]
[1,1,1]

可以看到拓展Laplace算子的效果基本算Laplace和Sobel的结合

Canny算子

看来上面的算子估计你差不多已经绝望了,这是什么辣鸡玩意,完全不能用啊!先别着急绝望,这次我们先看Canny算子的结果

哇!厉害了有木有!什么?我自己画的?不存在的不存在的,看完你也可以写出来。再说旁边都带着标尺,怎么可能是我自己画的

Canny算子的计算我们将不采用上述四种直接使用算子的办法,我们要一点一点的搞事情

首先Canny算子计算的步骤大概分成这么几步:

1.图像灰度化

2.用高斯滤波平滑图像

3.计算梯度方向和大小

4.非极大值抑制

5.双阈值选取和边缘链接

灰度化

首先灰度化在dHash那一篇博文中已经说过了(传送门 : http://blog.nobug.in/archives/404)跳过

高斯滤波

接下来要进行的步骤是高斯滤波,什么是高斯滤波,所谓的高斯滤波就是就是高斯掩膜也就是高斯核,与图像进行卷积。前面已经介绍了卷积,下面看看如何搞定高斯核

要搞定高斯核实际上就要了解高斯函数,上面给出了高斯函数,因为我们的高斯核是二维高斯分布矩阵,直接使用高斯函数生成的高斯核不是很满足我们的要求,因此需要在计算的时候对内容进行适当的偏移

计算梯度方向和大小

高斯滤波结束以后,我们需要对高斯滤波的图像计算梯度方向,在“首先一栏里”我们已经简述了梯度方向和大小的计算过程,这里我们在这里对X和Y方向的前向差分进行以下适当的增强

X:,Y:

使用两组前向差分取平均值

在下一步需要使用到梯度的方向,因此这里也要注意保留X,Y的计算结果

非极大值抑制(Non-Maximum Suppression)

在上一步里我们计算完了梯度,在“首先”里我们说到梯度包含了边缘信息,但是并不是所有包含的都是边缘信息,因此需要我们进行筛选。前面又提到边缘部分的灰度差异最明显,也就是说如果这个点是边缘点,那么他一定是附近像素中灰度最大的那一个,也就是局部最大值。而非极大值抑制需要做的工作就是,找到局部最大值

首先假设C就是我们要求的点,g1 g2 g3 g4是与该梯度相关的像素点,那么如果C大于他周围的点,就可以说明C是局部最大值,而上一步我们计算出了梯度方向,蓝线就是梯度方向,梯度上一定存在边缘信息,也就是说如果C大于dTmp1和dTmp2那么就可以说明C是局部最大值点,但是因为梯度方向的不确定再加上图像矩阵是离散的,所以dTmp1和dTmp2并不一定存在,这个时候就要需要用到插值(插值概念传送门:http://blog.sina.com.cn/s/blog_4df4d74401014uaj.html

上面是针对dTmp1和dTmp2的插值公式,想要求出dTmp1和dTmp2的值那么就需要知道weight值,也就是权重。在这里weight实际上是一个比例系数,即dTmp1到g1的距离除以g1到g2的距离,可以用梯度方向来得到。现在知道了X方向和Y方向的方向导数。因此这个问题需要分类讨论:分为两大类情况:X方向的分量大于Y方向的分量,Y方向的分量大于X方向的分量,其中每种情况可以分为两小种情况:X,Y同方向,X,Y不同方向

首先来讨论X方向的分量大于Y方向的分量,一定注意坐标原点现在在左上角,因此现在的权重为|gy| / |gx|,当gx与gy方向相同的时候为左图可以得到四个关键坐标点,当gx大于gy为右图,也可以得到四个关键坐标点

然后再来讨论Y方向的分量大于X方向的分量,还是要注意不要忘记坐标原点在左上角,因此现在的权重为|gx| / |gy|,当gx与gy方向相同的时候为左图,当gx与gy方向不同为右图

带入前面给的公式进行计算就可以得到插值点,然后再比较C点和两个插值点的大小,如果C点为局部最大值,则保留,不是则舍弃

双阈值选取和边缘链接

选取高阈值和低阈值,比率为2:1或3:1,取出上一步得到的结果重新定义高低阈值,将大于高阈值的点保留确定为边缘点,低于低阈值的点舍弃。如果在高阈值和低阈值之间的点,只有和高阈值点相邻的点才会被认为是边缘点

OK,至此Canny算子就实现了,算法还需要进行进一步的优化

代码详见github

涉及到基础环境配置的问题不在本文的讨论范围之内

文中部分图片来自互联网

About the author

NOBUG.IN

Add comment

By NOBUG.IN

Your sidebar area is currently empty. Hurry up and add some widgets.