65.9K
CodeProject 正在变化。 阅读更多。
Home

C# 和 GDI+ 图像处理入门,第三部分 - 边缘检测过滤器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.92/5 (68投票s)

2002年4月1日

CPOL

6分钟阅读

viewsIcon

657276

downloadIcon

15077

一系列文章的第三篇,将用 C# 和 GDI+ 构建一个图像处理库

Sample Image

引言

欢迎回来。这可能是我这个系列中最后一篇文章了,我想先集中精力学习一些其他的 C# 知识,等有更多时间的时候再回来继续这个系列。

概述

本文将重点介绍最常见的图像处理任务之一:边缘检测。我们将探讨多种方法来实现边缘检测,并介绍一种使用这些信息的应用:边缘增强滤镜。我们将从上一篇文章中学到的知识开始,即使用卷积滤镜进行边缘检测。

卷积滤镜 - Sobel, Prewitt 和 Kirsh

我们将使用三种不同的卷积掩码来检测边缘,它们很可能以发明者的名字命名。在每种情况下,我们都会将水平版本的滤镜应用到一个位图中,将垂直版本的滤镜应用到另一个位图中,然后使用公式 pixel = sqrt(pixel1 * pixel1 + pixel2 * pixel2) 来合并它们。希望您对前面的文章足够熟悉,能够理解实现这些功能的代码。卷积掩码如下所示:

Sobel
1 2 1
0 0 0
-1 -2 -1 /1+0
Prewitt
1 1 1
0 0 0
-1 -1 -1 /1+0
Kirsh
5 5 5
-3 -3 -3
-3 -3 -3 /1+0

这些滤镜执行水平边缘检测,旋转 90 度得到垂直边缘,然后进行合并。

它们如何工作?

边缘检测滤镜本质上是通过查找图像中的对比度来工作的。这可以通过多种方式实现,卷积滤镜通过对一个边缘应用负权重,对另一个边缘应用正权重来实现。当像素值相同时,其效果趋向于零;当存在对比度时,其效果会增加。这正是我们的浮雕滤镜的工作原理,使用 127 的偏移量会再次使这些滤镜看起来与我们之前的浮雕滤镜相似。以下示例按照上面滤镜的顺序显示了不同的滤镜类型。图像带有工具提示,您可以确定是哪一个。这三种滤镜还允许指定阈值。任何低于此阈值的值都将被钳制到该阈值。在测试中,我将阈值保持为 0。

My kids    Sobel Edge Detection

Prewitt Edge Detection    Kirsh Edge Detection

水平和垂直边缘检测

为了仅在水平或垂直平面上执行边缘检测操作,我们再次可以使用卷积方法。然而,与其使用我们为 3x3 滤镜设计的框架,不如从头开始编写代码,以便我们的滤镜(将是 Prewitt 滤镜)要么非常宽,要么非常高。我选择了 7 作为合适的数值,我们的水平滤镜是 7x3,垂直滤镜是 3x7。代码与我们已经完成的工作相似度不高,因此没有特别展示,但如果您想看的话,代码就在那里。下面首先是我们水平滤镜的结果,然后是垂直滤镜的结果。

Horizontal Edge Detection Vertical Edge Detection

生活不止于卷积

卷积滤镜可以做一些很酷的事情,如果您在网上搜索,您可能会认为它们是所有图像处理的幕后功臣。然而,更准确地说,您在 Photoshop 中看到的滤镜类型是专门为直接实现卷积滤镜只能模仿的功能而编写的。我再次以 Photoshop 的浮雕滤镜及其各种选项为例来证明这一点。

卷积在边缘检测方面的问题并不在于过程不令人满意,而在于它是不必要的昂贵。我将介绍另外两种边缘检测方法,这两种方法都涉及到我们直接迭代图像,并对相邻像素进行一系列比较,但它们处理结果值的方式与卷积滤镜不同。

同质性边缘检测

如果我们要感知图像中的边缘,那么就意味着两个物体之间存在颜色变化,这样边缘才能显现出来。换句话说,如果我们取一个像素,并将其值存储为其起始值与它的八个邻居值之间的最大差值,那么在像素相同的地方将是黑色,在颜色差异越大(越硬)的地方,值就越高。这样我们就能检测到图像中的边缘。此外,如果我们允许设置一个阈值,并将低于此阈值的值设置为 0,我们就可以根据需要消除柔和的边缘。执行此操作的代码后面是阈值为 0 的示例和阈值为 127 的示例。

