自动驾驶感知-车道线系列(二)——Canny边缘检测

Canny边缘检测

  • 前言
  • 一、Canny是什么?
  • 二、算法详细步骤
    • 1. 平滑处理
    • 2. 梯度检测
    • 3. 非极大值抑制
    • 4. 滞后阈值处理
  • 三、函数原型
  • 四、应用实例
  • 五、总结


前言

边缘检测是图像处理和计算机视觉中的基本问题,边缘检测的目的是标识数字图像中亮度变化明显的点。图像属性中的显著变化通常反映了属性的重要事件和变化。这些包括:(i)深度上的不连续;(ii)表面方向不连续;(iii)物质属性变化;(iv)场景照明变化。
边缘检测算子包括:

  • 一阶: Roberts Cross算子,Prewitt算子,Sobel算子,Kirsch算子,罗盘算子;
  • 二阶: Marr-Hildreth,在梯度方向的二阶导数过零点,Canny算子,Laplacian算子。

其中,Canny算子(或者这个算子的变体)是最常用的边缘检测方法。

一、Canny是什么?

Canny边缘检测方法是由Canny于1986年提出的一种被公认为效果较好的边缘检测方法。
在介绍Canny算法的具体流程之前,先说一下边缘检测方法的3项指标:

  • 低失误率,即不能漏检也不能错检;
  • 高的位置精度,即标定的边缘像素点与真正的边缘中心之间距离应该为最小;
  • 每个边缘应该有唯一的响应,即得到单像素宽度的边缘。

Canny的贡献不仅是提出了一种边缘检测算子,而且还给出了衡量边缘检测算子好坏的准则:

  • 信噪比准则;
  • 定位精度准则;
  • 单边缘响应准则。

二、算法详细步骤

下面我们就重点介绍一下Canny算子的实现过程:平滑处理,梯度检测,非极大值抑制和滞后阈值处理。

1. 平滑处理

所有的边缘都极易受到噪声的干扰,为了防止因噪声所引起的错误的检测结果,有必要应用平滑滤波的方法滤除噪声.高斯滤波方法是最常用的滤波方法,二维图像应用二维高斯函数,它的定义为:
G ( u , v ) = 1 2 π σ 2 e − u 2 + v 2 2 σ 2 G(u, v) = \frac{1}{2\pi\sigma^2}e^{-\frac{u^2+v^2}{2\sigma^2}} G(u,v)=2πσ21e2σ2u2+v2
式中, σ \sigma σ表示高斯函数的标准差.只要把输入图像与二维高斯函数进行卷积,即可得到平滑处理后的图像.考虑到数字图像为离散化的形式,我们往往把高斯函数转换为离散化的高斯内核模板的形式,如标准差为1.4的模板尺寸为5*5的归一化高斯内核模板为:
K = 1 159 ∗ [ 2 4 5 4 2 4 9 12 9 4 5 12 15 12 5 4 9 12 9 4 2 4 5 4 2 ] K = \frac{1}{159}* \begin{bmatrix} 2&4&5&4&2\\ 4&9&12&9&4\\ 5&12&15&12&5\\ 4&9&12&9&4\\ 2&4&5&4&2 \end{bmatrix} K=1591245424912945121512549129424542

2. 梯度检测

梯度是图像灰度值变化剧烈的地方,它可以通过Roberts算子,Prewitt算子,Sobel算子等最简单的模板检测方法得到.常用的是Sobel算子,它是由两个模板组成:
S G X = [ − 1 0 1 − 2 0 2 − 1 0 1 ] S G Y = [ 1 2 1 0 0 0 − 1 − 2 − 1 ] S_{GX} = \begin{bmatrix} -1&0&1\\ -2&0&2\\ -1&0&1 \end{bmatrix} S_{GY} = \begin{bmatrix} 1&2&1\\ 0&0&0\\ -1&-2&-1 \end{bmatrix} SGX=121000121SGY=101202101
把这两个模板分别与图像进行卷积运算,则分别得到水平方向的梯度 G x G_x Gx和垂直方向的梯度 G y G_y Gy.最终的梯度幅值 G G G往往是由欧几里得距离( L 2 L2 L2范数)求得:
G = G x 2 + G y 2 G = \sqrt{G_x^2 + G_y^2} G=Gx2+Gy2
但有时为了简化,梯度值也可由曼哈顿距离( L 1 L1 L1范数)得到:
G = ∣ G x ∣ + ∣ G y ∣ G = |G_x| + |G_y| G=Gx+Gy
而梯度幅角 θ \theta θ为:
θ = a r c t a n ( ∣ G y ∣ ∣ G x ∣ ) \theta = arctan(\frac{|G_y|}{|G_x|}) θ=arctan(GxGy)
由上式得到的角度可以是任意值,但这里我们需要把梯度幅角四舍五入到代表水平方向,垂直方向和2个对角线方向的4个方向上,即 0 ° 0\degree 0° 45 ° 45\degree 45° 90 ° 90\degree 90° 135 ° 135\degree 135°.例如,梯度幅角为 − 22.5 ° -22.5\degree 22.5°~ 22.5 ° 22.5\degree 22.5°时,将被统一设置为 0 ° 0\degree 0°

3. 非极大值抑制

