Skip to content

Commit 7632f8e

Browse files
Add Matrix4x4 transform overloads and tests
1 parent 461c021 commit 7632f8e

9 files changed

Lines changed: 247 additions & 5 deletions

File tree

src/ImageSharp/Primitives/Point.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System.ComponentModel;
55
using System.Numerics;
66
using System.Runtime.CompilerServices;
7+
using SixLabors.ImageSharp.Processing.Processors.Transforms;
78

89
namespace SixLabors.ImageSharp;
910

@@ -234,6 +235,17 @@ public Point(Size size)
234235
[MethodImpl(MethodImplOptions.AggressiveInlining)]
235236
public static Point Transform(Point point, Matrix3x2 matrix) => Round(Vector2.Transform(new Vector2(point.X, point.Y), matrix));
236237

238+
/// <summary>
239+
/// Transforms a point by a specified 4x4 matrix, applying a projective transform
240+
/// flattened into 2D space.
241+
/// </summary>
242+
/// <param name="point">The point to transform.</param>
243+
/// <param name="matrix">The transformation matrix used.</param>
244+
/// <returns>The transformed <see cref="Point"/>.</returns>
245+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
246+
public static Point Transform(Point point, Matrix4x4 matrix)
247+
=> Round(TransformUtilities.ProjectiveTransform2D(point.X, point.Y, matrix));
248+
237249
/// <summary>
238250
/// Deconstructs this point into two integers.
239251
/// </summary>

src/ImageSharp/Primitives/PointF.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System.ComponentModel;
55
using System.Numerics;
66
using System.Runtime.CompilerServices;
7+
using SixLabors.ImageSharp.Processing.Processors.Transforms;
78

89
namespace SixLabors.ImageSharp;
910

@@ -246,6 +247,17 @@ public PointF(SizeF size)
246247
[MethodImpl(MethodImplOptions.AggressiveInlining)]
247248
public static PointF Transform(PointF point, Matrix3x2 matrix) => Vector2.Transform(point, matrix);
248249

250+
/// <summary>
251+
/// Transforms a point by a specified 4x4 matrix, applying a projective transform
252+
/// flattened into 2D space.
253+
/// </summary>
254+
/// <param name="point">The point to transform.</param>
255+
/// <param name="matrix">The transformation matrix used.</param>
256+
/// <returns>The transformed <see cref="PointF"/>.</returns>
257+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
258+
public static PointF Transform(PointF point, Matrix4x4 matrix)
259+
=> TransformUtilities.ProjectiveTransform2D(point.X, point.Y, matrix);
260+
249261
/// <summary>
250262
/// Deconstructs this point into two floats.
251263
/// </summary>

src/ImageSharp/Primitives/Rectangle.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,20 @@ public static RectangleF Transform(Rectangle rectangle, Matrix3x2 matrix)
266266
return new RectangleF(topLeft, new SizeF(bottomRight - topLeft));
267267
}
268268

269+
/// <summary>
270+
/// Transforms a rectangle by the given 4x4 matrix, applying a projective transform
271+
/// flattened into 2D space.
272+
/// </summary>
273+
/// <param name="rectangle">The source rectangle.</param>
274+
/// <param name="matrix">The transformation matrix.</param>
275+
/// <returns>A transformed rectangle.</returns>
276+
public static RectangleF Transform(Rectangle rectangle, Matrix4x4 matrix)
277+
{
278+
PointF bottomRight = PointF.Transform(new PointF(rectangle.Right, rectangle.Bottom), matrix);
279+
PointF topLeft = PointF.Transform(new PointF(rectangle.Location.X, rectangle.Location.Y), matrix);
280+
return new RectangleF(topLeft, new SizeF(bottomRight - topLeft));
281+
}
282+
269283
/// <summary>
270284
/// Converts a <see cref="RectangleF"/> to a <see cref="Rectangle"/> by performing a truncate operation on all the coordinates.
271285
/// </summary>