public static bool EdgeDetectHomogenity(Bitmap b, byte nThreshold)
{
    // This one works by working out the greatest difference between a 
    // pixel and it's eight neighbours. The threshold allows softer edges to 
    // be forced down to black, use 0 to negate it's effect.
    Bitmap b2 = (Bitmap) b.Clone();

    // GDI+ still lies to us - the return format is BGR, NOT RGB.
    BitmapData bmData = b.LockBits(new Rectangle(0, 0, b.Width, b.Height), 
                                   ImageLockMode.ReadWrite, PixelFormat.Format24bppRgb);
    BitmapData bmData2 = b2.LockBits(new Rectangle(0, 0, b.Width, b.Height), 
                                     ImageLockMode.ReadWrite, PixelFormat.Format24bppRgb);

    int stride = bmData.Stride;
    System.IntPtr Scan0 = bmData.Scan0;
    System.IntPtr Scan02 = bmData2.Scan0;

    unsafe
    {
        byte * p = (byte *)(void *)Scan0;
        byte * p2 = (byte *)(void *)Scan02;

        int nOffset = stride - b.Width*3;
        int nWidth = b.Width * 3;

        int nPixel = 0, nPixelMax = 0;

        p += stride;
        p2 += stride;

        for(int y=1;y<b.Height-1;++y)
        {
            p += 3;
            p2 += 3;

            for(int x=3; x < nWidth-3; ++x )
            {
                nPixelMax = Math.Abs(p2[0] - (p2+stride-3)[0]);
                nPixel = Math.Abs(p2[0] - (p2 + stride)[0]);
                if (nPixel>nPixelMax) nPixelMax = nPixel;

                nPixel = Math.Abs(p2[0] - (p2 + stride + 3)[0]);
                if (nPixel>nPixelMax) nPixelMax = nPixel;

                nPixel = Math.Abs(p2[0] - (p2 - stride)[0]);
                if (nPixel>nPixelMax) nPixelMax = nPixel;

                nPixel = Math.Abs(p2[0] - (p2 + stride)[0]);
                if (nPixel>nPixelMax) nPixelMax = nPixel;

                nPixel = Math.Abs(p2[0] - (p2 - stride - 3)[0]);
                if (nPixel>nPixelMax) nPixelMax = nPixel;

                nPixel = Math.Abs(p2[0] - (p2 - stride)[0]);
                if (nPixel>nPixelMax) nPixelMax = nPixel;

                nPixel = Math.Abs(p2[0] - (p2 - stride + 3)[0]);
                if (nPixel>nPixelMax) nPixelMax = nPixel;

                if (nPixelMax < nThreshold) nPixelMax = 0;

                p[0] = (byte) nPixelMax;

                ++ p;
                ++ p2;
            }

            p += 3 + nOffset;
            p2 += 3 + nOffset;
        }
    }

    b.UnlockBits(bmData);
    b2.UnlockBits(bmData2);

    return true;
    
}

Homogenousl Edge Detection, 0 threshold Homogenousl Edge Detection, 127 threshold

差异边缘检测

差异边缘检测的工作方式类似,但它检测的是我们正在设置的像素周围像素对之间的差异。它计算出可以通过穿过中间像素的直线形成的四对像素的差值的最大值。阈值与同质性滤镜的工作方式相同。同样,这里是代码,后面是两个示例,一个没有阈值,一个阈值为 127。

public static bool EdgeDetectDifference(Bitmap b, byte nThreshold)
{
    // This one works by working out the greatest difference between a 
    // pixel and it's eight neighbours. The threshold allows softer edges 
    // to be forced down to black, use 0 to negate it's effect.
    Bitmap b2 = (Bitmap) b.Clone();

    // GDI+ still lies to us - the return format is BGR, NOT RGB.
    BitmapData bmData = b.LockBits(new Rectangle(0, 0, b.Width, b.Height), 
                                   ImageLockMode.ReadWrite, PixelFormat.Format24bppRgb);
    BitmapData bmData2 = b2.LockBits(new Rectangle(0, 0, b.Width, b.Height), 
                                     ImageLockMode.ReadWrite, PixelFormat.Format24bppRgb);

    int stride = bmData.Stride;
    System.IntPtr Scan0 = bmData.Scan0;
    System.IntPtr Scan02 = bmData2.Scan0;

    unsafe
    {
        byte * p = (byte *)(void *)Scan0;
        byte * p2 = (byte *)(void *)Scan02;

        int nOffset = stride - b.Width*3;
        int nWidth = b.Width * 3;

        int nPixel = 0, nPixelMax = 0;

        p += stride;
        p2 += stride;

        for(int y=1;y<b.Height-1;++y)
        {
            p += 3;
            p2 += 3;

            for(int x=3; x < nWidth-3; ++x )
            {
                nPixelMax = Math.Abs((p2 - stride + 3)[0] - (p2+stride-3)[0]);
                nPixel = Math.Abs((p2 + stride + 3)[0] - (p2 - stride - 3)[0]);
                if (nPixel>nPixelMax) nPixelMax = nPixel;

                nPixel = Math.Abs((p2 - stride)[0] - (p2 + stride)[0]);
                if (nPixel>nPixelMax) nPixelMax = nPixel;

                nPixel = Math.Abs((p2+3)[0] - (p2 - 3)[0]);
                if (nPixel>nPixelMax) nPixelMax = nPixel;

                if (nPixelMax < nThreshold) nPixelMax = 0;

                p[0] = (byte) nPixelMax;

                ++ p;
                ++ p2;
            }

            p += 3 + nOffset;
            p2 += 3 + nOffset;
        }
    }

    b.UnlockBits(bmData);
    b2.UnlockBits(bmData2);

    return true;
    
}

