【问题标题】:Accurately mapping pixels scaled by Graphics.DrawImage精确映射由 Graphics.DrawImage 缩放的像素
【发布时间】:2011-07-06 07:18:36
【问题描述】:

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

理想情况下,我想编写如下函数:

 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 实现,我将不胜感激。

【问题讨论】:

  • 是否可以放弃 .DrawImage 并自己进行缩放?显然你不能直接绘制到屏幕上,而是必须通过中间位图,但你会获得 100% 的可预测性! :-)
  • 好建议 danbystrom,我确实开始研究那个替代方案。虽然我还是很好奇 DrawImage 是怎么做到的。
  • 您是否尝试过检查图形 ctm(使用 Graphics 属性 Transform)?有人会假设它是单位矩阵,但它可能值得检查。如果那里有某种转换,这可能会影响舍入。

标签: bitmap gdi+ scaling drawimage stretchblt


【解决方案1】:

尝试使用有理数(小学教给我们的等价分数)进行整数算术。它避免了舍入问题:

我将在一个轴上执行此操作(下面用 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,目标长度是 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 等)。

【讨论】:

  • 我非常感谢您的帖子,但我认为它不会让我更接近答案。据我了解,您正试图通过在最后一分钟之前避免舍入(或地板)来获得更好的精度。但是我不认为问题源于缺乏精确性。而是找到一个重现我从 DrawImage 获得的结果的公式的问题。不幸的是,您的公式无法做到这一点。 (在下一条评论中继续,3 月 30 日
  • 上一条评论的延续,从 3 月 30 日起)如果我在上面给出的 3x1 示例中使用它,计算像素 1 的 required_source_position(第一个蓝色像素)目的地),它会产生红色值而不是所需的蓝色值。即required_source_position = floor((current_dest_position*sourceN)/destN) = floor((1*1)/2) = floor(0.5) = 0 = red。如果我误解了您的答案,请随时纠正我并展示它如何复制上述所有示例的 DrawImage 结果。不过,再次感谢您对此进行尝试。
  • 在大多数情况下,如果算法是不稳定的(非确定性的)我不使用它。对于许多计算机科学教授的建议,有一些话要说,即在编码时始终了解程序的不变量。使用非确定性算法(例如机器学习)的唯一好处是当您需要概率和/或统计结果时。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-12-16
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-04-06
相关资源
最近更新 更多