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






4.91/5 (116投票s)
本系列文章的第二篇,将用 C# 和 GDI+ 构建一个图像处理库。
概述
欢迎回来阅读本系列的第二部分。本部分将介绍卷积滤波器的世界。这也是我们程序提供一级撤销功能的第一个版本。我们稍后会在此基础上进行扩展,但目前我认为必须让您能够撤销实验,而不必每次都重新加载图像。
那么什么是卷积滤波器呢?本质上,它是一个矩阵,如下所示
其思想是,我们正在处理的像素及其周围的八个像素都具有一个权重。矩阵的总值除以一个因子,并且可以选择将一个偏移量添加到最终值。上面的矩阵称为单位矩阵,因为图像通过它时不会改变。通常,因子是矩阵中所有值相加得到的值,这确保最终值将在 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# 没有默认值,所以我将它们放在注释中供您参考。多次应用此滤波器的最终结果如下所示
高斯模糊
高斯模糊滤波器定位图像中的显著颜色过渡,然后创建中间颜色以柔化边缘。滤波器如下所示
高斯模糊 | |||
---|---|---|---|
1 | 2 | 1 | |
2 | 4 | 2 | |
1 | 2 | 1 | /16+0 |
中间值是您可以使用提供的滤波器进行更改的值,您可以看到默认值特别会产生圆形效果,像素距离边缘越远,权重越小。实际上,这种平滑会生成一个图像,类似于失焦的镜头。
锐化
另一方面,锐化滤波器看起来像这样
锐化 | |||
---|---|---|---|
0 | -2 | 0 | |
-2 | 11 | -2 | |
0 | -2 | 0 | /3+0 |
如果你将它与高斯模糊进行比较,你会发现它几乎完全相反。它通过增强像素之间的差异来锐化图像。被赋予负权重的像素与被修改的像素之间的差异越大,主像素值的变化就越大。锐化程度可以通过改变中心权重来调整。为了更好地展示效果,我在此示例中从模糊的图片开始。
均值移除
均值移除滤波器也是一种锐化滤波器,它看起来像这样
均值移除 | |||
---|---|---|---|
-1 | -1 | -1 | |
-1 | 9 | -1 | |
-1 | -1 | -1 | /1+0 |
与之前的滤波器不同,之前的滤波器只在水平和垂直方向上工作,这个滤波器也向对角线方向扩散其影响,对同一源图像产生以下结果。再次强调,中心值是需要改变以改变效果程度的值。
浮雕
卷积滤波器最壮观的效果可能是浮雕。浮雕实际上只是一种边缘检测滤波器。我将在之后介绍另一种简单的边缘检测滤波器,您会发现它们非常相似。边缘检测通常通过沿着轴线抵消正值和负值来工作,因此两个像素之间的差异越大,返回的值就越高。使用浮雕滤波器,由于我们的滤波器值相加为 0 而不是 1,我们使用 127 的偏移量来使图像变亮,否则大部分图像都会被钳制为黑色。
我实现的滤波器看起来像这样
浮雕拉普拉斯 | |||
---|---|---|---|
-1 | 0 | -1 | |
0 | 4 | 0 | |
-1 | 0 | -1 | /1+127 |
它看起来像这样
如您所见,这种浮雕在两个对角线方向上都有效。我还包含了一个自定义对话框,您可以在其中输入自己的滤镜,您可能想尝试以下一些用于浮雕的滤镜
|
|
|
|
|
水平和垂直方向的滤镜之所以不同,只是为了展示两种变体。您也可以通过围绕中心点旋转值来旋转这些滤镜。例如,您会注意到我使用的滤镜是水平/垂直滤镜旋转一度后的滤镜。
不要得意忘形
虽然这有点酷,但如果你运行 Photoshop,你会注意到它提供了比我这里展示的浮雕更多的功能。Photoshop 使用更具体编写的滤镜创建浮雕,并且只有部分功能可以通过卷积滤镜模拟。我花了一些时间编写了一个更灵活的浮雕滤镜,一旦我们介绍了双线性滤波等,我可能会写一篇关于更完整的浮雕滤镜的文章。
边缘检测
最后,一个简单的边缘检测滤波器,作为下一篇文章的预告,下一篇文章将探讨多种检测边缘的方法。该滤波器看起来像这样
边缘检测 | |||
---|---|---|---|
1 | 1 | 1 | |
0 | 0 | 0 | |
-1 | -1 | -1 | /1+127 |
像所有边缘检测滤镜一样,此滤镜不关心被检测像素的值,而是关心其周围像素之间的差异。目前它将检测水平边缘,并且像浮雕滤镜一样,可以旋转。正如我之前所说,浮雕滤镜本质上是在进行边缘检测,这一个只是增强了效果。
未来的计划
下一篇文章将介绍各种边缘检测方法。我还鼓励您在网上搜索卷积滤波器。comp.graphics.algorithms 新闻组倾向于 3D 图形,但如果您在 Google 新闻等档案中搜索“卷积”,您会在自定义对话框中找到更多可以尝试的想法。