边缘检测是图像处理和计算机视觉中,尤其是特征提取中的一个研究领域。图像边缘检测大幅度的减少了数据量,并且剔除了可以认为不相关的信息,保留了图像重要的结构属性。边缘检测,就是找到原始图像中画面出现跳变的地方。这里的跳变,可以理解为相邻像素的像素值出现了较大的变化,而梯度方向为灰度最大变化率的方向(求导解决)。
在实际的图像分割中,往往只用到一阶和二阶导数,虽然原理上,可以用更高阶的导数,但是因为噪声的影响,在纯粹二阶的导数操作中就会出现对噪声的敏感现象,三阶以上的导数信息往往失去了应用价值。二阶导数还可以说明灰度突变的类型。在某些情况下,如灰度变化均匀的图像,只利用一阶导数可能找不到边界,此时二阶导数就能提供很有用的信息。二阶导数对噪声也比较敏感,解决的方法是先对图像进行平滑滤波,消除部分噪声,再进行边缘检测。不过,利用二阶导数信息的算法是基于过零检测的,因此得到的边缘点数比较少,有利于后继的处理和识别工作。
边缘检测算子可分为以下几种:
下面主要将canny算子和sobel算子的原理,其他算子只写出了实现函数.
索贝尔算子(Sobeloperator)主要用作边缘检测,在技术上,它是一离散性差分算子,用来运算图像亮度函数的灰度之近似值。在图像的任何一点使用此算子,将会产生对应的灰度矢量或是其法矢量。
Sobel算子根据像素点上下、左右邻点灰度加权差,在边缘处达到极值这一现象检测边缘.Sobel算子的俩个梯度矩阵Gx和Gy, Gx 用来计算横向的梯度, Gy 用来计算纵向的梯度。
具体公式如下:
原图中的作用点像素值通过卷积之后为:
可以化简成(为了提升效率):
如果梯度G大于阈值,这认为(x, y)是边缘点.
Sobel算子根据像素点上下、左右邻点灰度加权差,在边缘处达到极值这一现象检测边缘。对噪声具有平滑作用,提供较为精确的边缘方向信息,边缘定位精度不够高。Sobel算子检测方法对灰度渐变和噪声较多的图像处理效果较好,sobel算子对边缘定位不是很准确,图像的边缘不止一个像素;当对精度要求不是很高时,是一种较为常用的边缘检测方法。
优点:计算简单,速度很快;
缺点:计算方向单一,对复杂纹理的情况显得乏力;直接用阈值来判断边缘点欠合理解释,会造成较多的噪声点误判。
Canny边缘检测是一种比较新的边缘检测算子,具有很好地边缘检测性能,该算子功能比前面几种都要好,但是它实现起来较为麻烦,Canny算子是一个具有滤波,增强,检测的多阶段的优化算子,在进行处理前,Canny算子先利用高斯平滑滤波器来平滑图像以除去噪声,Canny分割算法采用一阶偏导的有限差分来计算梯度幅值和方向,在处理过程中,Canny算子还将经过一个非极大值抑制的过程,最后Canny算子还采用两个阈值来连接边缘(高低阈值输出二值图像)。
高斯滤波器是一种线性滤波器,能够有效的抑制噪声,平滑图像。
计算方式如下:
原始坐标 带入高斯函数 归一化
从第二张图可以看出,中心点处的值最大,离中心点越近的值越大,这符合高斯分布;之后进行归一化,就得到高斯滤波核;再进行卷积操作。
2. 用一阶偏导有限差分计算梯度幅值和方向.
可选用的模板:sobel算子、Prewitt算子、Roberts模板等等;
一般采用sobel算子,OpenCV也是如此,利用sobel水平和垂直算子与输入图像卷积计算dx、dy:
Gx是对水平方向求梯度值, Gy是对垂直方向求梯度值(计算方式就是卷积操作,只不过是在俩个方向上做卷积)。
使用以下公式计算梯度幅值和方向:
梯度方向近似到四个可能角度之一(一般 0, 45, 90, 135)
求出θ后, θ 的值可能不是这些角度, 就看θ离哪条直线近, 则这条直线的角度就是θ.这样分的好处是:方便了后面非极大值抑制的计算。因为图像中像素点的分布是离散的,这样四个方向和他们的反方向上都有像素点。
梯度幅值:变换率最大 梯度方向:是后面准确的找出细化边缘的前提。
这步其实就是图像增强,并没有找到真正的边。
3. 对梯度幅值进行非极大值抑制(NMS).
将某点的幅值和沿着梯度方向的俩个幅值进行比较,若该点的幅值大于这俩个方向上的幅值,则保留;否则置为0.这样就可以得到局部最大值点,也就是极大值,则认为这个像素点是边缘点,就可以得到细化的边.
如图所示,g1、g2、g3、g4都代表像素点,很明显它们是c的八领域中的4个,左图中c点是我们需要判断的点,蓝色的直线是它的梯度方向,也就是说c要是局部极大值,它的梯度幅值M需要大于直线与g1g2和g2g3的交点,dtmp1和dtmp2处的梯度幅值。但是dtmp1和dtmp2不是整像素,而是亚像素,也就是坐标是浮点的,那怎么求它们的梯度幅值呢?利用线性插值,例如dtmp1在g1、g2之间,g1、g2的幅值都知道,我们只要知道dtmp1在g1、g2之间的比例,就能得到它的梯度幅值,而比例是可以靠夹角计算出来的,夹角又是梯度的方向。比如,假设g1处的幅值是1, g2处的幅值是4,θ为60°,则g1和g2的距离是d1 = 3,dTmp1和g2的距离为√3 ,则可求得dTmp1处的幅值为 4 - √3 。
线性插值: 线性插值是一种针对一维数据的插值方法,它根据一维数据序列中需要插值的点的左右邻近两个数据点来进行数值的估计。当然了它不是求这两个点数据大小的平均值(当然也有求平均值的情况),而是根据到这两个点的距离来分配它们的比重的。
4. 用双阈值算法检测和连接边缘(滞后边界跟踪)
滞后阈值需要两个阈值(高阈值和低阈值),阈值设置的太高,会使得真正的边缘点过滤掉;阈值设置的太低,会使得噪音也被认为边缘点。
a. 如果某一像素位置的幅值超过 高 阈值, 该像素被保留为边缘像素(强边缘点)。
b. 如果某一像素位置的幅值小于 低 阈值, 该像素被排除。
c. 如果某一像素位置的幅值在两个阈值之间(弱边缘点),该像素仅仅在连接到一个高于 高 阈值的像素时被保留。
Canny 推荐的 高:低 阈值比在 2:1 到3:1之间。
强边缘点可以认为是真的边缘。弱边缘点则可能是真的边缘,也可能是噪声或颜色变化引起的。为得到精确的结果,后者引起的弱边缘点应该去掉。通常认为真实边缘引起的弱边缘点和强边缘点是连通的,而由噪声引起的弱边缘点则不会。所谓的滞后边界跟踪算法检查一个弱边缘点的8连通领域像素,只要有强边缘点存在,那么这个弱边缘点被认为是真是边缘保留下来。
Canny 的目标是找到一个最优的边缘检测算法,最优边缘检测的含义是:
好的检测 - 算法能够尽可能多地标识出图像中的实际边缘。
好的定位 - 标识出的边缘要尽可能与实际图像中的实际边缘尽可能接近。
最小响应 - 图像中的边缘只能标识一次,并且可能存在的图像噪声不应标识为边缘。
较大的阈值2用于检测图像中明显的边缘,但一般情况下检测的效果不会那么完美,边缘检测出来是断断续续的。所以这时候用较小的第一个阈值用于将这些间断的边缘连接起来。
实现函数: cv2.Canny(image(必须为单通道图), threshold1, threshold2 )
import cv2
import numpy as np
import matplotlib.pyplot as plt
# sobel算子、Laplacian算子、Canny算子、scharr算子、Roberts算子、prewitt算子
# ********************Sobel边缘检测*****************************
def edge_sobel(src):
kernelSize = (3, 3)
gausBlurImg = cv2.GaussianBlur(src, kernelSize, 0)
# 转换为灰度图
channels = src.shape[2]
if channels > 1:
src_gray = cv2.cvtColor(gausBlurImg, cv2.COLOR_RGB2GRAY)
else:
src_gray = src.clone()
scale = 1
delta = 0
depth = cv2.CV_16S
# 求X方向梯度(创建grad_x, grad_y矩阵)
grad_x = cv2.Sobel(src_gray, depth, 1, 0)
abs_grad_x = cv2.convertScaleAbs(grad_x)
# 求Y方向梯度
grad_y = cv2.Sobel(src_gray, depth, 0, 1)
abs_grad_y = cv2.convertScaleAbs(grad_y)
# 合并梯度(近似)
edgeImg = cv2.addWeighted(abs_grad_x, 0.5, abs_grad_y, 0.5, 0)
return edgeImg
# ********************Laplacian边缘检测*****************************
def edge_laplacian(src):
scale = 1
delta = 0
depth = cv2.CV_16S
if src.shape[2] > 1:
src_gray = cv2.cvtColor(src, cv2.COLOR_RGB2GRAY)
else:
src_gray = src.clone()
kernelSize = (3, 3)
gausBlurImg = cv2.GaussianBlur(src_gray, kernelSize, 0)
laplacianImg = cv2.Laplacian(gausBlurImg, depth, kernelSize)
edgeImg = cv2.convertScaleAbs(laplacianImg)
return edgeImg
# ********************Canny边缘检测*****************************
def edge_canny(src, threshold1, threshold2):
kernelSize = (3, 3)
gausBlurImg = cv2.GaussianBlur(src, kernelSize, 0)
edgeImg = cv2.Canny(gausBlurImg, threshold1, threshold2)
return edgeImg
# ********************主函数*****************************
# imgSrc = cv2.imread( "3.jpg", 1) #复杂场景
imgSrc = cv2.imread("../images/2.png") # 简单场景
img = cv2.resize(imgSrc, (0, 0), fx=0.25, fy=0.25, interpolation=cv2.INTER_NEAREST)
sobelImg = edge_sobel(img)
laplacianImg = edge_laplacian(img)
cannyImg = edge_canny(img, 20, 60)
# scharr算子
img1 = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
scharrx = cv2.Scharr(img1, cv2.CV_64F, 1, 0)
scharry = cv2.Scharr(img1, cv2.CV_64F, 0, 1)
scharrx = cv2.convertScaleAbs(scharrx)
scharry = cv2.convertScaleAbs(scharry)
scharrxy = cv2.addWeighted(scharrx, 0.5, scharry, 0.5, 0)
# Roberts算子
grayImage = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
kernelx = np.array([[-1, 0], [0, 1]], dtype=int)
kernely = np.array([[0, -1], [1, 0]], dtype=int)
x = cv2.filter2D(grayImage, cv2.CV_16S, kernelx)
y = cv2.filter2D(grayImage, cv2.CV_16S, kernely)
# 转uint8
absX = cv2.convertScaleAbs(x)
absY = cv2.convertScaleAbs(y)
Roberts = cv2.addWeighted(absX, 0.5, absY, 0.5, 0)
# prewitt算子
kernelx = np.array([[1, 1, 1], [0, 0, 0], [-1, -1, -1]], dtype=int)
kernely = np.array([[-1, 0, 1], [-1, 0, 1], [-1, 0, 1]], dtype=int)
x = cv2.filter2D(grayImage, cv2.CV_16S, kernelx)
y = cv2.filter2D(grayImage, cv2.CV_16S, kernely)
# 转uint8
absX = cv2.convertScaleAbs(x)
absY = cv2.convertScaleAbs(y)
Prewitt = cv2.addWeighted(absX, 0.5, absY, 0.5, 0)
# cv2.imshow( "Origin", img )
# cv2.imshow('scharr', scharrxy)
# cv2.imshow('Robert', Roberts)
# cv2.imshow('prewitt', Prewitt)
# cv2.imshow("sobellaplaciancanny", np.hstack([sobelImg, laplacianImg, cannyImg]))
# cv2.imshow("scharrxyRobertsPrewitt", np.hstack([scharrxy, Roberts, Prewitt]))
titles = ['sobelImg', 'laplacianImg ', 'cannyImg', 'scharrxy', ' Roberts', 'Prewitt']
images = [sobelImg, laplacianImg, cannyImg, scharrxy, Roberts, Prewitt]
for i in range(6):
plt.subplot(2, 3, i + 1), plt.imshow(images[i], 'gray')
plt.title(titles[i])
plt.xticks([]), plt.yticks([])
plt.show()
cv2.waitKey(0)
cv2.destroyAllWindows()