OpenCV2学习笔记(七)

使用Canny算子检测轮廓

在:http://blog.csdn.net/liyuefeilong/article/details/43927909 中,主要讨论了使用sobel算子和拉普拉斯变换进行边缘检测。其中主要使用了了对梯度大小进行阈值化以得到二值的边缘图像的方法。在一幅图像中,边缘往往包含着重要的视觉信息,因为它们描绘出图像元素的轮廓。然而,仅仅使用简单的二值边缘图像有两大缺陷:

1. 使用这种方法检测得到的边缘过粗,这意味着难以实现物体的精确定位。

2. 难以找到这样的阀值,既能足够低检测到所有重要的边缘,同时也不至于包含过多次要的边缘。

这两个问题正是此节所使用的Canny算法所要尝试解决的。Canny算子是JohnCanny于1986年提出的,它与Marr(LoG)边缘检测方法类似,属于先平滑后求导数的方法。

Canny算子通常基于Sobel算子(当然,也可以使用其他的梯度算子)。一般来说,图像梯度幅值矩阵中的元素值越大,说明图像中该像素点的梯度值越大,但这并不能说明该点就是边缘。在Canny算法中,非极大值抑制是进行边缘检测的重要步骤,通俗意义上是指寻找像素点局部最大值,将非极大值点所对应的灰度值置为0,这样可以剔除掉一大部分非边缘的点。较高的亮度梯度比较有可能是边缘,但是没有一个确切的值来限定多大的亮度梯度是边缘多大又不是,所以 Canny使用了磁滞阀值化的策略:

1. 磁滞阀值化的原理是使用高与低两个阈值。假设图像中的重要边缘都是连续的曲线,这样就可以跟踪给定曲线中模糊的部分,并且避免将没有组成曲线的噪声像素当成边缘。所以使用一个较大的阈值,标识出比较确信的真实边缘,使用前面导出的方向信息,从这些真正的边缘开始在图像中跟踪整个的边缘。

2. 在跟踪的时候,我们使用一个较小的阈值,这样就可以跟踪曲线的模糊部分直到我们回到起点。这样根据两个阈值分别进行划分,得到两幅边缘图。

3. Canny算法组合两幅边缘图以生成一副“最优”的轮廓图。如果存在连续的边缘点,则将低阀值图像中的边缘点与高阀值图像中的边缘相连接,那么就保留低阀值图像中的边缘点。

Canny算法通常处理的图像为灰度图,因此若输入的是彩色图像,则需要进行灰度化。对于RGB图像,通常灰度化采用的方法主要有: 
1. Gray=(R+G+B)/3; 
2. Gray=0.299R+0.587G+0.114B;(这种参数考虑到了人眼的生理特点) 
注意,在程序设计时要考虑到图像格式中RGB的顺序通常为BGR。然而,opencv支持将输入图像直接转化为灰度图像,因此可以跳过这个步骤。

Canny算法对于两个阈值的选择有一定的要求。对于较低那个阈值,应该包括所有被认为是属于明显图像轮廓的边缘像素。而较高的阈值的角色应该是定义属于所有重要轮廓的边缘,它应该排除所有异常值。

在OpenCV中,实现Canny算法的函数是cv::Canny,首先需要知道的是,cv::Canny的两个阈值都需要用户亲自输入,且使用的边缘梯度是用sobel算子,该函数的调用方法如下:

cv::contours; 
cv::Canny(image,     // 输入的灰度图像
      contours,  // 输出轮廓
      125,       // 低阈值
      200);      // 高阈值

首先创建一个Qt控制台项目,并新建一个名为Canny算法的类,修改canny.h:

#ifndef CANNY_H
#define CANNY_H

#define PI 3.1415926

#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>

class Canny
{

private:
    cv::Mat img;   // 输入图像
    cv::Mat sobel;
    int aperture;  // 存放阈值
    cv::Mat sobelMagnitude;   // Sobel大小
    cv::Mat sobelOrientation; // Sobel方向

public:
    Canny():aperture(3){}
    // 设定阈值
    void setAperture(int a)
    {
        aperture = a;
    }

    // 获取阈值
    int getAperture() const;

    // 计算Sobel结果
    void computeSobel(const cv::Mat &image);

    // 获取幅度
    cv::Mat getMagnitude();

    // 获取Sobel方向
    cv::Mat getOrientation();

    // 输入门限获取二值图像
    cv::Mat getBinaryMap(double Threhhold);

    // 转化为CV_8U图像
    cv::Mat getSobelImage();

    // 获取梯度
    cv::Mat getSobelOrientationImage();
};

#endif // CANNY_H

接着,对各个函数进行定义,修改canny.cpp文件:

#include "canny.h"
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>

// 获取阈值
int Canny::getAperture() const
{
    return aperture;
}

