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

C# 和 GDI+ 图像处理入门,第二部分 - 卷积过滤器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.91/5 (116投票s)

2002年3月24日

CPOL

8分钟阅读

viewsIcon

1694526

downloadIcon

15462

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

Sample Image

概述

欢迎回来阅读本系列的第二部分。本部分将介绍卷积滤波器的世界。这也是我们程序提供一级撤销功能的第一个版本。我们稍后会在此基础上进行扩展,但目前我认为必须让您能够撤销实验,而不必每次都重新加载图像。

那么什么是卷积滤波器呢?本质上,它是一个矩阵,如下所示

Sample Image

其思想是,我们正在处理的像素及其周围的八个像素都具有一个权重。矩阵的总值除以一个因子,并且可以选择将一个偏移量添加到最终值。上面的矩阵称为单位矩阵,因为图像通过它时不会改变。通常,因子是矩阵中所有值相加得到的值,这确保最终值将在 0-255 范围内。如果不是这种情况,例如,在浮雕滤波器中,值为 0,则通常使用 127 的偏移量。我还应该提到,卷积滤波器有各种尺寸,7x7 并非闻所未闻,特别是边缘检测滤波器不是对称的。此外,滤波器越大,我们无法处理的像素越多,因为我们无法处理没有我们矩阵所需数量的周围像素的像素。在我们的例子中,图像的外边缘深度为一像素的区域将不会被处理。

一个框架

首先,我们需要建立一个框架来编写这些滤波器,否则我们会发现自己一遍又一遍地编写相同的代码。由于我们的滤波器现在依赖于周围的值来获得结果,我们需要一个源位图和一个目标位图。我倾向于创建传入位图的副本,并使用该副本作为源,因为它最终会被丢弃。为了方便这一点,我定义了一个矩阵类,如下所示

public class ConvMatrix
{
    public int TopLeft = 0, TopMid = 0, TopRight = 0;
    public int MidLeft = 0, Pixel = 1, MidRight = 0;
    public int BottomLeft = 0, BottomMid = 0, BottomRight = 0;
    public int Factor = 1;
    public int Offset = 0;
    public void SetAll(int nVal)
    {
        TopLeft = TopMid = TopRight = MidLeft = Pixel = MidRight = 
                  BottomLeft = BottomMid = BottomRight = nVal;
    }
}

我相信你注意到了它默认是一个单位矩阵。我还定义了一个方法,将矩阵的所有元素设置为相同的值。

像素处理代码比我们上一篇文章复杂,因为我们需要访问九个像素和两个位图。我通过定义用于跳过一行和两行的常量来实现这一点(因为我们希望在主循环中尽可能避免计算,我们定义了两者而不是将其自身加一或乘以 2)。然后我们可以使用这些值来编写我们的代码。由于我们对不同颜色的初始偏移量是 0、1 和 2,我们最终将 3 和 6 添加到这些值中的每一个,以创建三个像素的索引,并使用我们的常量添加行。为了确保我们没有任何值从图像底部跳到顶部,我们需要创建一个 int,用于计算每个像素值,然后钳制并存储。这是整个函数

