Canny算子边缘检测原理讲解及其完整C语言实现(不使用opencv)

写在前面

  • 作者:队友调车我吹空调
  • 日期:2023/05/17
  • 版权:遵循CC 4.0 BY-SA版权协议

这里是后期的笔者,本文算是笔者的学习笔记,主要是在单片机中使用的,再者就是由于某些原因,笔者不想使用opencv,因此尝试跟着原理手搓了这份代码,笔者也尽力将代码写到最简和效率最优了。然而即使如此,Canny算子运算量依然非常大。高斯滤波的时候遍历了一次图像,计算梯度的时候遍历了四次,非极大抑制的时候遍历了一次。双阈值和滞后边界跟踪总共遍历一次。也就是说整套算法下来一共遍历了图像7次,完成跑下来爆内存,笔者所使用的TC387都险些跑不下来。

所以在最开始,首先要对图像做一些处理。

在进行高斯滤波器前,将图像裁剪到合适的大小,在这里笔者比较推荐30*80的大小。笔者在TC387上使用单独一个核是可以较快运行的。如果对细节要求不高的同学可以再降采样一次。

笔者在这里给大家提供一个图像裁剪的函数。

/*************************************************************************
*  函数名称:cropImage
*  功能说明:将大小为[MT9V03X_H][MT9V03X_W]的图像裁剪到[PICTURE_H][PICTURE_W]
*  参数说明:picture_in[MT9V03X_H][MT9V03X_W] 输入图像
*  函数返回:void
*  修改时间:2023/05/17
*  备一一注:
*************************************************************************/
void cropImage(const uint8_t picture_in[MT9V03X_H][MT9V03X_W], uint8_t picture_out[PICTURE_H][PICTURE_W])
{
    int start_row = (MT9V03X_H - PICTURE_H) / 2;
    int start_col = (MT9V03X_W - PICTURE_W) / 2;

    for (int i = 0; i < PICTURE_H; i++) {
        for (int j = 0; j < PICTURE_W; j++) {
            picture_out[i][j] = picture_in[start_row + i][start_col + j];
        }
    }
}

笔者在正是内容开始前,先谈一谈自己对Canny算子的理解。其实认真看了我下边的内容的话,是能够感觉到Canny算子的思想非常简单的,先高斯滤波将图像细节处理一下,接下来计算梯度幅值、梯度方向,梯度变化最剧烈的地方就是边界;找到边界以后,对边界滤一下波,除去最不可能是边界的地方,标出最可能是边界的地方,再用边界跟踪算法处理有一点可能是边界的地方。

Canny算子的核心思想可以看成是使用了两次Sobel算子,但是效果和计算量与Sobel算子都是不可同日而语的。

概览

Canny边缘检测是一种非常流行的边缘检测算法,是John Canny在1986年提出的。它是一个多阶段的算法,即由多个步骤构成。本文主要讲解了Canny算子的原理及实现过程。通常情况下边缘检测的目的是在保留原有图像属性的情况下,显著减少图像的数据规模。有多种算法可以进行边缘检测,虽然Canny算法年代久远,但可以说它是边缘检测的一种标准算法,而且仍在研究中广泛使用。

步骤

  1. 应用高斯滤波来平滑图像,目的是去除噪声
  2. 找寻图像的强度梯度(intensity gradients)
  3. 应用非最大抑制(non-maximum suppression)技术来消除边误检(本来不是但检测出来是)
  4. 应用双阈值的方法来决定可能的(潜在的)边界
  5. 利用滞后技术来跟踪边界

详解

高斯平滑滤波

滤波是为了去除噪声,选用高斯滤波也是因为在众多噪声滤波器中,高斯表现最好(如果尝试其他滤波器,如均值滤波、中值滤波等等,就会发现高斯的效果最好)。

一个大小为(2k+1)x(2k+1)的高斯滤波器核(核一般都是奇数尺寸的)的生成方程式由下式给出:
高斯滤波计算公式

高斯滤波

高斯滤波是一种线性平滑滤波,适用于消除高斯噪声,广泛应用于图像处理的减噪过程

通俗的讲,高斯滤波就是对整幅图像进行加权平均的过程,每一个像素点的值,都由其本身和邻域内的其他像素值经过加权平均后得到

