C# 和 GDI+ 图像处理入门 第 5 部分 - 位移滤镜,包括扭曲






4.90/5 (141投票s)
2002 年 12 月 24 日
6分钟阅读

989715

10277
在第五篇文章中,我们构建了一个框架,用于生成通过改变像素位置而非颜色的滤镜。
引言
再次欢迎大家阅读我的图像处理系列文章。这次我想谈谈位移滤镜。大多数关于图像处理的信息都与前面的文章类似,讨论的是通过改变像素的颜色值来改变图像。而今天我们要看的滤镜则是通过改变每个像素的位置来改变图像。我收到了关于上一篇文章的大量电子邮件,询问我为什么要费力编写代码来调整图像大小。答案是上一篇文章解释了双线性过滤,这是一种移动像素以便它们被绘制到物理像素之间的理论位置的方法。我们将在本文中使用此功能,但不会详细解释,而是建议您回顾一下上一篇文章[^],如果您不熟悉双线性过滤的话。
框架
我们将再次从实现一个可以用来创建滤镜的框架开始。我们的基本方法是创建一个二维点数组。该数组的大小与图像相同,每个点将存储该索引处像素的新位置。我们将通过两种方式实现这一点:一种存储相对位置,另一种存储绝对位置。最后,我们将创建自己的 Point 结构,该结构包含两个 double
值而不是 int
值,我们将使用它来编写执行双线性过滤的实现。
C# 中的数组
我必须承认,在这之前我从未用 C# 做过任何关于二维数组的事情,而且它们非常棒。代码如下所示:
Point [,] pt = new Point[nWidth, nHeight];
这会动态创建一个二维数组,我们可以使用像 pt[2, 3]
这样的表示法来访问像素,而不是 C++ 的 pt[2][3]
。这不仅比 C++ 更简洁,而且 Point[,]
是一个可以传递给函数的有效参数,使得传递编译时大小未知数组变得轻而易举。
偏移滤镜
我们将编写的第一个辅助函数将采用相对位置,例如,如果我们想将像素 2, 4 移动到位置 5, 2,那么 pt[2, 4]
将等于 3, -2。我们可以使用 Set/GetPixel
来做到这一点,但我们将继续使用直接访问,这可能更快。由于我们现在必须跨越任意数量的行来访问图像中的任何像素,我们将通过使用 BitmapData
的 Stride
成员来实现,我们可以将其乘以 Y 值来获取向下多少行。然后将我们的 X 值乘以 3,因为我们使用的格式是每像素 3 字节(24 位)。代码如下所示:
public static bool OffsetFilter(Bitmap b, Point[,] offset ) { Bitmap bSrc = (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 bmSrc = bSrc.LockBits(new Rectangle(0, 0, bSrc.Width, bSrc.Height), ImageLockMode.ReadWrite, PixelFormat.Format24bppRgb); int scanline = bmData.Stride; System.IntPtr Scan0 = bmData.Scan0; System.IntPtr SrcScan0 = bmSrc.Scan0; unsafe { byte * p = (byte *)(void *)Scan0; byte * pSrc = (byte *)(void *)SrcScan0; int nOffset = bmData.Stride - b.Width*3; int nWidth = b.Width; int nHeight = b.Height; int xOffset, yOffset; for(int y=0;y < nHeight;++y) { for(int x=0; x < nWidth; ++x ) { xOffset = offset[x,y].X; yOffset = offset[x,y].Y; p[0] = pSrc[((y+yOffset) * scanline) + ((x+xOffset) * 3)]; p[1] = pSrc[((y+yOffset) * scanline) + ((x+xOffset) * 3) + 1]; p[2] = pSrc[((y+yOffset) * scanline) + ((x+xOffset) * 3) + 2]; p += 3; } p += nOffset; } } b.UnlockBits(bmData); bSrc.UnlockBits(bmSrc); return true; }
您会注意到框架中有一个布尔成功代码,但它并没有真正使用。OffsetFilterAbs 的功能几乎相同,只是如果我们想将任何像素移动到位置 3, 2,那么为该位置存储的点将是 3, 2 而不是偏移量。OffsetFilterAntiAlias 的复杂性要高得多,因为它实现了一个双线性过滤器,如果您不理解该代码,请参考上一篇文章 [^]。
现在,滤镜
所有滤镜的基本格式都是创建一个数组,用值(相对或绝对)填充它,然后将位图和数组传递给相应的函数。其中许多滤镜涉及大量的三角函数,我不会详细讨论,而是专注于滤镜的功能及其参数。
Flip
我认为,如果我们要在图像中移动像素,最明显的事情就是翻转图像。我将展示此代码,因为它是一个简单的示例,它将比扭曲等后续示例更能突出底层过程。最终结果很明显,因此我不会用示例来消耗您的带宽。
public static bool Flip(Bitmap b, bool bHorz, bool bVert) { Point [,] ptFlip = new Point[b.Width,b.Height]; int nWidth = b.Width; int nHeight = b.Height; for (int x = 0; x < nWidth; ++x) for (int y = 0; y < nHeight; ++y) { ptFlip[x, y].X = (bHorz) ? nWidth - (x+1) : x; ptFlip[x,y].Y = (bVert) ? nHeight - (y + 1) : y; } OffsetFilterAbs(b, ptFlip); return true; }
随机抖动
此滤镜接收一个数字,并将每个像素按在该数字范围内的随机量进行移动。这非常有效,多次执行会产生非常有效的油画效果。
![]() |
![]() |
扭曲
这个滤镜是我个人的圣杯,也是我构思这些东西的动力。基本上,它从中间开始,并以圆形移动,随着旋转度数的增加而增加半径。由于使用了三角函数,它极大地受益于可选项双线性过滤器。我将同时展示此图像的正常示例和双线性过滤示例,然后所有提供过滤器的其他图像,我都将显示已应用过滤器。传递的参数是一个非常小的数字,示例中为 0.05。
![]() |
![]() |
![]() |
球体
球体滤镜是通过试验产生的一个滤镜示例。我试图实现图像被包裹在一个球体上的效果。我认为效果不是很好,但它很有趣,并且是此类想法的一个起点。
![]() |
![]() |
时间扭曲
这是另一个有趣的滤镜,它会导致图像在远处消失时发生扭曲。示例使用了 15 的因子。
![]() |
![]() |
摩尔纹
在玩转扭曲想法时,我发现如果我增加半径移出的速率,我就可以得到一个宽扭曲,或者使用正确的参数,就可以产生摩尔纹效果。示例使用了 3 的因子。
![]() |
![]() |
水波
一个更有用的滤镜是使物体看起来像在水下的滤镜。可以通过添加额外的伪影(如涟漪和波纹)来改进。实际上,此滤镜在 X 和 Y 方向上都通过正弦波。
![]() |
![]() |
像素化
这是一个可以通用实现的滤镜示例,但用特定代码实现会更好。像素化是指当图像放大时,曲线会变得块状。此滤镜通过创建与左上角颜色相同的块来提供马赛克效果,还可以绘制线条来标记单个块。更好的实现将使用所讨论块内的平均颜色,而不是左上角的颜色,但即使这样也效果相当不错。
![]() |
![]() |
![]() |
结论
提供的滤镜旨在展示使用位移框架可以实现的一些功能,并提供各种示例,您可以从中推导出自己的滤镜。我希望您觉得这些示例有用,并希望该框架能为您探索底层概念提供一个良好的起点。我希望接下来演示如何编写一个特定的单次滤镜,并讨论为什么这始终是最灵活的方法,尽管变换矩阵和位移技术是建立初步想法和实现通用概念的绝佳方法。
更新
版本 1.01 :在三个主要滤镜中添加了一些边界检查代码,以便在传入任何值超出范围时滤镜不会崩溃。一些滤镜会在边缘生成一些超出范围的值,通过这种方式检查可以处理比创建图像边框更多的值。