3

我有一个用于拉伸和绘制位图的 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 提出一个更简单(但仍然准确)的实现,我将不胜感激。

4

1 回答 1

0

尝试使用有理数的整数算术(相当于小学教我们的分数)。它避免了舍入问题:

我将在一个轴上执行此操作(由下面的 N 表示)

原始位置可以被认为在 [0,origN] 范围内 目标(缩放)位置可以被认为在 [0,destN] 范围内

这意味着您可以将原始位置合理地表示为:

 origPos                    destPos
---------  for orig, and,  --------- for dest
  origN                      destN

要缩放一个在 dest 轴上迭代并使用等效分数的有理数,这些分数可以存储为整数,直到最后一分钟除以计算源位置::

for current_dest_position in range(destLength):
    required_source_position=floor( (current_dest_position*sourceN)/destN )

srcN 和 destN 总是比总宽度小一(它与位置 0 是有效像素有关)所以源长度是 16,dest 长度是 64,然后 srcN 是 15,destN 是 63(范围(k)上面的运算符导致迭代 [0,k-1])。如果您的语言没有提供一种简单的方法来强制整数除法,则需要地板,这是大多数在 C/C++ 中使用鸭子类型值(javascript、php、lua、python 等)的语言,您可以将除法转换为 int和:

required_source_position=(int)((current_dest_position*sourceN)/destN);

这解释了一个轴上的过程。它很容易用于其他轴,其他轴具有嵌套循环,并将上述示例中的 N 替换为轴(X、Y、Z 等)。

于 2011-03-29T21:42:07.207 回答