高斯滤波的具体操作是:用一个模板(或称卷积、掩模)扫描图像中的每一个像素,用模板确定的邻域内像素的加权平均灰度值去替代模板中心像素点的值

对于均值滤波和方框滤波来说,其邻域内每个像素的权重是相等的。而在高斯滤波中,会将中心点的权重值加大远离中心点的权重值减小,在此基础上计算邻域内各个像素值不同权重的和。

针对最左侧的图像内第4行第3列位置上像素值为226的像素点进行高斯卷积,其运算规则为将该领域内的像素点按照不同的权重计算和。
在这里插入图片描述
在这里插入图片描述

在实际使用中,高斯滤波使用的可能是不同大小的卷积核。例如, 3×3、5×5、7×7 大小的卷积核。在高斯滤波中,核的宽度和高度可以不相同,但是它们都必须是奇数
在这里插入图片描述

每一种尺寸的卷积核都可以有多种不同形式的权重比例。例如,同样是5×5的卷积核,可能是两种不同的权重比。
在这里插入图片描述

在实际计算中,卷积核是归一化处理的。

————————————————
版权声明:本文为CSDN博主「半濠春水」的原创文章,遵循CC 4.0 BY-SA版权协议。
原文链接:https://blog.csdn.net/weixin_51571728/article/details/121527964

代码实现

#include <stdio.h>
#include <math.h>

#define PICTURE_H 120
#define PICTURE_W 188

void gaussianBlur(unsigned char picture_in[PICTURE_H][PICTURE_W], int ksize, double sigma, unsigned char picture_out[PICTURE_H][PICTURE_W])
{
    int i, j, m, n;
    int radius = ksize / 2;
    double weightSum;
    double weightTotal;
    double weightMatrix[ksize][ksize];

    // 计算高斯权重矩阵
    for (i = 0; i < ksize; i++)
    {
        for (j = 0; j < ksize; j++)
        {
            m = i - radius;
            n = j - radius;
            weightMatrix[i][j] = exp(-((m * m + n * n) / (2 * sigma * sigma)));
            weightSum += weightMatrix[i][j];
        }
    }

    // 高斯滤波处理
    for (i = 0; i < PICTURE_H; i++)
    {
        for (j = 0; j < PICTURE_W; j++)
        {
            double pixelValue = 0.0;
            weightTotal = 0.0;

            for (m = -radius; m <= radius; m++)
            {
                for (n = -radius; n <= radius; n++)
                {
                    if (i + m >= 0 && i + m < PICTURE_H && j + n >= 0 && j + n < PICTURE_W)
                    {
                        pixelValue += picture_in[i + m][j + n] * weightMatrix[m + radius][n + radius];
                        weightTotal += weightMatrix[m + radius][n + radius];
                    }
                }
            }

            picture_out[i][j] = (unsigned char)(pixelValue / weightTotal);
        }
    }
}

调用示例

int main()
{
    // 定义输入和输出图像数组
    unsigned char picture_in[PICTURE_H][PICTURE_W];
    unsigned char picture_out[PICTURE_H][PICTURE_W];

    // 假设已经读取了输入图像到picture_in数组中

    int ksize = 3;   // 滤波核大小
    double sigma = 1.0;  // 水平方向上的标准差

    // 调用高斯滤波函数
    gaussianBlur(picture_in, ksize, sigma, picture_out);

    // 输出滤波后的图像或进行其他处理

    return 0;
}

调用效果

结果是下面两张图片,可以看到图像的边缘被明显地模糊了,图像的抗噪能力变强了。
在这里插入图片描述

总结:

总结一下这一步:高斯滤波其实就是将所指像素用周围的像素的某种均值代替(即卷积核),卷积核尺寸越大去噪能力越强,因此噪声越少,但图片越模糊canny检测算法抗噪声能力越强,但模糊的副作用也会导致定位精度不高
高斯的卷积核大小推荐:一般情况下,尺寸5 * 53 * 3就行。

计算梯度的大小和方向

对于一张图片来说,梯度能很好地反映其像素的变化情况,而梯度变化越大,说明相邻像素之间存在着较大差异,放大到整张图片来说,就是在某一块区域存在边缘,从视觉上来说就是用黑到白(灰度图片读入)。