// 计算Sobel结果
void Canny::computeSobel(const cv::Mat &image)
{
    cv::Mat sobelX;
    cv::Mat sobelY;
    cv::Sobel(image,sobelX,CV_32F,1,0,aperture);
    cv::Sobel(image,sobelY,CV_32F,0,1,aperture);
    cv::cartToPolar(sobelX,sobelY,sobelMagnitude,sobelOrientation);

}

// 获取幅度
cv::Mat Canny::getMagnitude()
{
    return sobelMagnitude;
}

// 获取Sobel方向
cv::Mat Canny::getOrientation()
{
    return sobelOrientation;
}

// 输入门限获取二值图像
cv::Mat Canny::getBinaryMap(double Threhhold)
{
    cv::Mat bgImage;
    threshold(sobelMagnitude,bgImage,Threhhold,255,cv::THRESH_BINARY_INV);
    return bgImage;
}

// 转化为CV_8U图像
cv::Mat Canny::getSobelImage()
{
    cv::Mat bgImage;
    double minval,maxval;
    cv::minMaxLoc(sobelMagnitude,&minval,&maxval);
    sobelMagnitude.convertTo(bgImage,CV_8U,255/maxval);
    return bgImage;
}
// 获取梯度
cv::Mat Canny::getSobelOrientationImage()
{
    cv::Mat bgImage;
    sobelOrientation.convertTo(bgImage,CV_8U,90/PI);
    return bgImage;
}

最后修改main函数:

#include <QCoreApplication>
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <QDebug>
#include "canny.h"

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);
    cv::Mat image = cv::imread("C:/peng.jpg",0);
    if(! image.data)
         qDebug() << "No input image";
    cv::imshow("Original Image",image);

    // 计算sobel
    Canny h;
    h.computeSobel(image);

    // 获取sobel的大小和方向
    cv::imshow("orientation of sobel",h.getSobelOrientationImage());
    cv::imshow("Magnitude of sobel",h.getSobelImage());

    // 使用两种阈值的检测结果
    cv::imshow("The Lower Threshold",h.getBinaryMap(125));
    cv::imshow("The Higher Threshold",h.getBinaryMap(225));

    // 使用canny算法
    cv::Mat contours;
    cv::Canny(image,contours,125,225);
    cv::Mat contoursInv;
    cv::threshold(contours,contoursInv,128,255,cv::THRESH_BINARY_INV);
    cv::imshow("Edge Image",contoursInv);

    return a.exec();
}

在输出结果中,可以观察到使用Sobel算子得出的方向和大小:

以及使用两个阈值得到的两个检测结果及融合两个结果所得出的最终结果:

当然,如果只是想直接实现Canny算法,无需建立Canny类,直接在main函数中添加以下内容即可:

#include <QCoreApplication>
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <QDebug>

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    cv::Mat image = cv::imread("c:/peng.jpg", 0);
    if(!image.data)
    {
        qDebug() << "No input image";
    }
    cv::Mat result;
    cv::Canny(image, result, 150, 220);
    cv::namedWindow("Original Image");
    cv::imshow("Original Image", image);
    cv::namedWindow("Canny Result");
    cv::imshow("Canny Result", result);

    return a.exec();
}

效果:

以下是Sobel算子进行边缘检测的输出效果:

对比Sobel算子,Canny算子能得到较薄的边缘,这是因为Canny算法采用了额外的策略来提升图像的质量。在使用磁滞阈值化之前,所有在梯度大小并非最大值的边缘点都被移除。这样一来梯度的朝向总是与边缘垂直,因此该方向的局部梯度最大值对应的是轮廓强度最大的点。Canny 算法适用于不同的场合。它的参数允许根据不同实现的特定要求进行调整以识别不同的边缘特性。

需要注意的是,使用Canny 算法检测轮廓时需要考虑一些可以调整的参数,它们将影响到算法的计算的时间与实效。

1. 高斯滤波器的大小:第一步所用的平滑滤波器将会直接影响 Canny 算法的结果。较小的滤波器产生的模糊效果也较少,这样就可以检测较小、变化明显的细线。较大的滤波器产生的模糊效果也较多,将较大的一块图像区域涂成一个特定点的颜色值。这样带来的结果就是对于检测较大、平滑的边缘更加有用,例如彩虹的边缘。

2. 阈值:使用两个阈值比使用一个阈值更加灵活,但是它还是有阈值存在的共性问题。设置的阈值过高,可能会漏掉重要信息;阈值过低,将会把枝节信息看得很重要。很难给出一个适用于所有图像的通用阈值。目前还没有一个经过验证的实现方法。

最近发现了一篇博客,深受启发:http://blog.csdn.net/likezhaobin/article/details/6892629 给出了一个无需设定阈值的Canny算法,其效果需要进一步论证。

文章导航