Browse Source

Add Matrix4x4 transform overloads and tests

pull/3077/head
James Jackson-South 2 months ago
parent
commit
7632f8e941
  1. 12
      src/ImageSharp/Primitives/Point.cs
  2. 12
      src/ImageSharp/Primitives/PointF.cs
  3. 14
      src/ImageSharp/Primitives/Rectangle.cs
  4. 14
      src/ImageSharp/Primitives/RectangleF.cs
  5. 16
      src/ImageSharp/Processing/Processors/Transforms/TransformUtilities.cs
  6. 63
      tests/ImageSharp.Tests/Primitives/PointFTests.cs
  7. 45
      tests/ImageSharp.Tests/Primitives/PointTests.cs
  8. 43
      tests/ImageSharp.Tests/Primitives/RectangleFTests.cs
  9. 33
      tests/ImageSharp.Tests/Primitives/RectangleTests.cs

12
src/ImageSharp/Primitives/Point.cs

@ -4,6 +4,7 @@
using System.ComponentModel;
using System.Numerics;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Processing.Processors.Transforms;
namespace SixLabors.ImageSharp;
@ -234,6 +235,17 @@ public struct Point : IEquatable<Point>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Point Transform(Point point, Matrix3x2 matrix) => Round(Vector2.Transform(new Vector2(point.X, point.Y), matrix));
/// <summary>
/// Transforms a point by a specified 4x4 matrix, applying a projective transform
/// flattened into 2D space.
/// </summary>
/// <param name="point">The point to transform.</param>
/// <param name="matrix">The transformation matrix used.</param>
/// <returns>The transformed <see cref="Point"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Point Transform(Point point, Matrix4x4 matrix)
=> Round(TransformUtilities.ProjectiveTransform2D(point.X, point.Y, matrix));
/// <summary>
/// Deconstructs this point into two integers.
/// </summary>

12
src/ImageSharp/Primitives/PointF.cs

@ -4,6 +4,7 @@
using System.ComponentModel;
using System.Numerics;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Processing.Processors.Transforms;
namespace SixLabors.ImageSharp;
@ -246,6 +247,17 @@ public struct PointF : IEquatable<PointF>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static PointF Transform(PointF point, Matrix3x2 matrix) => Vector2.Transform(point, matrix);
/// <summary>
/// Transforms a point by a specified 4x4 matrix, applying a projective transform
/// flattened into 2D space.
/// </summary>
/// <param name="point">The point to transform.</param>
/// <param name="matrix">The transformation matrix used.</param>
/// <returns>The transformed <see cref="PointF"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static PointF Transform(PointF point, Matrix4x4 matrix)
=> TransformUtilities.ProjectiveTransform2D(point.X, point.Y, matrix);
/// <summary>
/// Deconstructs this point into two floats.
/// </summary>

14
src/ImageSharp/Primitives/Rectangle.cs

@ -266,6 +266,20 @@ public struct Rectangle : IEquatable<Rectangle>
return new RectangleF(topLeft, new SizeF(bottomRight - topLeft));
}
/// <summary>
/// Transforms a rectangle by the given 4x4 matrix, applying a projective transform
/// flattened into 2D space.
/// </summary>
/// <param name="rectangle">The source rectangle.</param>
/// <param name="matrix">The transformation matrix.</param>
/// <returns>A transformed rectangle.</returns>
public static RectangleF Transform(Rectangle rectangle, Matrix4x4 matrix)
{
PointF bottomRight = PointF.Transform(new PointF(rectangle.Right, rectangle.Bottom), matrix);
PointF topLeft = PointF.Transform(new PointF(rectangle.Location.X, rectangle.Location.Y), matrix);
return new RectangleF(topLeft, new SizeF(bottomRight - topLeft));
}
/// <summary>
/// Converts a <see cref="RectangleF"/> to a <see cref="Rectangle"/> by performing a truncate operation on all the coordinates.
/// </summary>

14
src/ImageSharp/Primitives/RectangleF.cs

@ -241,6 +241,20 @@ public struct RectangleF : IEquatable<RectangleF>
return new RectangleF(topLeft, new SizeF(bottomRight - topLeft));
}
/// <summary>
/// Transforms a rectangle by the given 4x4 matrix, applying a projective transform
/// flattened into 2D space.
/// </summary>
/// <param name="rectangle">The source rectangle.</param>
/// <param name="matrix">The transformation matrix.</param>
/// <returns>A transformed <see cref="RectangleF"/>.</returns>
public static RectangleF Transform(RectangleF rectangle, Matrix4x4 matrix)
{
PointF bottomRight = PointF.Transform(new PointF(rectangle.Right, rectangle.Bottom), matrix);
PointF topLeft = PointF.Transform(rectangle.Location, matrix);
return new RectangleF(topLeft, new SizeF(bottomRight - topLeft));
}
/// <summary>
/// Creates a rectangle that represents the union between <paramref name="a"/> and <paramref name="b"/>.
/// </summary>

