Accurately mapping pixels scaled by Graphics.DrawImage

1.3k views Asked by At

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:

Zoom example

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:

Scaling examples

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.

1

There are 1 answers

3
Brian Jack On

Try integer arithmetic using rational numbers (equivalent fractions like they teach us in grade school). It avoids the rounding issue:

I'll do this in one axis (represented by N below)

original position may be thought of as in the range [0,origN] target (scaled) position may be thought of as in the range [0,destN]

meaning you can represent the original position rationally as:

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

To scale one iterates on the dest axis and uses equivalent fractions of rationals which may be stored as integers until the last-minute divide to calculate the source position::

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

srcN and destN are always one less than the total width (it has to do with position 0 being a valid pixel) so is source length was say 16 and dest length was 64 then srcN is 15 and destN is 63 (the range(k) operator in the above results in iterating over [0,k-1]). The floor is required if your language does not provide an easy way to force integer division which is most languages that use duck typed values (javascript, php, lua, python, etc.) in C/C++ you can just cast the division to int with:

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

This explains the process in one axis. It is easily used for other axes with nested loops for the other axes and replacing N in the above examples with the axis (X,Y,Z,etc).