准确映射由Graphics.DrawImage缩放的像素

时间:2011-03-01 07:03:10

标签: bitmap gdi+ scaling drawimage stretchblt

我有一个使用Graphics.DrawImage来拉伸和绘制位图的Winforms应用程序,我需要帮助理解源像素如何映射到目标。

理想情况下,我想写一个像:

这样的函数
 Point MapPixel(Point p, Size src, Size dst)

取源图像上的像素坐标,并返回缩放目标上与其对应的“左上”像素的坐标。

为清楚起见,这是一个简单的例子,其中2x2位图缩放为4x4:

Zoom example

箭头说明了如何将点(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如何映射蓝色像素的数字:

Scaling examples

我不能为我的生活弄清楚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%准确度的唯一方法是通过一个丑陋的黑客来生成一个“提示”源图像,其中每个像素都设置为一种独特的颜色。它通过检查提示图像中的颜色来映射坐标,然后在提示图像的缩放版本中查找该颜色。优化是可能的(例如,提示图像具有单像素宽度或高度,并且上面的天真逻辑用于猜测近似答案并从那里向外工作)但它仍然是丑陋的。

如果有人能够了解DrawImage背后的管道并帮助我为MapPixel提供更简单(但仍然准确)的实现,我将不胜感激。

1 个答案:

答案 0 :(得分: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 ++中使用duck typed值(javascript,php,lua,python等)的语言,你可以将除法转换为int用:

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

这解释了一个轴的过程。它很容易用于其他轴,其他轴带有嵌套循环,并用轴(X,Y,Z等)代替上述例子中的N.