16
src/ImageSharp/Processing/Processors/Transforms/TransformUtilities.cs

@ -69,11 +69,17 @@ internal static class TransformUtilities
[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.
// Transforms the 2D point (x, y) as the homogeneous coordinate (x, y, 0, 1) and
// performs the perspective divide (X/W, Y/W) to project back into Cartesian 2D space.
//
// For affine matrices (M14=0, M24=0, M34=0, M44=1) W is always 1 and the divide
// is a no-op, producing the same result as Vector2.Transform(v, Matrix4x4).AsVector2()
// (the approach used by .NET 10+).
//
// For projective matrices (taper, quad distortion) W varies per point and the divide
// is essential for correct perspective mapping. W <= 0 means the point has crossed the
// vanishing line of the projection; clamping to epsilon avoids division by zero or
// negative values that would flip/mirror the output.
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);

63
tests/ImageSharp.Tests/Primitives/PointFTests.cs

@ -133,6 +133,69 @@ public class PointFTests
Assert.Equal(new PointF(30, 30), pout);
}
[Fact]
public void TransformMatrix4x4_AffineMatchesMatrix3x2()
{
PointF p = new(13, 17);
Matrix3x2 m3 = Matrix3x2Extensions.CreateRotationDegrees(45, PointF.Empty);
Matrix4x4 m4 = new(m3);
PointF r3 = PointF.Transform(p, m3);
PointF r4 = PointF.Transform(p, m4);
Assert.Equal(r3.X, r4.X, ApproximateFloatComparer);
Assert.Equal(r3.Y, r4.Y, ApproximateFloatComparer);
}
[Fact]
public void TransformMatrix4x4_Identity()
{
PointF p = new(42.5F, -17.3F);
PointF result = PointF.Transform(p, Matrix4x4.Identity);
Assert.Equal(p.X, result.X, ApproximateFloatComparer);
Assert.Equal(p.Y, result.Y, ApproximateFloatComparer);
}
[Fact]
public void TransformMatrix4x4_Translation()
{
PointF p = new(10, 20);
Matrix4x4 m = Matrix4x4.CreateTranslation(5, -3, 0);
PointF result = PointF.Transform(p, m);
Assert.Equal(15F, result.X, ApproximateFloatComparer);
Assert.Equal(17F, result.Y, ApproximateFloatComparer);
}
[Fact]
public void TransformMatrix4x4_Scale()
{
PointF p = new(10, 20);
Matrix4x4 m = Matrix4x4.CreateScale(2, 3, 1);
PointF result = PointF.Transform(p, m);
Assert.Equal(20F, result.X, ApproximateFloatComparer);
Assert.Equal(60F, result.Y, ApproximateFloatComparer);
}
[Fact]
public void TransformMatrix4x4_Projective()
{
// A taper matrix with M14 != 0 produces W != 1, requiring perspective divide.
PointF p = new(100, 50);
Matrix4x4 m = Matrix4x4.Identity;
m.M14 = 0.005F; // perspective component
PointF result = PointF.Transform(p, m);
// W = x*M14 + M44 = 100*0.005 + 1 = 1.5
// X = x*M11 + M41 = 100, Y = y*M22 + M42 = 50
// result = (100/1.5, 50/1.5)
Assert.Equal(100F / 1.5F, result.X, ApproximateFloatComparer);
Assert.Equal(50F / 1.5F, result.Y, ApproximateFloatComparer);
}
[Theory]
[InlineData(float.MaxValue, float.MinValue)]
[InlineData(float.MinValue, float.MaxValue)]

45
tests/ImageSharp.Tests/Primitives/PointTests.cs

