Browse Source

Fix transform bounds calculations

pull/2791/head
James Jackson-South 2 years ago
parent
commit
e1555fd4ba
  1. 38
      src/ImageSharp/Processing/AffineTransformBuilder.cs
  2. 4
      src/ImageSharp/Processing/Processors/Transforms/Linear/LinearTransformUtility.cs
  3. 5
      src/ImageSharp/Processing/Processors/Transforms/Linear/RotateProcessor.cs
  4. 5
      src/ImageSharp/Processing/Processors/Transforms/Linear/SkewProcessor.cs
  5. 274
      src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs
  6. 39
      src/ImageSharp/Processing/ProjectiveTransformBuilder.cs
  7. 18
      tests/ImageSharp.Tests/Processing/Processors/Transforms/AffineTransformTests.cs
  8. 10
      tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformTests.cs

38
src/ImageSharp/Processing/AffineTransformBuilder.cs

@ -12,7 +12,6 @@ namespace SixLabors.ImageSharp.Processing;
public class AffineTransformBuilder
{
private readonly List<Func<Size, Matrix3x2>> transformMatrixFactories = new();
private readonly List<Func<Size, Matrix3x2>> boundsMatrixFactories = new();
/// <summary>
/// Prepends a rotation matrix using the given rotation angle in degrees
@ -31,8 +30,7 @@ public class AffineTransformBuilder
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
public AffineTransformBuilder PrependRotationRadians(float radians)
=> this.Prepend(
size => TransformUtils.CreateRotationTransformMatrixRadians(radians, size),
size => TransformUtils.CreateRotationBoundsMatrixRadians(radians, size));
size => TransformUtils.CreateRotationTransformMatrixRadians(radians, size));
/// <summary>
/// Prepends a rotation matrix using the given rotation in degrees at the given origin.
@ -68,9 +66,7 @@ public class AffineTransformBuilder
/// <param name="radians">The amount of rotation, in radians.</param>
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
public AffineTransformBuilder AppendRotationRadians(float radians)
=> this.Append(
size => TransformUtils.CreateRotationTransformMatrixRadians(radians, size),
size => TransformUtils.CreateRotationBoundsMatrixRadians(radians, size));
=> this.Append(size => TransformUtils.CreateRotationTransformMatrixRadians(radians, size));
/// <summary>
/// Appends a rotation matrix using the given rotation in degrees at the given origin.
@ -145,9 +141,7 @@ public class AffineTransformBuilder
/// <param name="degreesY">The Y angle, in degrees.</param>
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
public AffineTransformBuilder PrependSkewDegrees(float degreesX, float degreesY)
=> this.Prepend(
size => TransformUtils.CreateSkewTransformMatrixDegrees(degreesX, degreesY, size),
size => TransformUtils.CreateSkewBoundsMatrixDegrees(degreesX, degreesY, size));
=> this.Prepend(size => TransformUtils.CreateSkewTransformMatrixDegrees(degreesX, degreesY, size));
/// <summary>
/// Prepends a centered skew matrix from the give angles in radians.
@ -156,9 +150,7 @@ public class AffineTransformBuilder
/// <param name="radiansY">The Y angle, in radians.</param>
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
public AffineTransformBuilder PrependSkewRadians(float radiansX, float radiansY)
=> this.Prepend(
size => TransformUtils.CreateSkewTransformMatrixRadians(radiansX, radiansY, size),
size => TransformUtils.CreateSkewBoundsMatrixRadians(radiansX, radiansY, size));
=> this.Prepend(size => TransformUtils.CreateSkewTransformMatrixRadians(radiansX, radiansY, size));
/// <summary>
/// Prepends a skew matrix using the given angles in degrees at the given origin.
@ -187,9 +179,7 @@ public class AffineTransformBuilder
/// <param name="degreesY">The Y angle, in degrees.</param>
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
public AffineTransformBuilder AppendSkewDegrees(float degreesX, float degreesY)
=> this.Append(
size => TransformUtils.CreateSkewTransformMatrixDegrees(degreesX, degreesY, size),
size => TransformUtils.CreateSkewBoundsMatrixDegrees(degreesX, degreesY, size));
=> this.Append(size => TransformUtils.CreateSkewTransformMatrixDegrees(degreesX, degreesY, size));
/// <summary>
/// Appends a centered skew matrix from the give angles in radians.
@ -198,9 +188,7 @@ public class AffineTransformBuilder
/// <param name="radiansY">The Y angle, in radians.</param>
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
public AffineTransformBuilder AppendSkewRadians(float radiansX, float radiansY)
=> this.Append(
size => TransformUtils.CreateSkewTransformMatrixRadians(radiansX, radiansY, size),
size => TransformUtils.CreateSkewBoundsMatrixRadians(radiansX, radiansY, size));
=> this.Append(size => TransformUtils.CreateSkewTransformMatrixRadians(radiansX, radiansY, size));
/// <summary>
/// Appends a skew matrix using the given angles in degrees at the given origin.
@ -267,7 +255,7 @@ public class AffineTransformBuilder
public AffineTransformBuilder PrependMatrix(Matrix3x2 matrix)
{
CheckDegenerate(matrix);
return this.Prepend(_ => matrix, _ => matrix);
return this.Prepend(_ => matrix);
}
/// <summary>
@ -283,7 +271,7 @@ public class AffineTransformBuilder
public AffineTransformBuilder AppendMatrix(Matrix3x2 matrix)
{
CheckDegenerate(matrix);
return this.Append(_ => matrix, _ => matrix);
return this.Append(_ => matrix);
}
/// <summary>
@ -340,13 +328,13 @@ public class AffineTransformBuilder
// Translate the origin matrix to cater for source rectangle offsets.
Matrix3x2 matrix = Matrix3x2.CreateTranslation(-sourceRectangle.Location);
foreach (Func<Size, Matrix3x2> factory in this.boundsMatrixFactories)
foreach (Func<Size, Matrix3x2> factory in this.transformMatrixFactories)
{
matrix *= factory(size);
CheckDegenerate(matrix);
}
return TransformUtils.GetTransformedSize(size, matrix);
return TransformUtils.GetTransformedSize(matrix, size);
}
private static void CheckDegenerate(Matrix3x2 matrix)
@ -357,17 +345,15 @@ public class AffineTransformBuilder
}
}
private AffineTransformBuilder Prepend(Func<Size, Matrix3x2> transformFactory, Func<Size, Matrix3x2> boundsFactory)
private AffineTransformBuilder Prepend(Func<Size, Matrix3x2> transformFactory)
{
this.transformMatrixFactories.Insert(0, transformFactory);
this.boundsMatrixFactories.Insert(0, boundsFactory);
return this;
}
private AffineTransformBuilder Append(Func<Size, Matrix3x2> transformFactory, Func<Size, Matrix3x2> boundsFactory)
private AffineTransformBuilder Append(Func<Size, Matrix3x2> transformFactory)
{
this.transformMatrixFactories.Add(transformFactory);
this.boundsMatrixFactories.Add(boundsFactory);
return this;
}
}

