【数字图像处理】Canny边缘检测C语言实现

(一)边缘检测步骤

对于边缘检测我们通常需要完成的事情有:

1. 彩色图像转换为灰度图像

2. 对图像进行高斯模糊

3. 计算图像梯度,根据梯度计算图像边缘幅值与角度(这里其实用到了微分边缘检测算子来计算梯度幅值方向)

4. 非最大信号压制处理(边缘细化)

5. 双阈值边缘连接处理

6. 二值化图像输出结果

(二)图像的梯度与方向角计算

边缘检测的第一步就是计算图像的梯度与方向角。并在此基础上计算出每一个在X,Y方向梯度确定的情况下的幅值。
图像梯度是指图像某像素在x和y两个方向上的变化率(与相邻像素比较),是一个二维向量,由2个分量组成,X轴的变化、Y轴的变化 。
其中X轴的变化是指当前像素右侧(X加1)的像素值减去当前像素左侧(X减1)的像素值。
同理,Y轴的变化是当前像素下方(Y加1)的像素值减去当前像素上方(Y减1)的像素值。
计算出来这2个分量,形成一个二维向量,就得到了该像素的图像梯度。取反正切arctan,可得到梯度角度。
这个求图像梯度的过程可以通过一个卷积核来实现:以[-1,0,1]为例
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
图像梯度的绝对值为

在这里插入图片描述
图像梯度的角度为
在这里插入图片描述
在此基础上图像处理的专家们又研究出了多种可以计算梯度的算子。常见边缘检测算子:Roberts 、Sobel 、Prewitt、Laplacian、Log/Marr、Canny、Kirsch、Nevitia。这里我们以Roberts 、Sobel 、Prewitt来分别实现。图像的梯度运算。
Robert算子是第一个边缘检测算子,提出者Lawrence Roberts in 1963。
Sobel边缘算子,当年作者并没有公开发表过论文,仅仅是在一次博士生课题讨论会(1968)上提出(“A 3x3 Isotropic Gradient Operator for Image Processing”),后在1973年出版的一本专著(“Pattern Classification and Scene Analysis”)的脚注里作为注释出现和公开的。提出者Irwin Sobel。
Prewitt算子来自J.M.S. Prewitt “Object Enhancement and Extraction” in “Picture processing and Psychopictorics”, Academic Press,1970。
我们看这三种边缘检测算子模板及写成差分的形式:

在这里插入图片描述根据以上公式可以确定梯度与角度的计算方式,下面以sobel算子为例:
33 Sobel两个方向的算子在图像上滑动,模板与其覆盖的图像33区域9个像素进行卷积,求和后得到此方向的边缘检测幅值。

在这里插入图片描述

f(x,y)为图像,Gx和Gy分别是水平和竖直方向算子的卷积结果,G则是最终得到的边缘幅值,θ值则是边缘方向。
在这里插入图片描述
下面我们来这常见的几种卷积核进行比较(example/canny/canny.c):

(1)sobel算子

在这里插入图片描述

(2) Prewitt算子

在这里插入图片描述

(3) Robert算子

在这里插入图片描述

(4)三种算子效果比较

Roberts算子 对具有陡峭低噪声的图像处理效果很好,但是利用Roberts算子提取边缘的结果比较粗,因此对于边缘的定位不会非常准确。
Sobel 算子 对灰度渐变和噪声较多的图像处理效果比较好,Sobel算子对边缘定位比较准确。
Prewitt算子 对灰度渐变和噪声较多的图像处理效果较好
综上我们选择Sobel算子作为我们的梯度运算方法。

(三)非极大值抑制

canny算子中非最大抑制(Non-maximum suppression)是回答这样一个问题: “当前的梯度值在梯度方向上是一个局部最大值吗?” 所以,要把当前位置的梯度值与梯度方向上两侧的梯度值进行比较。
非极大值抑制可以帮助抑制除局部最大值之外的所有梯度值(通过将它们设置为0) ,使其指示具有最强烈的强度值变化的位置。以目标检测为例,目标检测的过程中在同一目标的位置上会产生大量的候选框,这些候选框相互之间可能会有重叠,此时我们需要利用非极大值抑制找到最佳的目标边界框,消除冗余的边界框。
在John Canny提出的Canny算子的论文中,非最大值抑制就只是在0、90、45、135四个梯度方向上进行的,每个像素点梯度方向按照相近程度用这四个方向来代替。这种情况下,非最大值抑制所比较的相邻两个像素就是:
1) 0:左边 和 右边
2)45:右上 和 左下
3)90: 上边 和 下边
4)135: 左上 和 右下
在这里插入图片描述

