Graphics2D transformation result does not match manual transformation
Asked Answered
S

4

7

I am using Java's Graphics2D to draw on a component using AffineTransform's to manipulate my drawing. Graphics2D offers an method transform for this, which takes an AffineTransform.

Sometimes I need to manipulate a point manually without using the builtin-transformation. But when I try to transform a point using the same transformation I gave to Graphics2D.transform sometimes the resulting point is not the same.

The following code reproduces the problem (It's Scala code, but I think you can imagine the Java code.):

   var transformationMatrix = new AffineTransform()
   /*
    * transformationMatrix is modified throughout the program
    * ...
    */
   override def paintComponent(g: Graphics2D) = {
      super.paintComponent(g)
      /* 1. transform using graphics transform */
      g.transform(transformationMatrix)
      g.setColor(Color.RED)
      g.fill(new Rectangle(0, 0, 1, 1))
      /* 2. transform point manually */
      g.setTransform(new AffineTransform) // reset transformation to standard
      val p0 = new Point(0, 0)
      val pDest = new Point()
      transformationMatrix.transform(p0, pDest)
      g.setColor(Color.BLUE)
      g.fill(new Rectangle(pDest.x, pDest.y, 1, 1)
   }

Expected behaviour

The blue rectangle (manually calculated) overdraws the red one (calculated by transform).

Experienced behaviour

The blue rectangle has an offset of 1

I admit that my transformationMatrix is not really integer, but that should'nt be the problem, should it?

   affineTransform = 1.1, 0.0, 520.55
                     0.0, 1.1, 182.54999999999995
                     0.0, 0.0,    1.0

Is this a bug or am I missing some deep insight?

Edit: You can reproduce the bug, if you set transformationMatrix to

transformationMatrix = new AffineTransform(1.1, 0.0, 0.0, 1.1, 521.55, 183.54999999999995)

at the beginning of paintComponent. Please note, that g is of type Graphics2D.

Schaffer answered 9/7, 2012 at 20:23 Comment(2)
I wonder if there may be a different transform active when paint() is called. I would try g.getTransform() at the start of the method and use the result to restore the 'standard' transform.Cortico
That does not help either. g.getTransform() returns identity matrix and saving it and restoring later leads to the same behaviour.Schaffer
S
5

Well, you are doing two different things.

In (1) you are subjecting a shape (and it is irrelevant that it is Rectangle and not Rectangle2D.Double) to a transform that yields fractional coordinates. It only is painted aliased, because you haven't set specific rendering hints (RenderingHints.KEY_ANTIALIASING -> RenderingHints.VALUE_ANTIALIAS_ON, and RenderingHints.KEY_STROKE_CONTROL -> RenderingHints.VALUE_STROKE_PURE).

In (2) you are subjecting a point to the transform, and coerce it into aliased coordinates (Point instead of Point2D.Double). Then successively construct a rectangle from that point.

Clearly there may be very different things happening under the hood, and I wouldn't expect at all that transforming into an integer point versus painting floating point shapes in an aliasing graphics context yield the same results.

(Without testing) I would guess that a valid equivalent statement for (1) would be

g.fill(transformationMatrix.createTransformedShape(new Rectangle(0, 0, 1, 1)))
Schizogenesis answered 18/7, 2012 at 0:0 Comment(0)
P
7

Your transform is basically just a translation by (520.55, 182.55). And because it has fractional pixel values it is in fact sensitive to choice of roundoff. If you have anti-aliasing on, you'll actually get a 4-pixel red blob covering the pixels that are overlapped. Otherwise, the behavior (disagreement) you're seeing is reasonable given the ambiguity between rounding to integer and truncating to integer.

Preprandial answered 9/7, 2012 at 20:57 Comment(7)
No, its not only a translation, I also have a scaling factor of 1.1. I have no antialiasing turned on. So you are thinking that one implementation is rounding to integer (Manual transform), while the other one is truncating to integer (Graphics2D Transform)? Sounds somehow plausible, but would be very strange, I think. Any ideas how to fix this?Schaffer
@Schaffer - The scaling factor of 1.1 is irrelevant for the purposes of rounding, which is why I said it was "basically" just a translation (instead of saying it was just a translation). Graphics2D default drawing style is pixel-rounded and upper-left biased, so that if you draw an N*M rectangle, you get N*M pixels starting at the origin and going N-1 and M-1 pixels out in the x and y directions. If you must line up to the single pixel without antialiasing, my only suggestion is to do all the math manually (and use Shape). The behavior you want isn't promised by the API.Preprandial
"Graphics2D default drawing style is pixel-rounded": As far as I can judge, this is not the case: (520.55, 182.55) is rounded (=truncated) to (520, 182). Or am I missing something?Schaffer
And the factor of 1.1 is relevant for the purpose of rounding. There is no problem, when I set the scaling factor to 1.0 (with the same translation).Schaffer
@Schaffer - Well, that's really weird, then. It doesn't even affect the position of the upper left corner. In any case, what I meant by "pixel-rounded" was rounded up and left, at least for filling objects. What happens if you do it manually with Rectangle2D.Double (and Point2D.Double)?Preprandial
Using Rectangle2D.Double and Point2D.Double yields the same result as using Rectangle and Point. So I get (521, 183), as expected.Schaffer
@Schaffer - Using 1.1 as the width and height, or using 1.0?Preprandial
S
5

Well, you are doing two different things.

In (1) you are subjecting a shape (and it is irrelevant that it is Rectangle and not Rectangle2D.Double) to a transform that yields fractional coordinates. It only is painted aliased, because you haven't set specific rendering hints (RenderingHints.KEY_ANTIALIASING -> RenderingHints.VALUE_ANTIALIAS_ON, and RenderingHints.KEY_STROKE_CONTROL -> RenderingHints.VALUE_STROKE_PURE).

In (2) you are subjecting a point to the transform, and coerce it into aliased coordinates (Point instead of Point2D.Double). Then successively construct a rectangle from that point.

Clearly there may be very different things happening under the hood, and I wouldn't expect at all that transforming into an integer point versus painting floating point shapes in an aliasing graphics context yield the same results.

(Without testing) I would guess that a valid equivalent statement for (1) would be

g.fill(transformationMatrix.createTransformedShape(new Rectangle(0, 0, 1, 1)))
Schizogenesis answered 18/7, 2012 at 0:0 Comment(0)
B
2

When you are performing the first step, g.transform(transformationMatrix), the Graphics composes that with the already present transformations. On the second step you are overrinding it with, g.setTransform(new AffineTransform), thus losing the previous transformation if any. You are assuming you are back to the start but it might not be true.

Make a getTransform() before step 1 and another after step 2 to verify those are the same.

Bootless answered 16/7, 2012 at 16:18 Comment(1)
Good point! Unfortunately, thats not the problem. Printing the transformation with println(g.getTransform) prints identity and using setTransform instead of transform leads to the same behaviour.Schaffer
S
1

Whenever you work with floating point coordinates, you should use the '2D' version of graphical objects if you want correct results. I didn't read that from 'book', so I can't quote, it is just experience with it.

Here is my ugly java code that produces result that you are expecting.

AffineTransform transformationMatrix = AffineTransform.getTranslateInstance(520.55, 182.54999999999995);
transformationMatrix.scale(1.1, 1.1);
((Graphics2D) previewGraphics).transform(transformationMatrix);
previewGraphics.setColor(Color.RED);
((Graphics2D) previewGraphics).fill(new Rectangle(0,0,1,1));
((Graphics2D) previewGraphics).setTransform(new AffineTransform());
Point2D p0 = new Point2D.Double(0, 0);
Point2D pDest = new Point2D.Double();
transformationMatrix.transform(p0, pDest);
previewGraphics.setColor(Color.BLUE);
((Graphics2D) previewGraphics).fill((Shape) new Rectangle2D.Double(pDest.getX(), pDest.getY(), 1, 1));
Staggers answered 17/7, 2012 at 7:3 Comment(2)
Does not work. I'm already getting an Graphics2D as g (this is automatically done in Scala) and even when I'm using Point2D and Rectangle2D the problem persists. The reason that you are not experiencing the problem is, that it only occurs for special values of translation and scaling. Furthermore, the problem does not lie in the rounding of AffineTransform=>transform (this rounds correctly) but rather in the rounding inside of Graphics2D (which sometimes truncates instead of rounds in some cases).Schaffer
@Schaffer If you want post the actual values that produce the error, I'll gladly test on different JVMs. By the way Graphics object's underlying implementation is all native bindings, so the problem might be platform specific?Staggers

© 2022 - 2024 — McMap. All rights reserved.