SixLabors / ImageSharp

:camera: A modern, cross-platform, 2D Graphics library for .NET
https://sixlabors.com/products/imagesharp/
Other
7.26k stars 844 forks source link

Rotation of AffineTransform seems to offset the image by 1 pixel #2753

Open Socolin opened 3 weeks ago

Socolin commented 3 weeks ago

Prerequisites

ImageSharp version

3.1.4

Other ImageSharp packages and versions

ImageSharp.Drawing 2.1.3

Environment (Operating system, version and so on)

Ubuntu 22.04, AMD Ryzen 9 7950X

.NET Framework version

8.0

Description

When using the rotation through the AffineTransformBuilder the result is not the same as the .Rotate() there seems to be 1 pixel offset.

Here the sample code of what I would expect

using (var img = new Image<Rgba32>(100, 100, Color.DimGray))
{
    img.Mutate(c => c
        .Rotate(180)
        .Fill(Color.Aqua, new RectangleF(49, 49, 2, 2))
    );
    img.Save("rotation-rotate.png");
}

Which give this result: image

Steps to Reproduce

When I do the same with the .Transform() I'm getting this:

using (var img = new Image<Rgb24>(100, 100, Color.DimGray))
{
    img.Mutate(c => c
        .Transform(new  AffineTransformBuilder().AppendRotationDegrees(180))
        .Fill(Color.Aqua, new RectangleF(49, 49, 2, 2))
    );
    img.Save("rotation-center.png");
}

image image

Another example with non centered rotation

for (int i = 0; i <= 10; i++)
{

    using (var img = new Image<Rgba32>(100, 100, Color.DimGray))
    {
        img.Mutate(c => c
            .Transform(new  AffineTransformBuilder().AppendTranslation(new Vector2(100, 100)))
            .Transform(new  AffineTransformBuilder().AppendRotationDegrees(i * 18, new Vector2(100, 100)))
            .Fill(Color.Aqua, new RectangleF(99, 99, 2, 2))
        );
        img.Save($"rotation-{i}.png");
    }
}

image image

The rotation seems to be centered 1 pixel too much toward the bottom-right

Images

No response

tocsoft commented 3 weeks ago

I'm not 100% up on this area but wouldn't the bottom right location be (99, 99) as the pixels are zero indexed?

I can't remember fully correctly but it could be confusion caused by the difference in how Drawing vs pixel manipulation work where, I believe, one is pixel centre based on the other is pixel boundary based.

My caveat here is that I might be entirely wrong about all this as I'm trying to dredge up some memories from quite some time ago.

JimBobSquarePants commented 3 weeks ago

Yeah, I think you’re diagnosis is correct; it should be zero based.

Socolin commented 3 weeks ago

Just to clarify because I'm not sure about what you said, is the problem with my example ? or is it a bug in ImageSharp ?

tocsoft commented 3 weeks ago

Just to clarify because I'm not sure about what you said, is the problem with my example ? or is it a bug in ImageSharp ?

Taking a closer look at your code examples it looks like a bug for the .Transform(new AffineTransformBuilder().AppendRotationDegrees(180)) to me.

The .Transform(new AffineTransformBuilder().AppendRotationDegrees(i * 18, new Vector2(100, 100))) case however looks more suspect as I would have expected that line to have been a rotation around [99, 99] but if there's a bug in of those call sites then there could very will be a bug that effects both.

Socolin commented 3 weeks ago

Ok thanks.

The .Transform(new AffineTransformBuilder().AppendRotationDegrees(i * 18, new Vector2(100, 100))) case however looks more suspect as I would have expected that line to have been a rotation around [99, 99] but if there's a bug in of those call sites then there could very will be a bug that effects both.

When it's rotating arround 100, 100, as I understand it, it should rotate arround the bottom-right edge of the pixel 99,99

So here another example, I resized it and added a cross to show where I think the rotation point should be

Before rotation image After rotation image

using (var img = new Image<Rgba32>(8, 8, Color.DimGray))
{
    img.Mutate(c => c
        .Transform(new  AffineTransformBuilder().AppendTranslation(new Vector2(8, 8)))
        .Transform(new  AffineTransformBuilder().AppendRotationDegrees(180, new Vector2(8, 8)))
        .Fill(Color.Aqua, new RectangleF(7, 7, 2, 2))
        .Resize(new Size(800, 800), KnownResamplers.Box, true)
        .DrawLine(Color.Red, 1, new PointF(400, 350), new PointF(400, 450))
        .DrawLine(Color.Red, 1, new PointF(350, 400), new PointF(450, 400))
        .BackgroundColor(Color.DarkGreen)
    );
    img.Save($"rotation-180.png");
}
JimBobSquarePants commented 3 weeks ago

I think I know what the issue is here. Will have a look over the weekend.

JimBobSquarePants commented 2 weeks ago

I was right.... I was hoping I wasn't.

The result is offset by 1 pixel in each direction because the transformation matrix is centered using a 1-based coordinate system, which assumes the image's center is at (width * 0.5, height * 0.5). However, image pixels are zero-based, meaning their coordinates start from 0. This discrepancy causes a misalignment, as the matrix does not account for the zero-based nature of pixel indices, leading to a 1-pixel offset when the transformation is applied.

Fix:

To correct this issue, we need to use two separate matrices:

  1. Transformation Matrix for Pixel Operations: This matrix accounts for the zero-based nature of pixel indices. It adjusts the center calculation to ensure the transformations (translation, rotation, etc.) align correctly with the pixel grid. This matrix is used to perform the actual image transformation.

  2. Bounding Box Calculation Matrix: This matrix does not adjust for the zero-based pixel grid but instead accurately represents the intended transformation. It is used to compute the transformed image's size and bounds by transforming the image's corners and determining the extents.

By using these two matrices, we can ensure that both the pixel operations and the size/bounds calculations are handled correctly, resolving the 1-pixel offset issue and maintaining accurate and expected transformation results.

I'll get stuck in...

JimBobSquarePants commented 2 weeks ago

PR opened