OpenCv入门(五)——改进边缘算法Canny

原理:通过图像信号函数的极大值来判定图像的边缘像素点。

最优边缘检测主要以下面三个参数为评判标准:

低错误率:标识出尽可能多的实际边缘以及减少噪声产生。

高定位性:表示出边缘要与图像中的实际边缘尽可能接近。

最小响应:图像的边缘标记具有唯一性,虚假响应边缘应该得到最大抑制。

目录

(一)Canny的原理

(二)Canny库函数的实现

(三)再次解释


(一)Canny的原理

检测步骤:

  • 消除噪声

边缘检测的算法主要是基于图像强度的一阶和二阶微分操作,但导数通常对噪声很敏感,边缘检测算法常常需要根据图像源的数据进行预处理操作,因此必须采用滤波器来改善与噪声有关的边缘检测的性能。

这个预处理就是操作,就是在进行Canny算子边缘检测前,应当先对原始数据进行高斯模板进行卷积操作,可得到的图像与原始图像相比有些轻微的模糊

通常使用高斯平滑滤波器卷积降噪:

 

 消除噪声的代码如下:

CV::GausssianBlur(src,image,Size(3,3),1.5);
  • 计算幅度与方向

梯度可以使用一阶有限差分,也就是上一篇文章讲述过的图像在x和y方向上偏导数的两个矩阵,用一对卷积阵列分布作用于对应的水平与垂直方向。Canny算子中使用的sobel模板如下:

Gx为水平x方向掩码模板,Gy为垂直y方向掩码模板,K为邻域标记矩阵。

那么可以根据上面三个矩阵,可计算出图像梯度幅值以及相应的方向:

那么梯度的方向近似到四个可能的角度之一,一般取值为0,45,90,135。分别取其atan系数,就可以遍历图像了。

那么计算梯度幅值与方向如代码如下所示:

//使用Sobel计算相应的梯度幅值及方向
cv::Mat magX = cv::Mat(src.rows,src.cols,CV_32F);
cv::Mat magY = cv::Mat(src.rows,src.cols,CV_32F);
cv::Sobel(image,magX,CV_32F,1,0,3);
cv::Sobel(image,magY,CV_32F,0,1,3);
//计算斜率
cv::Mat slopes = cv::Mat(image.rows,image.cols,CV_32F);
divide(magY,magX,slopes);
//计算每个点的梯度
cv::Mat sum = cv::Mat(image.rows,image.cols,CV_64F);
cv::Mat prodX = cv::Mat(image.rows,image.cols,CV_64F);
cv::Mat prodY = cv::Mat(image.rows,image.cols,CV_64F);
//平方
multiply(magX,magY,prodX);
multiply(magY,magY,prodY);
sum = prodX + prodY;
//开平方,求出G
sqrt(sum,sum);
cv::Mat magnitude = sum.clone();

  • 非极大值抑制

在上一篇边缘检测的文章中是有做介绍的,在Canny算法中,非极大值抑制是进行边缘检测的重要步骤。

非极大值抑制通俗的讲是这样的:

寻找像素点局部的最大值,将非极大值点所对应的灰度值设置为背景像素点,像素领域区域满足梯度值的局部最优值判断为该像素的边缘,对其余非极大值的相关信息进行抑制,利用该准则可以剔除大部分非边缘点。

那么利用程序可以这么写:

//非极大值抑制
void nonMaximumSuppression(Mat &magnitudeImage, 
  Mat &directionImage) 
{ 
    Mat checkImage = Mat(magnitudeImage.rows, 
        magnitudeImage.cols, CV_8U);  
    // 迭代器初始化
    MatIterator_<float>itMag = magnitudeImage.begin<float>();
    MatIterator_<float>itDirection = 
         directionImage.begin <float>();  
    MatIterator_<unsigned char>itRet = 
         checkImage.begin<unsigned char>();  
    MatIterator_<float>itEnd = magnitudeImage.end<float>(); 
    // 计算对应方向
    for (; itMag != itEnd; ++itDirection, ++itRet, ++itMag) 
    {
        // 将方向进行划分, 对每个方向进行幅值判断
        const Point pos = itRet.pos();  
        // 将所需要达到的角度转为弧度(坐标)
        float currentDirection = atan(*itDirection) *
         (180 / 3.142);  
        //防止溢出
        while(currentDirection < 0) 
            currentDirection += 180;
        *itDirection = currentDirection;
        // 边界限定,对相应方向进行判断
        if (currentDirection>22.5&&currentDirection <= 67.5) 
        {
            // 邻域位置极值判断
            if(pos.y > 0 && pos.x > 0 && *itMag <=
               magnitudeImage.at<float>(pos.y - 1, pos.x - 1))
            {
                magnitudeImage.at<float>(pos.y, pos.x) = 0;
            }
            if(pos.y < magnitudeImage.rows-1 && pos.x <
             magnitudeImage.cols-1 && *itMag <=
              magnitudeImage.at<float>(pos.y + 1, pos.x + 1))
            {
                magnitudeImage.at<float>(pos.y, pos.x) = 0;
            }
        }else if(currentDirection>67.5&&currentDirection<= 112.5)
        {
             // 邻域位置极值判断
            if(pos.y > 0 && *itMag <= 
              magnitudeImage.at<float>(pos.y-1, pos.x)) 
            {
                magnitudeImage.at<float>(pos.y, pos.x) = 0;
            }
            if(pos.y<magnitudeImage.rows-1 && 
              *itMag<=magnitudeImage.at<float>(pos.y+1, pos.x)) 
            {
                magnitudeImage.at<float>(pos.y, pos.x) = 0;
            }
        }else if(currentDirection>112.5&&currentDirection<=157.5)
         {
             // 邻域位置极值判断
            if(pos.y>0 && pos.x<magnitudeImage.cols-1 && 
             *itMag<=magnitudeImage.at<float>(pos.y-1, 
              pos.x+1)) {
                magnitudeImage.at<float>(pos.y, pos.x) = 0;;
            }
            if(pos.y < magnitudeImage.rows-1 && pos.x>0 && 
             *itMag<=magnitudeImage.at<float>(pos.y+1, 
             pos.x-1)) {
                magnitudeImage.at<float>(pos.y, pos.x) = 0;
            }
        }else 
        {
             // 邻域位置极值判断
            if(pos.x > 0 && *itMag <= 
             magnitudeImage.at<float>(pos.y, pos.x-1)) 
            {
                magnitudeImage.at<float>(pos.y, pos.x) = 0;
            }
            if(pos.x < magnitudeImage.cols-1 && *itMag <= 
              magnitudeImage.at<float>(pos.y, pos.x+1)) 
            {
                magnitudeImage.at<float>(pos.y, pos.x) = 0;
            }
        }       
    }    
}

  • 用滞后阈值算法求解图像边缘。

上面取得的边缘中存在伪边缘,是由于我们只使用了一个阈值,所以canny减少伪边缘数量的方法是采用滞后阈值法。Canny使用了滞后阈值,意思就是你要输出高阈值和低阈值,那么怎么用?

(1)如果某一像素位置的幅值超过高阈值,那么该像素保留为边缘像素。

(2)如果某一像素位置的幅值小于低阈值,那么该像素直接被排除。

(3)如果某一像素位置的幅值在两个阈值之间,该像素仅仅在一个连接到一个高于高阈值的像素时被保留。

Canny算法的双阈值中,高阈值检测出的而图像去除了大部分的噪声,但是也会损失了有用的边缘信息,低阈值检测得到的图像则保留着较多的边缘信息,推荐的高与低阈值比在2:1到3:1之间

// 边缘连接
void followEdges(int x, int y, Mat &magnitude, int tUpper,
 int tLower, Mat &edges) 
{   
    edges.at<float>(y, x) = 255;   
    for (int i = -1; i < 2; i++) {
        for (int j = -1; j < 2; j++) {
            // 边界限制
            if((i != 0) && (j != 0) && (x + i >= 0) &&
             (y + j >= 0) && (x + i <= magnitude.cols) &&
              (y + j <= magnitude.rows))
            {
                // 梯度幅值边缘判断及连接
                if((magnitude.at<float>(y + j, x + i) > tLower) 
                  && (edges.at<float>(y + j, x + i) !=255))
                 {
                    followEdges(x + i, y + j, magnitude,
                      tUpper, tLower, edges);
                 }
            }
        }
    }
}
// 边缘检测
void edgeDetect(Mat &magnitude, int tUpper, int tLower, 
 Mat &edges) 
{    
    int rows = magnitude.rows;
    int cols = magnitude.cols;    
    edges = Mat(magnitude.size(), CV_32F, 0.0);    
    for (int x = 0; x < cols; x++) 
    {
        for (int y = 0; y < rows; y++) 
        {
            // 梯度幅值判断
            if (magnitude.at<float>(y, x) >= tUpper)
            {
                followEdges(x, y, magnitude, tUpper, 
                 tLower, edges);
            }
        }
    }   
}

通过消除噪声,计算梯度幅度与方向、非极大值抑制及用滞后阈值算法求解图像边缘四个步骤就可以实现Canny边缘检测。

(二)Canny库函数的实现

void Canny(
			InputArray image,
			OutputArray edeges,
			double threshold1,
			double threshold2,
			int apertureSize=3,		//索贝尔操作的尺寸因子
			bool L2gradient = fasle	//标志位
);

这个调用的就挺简单的了,那就不再往下说了。

(三)再次解释

可能看到这里还是对canny的实现有点懵,但是步骤是明了的,那么学一个东西就要完全搞懂的是吧,那么就再继续往下讲。