public static bool Conv3x3(Bitmap b, ConvMatrix m)
{
    // Avoid divide by zero errors
    if (0 == m.Factor)
        return false; Bitmap 
    
    // GDI+ still lies to us - the return format is BGR, NOT RGB. 
    bSrc = (Bitmap)b.Clone();
    BitmapData bmData = b.LockBits(new Rectangle(0, 0, b.Width, b.Height), 
                        ImageLockMode.ReadWrite, 
                        PixelFormat.Format24bppRgb); 
    BitmapData bmSrc = bSrc.LockBits(new Rectangle(0, 0, bSrc.Width, bSrc.Height), 
                       ImageLockMode.ReadWrite, 
                       PixelFormat.Format24bppRgb); 
    int stride = bmData.Stride; 
    int stride2 = stride * 2; 

    System.IntPtr Scan0 = bmData.Scan0; 
    System.IntPtr SrcScan0 = bmSrc.Scan0; 

    unsafe { 
        byte * p = (byte *)(void *)Scan0; 
        byte * pSrc = (byte *)(void *)SrcScan0; 
        int nOffset = stride - b.Width*3; 
        int nWidth = b.Width - 2; 
        int nHeight = b.Height - 2; 
        
        int nPixel; 
        
        for(int y=0;y < nHeight;++y) 
        { 
            for(int x=0; x < nWidth; ++x ) 
            {
                nPixel = ( ( ( (pSrc[2] * m.TopLeft) + 
                    (pSrc[5] * m.TopMid) + 
                    (pSrc[8] * m.TopRight) + 
                    (pSrc[2 + stride] * m.MidLeft) + 
                    (pSrc[5 + stride] * m.Pixel) + 
                    (pSrc[8 + stride] * m.MidRight) + 
                    (pSrc[2 + stride2] * m.BottomLeft) + 
                    (pSrc[5 + stride2] * m.BottomMid) + 
                    (pSrc[8 + stride2] * m.BottomRight)) 
                    / m.Factor) + m.Offset); 
                    
                if (nPixel < 0) nPixel = 0; 
                if (nPixel > 255) nPixel = 255; 
                p[5 + stride]= (byte)nPixel; 
                
                nPixel = ( ( ( (pSrc[1] * m.TopLeft) + 
                    (pSrc[4] * m.TopMid) + 
                    (pSrc[7] * m.TopRight) + 
                    (pSrc[1 + stride] * m.MidLeft) + 
                    (pSrc[4 + stride] * m.Pixel) + 
                    (pSrc[7 + stride] * m.MidRight) + 
                    (pSrc[1 + stride2] * m.BottomLeft) + 
                    (pSrc[4 + stride2] * m.BottomMid) + 
                    (pSrc[7 + stride2] * m.BottomRight)) 
                    / m.Factor) + m.Offset); 
                    
                if (nPixel < 0) nPixel = 0; 
                if (nPixel > 255) nPixel = 255; 
                p[4 + stride] = (byte)nPixel; 
                
                nPixel = ( ( ( (pSrc[0] * m.TopLeft) + 
                               (pSrc[3] * m.TopMid) + 
                               (pSrc[6] * m.TopRight) + 
                               (pSrc[0 + stride] * m.MidLeft) + 
                               (pSrc[3 + stride] * m.Pixel) + 
                               (pSrc[6 + stride] * m.MidRight) + 
                               (pSrc[0 + stride2] * m.BottomLeft) + 
                               (pSrc[3 + stride2] * m.BottomMid) + 
                               (pSrc[6 + stride2] * m.BottomRight)) 
                    / m.Factor) + m.Offset); 
                    
                if (nPixel < 0) nPixel = 0; 
                if (nPixel > 255) nPixel = 255; 
                p[3 + stride] = (byte)nPixel; 
                
                p += 3; 
                pSrc += 3; 
            } 
            
            p += nOffset; 
            pSrc += nOffset; 
        } 
    } 
    
    b.UnlockBits(bmData); 
    bSrc.UnlockBits(bmSrc); 
    return true; 
}

这不是你想要一遍又一遍编写的东西,对吗?现在我们可以使用我们的 ConvMatrix 类来定义滤波器,并只需将它们传递给这个函数,它会为我们完成所有繁琐的工作。

平滑

鉴于我向您介绍的此滤波器的工作原理,很明显我们如何创建平滑效果。我们将值赋给所有像素,以便每个像素的权重分布到周围区域。代码如下所示

public static bool Smooth(Bitmap b, int nWeight /* default to 1 */)
{
    ConvMatrix m = new ConvMatrix();
    m.SetAll(1);
    m.Pixel = nWeight;
    m.Factor = nWeight + 8;

    return  BitmapFilter.Conv3x3(b, m);
}

如您所见,在我们的框架下编写滤波器非常简单。大多数这些滤波器至少有一个参数,不幸的是 C# 没有默认值,所以我将它们放在注释中供您参考。多次应用此滤波器的最终结果如下所示

My daughter, Hannah      She's so smooth

高斯模糊

高斯模糊滤波器定位图像中的显著颜色过渡,然后创建中间颜色以柔化边缘。滤波器如下所示

高斯模糊
1 2 1
2 4 2
1 2 1 /16+0