这样做的好处是简单, 但是这种简化的方法无法达到最好的效果, 因为,自然图像中的边缘梯度方向不一定是沿着这四个方向的。因此,就有很大的必要进行插值,找出在一个像素点上最能吻合其所在梯度方向的两侧的像素值。
然而,实际数字图像中的像素点是离散的二维矩阵,所以处在真正中心位置C处的梯度方向两侧的点是不一定存在的,或者说是一个亚像素(sub pixel)点,而这个不存在的点, 以及这个点的梯度值就必须通过对其两侧的点进行插值来得到。
步骤:

  1. 对角度在四个方向进行划分,形成新的角度图;
  2. 根据角度图(表征着梯度的方向),对幅值进行非极大值抑制。
    以3X3图像为例,按下图进行比较。
    在这里插入图片描述

非极大值抑制效果:
在这里插入图片描述
图表 13 非极大值抑制
可以看到大量的非边缘点被过滤掉了。

(四)图像双阈值化与边缘连接

对于一些图像有很强的分界特征,我们可以考虑用双阈值法进行二值化操作。
双阈值化思想:

  1. 首先要确定的是该方法同样也是对灰度图进行操作。
  2. 然后,我们预设两个特定的阈值量thresh1、thresh2,并且thresh1 < thresh2 。
  3. 阈值化的过程就是,将在 (thresh1,thresh2) 这个区间内的灰度值设置为maxVal,将其余部分设置为0
    双阈值化操作中的两个预设阈值量根据实际需要自行设置,需要说明的是,maxVal可以是某一个固定值(通常情况下8位无符号图像设置为最大灰度值255)。
    双阈值化类型如下式所示:
    在这里插入图片描述

在3.3.2中已经完成了很大一部分点的过滤,但是含有很多的弱边缘点就是说他们的幅值达不到要求,这个时候我们就需要想办法过滤掉这些幅值达不到要求的点。