Difference Edge Detection, 0 threshold Difference Edge Detection, 0 threshold

边缘增强

我们可以利用边缘检测的一个用途是增强图像中的边缘。概念很简单——我们应用一个边缘滤镜,但只存储派生出的值,如果它大于已有的值。因此,如果我们找到一个边缘,我们会将其变亮。最终结果是一个使物体轮廓变粗的滤镜。我们再次应用阈值,以便控制边缘必须多“硬”才能被增强。同样,我将提供代码,以及一个增强值为 0 和 127 的边缘的示例,但由于结果有点难以看清,我还会提供原始图像供您比较。别担心,您的浏览器已经缓存了起始图像,所以不会减慢页面加载速度 :-)

public static bool EdgeEnhance(Bitmap b, byte nThreshold)
{
    // This one works by working out the greatest difference between a 
    // nPixel and it's eight neighbours. The threshold allows softer 
    // edges to be forced down to black, use 0 to negate it's effect.
    Bitmap b2 = (Bitmap) b.Clone();

    // GDI+ still lies to us - the return format is BGR, NOT RGB.
    BitmapData bmData = b.LockBits(new Rectangle(0, 0, b.Width, b.Height), 
                                   ImageLockMode.ReadWrite, PixelFormat.Format24bppRgb);
    BitmapData bmData2 = b2.LockBits(new Rectangle(0, 0, b.Width, b.Height), 
                                    ImageLockMode.ReadWrite, PixelFormat.Format24bppRgb);

    int stride = bmData.Stride;
    System.IntPtr Scan0 = bmData.Scan0;
    System.IntPtr Scan02 = bmData2.Scan0;

    unsafe
    {
        byte * p = (byte *)(void *)Scan0;
        byte * p2 = (byte *)(void *)Scan02;

        int nOffset = stride - b.Width*3;
        int nWidth = b.Width * 3;

        int nPixel = 0, nPixelMax = 0;

        p += stride;
        p2 += stride;

        for (int y = 1; y < b.Height-1; ++y)
        {
            p += 3;
            p2 += 3;

            for (int x = 3; x < nWidth-3; ++x)
            {
                nPixelMax = Math.Abs((p2 - stride + 3)[0] - (p2 + stride - 3)[0]);

                nPixel = Math.Abs((p2 + stride + 3)[0] - (p2 - stride - 3)[0]);

                if (nPixel > nPixelMax) nPixelMax = nPixel;

                nPixel = Math.Abs((p2 - stride)[0] - (p2 + stride)[0]);

                if (nPixel > nPixelMax) nPixelMax = nPixel;

                nPixel = Math.Abs((p2 + 3)[0] - (p2 - 3)[0]);

                if (nPixel > nPixelMax) nPixelMax = nPixel;

                if (nPixelMax > nThreshold && nPixelMax > p[0])
                    p[0] = (byte) Math.Max(p[0], nPixelMax);

                ++ p;
                ++ p2;            
            }

            p += nOffset + 3;
            p2 += nOffset + 3;
        }
    }    

    b.UnlockBits(bmData);
    b2.UnlockBits(bmData2);

    return true;

My Kids Enhanced with a threshold of 0

My Kids Enhanced with a threshold of 127

我必须说,这里的效果有些微妙——我无法在图像中清晰地看到它,但在我打开程序并可以在它们之间切换时,它非常明显。

我希望您觉得这篇文章有用。关于图像处理我还有很多话要说,但还需要一段时间,因为我还有其他文章要写,并且需要进行一些项目来提高我在工作相关领域内的技能。但是,引用我最喜欢的演员的话来说,“I'll be back”(我会回来的)。

© . All rights reserved.