src/ImageSharp/Primitives/RectangleF.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,20 @@ public static RectangleF Transform(RectangleF rectangle, Matrix3x2 matrix)
241241
return new RectangleF(topLeft, new SizeF(bottomRight - topLeft));
242242
}
243243

244+
/// <summary>
245+
/// Transforms a rectangle by the given 4x4 matrix, applying a projective transform
246+
/// flattened into 2D space.
247+
/// </summary>
248+
/// <param name="rectangle">The source rectangle.</param>
249+
/// <param name="matrix">The transformation matrix.</param>
250+
/// <returns>A transformed <see cref="RectangleF"/>.</returns>
251+
public static RectangleF Transform(RectangleF rectangle, Matrix4x4 matrix)
252+
{
253+
PointF bottomRight = PointF.Transform(new PointF(rectangle.Right, rectangle.Bottom), matrix);
254+
PointF topLeft = PointF.Transform(rectangle.Location, matrix);
255+
return new RectangleF(topLeft, new SizeF(bottomRight - topLeft));
256+
}
257+
244258
/// <summary>
245259
/// Creates a rectangle that represents the union between <paramref name="a"/> and <paramref name="b"/>.
246260
/// </summary>

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

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -69,11 +69,17 @@ public static bool IsNaN(Matrix4x4 matrix)
6969
[MethodImpl(MethodImplOptions.AggressiveInlining)]
7070
public static Vector2 ProjectiveTransform2D(float x, float y, Matrix4x4 matrix)
7171
{
72-
// The w component (v4.W) resulting from the transformation can be less than 0 in certain cases,
73-
// such as when the point is transformed behind the camera in a perspective projection.
74-
// However, in many 2D contexts, negative w values are not meaningful and could cause issues
75-
// like flipped or distorted projections. To avoid this, we take the max of w and epsilon to ensure
76-
// we don't divide by a very small or negative number, effectively treating any negative w as epsilon.
72+
// Transforms the 2D point (x, y) as the homogeneous coordinate (x, y, 0, 1) and
73+
// performs the perspective divide (X/W, Y/W) to project back into Cartesian 2D space.
74+
//
75+
// For affine matrices (M14=0, M24=0, M34=0, M44=1) W is always 1 and the divide
76+
// is a no-op, producing the same result as Vector2.Transform(v, Matrix4x4).AsVector2()
77+
// (the approach used by .NET 10+).
78+
//
79+
// For projective matrices (taper, quad distortion) W varies per point and the divide
80+
// is essential for correct perspective mapping. W <= 0 means the point has crossed the
81+
// vanishing line of the projection; clamping to epsilon avoids division by zero or
82+
// negative values that would flip/mirror the output.
7783
const float epsilon = 0.0000001F;
7884
Vector4 v4 = Vector4.Transform(new Vector4(x, y, 0, 1F), matrix);
7985
return new Vector2(v4.X, v4.Y) / MathF.Max(v4.W, epsilon);

tests/ImageSharp.Tests/Primitives/PointFTests.cs

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,69 @@ public void SkewTest()
133133
Assert.Equal(new PointF(30, 30), pout);
134134
}
135135