在之前的图像处理中,我们从一维函数的跃变检测开始,循序渐进的对二维图像边缘检测的基本原理进行了通俗化的描述。那么实现图像的边缘检测,就是要用离散化梯度逼近函数根据二维灰度矩阵梯度向量来寻找图像灰度矩阵的灰度跃变位置,然后再图像中将这些位置的点连起来就构成了所谓的图像边缘。这个边缘包括什么呢,包括了二维图像上的 边缘、角点、纹理等基元图。

在实际情况中,理想的灰度阶跃及其线条边缘图像时很少见到的,同时大多数的传感器具有低频滤波特性,这样会使得阶跃边缘变为斜坡边缘,所以这种强度的变换,它并不是一个瞬间的变化,它是有距离性的。那么我们要使用滤波来解决这个问题。

具体步骤:

(1)转灰度

当我们获取了一个RGB图像,那么我们首先应该将其转为灰度图像:

方法一:Gray = (R+G+B)/3

方法二:Gray = 0.299R + 0.587G + 0.114B

(2)进行高斯滤波

求二维高斯函数,确定参数就可以得到二维核向量。需要注意的是,当我们所确定的高斯核参数越大,那么他就会越模糊,在求高斯核后,我们要对整个核进行归一化处理。

对图像进行滤波,其实就是根据待滤波的像素点及其领域点的灰度值按照一定的参数规则进行加权平均。这样就可以有效的滤掉图像种叠加的高频噪声。

可能有人会觉得滤波跟边缘检测是处于一个矛盾的概念,抑制了噪声也会使图像更加模糊,这确实是会增加边缘定位的不准确。那么你要是想提高灵敏度,那么噪点也会提高灵敏度,这确实是一个难以取舍的东西,但是该滤波还是得滤,高斯函数确定的核是可以在抗噪声干扰核和边缘检测精确定位之间提供较好的方案。

(3)用一阶偏导的有限差分来计算梯度的幅值和方向

一阶偏导的处理其实有很多种,诸如上篇文章中所提及到的Roberts、Sobel、Prewitt...

为什么要计算这个梯度的幅值和方向,其实就是在使用算子这个过滤器来确定图像的边缘强度和方向。

Gx是指梯度在x方向上的突变,也就是垂直边缘。

Gy是指梯度在y方向上的突变,也就是水平边缘。

 那么我们就可以得到幅值以及方向:

那么我们就可以根据我们算出来的幅值以及梯度方向的角度大概的位置,我们正常使用0,45,90,135度进行比较我们算出来的角度值,若是在某个规定区间内,那我们就可以确定现在的方向是哪个方向,确定方向后就可以进行领域内比较的方式:

梯度方向为水平:在3*3领域内左右方向比较

梯度方向为垂直:在3*3领域内上下方向比较

梯度方向为45°:在3*3领域内左上右下方向比较

梯度方向为135°:在3*3领域内左下右上方向比较

(4)非极大值抑制

对sobel输出使用非极大值抑制来观察每个检测边缘的强度和方向,选出局部最大像素,从而把最强边缘绘制成连续的、一个像素宽的细线。从上面的计算我们是可以得到这个结论:

Canny算子中的非极大值抑制是沿着梯度方向进行的,即是否为梯度方向上的极值点。

那么跟普通的非极大值抑制有什么区别?普通的非极大值抑制只是检测中心点的值是否为某个领域内的最大值,如果是,那么就保留,如果不是,那么就把它去除,这种情况下的非极大值比较简单。

那么canny所回答的,是当前的梯度值在梯度方向上是否是一个局部的最大值吗?所以我们才需要使用这个点,跟梯度方向上的两侧的梯度值进行比较。

用图来解答关于canny的非极大值抑制:

 

要进行非极大值抑制,那么我们就要首先确定像素点C的灰度值是否在其8值领域内是否为最大。其中的蓝色线,也就是c的梯度方向,梯度方向的交点dTmp1和dTmp2这两个点的值也可能会是局部最大值。那么,我们需要判断c点的灰度与这两个点灰度大小即可判断c点是否为其领域内的局部最大灰度点。如果经过判断,c点的灰度值小于这两个点中的任何一个,那么就说明C点不是局部极大值,那么我就可以排除c点,他就是个伪边缘。

那么回到如何求这么两个点,其实我们使用类似线性插值的数学方式,求出对应的方向,对方向进行匹配区间,就可以知道我们到底是在哪个梯度方向上进行寻找。

(5)用滞后阈值来分离最佳边缘

双阈值化的操作,设定一个高阈值,便于这些强边缘通过,再设定一个低阈值,任何低于该阈值的边缘即为弱边缘,那么就被舍弃。那么保留的是什么呢?位于高低阈值之间的边缘只有当其与另一个强边缘相连时才会得到保留。高阈值和低阈值之间的比率简以为 2:1、3:1。

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

那么以上,就是Canny算法的再次分析。

  • 4
    点赞
  • 41
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

郑烯烃快去学习

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值