3

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.

rkagerer
  • 4,157
  • 1
  • 26
  • 29
  • 1
    Would it be possible to abandon .DrawImage and do the scaling yourself? Obviuosly you can't draw directly to the scrren then but have to go throgh an intermediate bitmap, but you'd gain 100% predictability! :-) – Dan Byström Mar 01 '11 at 11:54
  • Good suggestion danbystrom, and I did actually start working on that alternative. Though I'm still curious how DrawImage does it. – rkagerer Mar 01 '11 at 12:09
  • Have you tried checking the graphics ctm (with the Graphics property Transform)? One would assume it is the identity matrix, but it could be worth checking. If there is some kind of transformation there, this could affect the rounding. – GarethOwen Mar 02 '11 at 10:52

1 Answers1

0

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).

Brian Jack
  • 468
  • 4
  • 11
  • I really appreciate your post, but I don't think it gets me any closer to the answer. As I understand it you're trying to obtain better precision by avoiding rounding (or flooring) until the last minute. However I don't believe the problem stems from lack of precision. Rather it's a problem of finding a formula that reproduces the results I get from DrawImage. Unfortunately your formula fails to do this. (_continued in next comment, 30-Mar_) – rkagerer Mar 30 '11 at 13:10
  • (_continuation of previous comment, from 30-Mar_) If I use it in the 3x1 example I gave above, to calculate required_source_position for pixel 1 (the first blue pixel in the destination), it yeilds a red value rather than the desired blue. i.e. `required_source_position = floor((current_dest_position*sourceN)/destN) = floor((1*1)/2) = floor(0.5) = 0 = red`. If I've misinterpreted your answer, please feel free to correct me and show how it replicates the DrawImage results for all the examples above. Again, though, thanks for taking a shot at this. – rkagerer Mar 30 '11 at 13:10
  • For the most part if an algorithm is flakey (non-deterministic) I don't use it. There is something to be said for the advice of many a computer science professor to know your program's invariants at all times while coding. The only time it is good to use non-deterministic algorithms (such as machine learning) is when you want probabalistic and/or statistical results. – Brian Jack Apr 11 '11 at 07:29