I have a Winforms application that uses Graphics.DrawImage
to stretch and draw a bitmap, and I need help understanding precisely how the source pixels map to the destination.
Ideally I want to write a function like:
Point MapPixel(Point p, Size src, Size dst)
which takes a pixel coordinate on the source image and returns the coordinate of the "upper-left" pixel corresponding to it on the scaled destination.
For clarity, here's a trivial example where a 2x2 bitmap is scaled to 4x4:
The arrow illustrates how feeding the point (1,0) into MapPixel:
MapPixel(new Point(1, 0), new Size(2, 2), new Size(4, 4))
should give a result of (2,0).
It's simple to make MapPixel work for the above example, using logic like:
double scaleX = (double)dst.Width / (double)src.Width;
x_dst = (int)Math.Round((double)x_src * scaleX);
However I've noticed this naive implementation produces errors due to rounding when dst.Width
is not an even multiple of src.Width
. In this case DrawImage needs to pick some pixels to draw larger than others in order to make the image fit, and I'm having trouble duplicating its logic.
The following code demonstrates the trouble by scaling a 2x1 bitmap to a few different widths:
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);
}
}
Here's what the original src
image and the output dst
images look like, along with some numbers showing how MapPixel needs to map the blue pixel:
I can't for the life of me figure out how DrawImage decides which pixel to make bigger. It seems to sometimes round up, and sometimes round down. I don't care which it chooses, but I need it to be predictable for my function to work correctly.
I've tried modifying my MapPixel example above to use MidpointRounding.AwayFromZero
, and even replaced Math.Round
with a function that rounds to the nearest odd number (slightly improves the results but still isn't perfect). I've also tried letting the Graphics class handle the scaling - i.e. I set ScaleTransform
, call DrawImageUnscaled
and then try to use TransformPoints
to convert the coordinates. Interestingly, the results of the TransformPoints method aren't always consistent with what DrawImage and DrawImageUnscaled do, either.
I've also tried digging into GDI+ for hints, but haven't turned up anything useful yet.
I don't want to have to resort to painting each pixel individually just to maintain predictability of where it will land.
In case you're wondering, the reason I'm using InterpolationMode.NearestNeighbor
is to avoid anti-aliasing (I need to maintain fidelity of the individual pixels), and PixelOffsetMode.Half
is included because if it's not there then DrawImage pans my bitmap by half a pixel.
A couple more examples of problem points include x=7 when scaling 4px to 13px, and x=8 when scaling 4px to 17px.
I can post complete unit test code if desired which allows you to drop in and verify a MapPixel function. So far the only way I've been able to achieve 100% accuracy is through an ugly hack that generates a "hint" source image where each pixel is set to a unique color. It maps a coordinate by checking what color it is in the hint image, then looking for that color in a scaled version of the hint image. Optimizations are possible (e.g. hint images are of single-pixel width or height, and the naive logic above is used to guess the approximate answer and work outward from there) but it's still ugly.
I'd appreciate if someone could shed some light on the plumbing behind DrawImage and help me come up with a simpler (but still accurate) implementation for MapPixel.