136+
[Fact]
137+
public void TransformMatrix4x4_AffineMatchesMatrix3x2()
138+
{
139+
PointF p = new(13, 17);
140+
Matrix3x2 m3 = Matrix3x2Extensions.CreateRotationDegrees(45, PointF.Empty);
141+
Matrix4x4 m4 = new(m3);
142+
143+
PointF r3 = PointF.Transform(p, m3);
144+
PointF r4 = PointF.Transform(p, m4);
145+
146+
Assert.Equal(r3.X, r4.X, ApproximateFloatComparer);
147+
Assert.Equal(r3.Y, r4.Y, ApproximateFloatComparer);
148+
}
149+
150+
[Fact]
151+
public void TransformMatrix4x4_Identity()
152+
{
153+
PointF p = new(42.5F, -17.3F);
154+
PointF result = PointF.Transform(p, Matrix4x4.Identity);
155+
156+
Assert.Equal(p.X, result.X, ApproximateFloatComparer);
157+
Assert.Equal(p.Y, result.Y, ApproximateFloatComparer);
158+
}
159+
160+
[Fact]
161+
public void TransformMatrix4x4_Translation()
162+
{
163+
PointF p = new(10, 20);
164+
Matrix4x4 m = Matrix4x4.CreateTranslation(5, -3, 0);
165+
PointF result = PointF.Transform(p, m);
166+
167+
Assert.Equal(15F, result.X, ApproximateFloatComparer);
168+
Assert.Equal(17F, result.Y, ApproximateFloatComparer);
169+
}
170+
171+
[Fact]
172+
public void TransformMatrix4x4_Scale()
173+
{
174+
PointF p = new(10, 20);
175+
Matrix4x4 m = Matrix4x4.CreateScale(2, 3, 1);
176+
PointF result = PointF.Transform(p, m);
177+
178+
Assert.Equal(20F, result.X, ApproximateFloatComparer);
179+
Assert.Equal(60F, result.Y, ApproximateFloatComparer);
180+
}
181+
182+
[Fact]
183+
public void TransformMatrix4x4_Projective()
184+
{
185+
// A taper matrix with M14 != 0 produces W != 1, requiring perspective divide.
186+
PointF p = new(100, 50);
187+
Matrix4x4 m = Matrix4x4.Identity;
188+
m.M14 = 0.005F; // perspective component
189+
190+
PointF result = PointF.Transform(p, m);
191+
192+
// W = x*M14 + M44 = 100*0.005 + 1 = 1.5
193+
// X = x*M11 + M41 = 100, Y = y*M22 + M42 = 50
194+
// result = (100/1.5, 50/1.5)
195+
Assert.Equal(100F / 1.5F, result.X, ApproximateFloatComparer);
196+
Assert.Equal(50F / 1.5F, result.Y, ApproximateFloatComparer);
197+
}
198+
136199
[Theory]
137200
[InlineData(float.MaxValue, float.MinValue)]
138201
[InlineData(float.MinValue, float.MaxValue)]

tests/ImageSharp.Tests/Primitives/PointTests.cs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,51 @@ public void SkewTest()
174174
Assert.Equal(new Point(30, 30), pout);
175175
}
176176

177+
[Fact]
178+
public void TransformMatrix4x4_AffineMatchesMatrix3x2()
179+
{
180+
Point p = new(13, 17);
181+
Matrix3x2 m3 = Matrix3x2Extensions.CreateRotationDegrees(45, Point.Empty);
182+
Matrix4x4 m4 = new(m3);
183+
184+
Point r3 = Point.Transform(p, m3);
185+
Point r4 = Point.Transform(p, m4);
186+
187+
Assert.Equal(r3, r4);
188+
}
189+
190+
[Fact]
191+
public void TransformMatrix4x4_Identity()
192+
{
193+
Point p = new(42, -17);
194+
Point result = Point.Transform(p, Matrix4x4.Identity);
195+
196+
Assert.Equal(p, result);
197+
}
198+
199+
[Fact]
200+
public void TransformMatrix4x4_Translation()
201+
{
202+
Point p = new(10, 20);
203+
Matrix4x4 m = Matrix4x4.CreateTranslation(5, -3, 0);
204+
Point result = Point.Transform(p, m);
205+
206+
Assert.Equal(new Point(15, 17), result);
207+
}
208+
209+
[Fact]
210+
public void TransformMatrix4x4_Projective()
211+
{
212+
Point p = new(100, 50);
213+
Matrix4x4 m = Matrix4x4.Identity;
214+
m.M14 = 0.005F;
215+
216+
Point result = Point.Transform(p, m);
217+
218+
// W = 100*0.005 + 1 = 1.5 => (100/1.5, 50/1.5) => rounded
219+
Assert.Equal(Point.Round(new PointF(100F / 1.5F, 50F / 1.5F)), result);
220+
}
221+
177222
[Theory]
178223
[InlineData(int.MaxValue, int.MinValue)]
179224
[InlineData(int.MinValue, int.MinValue)]