4
src/ImageSharp/Processing/Processors/Transforms/Linear/LinearTransformUtility.cs

@ -43,7 +43,7 @@ internal static class LinearTransformUtility
/// <returns>The <see cref="int"/>.</returns>
[MethodImpl(InliningOptions.ShortMethod)]
public static int GetRangeStart(float radius, float center, int min, int max)
=> Numerics.Clamp((int)MathF.Ceiling(center - radius), min, max);
=> Numerics.Clamp((int)MathF.Floor(center - radius), min, max);
/// <summary>
/// Gets the end position (inclusive) for a sampling range given
@ -56,5 +56,5 @@ internal static class LinearTransformUtility
/// <returns>The <see cref="int"/>.</returns>
[MethodImpl(InliningOptions.ShortMethod)]
public static int GetRangeEnd(float radius, float center, int min, int max)
=> Numerics.Clamp((int)MathF.Floor(center + radius), min, max);
=> Numerics.Clamp((int)MathF.Ceiling(center + radius), min, max);
}

5
src/ImageSharp/Processing/Processors/Transforms/Linear/RotateProcessor.cs

@ -29,14 +29,13 @@ public sealed class RotateProcessor : AffineTransformProcessor
public RotateProcessor(float degrees, IResampler sampler, Size sourceSize)
: this(
TransformUtils.CreateRotationTransformMatrixDegrees(degrees, sourceSize),
TransformUtils.CreateRotationBoundsMatrixDegrees(degrees, sourceSize),
sampler,
sourceSize)
=> this.Degrees = degrees;
// Helper constructor
private RotateProcessor(Matrix3x2 rotationMatrix, Matrix3x2 boundsMatrix, IResampler sampler, Size sourceSize)
: base(rotationMatrix, sampler, TransformUtils.GetTransformedSize(sourceSize, boundsMatrix))
private RotateProcessor(Matrix3x2 rotationMatrix, IResampler sampler, Size sourceSize)
: base(rotationMatrix, sampler, TransformUtils.GetTransformedSize(rotationMatrix, sourceSize))
{
}

5
src/ImageSharp/Processing/Processors/Transforms/Linear/SkewProcessor.cs