@ -174,6 +174,51 @@ public class PointTests
Assert.Equal(new Point(30, 30), pout);
}
[Fact]
public void TransformMatrix4x4_AffineMatchesMatrix3x2()
{
Point p = new(13, 17);
Matrix3x2 m3 = Matrix3x2Extensions.CreateRotationDegrees(45, Point.Empty);
Matrix4x4 m4 = new(m3);
Point r3 = Point.Transform(p, m3);
Point r4 = Point.Transform(p, m4);
Assert.Equal(r3, r4);
}
[Fact]
public void TransformMatrix4x4_Identity()
{
Point p = new(42, -17);
Point result = Point.Transform(p, Matrix4x4.Identity);
Assert.Equal(p, result);
}
[Fact]
public void TransformMatrix4x4_Translation()
{
Point p = new(10, 20);
Matrix4x4 m = Matrix4x4.CreateTranslation(5, -3, 0);
Point result = Point.Transform(p, m);
Assert.Equal(new Point(15, 17), result);
}
[Fact]
public void TransformMatrix4x4_Projective()
{
Point p = new(100, 50);
Matrix4x4 m = Matrix4x4.Identity;
m.M14 = 0.005F;
Point result = Point.Transform(p, m);
// W = 100*0.005 + 1 = 1.5 => (100/1.5, 50/1.5) => rounded
Assert.Equal(Point.Round(new PointF(100F / 1.5F, 50F / 1.5F)), result);
}
[Theory]
[InlineData(int.MaxValue, int.MinValue)]
[InlineData(int.MinValue, int.MinValue)]

43
tests/ImageSharp.Tests/Primitives/RectangleFTests.cs

@ -2,6 +2,7 @@
// Licensed under the Six Labors Split License.
using System.Globalization;
using System.Numerics;
namespace SixLabors.ImageSharp.Tests;
@ -243,6 +244,48 @@ public class RectangleFTests
Assert.Equal(expectedRect, r1);
}
[Fact]
public void TransformMatrix4x4_AffineMatchesMatrix3x2()
{
RectangleF rect = new(10, 20, 100, 50);
Matrix3x2 m3 = Matrix3x2.CreateTranslation(5, -3);
Matrix4x4 m4 = new(m3);
RectangleF r3 = RectangleF.Transform(rect, m3);
RectangleF r4 = RectangleF.Transform(rect, m4);
Assert.Equal(r3, r4);
}
[Fact]
public void TransformMatrix4x4_Identity()
{
RectangleF rect = new(10, 20, 100, 50);
RectangleF result = RectangleF.Transform(rect, Matrix4x4.Identity);
Assert.Equal(rect, result);
}
[Fact]
public void TransformMatrix4x4_Translation()
{
RectangleF rect = new(10, 20, 100, 50);
Matrix4x4 m = Matrix4x4.CreateTranslation(5, -3, 0);
RectangleF result = RectangleF.Transform(rect, m);
Assert.Equal(new RectangleF(15, 17, 100, 50), result);
}
[Fact]
public void TransformMatrix4x4_Scale()
{
RectangleF rect = new(10, 20, 100, 50);
Matrix4x4 m = Matrix4x4.CreateScale(2, 3, 1);
RectangleF result = RectangleF.Transform(rect, m);
Assert.Equal(new RectangleF(20, 60, 200, 150), result);
}
[Fact]
public void ToStringTest()
{

33
tests/ImageSharp.Tests/Primitives/RectangleTests.cs

@ -2,6 +2,7 @@
// Licensed under the Six Labors Split License.
using System.Globalization;
using System.Numerics;
namespace SixLabors.ImageSharp.Tests;
@ -294,6 +295,38 @@ public class RectangleTests
Assert.Equal(expectedRect, r1);
}
[Fact]
public void TransformMatrix4x4_AffineMatchesMatrix3x2()
{
Rectangle rect = new(10, 20, 100, 50);
Matrix3x2 m3 = Matrix3x2.CreateTranslation(5, -3);
Matrix4x4 m4 = new(m3);
RectangleF r3 = Rectangle.Transform(rect, m3);
RectangleF r4 = Rectangle.Transform(rect, m4);
Assert.Equal(r3, r4);
}
[Fact]
public void TransformMatrix4x4_Identity()
{
Rectangle rect = new(10, 20, 100, 50);
RectangleF result = Rectangle.Transform(rect, Matrix4x4.Identity);
Assert.Equal(new RectangleF(10, 20, 100, 50), result);
}
[Fact]
public void TransformMatrix4x4_Translation()
{
Rectangle rect = new(10, 20, 100, 50);
Matrix4x4 m = Matrix4x4.CreateTranslation(5, -3, 0);
RectangleF result = Rectangle.Transform(rect, m);
Assert.Equal(new RectangleF(15, 17, 100, 50), result);
}
[Fact]
public void ToStringTest()
{

Loading…
Cancel
Save