对于边缘检测我们通常需要完成的事情有:
边缘检测的第一步就是计算图像的梯度与方向角。并在此基础上计算出每一个在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):
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)点,而这个不存在的点, 以及这个点的梯度值就必须通过对其两侧的点进行插值来得到。
步骤:
非极大值抑制效果:
图表 13 非极大值抑制
可以看到大量的非边缘点被过滤掉了。
对于一些图像有很强的分界特征,我们可以考虑用双阈值法进行二值化操作。
双阈值化思想:
在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)
效果如下:
可以看到很多若边缘点被进一步的过滤掉。
#ifndef LI_CANNY_C
#define LI_CANNY_C
#include "cv.h"
#include "li_image_proc.h"
#include
#include
/**
* @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
/*
* @Descripttion:
* @version:
* @Author: Yueyang
* @email: [email protected]
* @Date: 2020-10-26 19:35:49
* @LastEditors: Yueyang
* @LastEditTime: 2020-11-12 10:01:13
*/
#include
#include
#include
#include
#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