C语言数字图像处理(九):阈值处理和边缘检测

C语言数字图像处理系列继续更新~

这一章节的完整代码在:9. Thresholding, Roberts, Prewitt, Sobel, and Edge Detection

如果你喜欢这个系列的文章或者感觉对你有帮助,请给我的仓库一个⭐️

1. 图像梯度和阈值处理

1.1 Roberts算子

Roberts 边缘提取算子有一个 2x2 的模板(mask),它使用两个对角相邻像素之间的差异,然后取两个对角像素之间差异的平方和。 模板如下:

​算法为:

图像和结果对比 (headCT_Vandy.pgm, building_original.pgm, noisy_fingerprint):

结果分析:

Roberts 算法使用局部差分算子来查找边缘。 当图像中的边缘接近+45度或-45度时,该算法效果更好。 Roberts算子对边缘的定位精度较高。 从处理结果来看,Roberts 算法对噪声比较敏感,因此适合于边缘比较明显、噪声较少的图像分割。

代码实现:

void Roberts(Image *image) {
    unsigned char *tempin, *tempout;
    float temp1, temp2;
    Image *outimage;
    outimage = CreateNewImage(image, (char*)"#testing function");
    tempin = image->data;
    tempout = outimage->data;
    
    for(int i = 0; i < image->Height-1; i++) {
        for(int j = 0; j < image->Width-1; j++) {
            temp1 = pow((float)tempin[image->Width * i + j] - (float)tempin[image->Width * (i+1) + j + 1], 2);
            temp2 = pow((float)tempin[image->Width * i + j + 1] - (float)tempin[image->Width * (i+1) + j], 2);
            tempout[image->Width * i + j] = (int)sqrt(temp1 + temp2);
        }
    }
    outimage = Threshold(outimage);
    SavePNMImage(outimage, (char*)"Roberts.pgm");
}

1.2 Prewitt算子:

Prewitt算子使用3×3的模板来计算区域内的值,其公式为:

相应的公式为:

图像和结果对比 (headCT_Vandy.pgm, building_original.pgm, noisy_fingerprint):

结果分析:

Prewitt算子采用3×3的模板来计算区域内的像素值,因此其边缘检测结果在水平和垂直方向上都比Robert算子明显。 Prewitt算子对噪声有一定的抑制作用。 抑制噪声的原理是像素平均,但Prewitt算子在定位边缘方面不如Roberts算子,而且可能会产生宽度为多个像素的边缘。 因此,Prewitt算子适合识别有一定噪声、灰度梯度明显的图像。

代码实现(完整代码见顶部GitHub):

for(int i = 0; i < image->Height; i++) {
    for(int j = 0; j < image->Width; j++) {
        index = 0;
        // record the values in the 3x3 square:
        for(int m = -1; m <= 1; m++) {
            for(int n = -1; n <= 1; n++) {
                // use boundary check:
                square[index++] = boundaryCheck(j + n, i + m, image->Width, image->Height) ? tempin[image->Width * (i + m) + (j + n)] : 0;
            }
        }
        temp1 = square[2] + square[5] + square[8] - square[0] - square[3] - square[6];
        temp2 = square[0] + square[1] + square[2] - square[6] - square[7] - square[8];
        tempout[image->Width * i + j] = abs(temp1) + abs(temp2);
    }
}

1.3 Sobel算子

 Sobel 算子是一阶微分算子:

其中\nabla f表示在 (x,y) 处最大变化率的方向。 我们用的水平和垂直卷积分别是:

G_x=\left[\begin{matrix}-1&0&+1\\-2&0&+2\\-1&0&+1\\\end{matrix}\right]\ \ \ and\ \ \ G_y=\left[\begin{matrix}-1&-2&-1\\0&0&0\\+1&+2&+1\\\end{matrix}\right].

然后结合每个像素的水平和垂直灰度值来计算新的灰度值:

G=\sqrt[2]{G_x^2+G_y^2}.

图像和结果对比 (headCT_Vandy.pgm, building_original.pgm, noisy_fingerprint):


结果分析:

Sobel算子是根据该像素上下、左右邻近像素的强度的加权差在边缘处达到极值来检测边缘的。 由于Sobel算子结合了高斯平滑和微分求导,因此其结果具有更强的抗噪声能力。 并且由于它对像素位置的影响进行加权,因此 Sobel 比 Prewitt 和 Roberts 具有更好的边缘方向信息。

代码实现(完整代码见顶部GitHub):

for(int i = 0; i < image->Height; i++) {
    for(int j = 0; j < image->Width; j++) {
        index = 0;
        // record the values in the 3x3 square:
        for(int m = -1; m <= 1; m++) {
            for(int n = -1; n <= 1; n++) {
                // use boundary check:
                square[index++] = boundaryCheck(j + n, i + m, image->Width, image->Height) ? tempin[image->Width * (i + m) + (j + n)] : 0;
            }
        }
        temp1 = abs((float)square[2] + (float)square[5]*2 + (float)square[8] - (float)square[0] - (float)square[3]*2 - (float)square[6]);
        temp2 = abs((float)square[6] + (float)square[7]*2 + (float)square[8] - (float)square[0] - (float)square[1]*2 - (float)square[2]);
        tempout[image->Width * i + j] = (int)sqrt(pow(temp1, 2) + pow(temp2, 2));
    }
}