步骤:
• 选取高阈值 T H 和 低 阈 值 T L , 比 率 为 2 : 1 或 3 : 1 。 ( 一 般 取 T H = 0.3 / 0.2 , T L = 0.1 T_H和低阈值T_L,比率为2:1或3:1。(一般取T_H=0.3/0.2,T_L=0.1 TH和低阈值TL,比率为2:1或3:1。(一般取TH=0.3/0.2,TL=0.1)
• 取出非极大值抑制后的图像中的最大梯度幅值,重新定义高低阈值。即: T H × M a x , T L × M a x T_H\times{Max},T_L\times{Max} TH×Max,TL×Max。(当然可以自己给定)
• 将 小 于 T L 的 点 抛 弃 , 赋 0 ; 将 大 于 T H 的 点 立 即 标 记 ( 这 些 点 就 是 边 缘 点 ) , 赋 1 。 将小于T_L的点抛弃,赋0;将大于T_H的点立即标记(这些点就是边缘点),赋1。 将小于TL的点抛弃,赋0;将大于TH的点立即标记(这些点就是边缘点),赋1。
• 将 大 于 T L , 小 于 T H 的 点 使 用 8 连 通 区 域 确 定 ( 即 : 只 有 与 T H 像 素 连 接 时 才 会 被 接 受 , 成 为 边 缘 点 , 赋 1 ) 将大于T_L,小于T_H的点使用8连通区域确定(即:只有与T_H像素连接时才会被接受,成为边缘点,赋1) 将大于TL,小于TH的点使用8连通区域确定(即:只有与TH像素连接时才会被接受,成为边缘点,赋1)
效果如下:
在这里插入图片描述
可以看到很多若边缘点被进一步的过滤掉。

(五)代码实现

li_canny.c

#ifndef LI_CANNY_C
#define LI_CANNY_C




#include "cv.h"
#include "li_image_proc.h"
#include <stdio.h>
#include <math.h>


/**
 * @name: Li_Canny
 * @msg:  参考文章 https://blog.csdn.net/HUSTER_Gy/article/details/102942452?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522160498444419724838560446%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fall.%2522%257D&request_id=160498444419724838560446&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~first_rank_v2~rank_v28-11-102942452.first_rank_ecpm_v3_pc_rank_v2&utm_term=canny%E8%BE%B9%E7%BC%98%E6%A3%80%E6%B5%8B%E7%AE%97%E6%B3%95c%E5%AE%9E%E7%8E%B0&spm=1018.2118.3001.4449
 *        图像砍尼检测
 * @param {Li_Image* img 原图像
 *         BYTE CannyType选择算子
 *         BYTE min      最大阈值
 *         BYTE max}     最小阈值
 * @return {*}
 */
LI_API
Li_Image* Li_Canny(Li_Image* img,BYTE CannyType,BYTE min,BYTE max)
{
    if(img==NULL||img->imgdepth!=LI_DEP_8U)return NULL;
    LILOG("CANNY");           
    Li_Image* out =Li_Copy_Image(img); 
    Li_Image* PP =Li_Copy_Image(img); 
    Li_Image* QQ =Li_Copy_Image(img);
    Li_Kernel* SX,*SY;
    double *P =(double*)malloc(sizeof(double)*img->width*img->height);/*x方向偏导*/
    double *Q =(double*)malloc(sizeof(double)*img->width*img->height); /*y方向偏导*/
    /*PI*-1/2* --  PI*1/2*/
    double *Threa =(double*)malloc(sizeof(double)*img->width*img->height);

    #ifdef DEBUG
    Li_Save_Image("before_minus.bmp",out);
    #endif 
    
    /*开始计算梯度与方向角*/
    switch (CannyType)
    {
    case LI_CANNY_MYDEFINE:
        {
            for(int i=0;i<img->height-1;i++)
                for(int j=0;j<img->width-1;j++)
                {
                    BYTE* ptr[4];
                    BYTE* ptr2;
                    /**
                     * 2  3
                     * 0  1
                    */
                    ptr[0]=img->at(img,j,i);
                    ptr[1]=img->at(img,j+1,i);
                    ptr[2]=img->at(img,j,i+1);
                    ptr[3]=img->at(img,j+1,i+1);
                    P[i*img->width+j]=(double)((double)*ptr[3]+*ptr[1]-*ptr[0]-*ptr[2])/2;
                    Q[i*img->width+j]=(double)((double)*ptr[0]+*ptr[1]-*ptr[2]-*ptr[3])/2;  
                    Threa[i*img->width+j]=atan(Q[i*img->width+j]/P[i*img->width+j]);
                    ptr2=out->at(out,j,i);
                    *ptr2=sqrt(P[i*img->width+j]*P[i*img->width+j]+Q[i*img->width+j]*Q[i*img->width+j]);
                }
        }
        break;
    
    case LI_CANNY_SOBEL:
        {
            for(int i=1;i<img->height-1;i++)
                for(int j=1;j<img->width-1;j++)
                {
                    BYTE* ptr[9];
                    BYTE* ptr2;
                    /**6  7  8
                     * 3  4  5
                     * 0  1  2
                     */
                    if(j-1>=0&&i-1>=0)
                    ptr[0]=(BYTE*)img->at(img,j-1,i-1);
                    if(j>=0&&i-1>=0)
                    ptr[1]=(BYTE*)img->at(img,j+0,i-1);
                    if(j+1<=img->width&&i-1>=0)
                    ptr[2]=(BYTE*)img->at(img,j+1,i-1);
                    if(j-1>=0&&i>=0)
                    ptr[3]=(BYTE*)img->at(img,j-1,i+0);
                    if(j>=0&&i>=0)
                    ptr[4]=(BYTE*)img->at(img,j+0,i+0);
                    if(j+1<=img->width&&i>=0)
                    ptr[5]=(BYTE*)img->at(img,j+1,i+0);
                    if(j-1>=0&&i+1<=img->height)
                    ptr[6]=(BYTE*)img->at(img,j-1,i+1);
                    if(j>=0&&i+1<=img->height)
                    ptr[7]=(BYTE*)img->at(img,j+0,i+1);
                    if(j+1<=img->width&&i+1<=img->height)
                    ptr[8]=(BYTE*)img->at(img,j+1,i+1);

                    P[i*img->width+j]=(double)((double)*ptr[2]+*ptr[5]*2+*ptr[8]-*ptr[0]-*ptr[3]*2-*ptr[6]);
                    Q[i*img->width+j]=(double)((double)*ptr[6]+*ptr[7]*2+*ptr[8]-*ptr[0]-*ptr[1]*2-*ptr[2]);    
                    Threa[i*img->width+j]=atan(Q[i*img->width+j]/P[i*img->width+j]);
                    ptr2=out->at(out,j,i);
                    *ptr2=sqrt(P[i*img->width+j]*P[i*img->width+j]+Q[i*img->width+j]*Q[i*img->width+j]);
                }
        }
        break;

        case LI_CANNY_ROBERTS:
         {
            for(int i=0;i<img->height-1;i++)
                for(int j=0;j<img->width-1;j++)
                {
                    BYTE* ptr[4];
                    BYTE* ptr2;
                    /**
                     * 2  3
                     * 0  1
                    */
                    ptr[0]=img->at(img,j,i);
                    ptr[1]=img->at(img,j+1,i);
                    ptr[2]=img->at(img,j,i+1);
                    ptr[3]=img->at(img,j+1,i+1);
                    ptr2=out->at(out,j,i);
                    *ptr2=abs(*ptr[2]-*ptr[1])+abs(*ptr[0]-*ptr[3]);
                    Threa[i*img->width+j]=atan(abs(*ptr[2]-*ptr[1])/abs(*ptr[0]-*ptr[3]));
                }         
         }
        break;

    case LI_CANNY_PREWITT:
          {
            for(int i=1;i<img->height-1;i++)
                for(int j=1;j<img->width-1;j++)
                {
                    BYTE* ptr[9];
                    BYTE* ptr2;
                    /**6  7  8
                     * 3  4  5
                     * 0  1  2
                     */
                    if(j-1>=0&&i-1>=0)
                    ptr[0]=(BYTE*)img->at(img,j-1,i-1);
                    if(j>=0&&i-1>=0)
                    ptr[1]=(BYTE*)img->at(img,j+0,i-1);
                    if(j+1<=img->width&&i-1>=0)
                    ptr[2]=(BYTE*)img->at(img,j+1,i-1);
                    if(j-1>=0&&i>=0)
                    ptr[3]=(BYTE*)img->at(img,j-1,i+0);
                    if(j>=0&&i>=0)
                    ptr[4]=(BYTE*)img->at(img,j+0,i+0);
                    if(j+1<=img->width&&i>=0)
                    ptr[5]=(BYTE*)img->at(img,j+1,i+0);
                    if(j-1>=0&&i+1<=img->height)
                    ptr[6]=(BYTE*)img->at(img,j-1,i+1);
                    if(j>=0&&i+1<=img->height)
                    ptr[7]=(BYTE*)img->at(img,j+0,i+1);
                    if(j+1<=img->width&&i+1<=img->height)
                    ptr[8]=(BYTE*)img->at(img,j+1,i+1);

                    P[i*img->width+j]=(double)((double)*ptr[2]+*ptr[5]+*ptr[8]-*ptr[0]-*ptr[3]-*ptr[6]);
                    Q[i*img->width+j]=(double)((double)*ptr[6]+*ptr[7]+*ptr[8]-*ptr[0]-*ptr[1]-*ptr[2]); 
                    Threa[i*img->width+j]=atan(Q[i*img->width+j]/P[i*img->width+j]);   
                    ptr2=out->at(out,j,i);
                    *ptr2=sqrt(P[i*img->width+j]*P[i*img->width+j]+Q[i*img->width+j]*Q[i*img->width+j]);
                }
        }      

        break;
    
    default:
        break;
    }

    #ifdef DEBUG
    Li_Save_Image("after_minus.bmp",out);
    #endif 

    /*非极大值抑制*/
    for(int j=1;j<out->height-1;j++)
        for(int i=1;i<out->width-1;i++)
        {
            double t=Threa[j*img->width+i];
            BYTE* ptr=out->at(out,i,j);
            double g=(double) *ptr;
            double g0, g1;
            if ((t >= -(3*M_PI/8)) && (t < -(M_PI/8)))
            {
                ptr=out->at(out,i-1,j-1);
                 g0=(double) *ptr;
                ptr=out->at(out,i+1,j+1);
                 g1=(double) *ptr;
            } 
            else if ((t >= -(M_PI/8)) && (t < M_PI/8))
            {
                ptr=out->at(out,i-1,j);
                 g0=(double) *ptr;
                ptr=out->at(out,i+1,j);
                 g1=(double) *ptr;
            }
            else if ((t >= M_PI/8) && (t < 3*M_PI/8))
            {
                ptr=out->at(out,i+1,j-1);
                 g0=(double) *ptr;
                ptr=out->at(out,i-1,j+1);
                 g1=(double) *ptr;
            }
            else
            {
                ptr=out->at(out,i,j-1);
                 g0=(double) *ptr;
                ptr=out->at(out,i,j+1);
                 g1=(double) *ptr;                
            }
        
            if (g <= g0 || g <= g1) {
                ptr=out->at(out,i,j);
                *ptr=0;
            }

        }
 
    /*阈值化操作*/
    #ifdef DEBUG
    Li_Save_Image("before_thre.bmp",out);
    #endif 
    Li_Image*out1=Li_Double_Threshold(out,min,max);
    #ifdef DEBUG
    Li_Save_Image("after_thre.bmp",out1);
    #endif 

    /*边缘链接*/
        for (int j = 1; j < out1->height-2; j++) 
        for (int i = 1; i < out1->width-2; i++) {
            BYTE* ptr=out1->at(out1,i,j);
            if(*ptr==255)
            {
                for (int m = -1; m < 1; m++) {
                    for (int n = -1; n < 1; n++) {
                    BYTE* temp=out1->at(out1,i+n,j+m);
                        if(*ptr!=0&&*ptr!=255)
                        *ptr=255;
                    }
                }             
            }
        }

        for (int j = 0; j < out1->height-1; j++) {
        for (int i = 0; i < out1->width-1; i++) {
            // 如果该点依旧是弱边缘点,及此点是孤立边缘点
             BYTE* ptr=out1->at(out1,i,j);
             if(*ptr!=255&&*ptr!=0)
             *ptr=0;
            }
        }
    Li_Destroy_Image(PP);
    Li_Destroy_Image(QQ);
    return out1;
}

#endif // !LI_CANNY_C

main.c

/*
 * @Descripttion: 
 * @version: 
 * @Author: Yueyang
 * @email: 1700695611@qq.com
 * @Date: 2020-10-26 19:35:49
 * @LastEditors: Yueyang
 * @LastEditTime: 2020-11-12 10:01:13
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <malloc.h>
#include "bmp.h"
#include "cv.h"
#include "li_image.h"
#include "li_painter.h"
#include "li_image_proc.h"

int main()
{
     BYTE* ptr=NULL;
     Li_Image* out =Li_Load_Image("./picture/panal (1).jpg",LI_JPEG);
     Li_Image* bmp=Li_Convert_Image(out,LI_JPEG_2_BMP);
     Li_Image* gray=Li_Convert_Image(bmp,LI_BMP_888_2_LI_BMP_8);

     Li_Image* smooth=Li_Smooth(gray,Li_GAUSS);
     Li_Image* img= Li_Canny(smooth,LI_CANNY_PREWITT,30,150);
     Li_Save_Image("canny_mydefine.bmp",img);

     LILOG("over");
     return 0; 
}

 

(六)写在后面

因为LiteCV项目才刚刚写了一个开头,代码中有错误的地方还望指出。我已经将项目同步到了github,我会实时更新这个代码仓库。
项目github地址:
LiteCV

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

与光同程

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

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

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

打赏作者

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

抵扣说明:

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

余额充值