目录

  • 前言
  • 正文
  • 原理
  • 高斯滤波过滤
  • 计算像素点的梯度方向(Sobel算子)
  • 非极大值抑制
  • 用双阈值算法检测和连接边缘
  • 通过抑制孤立的弱边缘最终完成边缘检测
  • 代码
  • 参考文献


前言

Canny边缘检测是从不同视觉对象中提取有用的结构信息并大大减少要处理的数据量的一种技术。我们这里主要用其来进行直线边缘检测。

正文

原理

Canny边缘检测算法主要分为以下五个步骤(参考自:Canny边缘检测算法)

  1. 使用高斯滤波器,以平滑图像,滤除噪声。
  2. 计算图像中每个像素点的梯度强度和方向。
  3. 应用非极大值抑制,以消除边缘检测带来的杂散响应。
  4. 应用双阈值检测来确定真实的和潜在的边缘。
  5. 通过抑制孤立的弱边缘最终完成边缘检测。

高斯滤波过滤

为了尽可能减少噪声对边缘检测结果的影响,所以必须滤除噪声以防止由噪声引起的错误检测。为了平滑图像,使用高斯滤波器与图像进行卷积,该步骤将平滑图像,以减少边缘检测器上明显的噪声影响。大小为(2k+1)x(2k+1)的高斯滤波器核的生成方程式由下式给出:

opencv边缘凸起检测 opencv 边缘检测 直线拟合_边缘检测

下面是一个sigma = 1.5,尺寸为3x3的高斯卷积核的例子(需要注意归一化):

opencv边缘凸起检测 opencv 边缘检测 直线拟合_opencv_02

这个卷积核是怎么来的,我们可以算一下。我就直接摘抄网上的一些图了:

假定中心点的坐标是(0,0),那么距离它最近的8个点的坐标如下:

opencv边缘凸起检测 opencv 边缘检测 直线拟合_灰度_03


远的点以此类推。

为了计算权重矩阵,需要设定σ的值。假定σ=1.5,则模糊半径为1的权重矩阵如下:

opencv边缘凸起检测 opencv 边缘检测 直线拟合_opencv_04


这个权重矩阵,就是你把上面sigma和x,y的值都带入那个公式就可以了。这9个点的权重总和等于0.4787147,如果只计算这9个点的加权平均,还必须让它们的权重之和等于1,因此上面9个值还要分别除以0.4787147,得到最终的权重矩阵。

opencv边缘凸起检测 opencv 边缘检测 直线拟合_边缘检测_05


接下来,计算高斯模糊

有了权重矩阵,就可以计算高斯模糊的值了。

假设现有9个像素点,灰度值(0-255)如下:

opencv边缘凸起检测 opencv 边缘检测 直线拟合_像素点_06


每个点乘以自己的权重值:

opencv边缘凸起检测 opencv 边缘检测 直线拟合_opencv边缘凸起检测_07


得到

opencv边缘凸起检测 opencv 边缘检测 直线拟合_灰度_08


将这9个值加起来,就是中心点的高斯模糊的值。

对所有点重复这个过程,就得到了高斯模糊后的图像。如果原图是彩色图片,可以对RGB三个通道分别做高斯模糊。

那么如果一个点处于边界,周边没有足够的点,怎么办?

一个变通方法,就是把已有的点拷贝到另一面的对应位置,模拟出完整的矩阵。

就类似于这样的一个效果:

opencv边缘凸起检测 opencv 边缘检测 直线拟合_opencv_09

你看上面我圈起来的蓝色的地方,就出现了,119-46<225-4。图像的边缘这样就因此便的平滑了。这就是高斯滤波的作用了。

计算像素点的梯度方向(Sobel算子)

  1. 边缘:灰度或结构等信息的突变处,边缘是一个区域的结束,也是另一个区域的开始,利用该特征可以分割图像。
  2. 边缘点:图像中具有坐标[x,y],且处在强度显著变化的位置上的点。
  3. 边缘段:对应于边缘点坐标[x,y]及其方位 ,边缘的方位可能是梯度角。
    索贝尔算子(Sobeloperator)主要用作边缘检测,在技术上,它是一离散性差分算子,用来运算图像亮度函数的灰度之近似值。在图像的任何一点使用此算子,将会产生对应的灰度矢量或是其法矢量。
    这里就直接贴图了:

    Sobel算子根据像素点上下、左右邻点灰度加权差,在边缘处达到极值这一现象检测边缘。对噪声具有平滑作用,提供较为精确的边缘方向信息,边缘定位精度不够高。当对精度要求不是很高时,是一种较为常用的边缘检测方法。

类似的结果图就是下面这样:

opencv边缘凸起检测 opencv 边缘检测 直线拟合_边缘检测_10


上面这个就是算出来的梯度值,但这上面那个sobel算子的符号好像反了,不用管这个,直接按照最上面的那张图算即可。

非极大值抑制

指寻找像素点局部最大值,将非极大值点所对应的灰度值置为0,这样可以剔除掉一大部分非边缘的点。要进行非极大值抑制,就首先要确定像素点C的灰度值在其8值邻域内是否为最大。图1中蓝色的线条方向为C点的梯度方向,这样就可以确定其局部的最大值肯定分布在这条线上,也即出了C点外,梯度方向的交点dTmp1和dTmp2这两个点的值也可能会是局部最大值。因此,判断C点灰度与这两个点灰度大小即可判断C点是否为其邻域内的局部最大灰度点。如果经过判断,C点灰度值小于这两个点中的任一个,那就说明C点不是局部极大值,那么则可以排除C点为边缘。这就是非极大值抑制的工作原理。