1.4 阈值处理

它遍历图像中的所有像素并获得最大强度,并乘以一个因子(在以下代码里面设为33%)以获得所需阈值。 如果原始像素的强度大于阈值,我们将其设置为白色(255),否则为黑色(0)。
*阈值处理的结果已包含在上述3种算法中。

代码实现:

Image *Threshold(Image *image) {
    unsigned char *tempin, *tempout;
    Image *outimage;
    int size = image->Width * image->Height, max = 0;
    outimage = CreateNewImage(image, (char*)"#testing function");
    tempin = image->data;
    tempout = outimage->data;
    
    for(int i = 0; i < size; i++) {
        if(tempin[i] > max) max = tempin[i];
    }
    int threshold = round((float)max * 0.33);
    
    for(int i = 0; i < size; i++) {
        if(tempin[i] >= threshold) tempout[i] = 255;
        else tempout[i] = 0;
    }
    return(outimage);
}

2. 边缘检测算法

2.1 Canny:

Canny的算法主要有四个步骤:
(1) 高斯滤波器:目的是抑制噪声。 导数的计算对噪声敏感,因此必须使用滤波器来提高噪声相关边缘检测器的性能。 这里使用的算法和模板是:

(2)计算梯度大小和方向:使用Sobel算子, 梯度大小和方向的公式为:

其中 g_x, g_y 是不同方向的梯度值。

(3)非极大值滤波:如果某个像素点属于边缘,则该像素点在梯度方向上的梯度值最大, 否则就不是边缘,其值应设置为0。总共有4个方向,中心方向应为最大值:

其中方向由上一步获得的角度确定。

(4) 双阈值算法检测:大于𝑚𝑎𝑥𝑉𝑎𝑙的像素被检测为边缘,低于𝑚𝑖𝑛𝑣𝑎𝑙的像素被检测为非边缘。 对于中间的像素,如果与判定为边缘的像素相邻,则判定为边缘,否则判定为非边缘。

图像和结果对比 (headCT_Vandy, noisy_fingerprint):

结果分析:

总体来说Canny 是一种优秀的边缘检测算法。 它几乎找到了所有边缘,并且检测到的边缘点都是单个的。 不同尺度下边缘点的位置偏差几乎相同。 它具有一定的抗噪能力,但无法处理“noise_fingerprint”这个程度。 此外,Canny算子可以更好地检测弱边缘。

代码按以下的顺序实现(完整代码见顶部GitHub中的`void Canny(Image *image)`):

(1) Step 1: Gaussian Filter

(2) Step 2: Sobel Operator and obtain the directions

(3) Step 3: Non-maximum suppression

(4) Step 4: Detection with double threshold algorithm

2.2 LoG算法

LoG算法包含三个主要的步骤:

(1) 高斯滤波器:Canny中相同:

(2) 计算拉普拉斯算子:

其中使用的 3x3 模板为:\begin{bmatrix} 1 & 1 & 1 \\ 1 & -8 & 1 \\ 1 & 1 & 1 \end{bmatrix}

(3) 在步骤 2 生成的图像中找零交叉点(Zero Crossings)。以 𝑃 为中心的 3*3 掩模,有四种情况要考虑:左/右、上/下,和两个对角。 如果任一方向上相对的一对像素之间的强度绝对差超过预设阈值,并且中心像素 𝑃 本身的强度足够高,则认为𝑃 是一个零交叉点。

图像和结果对比 (headCT_Vandy, noisy_fingerprint):

结果分析:

为了更有效地检测不同尺度的边缘,建议采用具有不同尺度参数的拉普拉斯高斯(LoG)算子。相较于Canny边缘检测器,LoG算子在定位边缘点时更为精确。然而,它的一个主要缺陷是对噪声较为敏感,这可能导致误检测虚假边缘,尤其是在弯曲边缘区域,定位误差可能更加显著。LoG检测器利用二阶导数的过零点来识别边缘,噪声的影响相对较大。不过,通过在LoG算子之前应用高斯滤波进行图像平滑处理,可以有效减少噪声的影响,从而提升边缘检测的准确性和可靠性。

代码实现(完整代码见顶部GitHub):

void LoG(Image *image) {
    unsigned char *tempin, *tempout, *tempout1;
    Image *outimage, *outimage1;
    int index, square[9];
    
    outimage = CreateNewImage(image, (char*)"#testing function");
    outimage1 = CreateNewImage(image, (char*)"#testing function");
    tempout1 = outimage1->data;
    
    // Step 1: Gaussian Blur:
    image = Gaussian(image);
    tempin = image->data;
    
    // Step 2: Laplacian:
    outimage = Laplacian(image);
    tempout = outimage->data;
    SavePNMImage(outimage, (char*)"Laplacian.pgm");
    
    // Step 3: Find zero crossings:
    for(int i = 0; i < image->Height; i++) {
        for(int j = 0; j < image->Width; j++) {
            index = 0;
            // record the values in the 3x3 square:
            for(int m = -1; m <= 1; m++) {
                for(int n = -1; n <= 1; n++) {
                    // use boundary check:
                    square[index++] = boundaryCheck(j + n, i + m, image->Width, image->Height) ? tempin[image->Width * (i + m) + (j + n)] : 0;
                }
            }
            if(square[4] > 80 && (abs(square[8]-square[0]) > 60 || abs(square[7]-square[1]) > 60 || abs(square[6]-square[2]) > 60 || abs(square[5]-square[3]) > 60))  {
                tempout1[image->Width * i + j] = 200;
            }
            else tempout1[image->Width * i + j] = 0;
            
        }
    }
    SavePNMImage(outimage1, (char*)"LoG.pgm");
}