梯度的计算分为大小方向,首先需要求出各个方向上的梯度,然后求平方根和切线

以下是x、y方向上梯度的计算方式:

x、y方向上梯度的计算方式

计算梯度的过程包括以下几个步骤:

  1. 首先,使用某种边缘检测算子(如Sobel算子)分别对图像在水平和垂直方向进行卷积运算。这将产生两个梯度图像,分别表示水平方向和垂直方向上的像素变化情况。
  2. 对于每个像素,可以使用求平方根的方法来计算梯度的大小。将水平方向和垂直方向上的梯度值平方,然后将它们相加,再对结果进行开方运算。这将给出每个像素位置上的梯度大小。
  3. 同时,可以使用反正切函数(如atan2)来计算梯度的方向。通过将水平方向上的梯度值除以垂直方向上的梯度值,可以得到每个像素位置上的梯度方向。

使用Sobel算子计算图像梯度幅值和梯度方向

Sobel算子可以分别在水平和垂直方向上对图像进行卷积操作,以捕捉图像中像素值的变化情况。以下是使用Sobel算子计算梯度的详细步骤:

灰度化:首先,将彩色图像转换为灰度图像。这可以通过将彩色图像的每个像素的红、绿和蓝通道值进行加权平均来实现,得到对应的灰度值。

建立Sobel算子:Sobel算子是一个3x3的卷积核,分为水平和垂直方向上的两个核。这两个核分别用于检测图像中水平和垂直方向上的边缘。下面是Sobel算子的示例:

水平方向Sobel算子:
-1  0  1
-2  0  2
-1  0  1

垂直方向Sobel算子:
-1 -2 -1
 0  0  0
 1  2  1

对图像进行卷积运算:将Sobel算子分别应用于图像的水平和垂直方向上,进行卷积运算。对于每个像素位置,将算子与其周围像素进行乘法运算,并将乘积的结果相加,得到新的像素值。

计算梯度幅值和方向:对于每个像素位置,使用水平方向和垂直方向上的卷积结果,分别计算梯度的幅值和方向。幅值可以通过求平方根来得到,方向可以通过反正切函数(如atan2)计算得到。

建立Sobel算子,计算每个像素点在四个方向上的梯度幅值
代码实现
typedef unsigned char uint8;
typedef unsigned short uint16;
typedef signed short int int16;

/****************************************************************************************************
*函数简介       基于soble算子计算四个方向的梯度幅值 
*参数说明       picture         输入二维数组名
*参数说明		gradient_h		水平方向梯度幅值
				gradient_v		垂直方向梯度幅值
				gradient_d1		正对角线方向梯度幅值 
				gradient_d2		反对角线方向梯度幅值
*返回参数       void
****************************************************************************************************/
void sobel_gradient(uint8 picture[PICTURE_H][PICTURE_W], int16 gradient_h[PICTURE_H][PICTURE_W], int16 gradient_v[PICTURE_H][PICTURE_W], int16 gradient_d1[PICTURE_H][PICTURE_W], int16 gradient_d2[PICTURE_H][PICTURE_W])
{
    /*卷积核大小*/
    uint8 KERNEL_SIZE = 3;
    uint16 xStart = KERNEL_SIZE / 2;
    uint16 xEnd = PICTURE_W - KERNEL_SIZE / 2;
    uint16 yStart = KERNEL_SIZE / 2;
    uint16 yEnd = PICTURE_H - KERNEL_SIZE / 2;
    uint16 i, j;

    for (i = yStart; i < yEnd; i++)
    {
        for (j = xStart; j < xEnd; j++)
        {
            /*计算不同方向梯度幅值*/
            // 水平方向
            gradient_h[i][j] = -(int16)picture[i - 1][j - 1] + (int16)picture[i - 1][j + 1]
                               -(int16)picture[i][j - 1] + (int16)picture[i][j + 1]
                               -(int16)picture[i + 1][j - 1] + (int16)picture[i + 1][j + 1];

            // 垂直方向
            gradient_v[i][j] = -(int16)picture[i - 1][j - 1] + (int16)picture[i + 1][j - 1]
                               -(int16)picture[i - 1][j] + (int16)picture[i + 1][j]
                               -(int16)picture[i - 1][j + 1] + (int16)picture[i + 1][j + 1];

            // 对角线方向1
            gradient_d1[i][j] = -(int16)picture[i - 1][j] + (int16)picture[i][j - 1]
                                -(int16)picture[i][j + 1] + (int16)picture[i + 1][j]
                                -(int16)picture[i - 1][j + 1] + (int16)picture[i + 1][j - 1];

            // 对角线方向2
            gradient_d2[i][j] = -(int16)picture[i - 1][j] + (int16)picture[i][j + 1]
                                -(int16)picture[i][j - 1] + (int16)picture[i + 1][j]
                                -(int16)picture[i - 1][j - 1] + (int16)picture[i + 1][j + 1];
        }
    }
}
调用示例
#define PICTURE_H 120
#define PICTURE_W 188

