引入
本人在学习Canny边缘算法的过程中,发现网上对Canny算法的介绍和开源还是挺多的,但大多基于Python,面向嵌入式、基于C语言的Canny算法的介绍有但不多。
本篇文章主要通过代码实现外加部分注释的形式,来分享一下我对Canny边缘算法的认识和总结。
声明:
本篇文章分享的代码均为伪代码,如果想获取完整的算法工程文件,可下载与本文章绑定的资源。
代码实现
简单介绍
常用的Canny边缘算法里包含了四个部分:高斯模糊、Sobel(Scharr)边缘算子、非极大值抑制(NMS)、双阈值法+边缘链接。
高斯模糊
int *Gaussain_Blur(int *image) {
static int Blur_image[MT9V03X_H][MT9V03X_W]; // 高斯模糊图像
int *map;
float Gaussain_weight_matrix[9] = { 0.0947416, 0.118318, 0.0947416, 0.118318, 0.147761, 0.118318, 0.0947416, 0.118318, 0.0947416 };
int temp_fix[9];
map = image; // 获取原灰度图
for (int j = 0; j < MT9V03X_W ; ++j) { // 第一行不做处理
Blur_image[0][j] = *(map);
map++;
}
for (int i = 1; i < MT9V03X_H - 1; ++i) {
Blur_image[i][0] = *(map); // 第一列不做处理
map++;
//高斯模糊处理
for (int j = 1; j < MT9V03X_W - 1; ++j) {
temp_fix[0] = *(map - MT9V03X_W - 1);
temp_fix[1] = *(map - MT9V03X_W );
temp_fix[2] = *(map - MT9V03X_W + 1);
temp_fix[3] = *(map - 1);
temp_fix[4] = *(map);
temp_fix[5] = *(map + 1);
temp_fix[6] = *(map + MT9V03X_W - 1);
temp_fix[7] = *(map + MT9V03X_W );
temp_fix[8] = *(map + MT9V03X_W + 1);
float fix_sum = 0;
for (int k = 0; k < 9; ++k) {//权重分配
fix_sum += (float)temp_fix[k] * Gaussain_weight_matrix[k];
}
Blur_image[i][j] = (int)fix_sum;//直接灰度值赋值,要用一个int(强制类型转化)来表示0-255之间的整数值
map++;
}
Blur_image[i][MT9V03X_W - 1] = *(map); // 最后一列不做处理
map++;
}
for (int j = 0; j < MT9V03X_W ; ++j) { // 最后一行不做处理
Blur_image[MT9V03X_H - 1][j] = *(map);
map++;
}
return *Blur_image;
}
Gaussain_weight_matrix对应的是高斯模糊算法的权重矩阵,当前这个是常用的权重矩阵,网上也有其他的权重矩阵,读者可自行查找使用。代码中的MT9V03X_H和MT9V03X_W分别表示的是摄像头拍摄到图像的高和宽,读者使用时改成自己摄像头所拍摄图像对应的高和宽就行,之后不再重复解释。
Sobel算子
int *Sobel_edge(int *image1){
static int Sobel_image[MT9V03X_H][MT9V03X_W]; //sobel边沿提取图像
int Gx,Gy,G=0;
int sobel_fix[9];
int *map;
map = image1;
for(int j=0;j<MT9V03X_W;j++){ //第一行不处理
Sobel_image[0][j]=*(map);
map++;
}
for(int i=1;i<MT9V03X_H-1;i++){
Sobel_image[i][0]=*(map); //第一列不处理
map++;
for(int j=1;j<MT9V03X_W-1;j++){
sobel_fix[0] = *(map - MT9V03X_W - 1);
sobel_fix[1] = *(map - MT9V03X_W);
sobel_fix[2] = *(map - MT9V03X_W + 1);
sobel_fix[3] = *(map - 1);
sobel_fix[4] = *(map);
sobel_fix[5] = *(map + 1);
sobel_fix[6] = *(map + MT9V03X_W - 1);
sobel_fix[7] = *(map + MT9V03X_W);
sobel_fix[8] = *(map + MT9V03X_W + 1);
Gx = abs(sobel_fix[2]-sobel_fix[0]+2*sobel_fix[5]-2*sobel_fix[3]+sobel_fix[8]-sobel_fix[6]);
Gx = Gx>255?255:Gx;//限幅
Gy = abs(sobel_fix[0]-sobel_fix[6]+2*sobel_fix[1]-2*sobel_fix[7]+sobel_fix[2]-sobel_fix[8]);
Gy = Gy>255?255:Gy;//限幅
G = (int)InvSqrt(Gx*Gx+Gy*Gy);//用到了卡马尔开方InvSqrt
Sobel_image[i][j]=G;
map++;
}
Sobel_image[i][MT9V03X_W-1]=*(map); //最后一列不处理
map++;
}
for(int j=0;j<MT9V03X_W;j++){
Sobel_image[MT9V03X_H-1][j]=*(map);
map++;
}
return *Sobel_image;
}
代码中Gx表示图像九宫格中X方向上的梯度,Gy表示图像九宫格中Y方向上的梯度,G表示X方向和Y方向梯度的融合。Gx和Gy的求值运算要对应Sobel算子的权重矩阵,G的求值运用到了快速高效算法卡马尔开方,读者也可以直接用math.h库里的sqrt函数,但据说这个函数算起来比较慢。
想深入了解Sobel算子可以参考这篇文章:Sobel算子及C++实现
Scharr算子可以看做是Sobel算子的改进版,算法逻辑基本是一样的,不同的就是在求Gx和Gy时用到了权重算子不一样,具体可以参考一下这篇文章:scharr算子函数及其使用
非极大值抑制(NMS)
在进行非极大值抑制时,需要先求出每一个像素点的方向梯度,及Gy/Gx,然后规定4个方向,分别为竖直方向、水平方向、左上斜向右下方向、左下斜向右上方向。确定好每个像素点的方向梯度之后,就可以对图像进行非极大值抑制操作。
void NMS(){
int loss=0;//非极大值抑制补偿,根据实际效果来确定用不用,可手动调,初始设置为0;
for(int i=1;i<MT9V03X_H-1;i++){
for(int j=1;j<MT9V03X_W-1;j++){
switch(dir[i][j])
{
case 0://水平方向
{
if( (Scharr_image[i][j]>Scharr_image[i][j+1]-loss) && (Scharr_image[i][j]>Scharr_image[i][j-1]-loss) )
nms[i][j]=Scharr_image[i][j]; //这里默认在scharr图像的基础上进行非极大值抑制,如果要修改的话切记这行以及下面的scharr都要修改
else nms[i][j]=0;
break;
}
case 1://左下右上
{
if( (Scharr_image[i][j]>Scharr_image[i+1][j-1]-loss) && (Scharr_image[i][j]>Scharr_image[i-1][j+1]-loss) )
nms[i][j]=Scharr_image[i][j];
else nms[i][j]=0;
break;
}
case 2://左上右下
{
if( (Scharr_image[i][j]>Scharr_image[i-1][j-1]-loss) && (Scharr_image[i][j]>Scharr_image[i+1][j+1]-loss) )
nms[i][j]=Scharr_image[i][j];
else nms[i][j]=0;
break;
}
case 3://竖直方向
{
if( (Scharr_image[i][j]>Scharr_image[i+1][j]-loss) && (Scharr_image[i][j]>Scharr_image[i-1][j]-loss) )
nms[i][j]=Scharr_image[i][j];
else nms[i][j]=0;
break;
}
default:
break;
}
}
}
}
双阈值法+边缘链接
void TwoThreshold(){
int Link_fix[8];//当前点不算,只读取八邻域的值
static uint16 lowThr,highThr;
lowThr=LOW_THRESHOLD;//低阈值设定
highThr=HIGH_THRESHOLD;//高阈值设定
for(int i=1;i<MT9V03X_H-1;i++){
for(int j=1;j<MT9V03X_W-1;j++){
if(nms[i][j]<lowThr) link[i][j]=0;
else if(nms[i][j]>highThr) link[i][j]=255;
else{
Link_fix[0]=nms[i-1][j-1];
Link_fix[1]=nms[i-1][j];
Link_fix[2]=nms[i-1][j+1];
Link_fix[3]=nms[i][j-1];
Link_fix[4]=nms[i][j+1];
Link_fix[5]=nms[i+1][j-1];
Link_fix[6]=nms[i+1][j];
Link_fix[7]=nms[i+1][j+1];
quick_sort(Link_fix,0,8);//快速排序算法,对八领域的灰度值进行排序;7最大,0最小
if(Link_fix[4]>highThr) link[i][j]=255;//满足条件,链接
else link[i][j]=0;//不满足条件,灰度值置为0(黑)
}
}
}
}
在这里,双阈值的两个阈值是我根据实际情况来设定的,调试起来不会特别麻烦,当然这里可以改成自适应的双阈值,这个就由读者根据实际情况自行修改了。在边缘链接算法中,我运用了快速排序算法quick_sort,读者也可以自己写个冒泡排序算法或者其他排序算法。
写在最后
以上代码主要是给读者写Canny算法时提供一些思路,如果读者想直接复制粘贴就能用的话肯定是不行的。另外,推荐读者在写比较模块化的函数时,能像高斯模糊或者Sobel算子函数那样,尽量不引入全局变量,最好要有输入参数有返回参数,这样代码的可移植性会好很多,尽量不要像上面NMS函数和边缘链接函数那样,不太好移植(个人建议)。不过我这里主要是提供思路,所以就不做另外的改进了,还望读者谅解。