我有一个使用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%准确度的唯一方法是通过一个丑陋的黑客来生成一个“提示”源图像,其中每个像素都设置为一种独特的颜色。它通过检查提示图像中的颜色来映射坐标,然后在提示图像的缩放版本中查找该颜色。优化是可能的(例如,提示图像具有单像素宽度或高度,并且上面的天真逻辑用于猜测近似答案并从那里向外工作)但它仍然是丑陋的。
如果有人能够了解DrawImage背后的管道并帮助我为MapPixel提供更简单(但仍然准确)的实现,我将不胜感激。
答案 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.