3. 全局阈值处理

全局阈值法的核心思想是选择一个阈值 𝑇,将图像像素划分为两个明确的部分,基本上是将图像分割为前景和背景。

全局阈值法的原则是将所有亮度值大于阈值 𝑇 的像素赋值为255,其他像素则被忽略。确定适当的 𝑇 需要经过以下步骤:

  1. 为全局阈值 𝑇 选择一个初始估计值
  2. 使用 𝑇 对图像进行分割,得到两组像素:𝐺1 包含所有大于 𝑇 的像素,而 𝐺2 包含所有小于或等于 𝑇 的像素
  3. 分别计算 𝐺1和 𝐺2 像素的平均亮度值 𝑚1 和 𝑚2
  4. 将 𝑇 更新为 𝑚1 和 𝑚2 的平均值,具体为 𝑇 = 1/2 ∗ (𝑚1 + 𝑚2)
  5. 重复步骤 2 至 4,直到 𝑇 在连续迭代中稳定下来

图像和结果对比 (polymersomes, noisy_fingerprint):

结果分析:

全局阈值法利用阈值 𝑇 来分割图像,有效地提取出暗背景下的亮对象。当图像前景和背景的直方图之间有一个明显的低谷时,这种方法特别有效。然而,这种方法不适用于具有固有噪声的二值图像,比如“noisy_fingerprint”,在这种图像中,所有白色部分的值都会超过阈值,因此在分割后不会有任何区别。

代码实现:

void GlobalThreshold(Image *image) {
    unsigned char *tempin, *tempout;
    Image *outimage;
    float T = 128; // provide an initial T
    int size = image->Width * image->Height, times = 0;
    outimage = CreateNewImage(image, (char*)"#testing function");
    tempin = image->data;
    tempout = outimage->data;
    
    while(times < 30) {
        float count1 = 0, count2 = 0, sum1 = 0, sum2 = 0;
        times++;
        
        for(int i = 0; i < size; i++) {
            if(tempin[i] >= T) {
                sum2 += tempin[i];
                count2++;
            }
            else {
                sum1 += tempin[i];
                count1++;
            }
        }
        float average1 = sum1 / count1;
        float average2 = sum2 / count2;
        T = (average1 + average2) / 2.0;// update the T1
    }
    // output the image:
    for(int i = 0; i < size; i++) {
        if(tempin[i] >= T) tempout[i] = 255;
        else tempout[i] = 0;
    }
    SavePNMImage(outimage, (char *)"Global Threshold.pgm");
}

4. 完整代码

 这一章节的完整代码在:9. Thresholding, Roberts, Prewitt, Sobel, and Edge Detection

更多关于数字图像处理的章节,以及所有的原图像在:Introduction to Digital Image Processing

如果你喜欢这个系列的文章或者对你有帮助,请给我的仓库一个⭐️。

-END-

  • 11
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
C语言是一种常用于编写底层程序的编程语言,它也可以用来进行数字图像处理。以下是一个简单的C语言数字图像处理的源代码示例: ```c #include <stdio.h> // 图像处理函数 void imageProcessing(int image[][3], int rows, int cols) { // 遍历图像的每个像素 for (int i = 0; i < rows; i++) { for (int j = 0; j < cols; j++) { // 图像处理操作,这里示例为将每个像素的值加1 image[i][j] += 1; } } } int main() { // 定义图像大小 int rows = 2; int cols = 3; // 定义图像数组 int image[2][3] = {{1, 2, 3}, {4, 5, 6}}; // 调用图像处理函数 imageProcessing(image, rows, cols); // 打印处理后的图像 printf("处理后的图像:\n"); for (int i = 0; i < rows; i++) { for (int j = 0; j < cols; j++) { printf("%d ", image[i][j]); } printf("\n"); } return 0; } ``` 以上示例代码中,`imageProcessing`函数用于对输入的图像进行处理,这里的处理操作为将每个像素的值加1。通过调用该函数,我们可以对指定的图像进行处理。 在主函数中,我们定义了一个2x3的图像数组,然后调用`imageProcessing`函数对图像进行处理。最后,我们打印处理后的图像,以验证处理操作是否成功。 这只是一个简单的示例代码,实际的数字图像处理可能需要更复杂的算法和操作。但通过C语言的灵活性和对底层访问的支持,我们可以使用C语言编写出更加高效和实用的数字图像处理代码。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值