中间值是您可以使用提供的滤波器进行更改的值,您可以看到默认值特别会产生圆形效果,像素距离边缘越远,权重越小。实际上,这种平滑会生成一个图像,类似于失焦的镜头。

My daughter, Hannah      Gaussian Blur

锐化

另一方面,锐化滤波器看起来像这样

锐化
0 -2 0
-2 11 -2
0 -2 0 /3+0

如果你将它与高斯模糊进行比较,你会发现它几乎完全相反。它通过增强像素之间的差异来锐化图像。被赋予负权重的像素与被修改的像素之间的差异越大,主像素值的变化就越大。锐化程度可以通过改变中心权重来调整。为了更好地展示效果,我在此示例中从模糊的图片开始。

My daughter, Hannah      Gaussian Blur, then Sharpened

均值移除

均值移除滤波器也是一种锐化滤波器,它看起来像这样

均值移除
-1 -1 -1
-1 9 -1
-1 -1 -1 /1+0

与之前的滤波器不同,之前的滤波器只在水平和垂直方向上工作,这个滤波器也向对角线方向扩散其影响,对同一源图像产生以下结果。再次强调,中心值是需要改变以改变效果程度的值。

My daughter, Hannah      Gaussian Blur, then Mean Removal

浮雕

卷积滤波器最壮观的效果可能是浮雕。浮雕实际上只是一种边缘检测滤波器。我将在之后介绍另一种简单的边缘检测滤波器,您会发现它们非常相似。边缘检测通常通过沿着轴线抵消正值和负值来工作,因此两个像素之间的差异越大,返回的值就越高。使用浮雕滤波器,由于我们的滤波器值相加为 0 而不是 1,我们使用 127 的偏移量来使图像变亮,否则大部分图像都会被钳制为黑色。

我实现的滤波器看起来像这样

浮雕拉普拉斯
-1 0 -1
0 4 0
-1 0 -1 /1+127

它看起来像这样

My daughter, Hannah      Hannah Embossed

如您所见,这种浮雕在两个对角线方向上都有效。我还包含了一个自定义对话框,您可以在其中输入自己的滤镜,您可能想尝试以下一些用于浮雕的滤镜

水平/垂直
0 -1 0
-1 4 -1
0 -1 0 /1+127
所有方向
-1 -1 -1
-1 8 -1
-1 -1 -1 /1+127
有损
1 -2 1
-2 4 -2
-2 1 -2 /1+127
仅水平
0 0 0
-1 2 -1
0 0 0 /1+127
仅垂直
0 -1 0
0 0 0
0 1 0 /1+127

水平和垂直方向的滤镜之所以不同,只是为了展示两种变体。您也可以通过围绕中心点旋转值来旋转这些滤镜。例如,您会注意到我使用的滤镜是水平/垂直滤镜旋转一度后的滤镜。

不要得意忘形

虽然这有点酷,但如果你运行 Photoshop,你会注意到它提供了比我这里展示的浮雕更多的功能。Photoshop 使用更具体编写的滤镜创建浮雕,并且只有部分功能可以通过卷积滤镜模拟。我花了一些时间编写了一个更灵活的浮雕滤镜,一旦我们介绍了双线性滤波等,我可能会写一篇关于更完整的浮雕滤镜的文章。

边缘检测

最后,一个简单的边缘检测滤波器,作为下一篇文章的预告,下一篇文章将探讨多种检测边缘的方法。该滤波器看起来像这样

边缘检测
1 1 1
0 0 0
-1 -1 -1 /1+127

像所有边缘检测滤镜一样,此滤镜不关心被检测像素的值,而是关心其周围像素之间的差异。目前它将检测水平边缘,并且像浮雕滤镜一样,可以旋转。正如我之前所说,浮雕滤镜本质上是在进行边缘检测,这一个只是增强了效果。

 

My daughter, Hannah      Hannahs Edges

未来的计划

下一篇文章将介绍各种边缘检测方法。我还鼓励您在网上搜索卷积滤波器。comp.graphics.algorithms 新闻组倾向于 3D 图形,但如果您在 Google 新闻等档案中搜索“卷积”,您会在自定义对话框中找到更多可以尝试的想法。

© . All rights reserved.