C# 和 GDI+ 图像处理入门,第四部分 - 双线性过滤器和调整大小






4.80/5 (45投票s)
第四部分介绍了如何编写一个可以调整图像大小并使用双线性过滤的滤镜。
我们又来了...
好吧,这是本系列的第四部分,感谢您一直坚持到现在。我希望为未来的文章打下一些基础,因为未来的文章会足够复杂,我不想在那个阶段加入双线性过滤,所以我在这里介绍它。简而言之,双线性过滤是一种提高我们选择像素精度的方法,它允许我们在绘制的像素之间进行选择。真的!为了说明它的效果,我们将首先编写一个resize滤镜,然后我们将添加一个双线性滤镜来看看它对最终结果的影响。
一个调整大小的滤镜。
如果你想任意调整图像大小,最简单的方法是计算源图像和目标图像在x和y轴上的差异因子,然后使用该因子来确定源图像中的哪个像素映射到目标图像中正在放置的颜色。注意,在这个滤镜中,我遍历目标图像并从中计算源像素,这确保了目标图像中的所有像素都得到了填充。
SetPixel?
在我展示代码之前,您会注意到我这次选择了使用Set/GetPixel
而不是获取指向位图数据的指针。这对我来说做了两件事:首先,它意味着我的代码不是“不安全”的;其次,它使代码变得简单得多,这在我们添加双线性过滤时很有帮助,因为双线性过滤已经做了足够多的工作,而我的示例不会因为需要的所有指针查找代码而显得杂乱,您将看到这一点。
代码
这是我用于调整位图大小的函数代码,它用先前复制的数据填充该位图,然后将其返回。请注意,与我其他的函数不同,我发现我必须返回新的Bitmap
,因为当我创建一个新尺寸的位图时,它不再是“in”参数所引用的同一个位图,因此我无法返回一个 bool
来表示成功。
public static Bitmap Resize(Bitmap b, int nWidth, int nHeight, bool bBilinear) { Bitmap bTemp = (Bitmap)b.Clone(); b = new Bitmap(nWidth, nHeight, bTemp.PixelFormat); double nXFactor = (double)bTemp.Width/(double)nWidth; double nYFactor = (double)bTemp.Height/(double)nHeight; if (bBilinear) { // Not yet 80) } else { for (int x = 0; x < b.Width; ++x) for (int y = 0; y < b.Height; ++y) b.SetPixel(x, y, bTemp.GetPixel((int)(Math.Floor(x * nXFactor)), (int)(Math.Floor(y * nYFactor)))); } return b; }
为了突出这种滤镜产生的瑕疵,我用Calvin的图像,多次增加了宽度并减小了高度(都减少10像素),得到了以下结果。


正如您所见,情况开始相当迅速地恶化。
双线性过滤
我们上面遇到的问题是,我们很多时候都没有抓取到我们想要的像素。例如,如果我们把一个100 x 100的图像缩放到160 x 110,那么我们的X轴比例是100/160,即0.625。换句话说,要填充第43列,我们需要查找列(43 * 0.625),即26.875。显然,我们无法查找这样的值,我们最终会得到第27列。在这种情况下,差异很小,但显然我们可以得到包含0.5的小数值,正好位于两个现有像素的中间。上面的图像显示了如此小的数值舍入如何累积导致图像质量下降。解决方案显然是查找值而不进行舍入。我们如何查找一个不存在的像素?我们从现有像素的值进行插值。通过读取所有周围像素的值,然后根据像素值的十进制部分对这些值进行加权,我们可以构造出子像素的值。例如,在上面的例子中,我们将第26列的值乘以0.875,将第27列的值乘以0.125来找到所需的确切值。
为了使示例更清晰,我使用了GetPixel
来读取我们想要查找的子像素周围区域的四个像素。在未来的示例中,我将使用直接像素访问,这会更快,但也会复杂得多。变量名选择是为了帮助阐明正在发生的事情。这是上面缺失的代码,当 bBilinear = true
时执行的代码。
if (bBilinear) { double fraction_x, fraction_y, one_minus_x, one_minus_y; int ceil_x, ceil_y, floor_x, floor_y; Color c1 = new Color(); Color c2 = new Color(); Color c3 = new Color(); Color c4 = new Color(); byte red, green, blue; byte b1, b2; for (int x = 0; x < b.Width; ++x) for (int y = 0; y < b.Height; ++y) { // Setup floor_x = (int)Math.Floor(x * nXFactor); floor_y = (int)Math.Floor(y * nYFactor); ceil_x = floor_x + 1; if (ceil_x >= bTemp.Width) ceil_x = floor_x; ceil_y = floor_y + 1; if (ceil_y >= bTemp.Height) ceil_y = floor_y; fraction_x = x * nXFactor - floor_x; fraction_y = y * nYFactor - floor_y; one_minus_x = 1.0 - fraction_x; one_minus_y = 1.0 - fraction_y; c1 = bTemp.GetPixel(floor_x, floor_y); c2 = bTemp.GetPixel(ceil_x, floor_y); c3 = bTemp.GetPixel(floor_x, ceil_y); c4 = bTemp.GetPixel(ceil_x, ceil_y); // Blue b1 = (byte)(one_minus_x * c1.B + fraction_x * c2.B); b2 = (byte)(one_minus_x * c3.B + fraction_x * c4.B); blue = (byte)(one_minus_y * (double)(b1) + fraction_y * (double)(b2)); // Green b1 = (byte)(one_minus_x * c1.G + fraction_x * c2.G); b2 = (byte)(one_minus_x * c3.G + fraction_x * c4.G); green = (byte)(one_minus_y * (double)(b1) + fraction_y * (double)(b2)); // Red b1 = (byte)(one_minus_x * c1.R + fraction_x * c2.R); b2 = (byte)(one_minus_x * c3.R + fraction_x * c4.R); red = (byte)(one_minus_y * (double)(b1) + fraction_y * (double)(b2)); b.SetPixel(x,y, System.Drawing.Color.FromArgb(255, red, green, blue)); } }
结果如下
您可以看到,这是一个好得多的结果。看起来它经过了一个柔化滤镜,但比上面的效果好多了。通过一个更复杂的称为双三次过滤的过程,可以获得稍好的代码,但我无意涵盖它,仅仅是因为我从未做过。
下一步
正如我上面所说,这篇文章的重点是说明双线性过滤是如何工作的。双线性滤镜将在我的下一篇文章中使用,该文章将讨论如何从头开始编写一个针对特定过程优化的滤镜,而不是像我们目前所使用的那种通用过程。届时我们将重新实现双线性滤镜以使其更优化,但希望这个版本已经帮助您理解了过程的这一部分,以便我们在下一篇文章中可以专注于其他方面。