@ -31,7 +31,6 @@ public sealed class SkewProcessor : AffineTransformProcessor
public SkewProcessor(float degreesX, float degreesY, IResampler sampler, Size sourceSize)
: this(
TransformUtils.CreateSkewTransformMatrixDegrees(degreesX, degreesY, sourceSize),
TransformUtils.CreateSkewBoundsMatrixDegrees(degreesX, degreesY, sourceSize),
sampler,
sourceSize)
{
@ -40,8 +39,8 @@ public sealed class SkewProcessor : AffineTransformProcessor
}
// Helper constructor:
private SkewProcessor(Matrix3x2 skewMatrix, Matrix3x2 boundsMatrix, IResampler sampler, Size sourceSize)
: base(skewMatrix, sampler, TransformUtils.GetTransformedSize(sourceSize, boundsMatrix))
private SkewProcessor(Matrix3x2 skewMatrix, IResampler sampler, Size sourceSize)
: base(skewMatrix, sampler, TransformUtils.GetTransformedSize(skewMatrix, sourceSize))
{
}

274
src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs

@ -68,6 +68,11 @@ internal static class TransformUtils
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector2 ProjectiveTransform2D(float x, float y, Matrix4x4 matrix)
{
// The w component (v4.W) resulting from the transformation can be less than 0 in certain cases,
// such as when the point is transformed behind the camera in a perspective projection.
// However, in many 2D contexts, negative w values are not meaningful and could cause issues
// like flipped or distorted projections. To avoid this, we take the max of w and epsilon to ensure
// we don't divide by a very small or negative number, effectively treating any negative w as epsilon.
const float epsilon = 0.0000001F;
Vector4 v4 = Vector4.Transform(new Vector4(x, y, 0, 1F), matrix);
return new Vector2(v4.X, v4.Y) / MathF.Max(v4.W, epsilon);
@ -81,9 +86,7 @@ internal static class TransformUtils
/// <returns>The <see cref="Matrix3x2"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Matrix3x2 CreateRotationTransformMatrixDegrees(float degrees, Size size)
=> CreateCenteredTransformMatrix(
new Rectangle(Point.Empty, size),
Matrix3x2Extensions.CreateRotationDegrees(degrees, PointF.Empty));
=> CreateCenteredTransformMatrix(Matrix3x2Extensions.CreateRotationDegrees(degrees, PointF.Empty), size);
/// <summary>
/// Creates a centered rotation transform matrix using the given rotation in radians and the source size.
@ -93,33 +96,7 @@ internal static class TransformUtils
/// <returns>The <see cref="Matrix3x2"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Matrix3x2 CreateRotationTransformMatrixRadians(float radians, Size size)
=> CreateCenteredTransformMatrix(
new Rectangle(Point.Empty, size),
Matrix3x2Extensions.CreateRotation(radians, PointF.Empty));
/// <summary>
/// Creates a centered rotation bounds matrix using the given rotation in degrees and the source size.
/// </summary>
/// <param name="degrees">The amount of rotation, in degrees.</param>
/// <param name="size">The source image size.</param>
/// <returns>The <see cref="Matrix3x2"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Matrix3x2 CreateRotationBoundsMatrixDegrees(float degrees, Size size)
=> CreateCenteredBoundsMatrix(
new Rectangle(Point.Empty, size),
Matrix3x2Extensions.CreateRotationDegrees(degrees, PointF.Empty));
/// <summary>
/// Creates a centered rotation bounds matrix using the given rotation in radians and the source size.
/// </summary>
/// <param name="radians">The amount of rotation, in radians.</param>
/// <param name="size">The source image size.</param>
/// <returns>The <see cref="Matrix3x2"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Matrix3x2 CreateRotationBoundsMatrixRadians(float radians, Size size)
=> CreateCenteredBoundsMatrix(
new Rectangle(Point.Empty, size),
Matrix3x2Extensions.CreateRotation(radians, PointF.Empty));
=> CreateCenteredTransformMatrix(Matrix3x2Extensions.CreateRotation(radians, PointF.Empty), size);
/// <summary>
/// Creates a centered skew transform matrix from the give angles in degrees and the source size.
@ -130,9 +107,7 @@ internal static class TransformUtils
/// <returns>The <see cref="Matrix3x2"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Matrix3x2 CreateSkewTransformMatrixDegrees(float degreesX, float degreesY, Size size)
=> CreateCenteredTransformMatrix(
new Rectangle(Point.Empty, size),
Matrix3x2Extensions.CreateSkewDegrees(degreesX, degreesY, PointF.Empty));
=> CreateCenteredTransformMatrix(Matrix3x2Extensions.CreateSkewDegrees(degreesX, degreesY, PointF.Empty), size);
/// <summary>
/// Creates a centered skew transform matrix from the give angles in radians and the source size.
@ -143,78 +118,28 @@ internal static class TransformUtils
/// <returns>The <see cref="Matrix3x2"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Matrix3x2 CreateSkewTransformMatrixRadians(float radiansX, float radiansY, Size size)
=> CreateCenteredTransformMatrix(
new Rectangle(Point.Empty, size),
Matrix3x2Extensions.CreateSkew(radiansX, radiansY, PointF.Empty));
/// <summary>
/// Creates a centered skew bounds matrix from the give angles in degrees and the source size.
/// </summary>
/// <param name="degreesX">The X angle, in degrees.</param>
/// <param name="degreesY">The Y angle, in degrees.</param>
/// <param name="size">The source image size.</param>
/// <returns>The <see cref="Matrix3x2"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Matrix3x2 CreateSkewBoundsMatrixDegrees(float degreesX, float degreesY, Size size)
=> CreateCenteredBoundsMatrix(
new Rectangle(Point.Empty, size),
Matrix3x2Extensions.CreateSkewDegrees(degreesX, degreesY, PointF.Empty));
/// <summary>
/// Creates a centered skew bounds matrix from the give angles in radians and the source size.
/// </summary>
/// <param name="radiansX">The X angle, in radians.</param>
/// <param name="radiansY">The Y angle, in radians.</param>
/// <param name="size">The source image size.</param>
/// <returns>The <see cref="Matrix3x2"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Matrix3x2 CreateSkewBoundsMatrixRadians(float radiansX, float radiansY, Size size)
=> CreateCenteredBoundsMatrix(
new Rectangle(Point.Empty, size),
Matrix3x2Extensions.CreateSkew(radiansX, radiansY, PointF.Empty));
=> CreateCenteredTransformMatrix(Matrix3x2Extensions.CreateSkew(radiansX, radiansY, PointF.Empty), size);
/// <summary>
/// Gets the centered transform matrix based upon the source rectangle.
/// </summary>
/// <param name="sourceRectangle">The source image bounds.</param>
/// <param name="matrix">The transformation matrix.</param>
/// <returns>The <see cref="Matrix3x2"/></returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Matrix3x2 CreateCenteredTransformMatrix(Rectangle sourceRectangle, Matrix3x2 matrix)
{
Rectangle destinationRectangle = GetTransformedBoundingRectangle(sourceRectangle, matrix);
// We invert the matrix to handle the transformation from screen to world space.
// This ensures scaling matrices are correct.
Matrix3x2.Invert(matrix, out Matrix3x2 inverted);
// Centered transforms must be 0 based so we offset the bounds width and height.
Matrix3x2 translationToTargetCenter = Matrix3x2.CreateTranslation(new Vector2(-(destinationRectangle.Width - 1), -(destinationRectangle.Height - 1)) * .5F);
Matrix3x2 translateToSourceCenter = Matrix3x2.CreateTranslation(new Vector2(sourceRectangle.Width - 1, sourceRectangle.Height - 1) * .5F);
// Translate back to world space.
Matrix3x2.Invert(translationToTargetCenter * inverted * translateToSourceCenter, out Matrix3x2 centered);
return centered;
}
/// <summary>
/// Gets the centered bounds matrix based upon the source rectangle.
/// </summary>
/// <param name="sourceRectangle">The source image bounds.</param>
/// <param name="matrix">The transformation matrix.</param>
/// <param name="size">The source image size.</param>
/// <returns>The <see cref="Matrix3x2"/></returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Matrix3x2 CreateCenteredBoundsMatrix(Rectangle sourceRectangle, Matrix3x2 matrix)
public static Matrix3x2 CreateCenteredTransformMatrix(Matrix3x2 matrix, Size size)
{
Rectangle destinationRectangle = GetTransformedBoundingRectangle(sourceRectangle, matrix);
Size destinationSize = GetUnboundedTransformedSize(matrix, size);
// We invert the matrix to handle the transformation from screen to world space.
// This ensures scaling matrices are correct.
Matrix3x2.Invert(matrix, out Matrix3x2 inverted);
Matrix3x2 translationToTargetCenter = Matrix3x2.CreateTranslation(new Vector2(-destinationRectangle.Width, -destinationRectangle.Height) * .5F);
Matrix3x2 translateToSourceCenter = Matrix3x2.CreateTranslation(new Vector2(sourceRectangle.Width, sourceRectangle.Height) * .5F);
// The source size is provided using the coordinate space of the source image.
// however the transform should always be applied in the pixel space.
// To account for this we offset by the size - 1 to translate to the pixel space.
Matrix3x2 translationToTargetCenter = Matrix3x2.CreateTranslation(new Vector2(-(destinationSize.Width - 1), -(destinationSize.Height - 1)) * .5F);
Matrix3x2 translateToSourceCenter = Matrix3x2.CreateTranslation(new Vector2(size.Width - 1, size.Height - 1) * .5F);
// Translate back to world space.
Matrix3x2.Invert(translationToTargetCenter * inverted * translateToSourceCenter, out Matrix3x2 centered);
@ -236,6 +161,12 @@ internal static class TransformUtils
{
Matrix4x4 matrix = Matrix4x4.Identity;
// The source size is provided using the Coordinate/Geometric space of the source image.
// However, the transform should always be applied in the Discrete/Pixel space to ensure
// that the transformation fully encompasses all pixels without clipping at the edges.
// To account for this, we subtract [1,1] from the size to translate to the Discrete/Pixel space.
// size -= new Size(1, 1);
/*
* SkMatrix is laid out in the following manner:
*
@ -345,52 +276,101 @@ internal static class TransformUtils
}
/// <summary>
/// Returns the rectangle bounds relative to the source for the given transformation matrix.
/// Returns the size relative to the source for the given transformation matrix.
/// </summary>
/// <param name="rectangle">The source rectangle.</param>
/// <param name="matrix">The transformation matrix.</param>
/// <param name="size">The source size.</param>
/// <returns>
/// The <see cref="Rectangle"/>.
/// The <see cref="Size"/>.
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Rectangle GetTransformedBoundingRectangle(Rectangle rectangle, Matrix3x2 matrix)
{
Rectangle transformed = GetTransformedRectangle(rectangle, matrix);
return new Rectangle(0, 0, transformed.Width, transformed.Height);
}
public static Size GetTransformedSize(Matrix3x2 matrix, Size size)
=> GetTransformedSize(matrix, size, true);
/// <summary>
/// Returns the rectangle relative to the source for the given transformation matrix.
/// Returns the size relative to the source for the given transformation matrix.
/// </summary>
/// <param name="rectangle">The source rectangle.</param>
/// <param name="matrix">The transformation matrix.</param>
/// <param name="size">The source size.</param>
/// <returns>
/// The <see cref="Rectangle"/>.
/// The <see cref="Size"/>.
/// </returns>
public static Rectangle GetTransformedRectangle(Rectangle rectangle, Matrix3x2 matrix)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Size GetTransformedSize(Matrix4x4 matrix, Size size)
{
if (rectangle.Equals(default) || Matrix3x2.Identity.Equals(matrix))
Guard.IsTrue(size.Width > 0 && size.Height > 0, nameof(size), "Source size dimensions cannot be 0!");
if (matrix.Equals(default) || matrix.Equals(Matrix4x4.Identity))
{
return rectangle;
return size;
}
Vector2 tl = Vector2.Transform(new Vector2(rectangle.Left, rectangle.Top), matrix);
Vector2 tr = Vector2.Transform(new Vector2(rectangle.Right, rectangle.Top), matrix);
Vector2 bl = Vector2.Transform(new Vector2(rectangle.Left, rectangle.Bottom), matrix);
Vector2 br = Vector2.Transform(new Vector2(rectangle.Right, rectangle.Bottom), matrix);
// Check if the matrix involves only affine transformations by inspecting the relevant components.
// We want to use pixel space for calculations only if the transformation is purely 2D and does not include
// any perspective effects, non-standard scaling, or unusual translations that could distort the image.
// The conditions are as follows:
bool usePixelSpace =
// 1. Ensure there's no perspective distortion:
// M34 corresponds to the perspective component. For a purely 2D affine transformation, this should be 0.
(matrix.M34 == 0) &&
// 2. Ensure standard affine transformation without any unusual depth or perspective scaling:
// M44 should be 1 for a standard affine transformation. If M44 is not 1, it indicates non-standard depth
// scaling or perspective, which suggests a more complex transformation.
(matrix.M44 == 1) &&
// 3. Ensure no unusual translation in the x-direction:
// M14 represents translation in the x-direction that might be part of a more complex transformation.
// For standard affine transformations, M14 should be 0.
(matrix.M14 == 0) &&
return GetBoundingRectangle(tl, tr, bl, br);
// 4. Ensure no unusual translation in the y-direction:
// M24 represents translation in the y-direction that might be part of a more complex transformation.
// For standard affine transformations, M24 should be 0.
(matrix.M24 == 0);
// Define an offset size to translate between pixel space and coordinate space.
// When using pixel space, apply a scaling sensitive offset to translate to discrete pixel coordinates.
// When not using pixel space, use SizeF.Empty as the offset.
// Compute scaling factors from the matrix
float scaleX = 1F / new Vector2(matrix.M11, matrix.M21).Length(); // sqrt(M11^2 + M21^2)
float scaleY = 1F / new Vector2(matrix.M12, matrix.M22).Length(); // sqrt(M12^2 + M22^2)
// Apply the offset relative to the scale
SizeF offsetSize = usePixelSpace ? new SizeF(scaleX, scaleY) : SizeF.Empty;
// Subtract the offset size to translate to the appropriate space (pixel or coordinate).
if (TryGetTransformedRectangle(new RectangleF(Point.Empty, size - offsetSize), matrix, out Rectangle bounds))
{
// Add the offset size back to translate the transformed bounds to the correct space.
return Size.Ceiling(ConstrainSize(bounds) + offsetSize);
}
return size;
}
/// <summary>
/// Returns the size relative to the source for the given transformation matrix.
/// </summary>
/// <param name="matrix">The transformation matrix.</param>
/// <param name="size">The source size.</param>
/// <returns>
/// The <see cref="Size"/>.
/// </returns>
private static Size GetUnboundedTransformedSize(Matrix3x2 matrix, Size size)
=> GetTransformedSize(matrix, size, false);
/// <summary>
/// Returns the size relative to the source for the given transformation matrix.
/// </summary>
/// <param name="matrix">The transformation matrix.</param>
/// <param name="size">The source size.</param>
/// <param name="constrain">Whether to constrain the size to ensure that the dimensions are positive.</param>
/// <returns>
/// The <see cref="Size"/>.
/// </returns>
public static Size GetTransformedSize(Size size, Matrix3x2 matrix)
private static Size GetTransformedSize(Matrix3x2 matrix, Size size, bool constrain)
{
Guard.IsTrue(size.Width > 0 && size.Height > 0, nameof(size), "Source size dimensions cannot be 0!");
@ -399,9 +379,20 @@ internal static class TransformUtils
return size;
}
Rectangle rectangle = GetTransformedRectangle(new Rectangle(Point.Empty, size), matrix);
// Define an offset size to translate between coordinate space and pixel space.
// Compute scaling factors from the matrix
float scaleX = 1F / new Vector2(matrix.M11, matrix.M21).Length(); // sqrt(M11^2 + M21^2)
float scaleY = 1F / new Vector2(matrix.M12, matrix.M22).Length(); // sqrt(M12^2 + M22^2)
SizeF offsetSize = new(scaleX, scaleY);
// Subtract the offset size to translate to the pixel space.
if (TryGetTransformedRectangle(new RectangleF(Point.Empty, size - offsetSize), matrix, out Rectangle bounds))
{
// Add the offset size back to translate the transformed bounds to the coordinate space.
return Size.Ceiling((constrain ? ConstrainSize(bounds) : bounds.Size) + offsetSize);
}
return ConstrainSize(rectangle);
return size;
}
/// <summary>
@ -409,46 +400,52 @@ internal static class TransformUtils
/// </summary>
/// <param name="rectangle">The source rectangle.</param>
/// <param name="matrix">The transformation matrix.</param>
/// <param name="bounds">The resulting bounding rectangle.</param>
/// <returns>
/// The <see cref="Rectangle"/>.
/// <see langword="true"/> if the transformation was successful; otherwise, <see langword="false"/>.
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Rectangle GetTransformedRectangle(Rectangle rectangle, Matrix4x4 matrix)
private static bool TryGetTransformedRectangle(RectangleF rectangle, Matrix3x2 matrix, out Rectangle bounds)
{
if (rectangle.Equals(default) || Matrix4x4.Identity.Equals(matrix))
if (rectangle.Equals(default) || Matrix3x2.Identity.Equals(matrix))
{
return rectangle;
bounds = default;
return false;
}
Vector2 tl = ProjectiveTransform2D(rectangle.Left, rectangle.Top, matrix);
Vector2 tr = ProjectiveTransform2D(rectangle.Right, rectangle.Top, matrix);
Vector2 bl = ProjectiveTransform2D(rectangle.Left, rectangle.Bottom, matrix);
Vector2 br = ProjectiveTransform2D(rectangle.Right, rectangle.Bottom, matrix);
Vector2 tl = Vector2.Transform(new Vector2(rectangle.Left, rectangle.Top), matrix);
Vector2 tr = Vector2.Transform(new Vector2(rectangle.Right, rectangle.Top), matrix);
Vector2 bl = Vector2.Transform(new Vector2(rectangle.Left, rectangle.Bottom), matrix);
Vector2 br = Vector2.Transform(new Vector2(rectangle.Right, rectangle.Bottom), matrix);
return GetBoundingRectangle(tl, tr, bl, br);
bounds = GetBoundingRectangle(tl, tr, bl, br);
return true;
}
/// <summary>
/// Returns the size relative to the source for the given transformation matrix.
/// Returns the rectangle relative to the source for the given transformation matrix.
/// </summary>
/// <param name="size">The source size.</param>
/// <param name="rectangle">The source rectangle.</param>
/// <param name="matrix">The transformation matrix.</param>
/// <param name="bounds">The resulting bounding rectangle.</param>
/// <returns>
/// The <see cref="Size"/>.
/// <see langword="true"/> if the transformation was successful; otherwise, <see langword="false"/>.
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Size GetTransformedSize(Size size, Matrix4x4 matrix)
private static bool TryGetTransformedRectangle(RectangleF rectangle, Matrix4x4 matrix, out Rectangle bounds)
{
Guard.IsTrue(size.Width > 0 && size.Height > 0, nameof(size), "Source size dimensions cannot be 0!");
if (matrix.Equals(default) || matrix.Equals(Matrix4x4.Identity))
if (rectangle.Equals(default) || Matrix4x4.Identity.Equals(matrix))
{
return size;
bounds = default;
return false;
}
Rectangle rectangle = GetTransformedRectangle(new Rectangle(Point.Empty, size), matrix);
Vector2 tl = ProjectiveTransform2D(rectangle.Left, rectangle.Top, matrix);
Vector2 tr = ProjectiveTransform2D(rectangle.Right, rectangle.Top, matrix);
Vector2 bl = ProjectiveTransform2D(rectangle.Left, rectangle.Bottom, matrix);
Vector2 br = ProjectiveTransform2D(rectangle.Right, rectangle.Bottom, matrix);
return ConstrainSize(rectangle);
bounds = GetBoundingRectangle(tl, tr, bl, br);
return true;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@ -482,6 +479,11 @@ internal static class TransformUtils
float right = MathF.Max(tl.X, MathF.Max(tr.X, MathF.Max(bl.X, br.X)));
float bottom = MathF.Max(tl.Y, MathF.Max(tr.Y, MathF.Max(bl.Y, br.Y)));
return Rectangle.Round(RectangleF.FromLTRB(left, top, right, bottom));
// Clamp the values to the nearest whole pixel.
return Rectangle.FromLTRB(
(int)Math.Floor(left),
(int)Math.Floor(top),
(int)Math.Ceiling(right),
(int)Math.Ceiling(bottom));
}
}

39
src/ImageSharp/Processing/ProjectiveTransformBuilder.cs

@ -12,7 +12,6 @@ namespace SixLabors.ImageSharp.Processing;
public class ProjectiveTransformBuilder
{
private readonly List<Func<Size, Matrix4x4>> transformMatrixFactories = new();
private readonly List<Func<Size, Matrix4x4>> boundsMatrixFactories = new();
/// <summary>
/// Prepends a matrix that performs a tapering projective transform.
@ -22,9 +21,7 @@ public class ProjectiveTransformBuilder
/// <param name="fraction">The amount to taper.</param>
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
public ProjectiveTransformBuilder PrependTaper(TaperSide side, TaperCorner corner, float fraction)
=> this.Prepend(
size => TransformUtils.CreateTaperMatrix(size, side, corner, fraction),
size => TransformUtils.CreateTaperMatrix(size, side, corner, fraction));
=> this.Prepend(size => TransformUtils.CreateTaperMatrix(size, side, corner, fraction));
/// <summary>
/// Appends a matrix that performs a tapering projective transform.
@ -34,9 +31,7 @@ public class ProjectiveTransformBuilder
/// <param name="fraction">The amount to taper.</param>
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
public ProjectiveTransformBuilder AppendTaper(TaperSide side, TaperCorner corner, float fraction)
=> this.Append(
size => TransformUtils.CreateTaperMatrix(size, side, corner, fraction),
size => TransformUtils.CreateTaperMatrix(size, side, corner, fraction));
=> this.Append(size => TransformUtils.CreateTaperMatrix(size, side, corner, fraction));
/// <summary>
/// Prepends a centered rotation matrix using the given rotation in degrees.
@ -52,9 +47,7 @@ public class ProjectiveTransformBuilder
/// <param name="radians">The amount of rotation, in radians.</param>
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
public ProjectiveTransformBuilder PrependRotationRadians(float radians)
=> this.Prepend(
size => new Matrix4x4(TransformUtils.CreateRotationTransformMatrixRadians(radians, size)),
size => new Matrix4x4(TransformUtils.CreateRotationBoundsMatrixRadians(radians, size)));
=> this.Prepend(size => new Matrix4x4(TransformUtils.CreateRotationTransformMatrixRadians(radians, size)));
/// <summary>
/// Prepends a centered rotation matrix using the given rotation in degrees at the given origin.
@ -88,9 +81,7 @@ public class ProjectiveTransformBuilder
/// <param name="radians">The amount of rotation, in radians.</param>
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
public ProjectiveTransformBuilder AppendRotationRadians(float radians)
=> this.Append(
size => new Matrix4x4(TransformUtils.CreateRotationTransformMatrixRadians(radians, size)),
size => new Matrix4x4(TransformUtils.CreateRotationBoundsMatrixRadians(radians, size)));
=> this.Append(size => new Matrix4x4(TransformUtils.CreateRotationTransformMatrixRadians(radians, size)));
/// <summary>
/// Appends a centered rotation matrix using the given rotation in degrees at the given origin.
@ -174,9 +165,7 @@ public class ProjectiveTransformBuilder
/// <param name="radiansY">The Y angle, in radians.</param>
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
public ProjectiveTransformBuilder PrependSkewRadians(float radiansX, float radiansY)
=> this.Prepend(
size => new Matrix4x4(TransformUtils.CreateSkewTransformMatrixRadians(radiansX, radiansY, size)),
size => new Matrix4x4(TransformUtils.CreateSkewBoundsMatrixRadians(radiansX, radiansY, size)));
=> this.Prepend(size => new Matrix4x4(TransformUtils.CreateSkewTransformMatrixRadians(radiansX, radiansY, size)));
/// <summary>
/// Prepends a skew matrix using the given angles in degrees at the given origin.
@ -214,9 +203,7 @@ public class ProjectiveTransformBuilder
/// <param name="radiansY">The Y angle, in radians.</param>
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
public ProjectiveTransformBuilder AppendSkewRadians(float radiansX, float radiansY)
=> this.Append(
size => new Matrix4x4(TransformUtils.CreateSkewTransformMatrixRadians(radiansX, radiansY, size)),
size => new Matrix4x4(TransformUtils.CreateSkewBoundsMatrixRadians(radiansX, radiansY, size)));
=> this.Append(size => new Matrix4x4(TransformUtils.CreateSkewTransformMatrixRadians(radiansX, radiansY, size)));
/// <summary>
/// Appends a skew matrix using the given angles in degrees at the given origin.
@ -283,7 +270,7 @@ public class ProjectiveTransformBuilder
public ProjectiveTransformBuilder PrependMatrix(Matrix4x4 matrix)
{
CheckDegenerate(matrix);
return this.Prepend(_ => matrix, _ => matrix);
return this.Prepend(_ => matrix);
}
/// <summary>
@ -299,7 +286,7 @@ public class ProjectiveTransformBuilder
public ProjectiveTransformBuilder AppendMatrix(Matrix4x4 matrix)
{
CheckDegenerate(matrix);
return this.Append(_ => matrix, _ => matrix);
return this.Append(_ => matrix);
}
/// <summary>
@ -357,13 +344,13 @@ public class ProjectiveTransformBuilder
// Translate the origin matrix to cater for source rectangle offsets.
Matrix4x4 matrix = Matrix4x4.CreateTranslation(new Vector3(-sourceRectangle.Location, 0));
foreach (Func<Size, Matrix4x4> factory in this.boundsMatrixFactories)
foreach (Func<Size, Matrix4x4> factory in this.transformMatrixFactories)
{
matrix *= factory(size);
CheckDegenerate(matrix);
}
return TransformUtils.GetTransformedSize(size, matrix);
return TransformUtils.GetTransformedSize(matrix, size);
}
private static void CheckDegenerate(Matrix4x4 matrix)
@ -374,17 +361,15 @@ public class ProjectiveTransformBuilder
}
}
private ProjectiveTransformBuilder Prepend(Func<Size, Matrix4x4> transformFactory, Func<Size, Matrix4x4> boundsFactory)
private ProjectiveTransformBuilder Prepend(Func<Size, Matrix4x4> transformFactory)
{
this.transformMatrixFactories.Insert(0, transformFactory);
this.boundsMatrixFactories.Insert(0, boundsFactory);
return this;
}
private ProjectiveTransformBuilder Append(Func<Size, Matrix4x4> transformFactory, Func<Size, Matrix4x4> boundsFactory)
private ProjectiveTransformBuilder Append(Func<Size, Matrix4x4> transformFactory)
{
this.transformMatrixFactories.Add(transformFactory);
this.boundsMatrixFactories.Add(boundsFactory);
return this;
}
}

18
tests/ImageSharp.Tests/Processing/Processors/Transforms/AffineTransformTests.cs

@ -231,6 +231,24 @@ public class AffineTransformTests
Assert.Equal(100, image.Height);
}
[Theory]
[WithSolidFilledImages(4, 4, nameof(Color.Red), PixelTypes.Rgba32)]
public void Issue2753<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> image = provider.GetImage();
AffineTransformBuilder builder =
new AffineTransformBuilder().AppendRotationDegrees(270, new Vector2(3.5f, 3.5f));
image.Mutate(x => x.BackgroundColor(Color.Red));
image.Mutate(x => x = x.Transform(builder));
image.DebugSave(provider);
Assert.Equal(4, image.Width);
Assert.Equal(8, image.Height);
}
[Theory]
[WithTestPatternImages(100, 100, PixelTypes.Rgba32)]
public void Identity<TPixel>(TestImageProvider<TPixel> provider)

10
tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformTests.cs

@ -128,11 +128,11 @@ public class ProjectiveTransformTests
using (Image<TPixel> image = provider.GetImage())
{
#pragma warning disable SA1117 // Parameters should be on same line or separate lines
var matrix = new Matrix4x4(
0.260987f, -0.434909f, 0, -0.0022184f,
0.373196f, 0.949882f, 0, -0.000312129f,
0, 0, 1, 0,
52, 165, 0, 1);
Matrix4x4 matrix = new(
0.260987f, -0.434909f, 0, -0.0022184f,
0.373196f, 0.949882f, 0, -0.000312129f,
0, 0, 1, 0,
52, 165, 0, 1);
#pragma warning restore SA1117 // Parameters should be on same line or separate lines
ProjectiveTransformBuilder builder = new ProjectiveTransformBuilder()

Loading…
Cancel
Save