opencv边缘凸起检测 opencv 边缘检测 直线拟合_opencv边缘凸起检测_11


上面的这个解释应该还是算比较清楚的,特别还要注意的两个点是:

1)中非最大抑制是回答这样一个问题:“当前的梯度值在梯度方向上是一个局部最大值吗?” 所以,要把当前位置的梯度值与梯度方向上两侧的梯度值进行比较;

2)梯度方向垂直于边缘方向。

但实际上,我们只能得到C点邻域的8个点的值,而dTmp1和dTmp2并不在其中,要得到这两个值就需要对该两个点两端的已知灰度进行线性插值,也即根据图1中的g1和g2对dTmp1进行插值,根据g3和g4对dTmp2进行插值,这要用到其梯度方向,这是上文Canny算法中要求解梯度方向矩阵Thita的原因。

完成非极大值抑制后,会得到一个二值图像,非边缘的点灰度值均为0,可能为边缘的局部灰度极大值点可设置其灰度为128。根据下文的具体测试图像可以看出,这样一个检测结果还是包含了很多由噪声及其他原因造成的假边缘。因此还需要进一步的处理。

用双阈值算法检测和连接边缘

Canny算法中减少假边缘数量的方法是采用双阈值法。选择两个阈值(关于阈值的选取方法在扩展中进行讨论),根据高阈值得到一个边缘图像,这样一个图像含有很少的假边缘,但是由于阈值较高,产生的图像边缘可能不闭合,未解决这样一个问题采用了另外一个低阈值。

在高阈值图像中把边缘链接成轮廓,当到达轮廓的端点时,该算法会在断点的8邻域点中寻找满足低阈值的点,再根据此点收集新的边缘,直到整个图像边缘闭合。

双阈值的玩法参考下面这张图:

opencv边缘凸起检测 opencv 边缘检测 直线拟合_opencv边缘凸起检测_12

通过抑制孤立的弱边缘最终完成边缘检测

到目前为止,被划分为强边缘的像素点已经被确定为边缘,因为它们是从图像中的真实边缘中提取出来的。然而,对于弱边缘像素,将会有一些争论,因为这些像素可以从真实边缘提取也可以是因噪声或颜色变化引起的。为了获得准确的结果,应该抑制由后者引起的弱边缘。通常,由真实边缘引起的弱边缘像素将连接到强边缘像素,而噪声响应未连接。为了跟踪边缘连接,通过查看弱边缘像素及其8个邻域像素,只要其中一个为强边缘像素,则该弱边缘点就可以保留为真实的边缘。

代码

我这里就直接调用OpenCV的一些函数去实现这样的一个效果。

import cv2 as cv
import numpy as np

# canny运算步骤:5步
# 1. 高斯模糊 - GaussianBlur
# 2. 灰度转换 - cvtColor
# 3. 计算梯度 - Sobel/Scharr
# 4. 非极大值抑制
# 5. 高低阈值输出二值图像

# 非极大值抑制:
# 算法使用一个3×3邻域作用在幅值阵列M[i,j]的所有点上;
# 每一个点上,邻域的中心像素M[i,j]与沿着梯度线的两个元素进行比较,
# 其中梯度线是由邻域的中心点处的扇区值ζ[i,j]给出。
# 如果在邻域中心点处的幅值M[i,j]不比梯度线方向上的两个相邻点幅值大,则M[i,j]赋值为零,否则维持原值;
# 此过程可以把M[i,j]宽屋脊带细化成只有一个像素点宽,即保留屋脊的高度值。

# 高低阈值连接
# T1,T2为阈值,凡是高于T2的都保留,凡是低于T1的都丢弃
# 从高于T2的像素出发,凡是大于T1而且相互连接的都保留。最终得到一个输出二值图像
# 推荐高低阈值比值为T2:T1 = 3:1/2:1,其中T2高阈值,T1低阈值

def edge_demo(image):
    blurred = cv.GaussianBlur(image,(3,3),0)#高斯模糊
    gray = cv.cvtColor(blurred,cv.COLOR_BGR2GRAY)# 将其变成灰度图

    grad_x = cv.Sobel(gray,cv.CV_16SC1,1,0)#用sobel算子求梯度。最后两个参数就是说是求的x,还是y的梯度
    grad_y = cv.Sobel(gray,cv.CV_16SC1,0,1)

    edge_output = cv.Canny(grad_x,grad_y,30,150)# 将两个梯度传入,然后传入高阈值与低阈值
    #edge_output = cv.Canny(gray,50,150)
    cv.imshow("gray",gray)
    cv.imshow("canny_dmeo",edge_output)


src = cv.imread("../images/boss2.jpg")
cv.namedWindow("input image",cv.WINDOW_AUTOSIZE)
cv.imshow('input image', src)
edge_demo(src)
cv.waitKey(0)  # 等有键输入或者1000ms后自动将窗口消除,0表示只用键输入结束窗口

cv.destroyAllWindows()

效果图

opencv边缘凸起检测 opencv 边缘检测 直线拟合_边缘检测_13