目录
一、边缘简介
1.1 何为边缘
图像的边缘是图像像素快速变化的地方,图像边缘不等同图像中物体的边界。边缘指的是图像中像素的值有突变的地方,而物体间的边界指的是现实场景中的存在于物体之间的边界。有可能有边缘的地方并非边界,也有可能边界的地方并无边缘,因为现实世界中的物体是三维的,而图像只具有二维信息,从三维到二维的投影成像不可避免的会丢失一部分信息;另外,成像过程中的光照和噪声也是不可避免的重要因素。
1.2 产生原因
物体的边界、物体表面方向的变化、不同的颜色、光照明暗的变化都可能造成图像边缘的产生。
二、边缘检测方法
2.1 一阶微分算子计算原理
如果我们将图像看作一个二维函数,则图像像素值的变化率就可以使用导数(梯度)表示,而边缘的梯度幅值会比较大。如下面的图像表示:
我们可以看到,对于经过图像边缘的一条线,如图中的红线,我们可以将其看作为一条关于像素值的函数f(x),表达式图像如上图中间曲线所示。我们可以看出,边缘所处位置即为函数f(x)一阶导数的极值点。我们可以将其扩展到图像(二维离散函数),边缘就是梯度幅值M的局部极值点。
在图像中,图像的梯度与x方向和y方向上的偏导数有关,图像梯度定义为:
图像在各方向上的导数如下图所示:
我们可以看到,图像梯度是具有方向和大小的,其方向指向了像素值变化率最大的方向,一般用角度θ来表达;而梯度大小表示了图像边缘的强弱程度,用梯度幅值M表达。梯度方向和梯度幅值的公式为:
在数字图像处理领域,通常将梯度幅值M(x,y)称为梯度。而在一阶微分算子检测边缘的方法中就是通过一阶微分算子计算得到图像在x方向和y方向的偏导数,再进一步的计算得到梯度幅值M,二值化M后得到图像边缘的。
假设在图像中用f(x,y)表示在点(x,y)处的像素值,则图像在x方向和y方向的一阶微分算子应该计算为:
由于图像是离散的二维函数,ε不能无限小,图像按照像素来离散,最小的ε就是1像素,因此上式就变成了如下的形式(ε=1):
我们可以从这里看到,图像的偏导数就是2个相邻像素之间差值。我们通过gx和gy便可以进一步计算得到梯度幅值M。在实际中,我们常用一个小区域的模板卷积来近似偏导计算。对gx和gy各采用一个模板,并将这两个模板组合起来构成一个梯度算子。根据模板的大小,和模板中系数值的不同,可以提出很多不同的检查算子。下面就列举了一个采用差分近似提出的一种梯度算子计算模板:
2.2 噪声对一阶微分算子的影响及解决方案
如果我们直接对图像进行一阶微分,很容易受到噪声的影响。以下图为例说明:
图中,当f(x)存在噪声时,直接进行一阶微分,得到的导数图像是凌乱的,有很大的波动,不能够直接找出边缘。此时我们应该先对图像进行平滑(降噪),之后在进行微分。例如,当我们使用高斯平滑之后,在进行微分就能够找到边缘所在了,如下图所示:
特别强调一个卷积运算性质:两个函数卷积后的导数等于其中一个函数的导数与另一个函数的卷积,即:
所以,我们可以将平滑和微分合成一个算子:
通过上面的例子,我们也看到在进行边缘检测是应该特别注意噪声对图像产生的影响,当有噪声时应该先进性平滑处理。也因此,在实际应用中,我们也经常将二维高斯的一阶微分进行数值近似,以用来计算图像的梯度,图像如下:
2.3 常见的一阶微分算子
Roberts算子、Prewitt算子和Sobel算子是三种常用微分边缘检测算子,这三个算子都以一阶导数为基础,先通过合适的微分算子计算出图像的梯度矩阵(记录图像的梯度幅值),再对梯度矩阵进行二值化(将梯度幅值大于阈值的点标记为边缘)从而得到图像的边缘,同时我们页可选择的将边缘细化(thinning)为一个像素宽度。
2.3.1 Roberts算子
Roberts边缘检测算子是根据任一对互相垂直方向上的差分可用来计算梯度的原理,采用对角线方向相邻像素之差进行梯度幅度检测,其检测水平、垂直方向边缘的性能要好于斜线方向边缘,并且检测定位精度比较高,但对噪声敏感。
Robert算子对应模板如下:
2.3.2 Prewitt算子
2*2的检测模板虽然简单,但是不方便找到中心点计算梯度,并且受噪声影响大。Prewitt算子模板的大小为3*3,关于中心点对称,携带了更多的图像信息,减少了噪声的影响。
Prewitt算子对应模板如下:
2.3.3 Sobel算子
Sobel边缘算子是一种类似Prewitt边缘检测算子的边缘模板算子,Sobel算子对于像素的位置的影响做了加权,可以较好的抑制(平滑)噪声,降低边缘模糊程度。
Sobel算子对应模板如下:
2.4 二阶微分算子计算原理
图像的边缘不仅可以通过一阶导数进行表述,还可以通过二阶导数进行寻找。边缘是一阶导数的极大值点,对应二阶导数的过零点,求出图像的二阶导数的过零点就能精确地找到图像的边缘点,如下面的图像显示:
在上面一节我们已经得到了图像在(x,y)点处x方向和y方向上的一阶偏导数:
对一阶偏导数求偏导,得到二阶偏导数:
2.5 常见的二阶微分算子
2.5.1 拉普拉斯(Laplace)算子
二维图像的拉普拉斯算子的定义为:
也就是我们在上文中推导出的二阶微分算子的结果,因为图像是离散的二维函数,所以我们也就得到了一个数值近似结果。拉普拉斯算子的模板形式如下:
拉普拉斯算子模板对90°旋转保持旋转不变性(各向同性),其他常用的拉普拉斯算子模板还有:
Laplacian算子一般不以其原始形式用于边缘检测,因为其作为一个二阶导数,Laplacian算子对噪声具有无法接受的敏感性;同时其幅值产生算边缘,这是复杂的分割不希望有的结果;并且Laplacian算子没有边缘方向信息,不能检测边缘的方向。可以明显的看出,Laplace算子虽然解决了一阶微分算子确定阈值的困难,但是却不能克服噪声的干扰。为此,继续介绍其他算子。
2.5.2 LoG算子
1980年,Marr和Hildreth提出将Laplace算子与高斯低通滤波相结合,提出了LoG(Laplace of Guassian)算子。其方法为:首先使用高斯算子对图像进行平滑,抑制噪声,然后对平滑后的图像使用拉普拉斯算子。所以LoG算子等效于:高斯平滑+拉普拉斯算子。
我们常用的二维高斯函数为:
根据前文提到的卷积运算性质,我们可以得到LoG算子:
根据σ的不同以及3σ原则可以建立不同的模板,σ是一个尺度参数,在图像处理中引入尺度以及建立多尺度空间是一个重要的突破,σ越大,图像越模糊滤除噪声效果越好,σ越小,效果相反。
常用LoG模板如下:
LoG算子因其形状,也被称为墨西哥草帽算子。
2.5.3 DoG算子
我们可以通过数学上的关系对LOG的计算进行简化,这边得到了DOG算子。
二维高斯函数对σ求导:
上文我们已经得到:
可以得到:
同时由导数定义知道:
所以:
可以看到,公式右边比LOG算子只是多了一个系数,而在实际应用中没有太大影响。根据上面公式结果,我们可以定义DoG为两个不同参数的高斯滤波结果之差:
当我们用DOG算子代替LoG算子与图像卷积的时候:
而在具体图像处理中,就是将两幅图像在不同参数下的高斯滤波结果相减,得到DoG图。
使用DoG时,近似的LOG算子σ值的选取为:
当使用这个值时,可以保证LoG和DoG的过零点相同,只是幅度大小不同。
2.6 Canny边缘检测
2.6.1 Canny边缘检测简介
Canny边缘检测是JOHN CANNY于1986年在论文《A Computational Approach to Edge Detection》中首次提出的,就此拉开了Canny边缘检测算法的序幕。
Canny边缘检测是从不同视觉对象中提取有用的结构信息并大大减少要处理的数据量的一种技术,目前已广泛应用于各种计算机视觉系统。Canny发现,在不同视觉系统上对边缘检测的要求较为类似,因此,可以实现一种具有广泛应用意义的边缘检测技术。边缘检测的一般标准包括:
- 以低的错误率检测边缘,也即意味着需要尽可能准确的捕获图像中尽可能多的边缘。
- 检测到的边缘应要与图像中的实际边缘尽可能的接近。
- 图像中给定的边缘应只被标记一次,并且在可能存在的图像噪声不应被检测为边缘。
为了满足这些要求,Canny使用了变分法。Canny检测器中的最优函数使用四个指数项的和来描述,它可以由高斯函数的一阶导数来近似。
在目前常用的边缘检测方法中,Canny边缘检测算法是具有严格定义的,可以提供良好可靠检测的方法之一。由于它具有满足边缘检测的三个标准和实现过程简单的优势,成为边缘检测最流行的算法之一。
Canny边缘检测步骤 | 应用及目的 |
1. 高斯滤波 | 去噪声降低错误率,有可能增大边缘宽度 |
2. 计算梯度幅值和方向 | 估计每一点处的边缘强度与方向,实际应用中,将梯度幅值大于阈值的点标记为边缘 |
3. 梯度非极大值抑制(NMS) | 对Sobel、Prewitt等算子的结果进一步细化 |
4. 双阈值(Double-Threshold)提取边缘 | 确定强边缘和弱边缘 |
2.6.2 Canny边缘检测原理及流程
①高斯滤波器平滑图像
参照 图像噪声及图像平滑(基于python-opencv实现) 一文
②计算图像梯度幅值和方向
可以采用Sobel算子、Prewitt算子、Roberts算子等进行梯度幅值的计算。计算原理可以参见本文第2.1节部分。我们可以知道图像梯度幅值和方向为:
但有时候我们也可以使用如下的公式进行梯度幅值的简化:
其梯度值越大,表示像素值的变化率越大,变化越明显,在实际的应用中,将梯度幅值大于阈值的点标记为边缘。如下图表示了某像素中心点的梯度向量、方位角以及边缘方向(任一点的边缘与梯度向量正交) :
③梯度非极大值抑制
为了实现图像中的边缘只能被标记一次,并且可能存在的图像噪声不应该被标记为边缘,我们沿着梯度方向对梯度幅值进行非极大值抑制。
如上图,图中波动的曲线为在某方向求得的图像的梯度幅值,小黑点为梯度幅值的局部极值点,粗黑线为某一阈值。我们很容易明白边缘是梯度幅值的局部极值点,并且在实际应用中我们将超过某一阈值的梯度幅值的局部极值点才定义为边缘,因此上图中的边缘点应该选为箭头指向的3个点。但是在局部极值点附近经常存在相近数值的点,如上图最顶端小黑点周围未被指出的曲线上的值,是不能将这些点作为边缘的,要抑制这些点。这也就是为什么要沿梯度方向进行极大值抑制的目的。
我们对图像的的每一个点,在梯度方向和梯度反方向各找n个像素点,比较当前点的和这n个点的梯度幅值大小,若当前点的梯度幅值不是这些点中的最大值,则当前点置0,否则保持当前点。我们以3*3区域为例进一步说明:
3*3区域内,边缘可以划分为垂直、水平、45°、135°4个方向,同样,梯度反向也为四个方向(与边缘方向正交)。因此为了进行非极大值,将所有可能的方向量化为4个方向,如下图:
量化的结果情况为:
非极大值抑制即为沿着上述4种类型的梯度方向,比较3*3邻域内对应邻域梯度幅值的大小:
在每一点上,领域中心 x 与沿着其对应的梯度方向的两个像素相比,若中心点梯度幅值为最大值,则保留,否则中心置0,这样可以抑制非极大值,保留局部梯度最大的点,以得到细化的边缘。
④双阈值提取边缘
我们使用阈值来滤除有噪声引起的梯度值的变化,减少噪声影响,步骤如下:
- 如果像素的梯度值大于高阈值TH,则将其标记为强边缘点
- 如果像素的梯度值小于高阈值TH且大于低阈值TL,则将其标记为弱边缘点
- 如果像素的梯度值小于低阈值TL,则将其抑制
我们将强边缘点的连接记为E1,弱边缘点的连接记为E2。
我们使用边缘连接策略找到图像的边缘,一般认为:
- 强边缘点是真的边缘;弱边缘点可能是真的边缘,也可能是噪声
- 通常认为强边缘点连通的弱边缘点是真实边缘
- 噪声导致的弱边缘点是孤立的
- 如果弱边缘点的邻域内有强边缘点,将改弱边缘点视为强边缘点
在应用中具体操作如下:
- 选取系数TH和TL,比率为2:1或3:1(一般取TH=0.3或0.2,TL=0.1)
- 将小于低阈值的点抛弃,赋0;将大于高阈值的点立即标记(这些点为确定边缘点),赋1或255
- 将小于高阈值,大于低阈值的点使用8连通区域确定(即:只有与TH像素连接时才会被接受,成为边缘点,赋 1或255)
三、基于python-opencv的实现
3.1 Sobel算子实现
使用cv2.Sobel()实现Sobel算子:
cv2.Sobel(src, # 参数是需要处理的图像;
ddepth, # 图像的深度,-1表示采用的是与原图像相同的深度。目标图像的深度必须大于等于原图像的深度
dx, # dx和dy表示的是求导的阶数,0表示这个方向上没有求导,一般为0、1、2。
dy[,
dst[, #输出图片
ksize[,#Sobel算子的大小,必须为1、3、5、7。
scale[, #缩放导数的比例常数,默认情况下没有伸缩系数;
delta[, #可选的增量,将会加到最终的dst中,同样,默认情况下没有额外的值加到dst中;
borderType #判断图像边界的模式。这个参数默认值为cv2.BORDER_DEFAULT。
]]]]])
Sobel图像测试:
import numpy as np
import maplotlib.pyplot as plt
import cv2
img_gray = cv2.imread('image.jpg', cv2.IMREAD_GRAYSCALE)
# sobel算子计算
sobel_x = cv2.Sobel(img_gray, cv2.CV_64F, 1, 0, ksize=5)
sobel_y = cv2.Sobel(img_gray, cv2.CV_64F, 0, 1, ksize=5)
# 梯度幅值
sobel_M = np.sqrt(sobel_x**2 + sobel_y**2)
# image show
plt.figure('image')
plt.subplot(221), plt.imshow(img_gray, cmap='gray'), plt.title('original')
plt.subplot(222), plt.imshow(sobel_x, cmap='gray'), plt.title('sobel x')
plt.subplot(223), plt.imshow(sobel_y, cmap='gray'), plt.title('sobel y')
plt.subplot(224), plt.imshow(sobel_M, cmap='gray'), plt.title('sobel M')
plt.show()
sobel算子效果:
在上面的例子中,第二个参数使用了cv2.CV_64F。当然我们可以通过参数-1来设定输出图像的深度(数据类型)与原图像保持一致,但是我们没有这么做。这是为什么?想象一下,一个从黑到白的边界的导数是正数,而一个从白到黑的边界的导数却是负数。如果原图像的深度是np.int8时,所有的负值都会被截断变成0。换句话就是把边界丢失掉。
对于处理成64位浮点型的图像,我们可以使用convertScaleAbs()函数将其转回原来的uint8形式。convertScaleAbs()的原型为:
dst = cv2.convertScaleAbs(src[, dst[, alpha[, beta]]])
#其中可选参数alpha是伸缩系数,beta是加到结果上的一个值
#结果返回uint8类型的图片,绝对值超过255的直接截断
代码如下:
# 转成uint8形式的图像
abs_x = cv2.convertScaleAbs(sobel_x)
abs_y = cv2.convertScaleAbs(sobel_y)
# 结合abs_x和abs_y
abs_xy = cv2.addWeighted(abs_x, 0.5, abs_y, 0.5, 0)
# image show
plt.figure('compare result')
plt.subplot(231), plt.imshow(sobel_M, cmap='gray'), plt.title('sobel M')
plt.subplot(234), plt.imshow(abs_xy, cmap='gray'), plt.title('abs xy')
plt.subplot(232), plt.imshow(sobel_x, cmap='gray'), plt.title('sobel x')
plt.subplot(235), plt.imshow(abs_x, cmap='gray'), plt.title('abs x')
plt.subplot(233), plt.imshow(sobel_y, cmap='gray'), plt.title('sobel y')
plt.subplot(236), plt.imshow(abs_y, cmap='gray'), plt.title('abs y')
plt.show()
其函数效果图:
3.2 Laplacian算子实现
使用cv2.Laplacian()函数实现拉普拉斯算子。
dst = cv2.Laplacian(src, ddepth[, dst[, ksize[, scale[, delta[, borderType]]]]])
第一个参数是需要处理的图像;
第二个参数是图像的深度,-1表示采用的是与原图像相同的深度。目标图像的深度必须大于等于原图像的深度;
其后是可选的参数:
dst不用解释了;
ksize是算子的大小,必须为1、3、5、7。默认为1。
scale是缩放导数的比例常数,默认情况下没有伸缩系数;
delta是一个可选的增量,将会加到最终的dst中,同样,默认情况下没有额外的值加到dst中;
borderType是判断图像边界的模式。这个参数默认值为cv2.BORDER_DEFAULT。
代码实现:
import numpy as np
import maplotlib.pyplot as plt
import cv2
img_gray = cv2.imread('image.jpg', cv2.IMREAD_GRAYSCALE)
laplacian = cv2.Laplacian(img_gray, cv2.CV_64F, ksize=5)
plt.figure('laplacian')
plt.subplot(121), plt.imshow(img_gray, cmap='gray'), plt.title('original')
plt.subplot(122), plt.imshow(laplacian, cmap='gray'), plt.title('laplacian')
plt.show()
Laplacian算子效果:
3.3 Canny边缘检测实现
opencv中使用cv2.Canny()函数实现Canny边缘检测。
edge = cv2.Canny(image, threshold1, threshold2[, edges[, apertureSize[, L2gradient ]]])
image:源图像
threshold1:阈值1 minVal
threshold2:阈值2 maxVal
apertureSize:可选参数,用于查找图像渐变的Sobel内核的大小,默认值为3
L2gradient:用于查找梯度幅度的方程式,默认情况下,它为False,使用简化的x和y方向梯度绝对值之和的公式进行计算,如果为True,则使用上文提到的更精确的公式。
代码实现:
import numpy as np
import maplotlib.pyplot as plt
import cv2
# read image
img_gray = cv2.imread('image.jpg', cv2.IMREAD_GRAYSCALE)
# Canny Edge
edge = cv2.Canny(img_gray, 80, 120, L2gradient=True)
# image show
plt.figure('edge')
plt.subplot(121), plt.imshow(img_gray, cmap='gray'), plt.title('original')
plt.subplot(122), plt.imshow(edge, cmap='gray'), plt.title('Canny edge')
plt.show()
Canny边缘检测效果:
参考
图像边缘检测——二阶微分算子(上)Laplace算子、LOG算子、DOG算子(Matlab实现)