tests/ImageSharp.Tests/Primitives/RectangleFTests.cs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the Six Labors Split License.
33

44
using System.Globalization;
5+
using System.Numerics;
56

67
namespace SixLabors.ImageSharp.Tests;
78

@@ -243,6 +244,48 @@ public void OffsetTest(float x, float y, float width, float height)
243244
Assert.Equal(expectedRect, r1);
244245
}
245246

247+
[Fact]
248+
public void TransformMatrix4x4_AffineMatchesMatrix3x2()
249+
{
250+
RectangleF rect = new(10, 20, 100, 50);
251+
Matrix3x2 m3 = Matrix3x2.CreateTranslation(5, -3);
252+
Matrix4x4 m4 = new(m3);
253+
254+
RectangleF r3 = RectangleF.Transform(rect, m3);
255+
RectangleF r4 = RectangleF.Transform(rect, m4);
256+
257+
Assert.Equal(r3, r4);
258+
}
259+
260+
[Fact]
261+
public void TransformMatrix4x4_Identity()
262+
{
263+
RectangleF rect = new(10, 20, 100, 50);
264+
RectangleF result = RectangleF.Transform(rect, Matrix4x4.Identity);
265+
266+
Assert.Equal(rect, result);
267+
}
268+
269+
[Fact]
270+
public void TransformMatrix4x4_Translation()
271+
{
272+
RectangleF rect = new(10, 20, 100, 50);
273+
Matrix4x4 m = Matrix4x4.CreateTranslation(5, -3, 0);
274+
RectangleF result = RectangleF.Transform(rect, m);
275+
276+
Assert.Equal(new RectangleF(15, 17, 100, 50), result);
277+
}
278+
279+
[Fact]
280+
public void TransformMatrix4x4_Scale()
281+
{
282+
RectangleF rect = new(10, 20, 100, 50);
283+
Matrix4x4 m = Matrix4x4.CreateScale(2, 3, 1);
284+
RectangleF result = RectangleF.Transform(rect, m);
285+
286+
Assert.Equal(new RectangleF(20, 60, 200, 150), result);
287+
}
288+
246289
[Fact]
247290
public void ToStringTest()
248291
{

tests/ImageSharp.Tests/Primitives/RectangleTests.cs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the Six Labors Split License.
33

44
using System.Globalization;
5+
using System.Numerics;
56

67
namespace SixLabors.ImageSharp.Tests;
78

@@ -294,6 +295,38 @@ public void OffsetTest(int x, int y, int width, int height)
294295
Assert.Equal(expectedRect, r1);
295296
}
296297

298+
[Fact]
299+
public void TransformMatrix4x4_AffineMatchesMatrix3x2()
300+
{
301+
Rectangle rect = new(10, 20, 100, 50);
302+
Matrix3x2 m3 = Matrix3x2.CreateTranslation(5, -3);
303+
Matrix4x4 m4 = new(m3);
304+
305+
RectangleF r3 = Rectangle.Transform(rect, m3);
306+
RectangleF r4 = Rectangle.Transform(rect, m4);
307+
308+
Assert.Equal(r3, r4);
309+
}
310+
311+
[Fact]
312+
public void TransformMatrix4x4_Identity()
313+
{
314+
Rectangle rect = new(10, 20, 100, 50);
315+
RectangleF result = Rectangle.Transform(rect, Matrix4x4.Identity);
316+
317+
Assert.Equal(new RectangleF(10, 20, 100, 50), result);
318+
}
319+
320+
[Fact]
321+
public void TransformMatrix4x4_Translation()
322+
{
323+
Rectangle rect = new(10, 20, 100, 50);
324+
Matrix4x4 m = Matrix4x4.CreateTranslation(5, -3, 0);
325+
RectangleF result = Rectangle.Transform(rect, m);
326+
327+
Assert.Equal(new RectangleF(15, 17, 100, 50), result);
328+
}
329+
297330
[Fact]
298331
public void ToStringTest()
299332
{

0 commit comments

Comments
 (0)