uint8 picture[PICTURE_H][PICTURE_W];
int16 gradient_h[PICTURE_H][PICTURE_W];
int16 gradient_v[PICTURE_H][PICTURE_W];
int16 gradient_d1[PICTURE_H][PICTURE_W];
int16 gradient_d2[PICTURE_H][PICTURE_W];

// 填充输入图像数组 picture

SOBEL(picture, gradient_h, gradient_v, gradient_d1, gradient_d2);

// 使用计算得到的梯度幅值进行后续处理
计算图像的梯度幅值和梯度方向
代码实现
/****************************************************************************************************
 * 计算图像的梯度幅值和方向
 * @param picture 输入图像数组,类型为 uint8,大小为 [PICTURE_H][PICTURE_W]。
 * @param Gx 输入图像的水平方向梯度数组,类型为 int16,大小为 [PICTURE_H][PICTURE_W]。
 * @param Gy 输入图像的垂直方向梯度数组,类型为 int16,大小为 [PICTURE_H][PICTURE_W]。
 * @param G 输出图像的梯度幅值数组,类型为 int16,大小为 [PICTURE_H][PICTURE_W]。
 * @param theta 输出图像的梯度方向数组,类型为 float,大小为 [PICTURE_H][PICTURE_W]。
 ****************************************************************************************************/
void calculate_gradient(uint8 picture[PICTURE_H][PICTURE_W], int16 Gx[PICTURE_H][PICTURE_W], int16 Gy[PICTURE_H][PICTURE_W], int16 G[PICTURE_H][PICTURE_W], float theta[PICTURE_H][PICTURE_W])
{
    // 计算图像梯度幅值和方向
    for (int i = 0; i < PICTURE_H; i++)
    {
        for (int j = 0; j < PICTURE_W; j++)
        {
            G[i][j] = sqrt(pow(Gx[i][j], 2) + pow(Gy[i][j], 2));
            theta[i][j] = atan2(Gy[i][j], Gx[i][j]) * 180 / PI;
        }
    }
}
调用示例
#include <stdio.h>

#define PICTURE_H 100 // 图像的高度
#define PICTURE_W 100 // 图像的宽度

void calculate_gradient(uint8 picture[PICTURE_H][PICTURE_W], int16 Gx[PICTURE_H][PICTURE_W], int16 Gy[PICTURE_H][PICTURE_W], int16 G[PICTURE_H][PICTURE_W], float theta[PICTURE_H][PICTURE_W]);

int main()
{
    // 假设有一个大小为 [PICTURE_H][PICTURE_W] 的图像和梯度数组
    uint8 picture[PICTURE_H][PICTURE_W];
    int16 Gx[PICTURE_H][PICTURE_W];
    int16 Gy[PICTURE_H][PICTURE_W];
    int16 G[PICTURE_H][PICTURE_W];
    float theta[PICTURE_H][PICTURE_W];

    // 填充图像和梯度数组的数据

    // 调用计算梯度函数
    calculate_gradient(picture, Gx, Gy, G, theta);

    // 打印梯度幅值和方向的示例
    for (int i = 0; i < PICTURE_H; i++)
    {
        for (int j = 0; j < PICTURE_W; j++)
        {
            printf("G[%d][%d] = %d\n", i, j, G[i][j]);
            printf("theta[%d][%d] = %f\n", i, j, theta[i][j]);
        }
    }

    return 0;
}
调用结果

梯度计算结果

