我有一个用于拉伸和绘制位图的 Winforms 应用程序,Graphics.DrawImage
我需要帮助来准确理解源像素如何映射到目标。
理想情况下,我想编写一个函数,例如:
Point MapPixel(Point p, Size src, Size dst)
它采用源图像上的像素坐标并返回在缩放目标上与其对应的“左上”像素的坐标。
为清楚起见,这是一个简单的示例,其中 2x2 位图缩放为 4x4:
箭头说明了如何将点 (1,0) 输入 MapPixel:
MapPixel(new Point(1, 0), new Size(2, 2), new Size(4, 4))
应该给出 (2,0) 的结果。
使用以下逻辑使 MapPixel 适用于上述示例很简单:
double scaleX = (double)dst.Width / (double)src.Width;
x_dst = (int)Math.Round((double)x_src * scaleX);
但是我注意到这种幼稚的实现会由于舍入dst.Width
而不是src.Width
. 在这种情况下,DrawImage 需要选择一些像素来绘制比其他像素更大的像素,以使图像适合,我在复制其逻辑时遇到了麻烦。
下面的代码通过将 2x1 位图缩放到几个不同的宽度来演示这个问题:
Bitmap src = new Bitmap(2, 1);
src.SetPixel(0, 0, Color.Red);
src.SetPixel(1, 0, Color.Blue);
Bitmap[] dst = {
new Bitmap(3, 1),
new Bitmap(5, 1),
new Bitmap(7, 1),
new Bitmap(9, 1)};
// Draw stretched images
foreach (Bitmap b in dst) {
using (Graphics g = Graphics.FromImage(b)) {
g.InterpolationMode = InterpolationMode.NearestNeighbor;
g.PixelOffsetMode = PixelOffsetMode.Half;
g.DrawImage(src, 0, 0, b.Width, b.Height);
}
}
这是原始src
图像和输出dst
图像的样子,以及一些显示 MapPixel 需要如何映射蓝色像素的数字:
我一生都无法弄清楚 DrawImage 是如何决定放大哪个像素的。它似乎有时向上取整,有时向下取整。我不在乎它选择哪个,但我需要它是可预测的,我的功能才能正常工作。
我尝试修改上面的 MapPixel 示例以使用MidpointRounding.AwayFromZero
,甚至替换Math.Round
为四舍五入到最接近的奇数的函数(稍微改善了结果,但仍然不完美)。我还尝试让 Graphics 类处理缩放 - 即我设置ScaleTransform
、调用DrawImageUnscaled
然后尝试使用它TransformPoints
来转换坐标。有趣的是,TransformPoints 方法的结果并不总是与 DrawImage 和 DrawImageUnscaled 所做的一致。
我也尝试挖掘 GDI+ 以获取提示,但还没有发现任何有用的东西。
我不想为了保持对它降落位置的可预测性而不得不单独绘制每个像素。
如果您想知道,我使用的原因InterpolationMode.NearestNeighbor
是避免抗锯齿(我需要保持单个像素的保真度),并且PixelOffsetMode.Half
包含在内是因为如果它不存在,那么 DrawImage 会将我的位图平移半个像素。
问题点的更多示例包括将 4px 缩放到 13px 时 x=7,以及将 4px 缩放到 17px 时 x=8。
如果需要,我可以发布完整的单元测试代码,允许您插入并验证 MapPixel 函数。到目前为止,我能够达到 100% 准确度的唯一方法是通过一个丑陋的 hack,它生成一个“提示”源图像,其中每个像素都设置为唯一的颜色。它通过检查提示图像中的颜色来映射坐标,然后在提示图像的缩放版本中查找该颜色。优化是可能的(例如,提示图像的宽度或高度为单像素,上面的幼稚逻辑用于猜测近似答案并从那里向外工作),但它仍然很丑陋。
如果有人能阐明 DrawImage 背后的管道并帮助我为 MapPixel 提出一个更简单(但仍然准确)的实现,我将不胜感激。