这一步骤的目的是使边缘细化.由上一步得到的边缘图像十分模糊,不符合好的边缘检测中的第三个指标,而非极大值抑制可以抑制那些局部不是梯度幅值最大值的边缘,而保留下来的具有局部最大值的像素点正是灰度值变化最剧烈的地方.这里的局部最大值是由在3*3的邻域内的梯度方向上比较梯度值得到的.例如:

  1. 当梯度方向为 0 ° 0\degree 0°时,图像的边缘是南-北方向,则在3*3的邻域内,当前像素与其左右两侧像素的梯度值进行比较,如果当前像素的梯度幅值最大,则保留,否则剔除;
  2. 当梯度方向为 90 ° 90\degree 90°时,图像的边缘是东-西方向,则在3*3的邻域内,当前像素与其上下两侧像素的梯度值进行比较,如果当前像素的梯度幅值最大,则保留,否则剔除;
  3. 当梯度方向为 135 ° 135\degree 135°时,图像的边缘是东北-西南方向,则在3*3的邻域内,当前像素与其左上角和右下角像素的梯度值进行比较,如果当前像素的梯度幅值最大,则保留,否则剔除;
  4. 当梯度方向为 45 ° 45\degree 45°时,图像的边缘是东南-西北方向,则在3*3的邻域内,当前像素与其右上角和左下角像素的梯度值进行比较,如果当前像素的梯度幅值最大,则保留,否则剔除.

还需说明的是,梯度方向的符号与非极大值抑制的结果无关,即无论是东-西方向还是西-东方向,两者是一样的.

4. 滞后阈值处理

由上一步得到的边缘仍有一小部分由于噪声或者颜色变化的影响而不是真正的边缘,这种现象的表现形式是尽管这些边缘的梯度幅值是局部最大值,但与其他边缘比,他们的梯度幅值很小,也就是绝对梯度幅值很小.处理它们也很简单,采用阈值法即可.但Canny采用的双阈值的方法,即设置高,低两个阈值,当梯度幅值大于高阈值时,该边缘为强边缘;当梯度幅值小于低阈值时,该边缘需要被剔除;当梯度值介于高,低阈值之间时,该边缘为弱边缘.
强边缘毫无疑问是需要被保留下来的,而弱边缘则需要采用边缘跟踪的方法来判断其是否为真正的边缘.在弱边缘的3*3的邻域内,如果有强边缘,则说明该弱边缘是属于这个强边缘的,所以需要被保留,否则被剔除掉.

三、函数原型

Canny函数的原型为:

void Canny(InputArray image, OutputArray edges, double threshold1, double threshold2, int apertureSize=3, bool L2gradient=false)

image表示输入图像;
edges表示输出边缘图像;
threshold1和threshold2表示滞后阈值法中所需要的高,低两个阈值;
apertureSize表示孔径尺寸,即Sobel算子的尺寸大小,OpenCV是采用Sobel算子来计算图像的梯度的,该值默认为3;
L2gradient表示在计算梯度幅值时是用L2范数还是用L1范数,该值默认为false,即采用L1范数.

源码解析内容太多,暂时不做详细整理,但是可以列一下源码中的几个特点:

  • 它没有执行经典Canny方法的步骤1,即滤波平滑这一步被省略,当然我们可以事先进行平滑处理后再运行Canny函数;
  • 只遍历了一次图像,就完成了步骤2和步骤3,具体过程是计算当前遍历行的梯度幅值,同时计算前一行的梯度幅值,并完成了3*3邻域内的非极大值抑制;
  • 步骤3和步骤4的内容并不是完全分开执行的,而是交错在了一起;
  • 在步骤4的边缘跟踪法中,程序是寻找强边缘的33邻域内的弱边缘的方法,而不是寻找弱边缘的33邻域内的强边缘.

四、应用实例

在进行边缘检测之前,我们先应用GaussianBlur函数对图像进行高斯平滑滤波,设它的标准差为1.6,而高斯内核尺寸是由该标准差确定的.

#include "opencv2/core/core.hpp"
#include "opencv2/highgui/highgui.hpp"
#include "opencv2/imgproc/imgproc.hpp"
#include "opencv2/imgcodecs/imgcodecs.hpp"
#include 

using namespace cv;
using namespace std;

int main(int argc, char** argv){
    Mat src, dst, edge;
    src = imread("happynewyear.jpeg");
    if(!src.data)
        return -1;
        
    GaussianBlur(src, dst, Size(0, 0), 1.6); //高斯滤波
    //Canny方法,高阈值为60, 低阈值为25
    Canny(dst, edge, 25, 60);

    namedWindow("Canny", WINDOW_AUTOSIZE);
    imshow("Canny", edge);
    
    imwrite("Canny.jpg", edge);
    
    waitKey(0);
    return 0;
}

我们看下效果(糟心的2020过去了,祝大家新年快乐!新的一年,继续奋斗!):
自动驾驶感知-车道线系列(二)——Canny边缘检测_第1张图片自动驾驶感知-车道线系列(二)——Canny边缘检测_第2张图片有几点需要注意一下:

  • imwrite
  bool imwrite(const string& filename, InputArray img, const vector& params=vector() )

其中,filename: 需要写入的文件名,会自己创建(eg. imwrite(“1.jpeg”, src)),注意要带后缀;

  • 编译cpp
    g++ happynewyear.cpp -o happynewyear `pkg-config opencv --cflags --libs`
    
    注意,pkg-config opencv --cflags --libs外面的引号是键盘左上角1旁边的那个引号.

五、总结

总体来讲,Canny边缘检测相对比较成熟,调用起来也比较方便.如果不是要针对边缘检测做研究和进一步优化的话,直接调用就可以了.如果需要对源码做优化,虽然原理相对比较简单,但是还是要对源码进行解读才可以.

你可能感兴趣的:(边缘检测,opencv,计算机视觉,cv)