可以看到,[-1, 0, 1], [-1, 0, 1], [-1, 0, 1] 对应了x方向上的边缘检测(在碰到x方向垂直的边界的时候计算出来的数值更大,所以像素更亮),[1, 1, 1], [0, 0, 0], [-1, -1, -1] 同理对应了y方向上的边缘检测。

非极大抑制

用Sobel算子或者是其他简单的边缘检测方法,计算出来的边缘太粗了,就是说原本的一条边用几条几乎重叠的边所代替了,导致视觉上看起来边界很粗。非极大抑制是一种瘦边经典算法。它抑制那些梯度不够大的像素点,只保留最大的梯度,从而达到瘦边的目的。

a) 将其梯度方向近似为以下值中的一个[0,45,90,135,180,225,270,315](即上下左右和45度方向)这一步是为了方便使用梯度
b) 比较该像素点,和其梯度方向正负方向的像素点的梯度强度,这里比较的范围一般为像素点的八邻域;
c) 如果该像素点梯度强度最大则保留,否则抑制(删除,即置为0);

代码实现

/****************************************************************************************************
 * 对图像的梯度幅值进行非极大值抑制,以细化边缘。
 *
 * @param G 梯度幅值数组,类型为 int16,大小为 [PICTURE_H][PICTURE_W]。
 * @param theta 梯度方向数组,类型为 float,大小为 [PICTURE_H][PICTURE_W]。
 * @param edge_threshold 边缘阈值,类型为 int16。
 * @param result 输出的二值化边缘图像数组,类型为 uint8,大小为 [PICTURE_H][PICTURE_W]。
 ****************************************************************************************************/
void non_maximum_suppression(int16 G[PICTURE_H][PICTURE_W], float theta[PICTURE_H][PICTURE_W], int16 edge_threshold, uint8 result[PICTURE_H][PICTURE_W])
{
    // 对梯度幅值进行非极大值抑制
    for (int i = 1; i < PICTURE_H - 1; i++)
    {
        for (int j = 1; j < PICTURE_W - 1; j++)
        {
            float angle = theta[i][j];

            // 将角度映射到 0 到 180 度之间
            if (angle < 0)
                angle += 180;

            // 判断当前像素点的梯度方向,并进行非极大值抑制
            if (((angle >= 0 && angle < 22.5) || (angle >= 157.5 && angle < 180)) ||  // 水平方向
                ((angle >= 22.5 && angle < 67.5) || (angle >= 202.5 && angle < 247.5)) ||  // 主对角线方向
                ((angle >= 67.5 && angle < 112.5) || (angle >= 247.5 && angle < 292.5)) ||  // 垂直方向
                ((angle >= 112.5 && angle < 157.5) || (angle >= 292.5 && angle < 337.5)))  // 次对角线方向
            {
                if (G[i][j] > G[i][j - 1] && G[i][j] > G[i][j + 1])
                {
                    // 梯度幅值大于相邻两个像素的幅值,保留边缘
                    result[i][j] = (G[i][j] >= edge_threshold) ? 255 : 0;
                }
                else
                {
                    result[i][j] = 0;
                }
            }
            else
            {
                if (G[i][j] > G[i - 1][j] && G[i][j] > G[i + 1][j])
                {
                    // 梯度幅值大于相邻两个像素的幅值,保留边缘
                    result[i][j] = (G[i][j] >= edge_threshold) ? 255 : 0;
                }
                else
                {
                    result[i][j] = 0;
                }
            }
        }
    }

    // 将图像边缘的边界像素置为 0
    for (int i = 0; i < PICTURE_H; i++)
    {
        result[i][0] = 0;
        result[i][PICTURE_W - 1] = 0;
    }
    for (int j = 0; j < PICTURE_W; j++)
    {
        result[0][j] = 0;
        result[PICTURE_H - 1][j] = 0;
    }
}

调用示例

// 假设有已计算好的梯度幅值和方向数组 G 和 theta
int16 G[PICTURE_H][PICTURE_W];
float theta[PICTURE_H][PICTURE_W];

// 定义边缘阈值
int16 edge_threshold = 100;

// 定义二值化边缘图像数组
uint8 result[PICTURE_H][PICTURE_W];

// 执行非极大值抑制
non_maximum_suppression(G, theta, edge_threshold, result);

示例中的边缘阈值 edge_threshold 是一个示例值,需要根据实际情况调整该阈值以达到满意的边缘检测效果。

该函数使用两个嵌套的循环遍历图像像素,并根据梯度方向进行非极大值抑制。代码中对于不同的梯度方向使用了不同的判断条件,并根据相邻像素的梯度幅值决定是否保留边缘。在循环结束后,代码还会将图像边缘的边界像素置为0。

调用结果

非极大抑制

双阈值(Double Thresholding)和滞后边界跟踪

经过非极大抑制后图像中仍然有很多噪声点。Canny算法中应用了一种叫双阈值的技术。

使用双阈值是常见的边缘检测后续处理步骤之一。双阈值处理可以更好地区分边缘和非边缘像素,并进一步提升边缘检测的准确性。

双阈值处理包括以下步骤:

  1. 高阈值和低阈值的选择:根据你的应用需求和图像特性,选择适当的高阈值和低阈值。高阈值用于定义强边缘像素,低阈值用于定义弱边缘像素。
  2. 边缘分类:根据阈值,将像素分类为强边缘像素、弱边缘像素和非边缘像素。一般来说,高于高阈值的像素被认为是强边缘,低于低阈值的像素被认为是非边缘,介于两者之间的像素被认为是弱边缘。
  3. 边缘连接:对于弱边缘像素,如果其与强边缘像素相邻,则将其重新分类为强边缘。这可以通过边缘跟踪算法(如霍夫变换或连通组件分析)来实现。

通过双阈值处理,可以获得更准确的边缘图像,强边缘像素代表明显的边界,而弱边缘像素代表可能的边界。这为后续的边缘连接和过滤提供了更好的基础。

双阈值,即设定一个阈值上界和阈值下界(通常由人为指定的),图像中的像素点如果大于阈值上界则认为必然是边界(称为强边界,strong edge),小于阈值下界则认为必然不是边界,两者之间的则认为是候选项(称为弱边界,weak edge),需进行进一步处理。我查阅资料,了解到上界一般是下界的2-3倍,实现出来的效果比较好。在Canny算法中,对于弱边缘像素,通常采用边缘跟踪(如霍夫变换或连通组件分析)来连接它们与强边缘像素,形成连续的边界线段。

双阈值技术

代码实现
/****************************************************************************************************
 * 应用双阈值技术将梯度幅值图像转换为边界图像
 *
 * @param G 梯度幅值数组,二维数组,大小为 PICTURE_H × PICTURE_W
 * @param edges 边界图像数组,二维数组,大小为 PICTURE_H × PICTURE_W
 * @param high_threshold 高阈值,用于判断强边界的阈值
 * @param low_threshold 低阈值,用于判断强边界和弱边界的阈值
 ****************************************************************************************************/
void apply_double_threshold(int16 G[PICTURE_H][PICTURE_W], uint8 edges[PICTURE_H][PICTURE_W], int16 high_threshold, int16 low_threshold)
{
    for (int i = 0; i < PICTURE_H; i++)
    {
        for (int j = 0; j < PICTURE_W; j++)
        {
            if (G[i][j] >= high_threshold)
            {
                // 像素强度大于等于高阈值,被认为是强边界
                edges[i][j] = 255; // 白色
            }
            else if (G[i][j] >= low_threshold)
            {
                // 像素强度在高阈值和低阈值之间,被认为是弱边界
                edges[i][j] = 128; // 灰色
            }
            else
            {
                // 像素强度小于低阈值,被认为是非边界
                edges[i][j] = 0; // 黑色
            }
        }
    }
}

这个函数接受梯度幅值数组 G、输出的边界数组 edges,以及高阈值 high_threshold 和低阈值 low_threshold 作为参数。它遍历每个像素,根据其梯度幅值将其分类为强边界、弱边界或非边界,并在边界数组中相应地设置像素值。

基于八邻域的边缘跟踪算法

代码实现
/****************************************************************************************************
 * 使用滞后边界跟踪对弱边界进行二次判断
 *
 * @param edges 边界图像数组,二维数组,大小为 PICTURE_H × PICTURE_W
 * @param high_threshold 高阈值,用于判断强边界的阈值
 * @param low_threshold 低阈值,用于判断强边界和弱边界的阈值
 ****************************************************************************************************/
void apply_hysteresis_threshold(uint8 edges[PICTURE_H][PICTURE_W], int16 high_threshold, int16 low_threshold)
{
    for (int i = 1; i < PICTURE_H - 1; i++)
    {
        for (int j = 1; j < PICTURE_W - 1; j++)
        {
            if (edges[i][j] == 128) // 如果当前像素为弱边界
            {
                // 检查当前像素周围的8个像素
                if (edges[i - 1][j - 1] == 255 || edges[i - 1][j] == 255 || edges[i - 1][j + 1] == 255 ||
                    edges[i][j - 1] == 255 || edges[i][j + 1] == 255 ||
                    edges[i + 1][j - 1] == 255 || edges[i + 1][j] == 255 || edges[i + 1][j + 1] == 255)
                {
                    // 如果周围任意一个像素是强边界,则将当前像素标记为强边界
                    edges[i][j] = 255;
                }
                else
                {
                    // 否则将当前像素标记为非边界
                    edges[i][j] = 0;
                }
            }
        }
    }
}

双阈值+边缘跟踪

使用双阈值后再调用边缘跟踪,遍历两次似乎效率太低,所以我们可以将这两个算法合成一个函数。

代码实现
/****************************************************************************************************
 * 应用双阈值技术将梯度幅值图像转换为边界图像,并使用边缘跟踪算法处理弱边界
 *
 * @param G 梯度幅值数组,二维数组,大小为 PICTURE_H × PICTURE_W
 * @param edges 边界图像数组,二维数组,大小为 PICTURE_H × PICTURE_W
 * @param high_threshold 高阈值,用于判断强边界的阈值
 * @param low_threshold 低阈值,用于判断强边界和弱边界的阈值
 ****************************************************************************************************/
void apply_double_threshold(int16 G[PICTURE_H][PICTURE_W], uint8 edges[PICTURE_H][PICTURE_W], int16 high_threshold, int16 low_threshold)
{
    for (int i = 0; i < PICTURE_H-1; i++)
    {
        for (int j = 0; j < PICTURE_W-1; j++)
        {
            if (G[i][j] >= high_threshold)
            {
                // 像素强度大于等于高阈值,被认为是强边界
                edges[i][j] = 255; // 白色
            }
            else if (G[i][j] >= low_threshold)
            {
                // 像素强度在高阈值和低阈值之间,被认为是弱边界
                // 检查当前像素周围的8个像素
                if (edges[i - 1][j - 1] == 255 || edges[i - 1][j] == 255 || edges[i - 1][j + 1] == 255 ||
                    edges[i][j - 1] == 255 || edges[i][j + 1] == 255 ||
                    edges[i + 1][j - 1] == 255 || edges[i + 1][j] == 255 || edges[i + 1][j + 1] == 255)
                {
                    // 如果周围任意一个像素是强边界,则将当前像素标记为强边界
                    edges[i][j] = 255;
                }
                else
                {
                    // 否则将当前像素标记为非边界
                    edges[i][j] = 0;
                }
            }
            else
            {
                // 像素强度小于低阈值,被认为是非边界
                edges[i][j] = 0; // 黑色
            }
        }
    }
}

在上述代码中,白色方块代表存在边缘(包括强弱边缘),遍历弱边缘中的每个像素,如果像素的八邻域存在强边缘对应的像素,则将这个弱边缘像素归为真正的边缘(从视觉上来理解,就是存在一条不确定的边缘,如果这条不确定的边缘旁存在真正的边缘,则将这条边归为真边,非则就将其删除)

调用结果

滞后边界跟踪1

封装

最后我们可以将以上代码封装成一个Canny函数。

其实读者们依次将笔者上述的所有函数复制并封装到源文件和头文件中使用即可
由于笔者比较懒,所以懒得封装了。

后续有空的时候,笔者会将所有内容封装成一个压缩包,放到付费文件里。
最后会将连接更新在文章的开头。

如果觉得这篇文章写得不错的话,不妨点赞并收藏,谢谢阅读。

参考文献

Canny算子的原理参考了这篇文章:计算机视觉中Canny算子详解。在本篇文章中笔者仅参考了计算公式和整体思路。C语言代码纯靠手搓。

  • 5
    点赞
  • 32
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值