From 5a79591a0c3f2e094b20fb909a32613cc21a6ff0 Mon Sep 17 00:00:00 2001 From: Socolin Date: Fri, 7 Jun 2024 22:08:30 -0400 Subject: [PATCH 1/8] Add QuadDistortion to ProjectiveTransformBuilder --- .../Helpers/GaussianEliminationSolver.cs | 89 +++++++++++++++++++ .../Common/Helpers/QuadDistortionHelper.cs | 76 ++++++++++++++++ .../Processing/ProjectiveTransformBuilder.cs | 23 +++++ .../Common/GaussianEliminationSolverTest.cs | 66 ++++++++++++++ .../Transforms/ProjectiveTransformTests.cs | 26 ++++++ ...X=200, Y=200 ]-PointF [ X=-50, Y=200 ].png | 3 + ...[ X=150, Y=150 ]-PointF [ X=0, Y=150 ].png | 3 + ...ntF [ X=0, Y=150 ]-PointF [ X=0, Y=0 ].png | 3 + ... X=140, Y=210 ]-PointF [ X=15, Y=125 ].png | 3 + 9 files changed, 292 insertions(+) create mode 100644 src/ImageSharp/Common/Helpers/GaussianEliminationSolver.cs create mode 100644 src/ImageSharp/Common/Helpers/QuadDistortionHelper.cs create mode 100644 tests/ImageSharp.Tests/Common/GaussianEliminationSolverTest.cs create mode 100644 tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithQuadDistortion_Rgba32_TestPattern150x150_PointF [ X=-50, Y=-50 ]-PointF [ X=200, Y=-50 ]-PointF [ X=200, Y=200 ]-PointF [ X=-50, Y=200 ].png create mode 100644 tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithQuadDistortion_Rgba32_TestPattern150x150_PointF [ X=0, Y=0 ]-PointF [ X=150, Y=0 ]-PointF [ X=150, Y=150 ]-PointF [ X=0, Y=150 ].png create mode 100644 tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithQuadDistortion_Rgba32_TestPattern150x150_PointF [ X=150, Y=0 ]-PointF [ X=150, Y=150 ]-PointF [ X=0, Y=150 ]-PointF [ X=0, Y=0 ].png create mode 100644 tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithQuadDistortion_Rgba32_TestPattern150x150_PointF [ X=25, Y=50 ]-PointF [ X=210, Y=25 ]-PointF [ X=140, Y=210 ]-PointF [ X=15, Y=125 ].png diff --git a/src/ImageSharp/Common/Helpers/GaussianEliminationSolver.cs b/src/ImageSharp/Common/Helpers/GaussianEliminationSolver.cs new file mode 100644 index 000000000..3bb9eb395 --- /dev/null +++ b/src/ImageSharp/Common/Helpers/GaussianEliminationSolver.cs @@ -0,0 +1,89 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. +using System.Numerics; + +namespace SixLabors.ImageSharp.Common.Helpers; + +/// +/// Represents a solver for systems of linear equations using the Gaussian Elimination method. +/// This class applies Gaussian Elimination to transform the matrix into row echelon form and then performs back substitution to find the solution vector. +/// This implementation is based on: https://www.algorithm-archive.org/contents/gaussian_elimination/gaussian_elimination.html +/// +/// The type of numbers used in the matrix and solution vector. Must implement the interface. +internal static class GaussianEliminationSolver + where TNumber : INumber +{ + /// + /// Solves the system of linear equations represented by the given matrix and result vector using Gaussian Elimination. + /// + /// The square matrix representing the coefficients of the linear equations. + /// The vector representing the constants on the right-hand side of the linear equations. + /// Thrown if the matrix is singular and cannot be solved. + /// + /// The matrix passed to this method must be a square matrix. + /// If the matrix is singular (i.e., has no unique solution), an will be thrown. + /// + public static void Solve(TNumber[][] matrix, TNumber[] result) + { + TransformToRowEchelonForm(matrix, result); + ApplyBackSubstitution(matrix, result); + } + + private static void TransformToRowEchelonForm(TNumber[][] matrix, TNumber[] result) + { + int colCount = matrix.Length; + int rowCount = matrix[0].Length; + int pivotRow = 0; + for (int pivotCol = 0; pivotCol < colCount; pivotCol++) + { + TNumber maxValue = TNumber.Abs(matrix[pivotRow][pivotCol]); + int maxIndex = pivotRow; + for (int r = pivotRow + 1; r < rowCount; r++) + { + TNumber value = TNumber.Abs(matrix[r][pivotCol]); + if (value > maxValue) + { + maxIndex = r; + maxValue = value; + } + } + + if (matrix[maxIndex][pivotCol] == TNumber.Zero) + { + throw new NotSupportedException("Matrix is singular and cannot be solve"); + } + + (matrix[pivotRow], matrix[maxIndex]) = (matrix[maxIndex], matrix[pivotRow]); + (result[pivotRow], result[maxIndex]) = (result[maxIndex], result[pivotRow]); + + for (int r = pivotRow + 1; r < rowCount; r++) + { + TNumber fraction = matrix[r][pivotCol] / matrix[pivotRow][pivotCol]; + for (int c = pivotCol + 1; c < colCount; c++) + { + matrix[r][c] -= matrix[pivotRow][c] * fraction; + } + + result[r] -= result[pivotRow] * fraction; + matrix[r][pivotCol] = TNumber.Zero; + } + + pivotRow++; + } + } + + private static void ApplyBackSubstitution(TNumber[][] matrix, TNumber[] result) + { + int rowCount = matrix[0].Length; + + for (int row = rowCount - 1; row >= 0; row--) + { + result[row] /= matrix[row][row]; + + for (int r = 0; r < row; r++) + { + result[r] -= result[row] * matrix[r][row]; + } + } + } +} diff --git a/src/ImageSharp/Common/Helpers/QuadDistortionHelper.cs b/src/ImageSharp/Common/Helpers/QuadDistortionHelper.cs new file mode 100644 index 000000000..9fd6e84c7 --- /dev/null +++ b/src/ImageSharp/Common/Helpers/QuadDistortionHelper.cs @@ -0,0 +1,76 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; + +namespace SixLabors.ImageSharp.Common.Helpers; + +/// +/// Provides helper methods for performing quad distortion transformations. +/// +internal static class QuadDistortionHelper +{ + /// + /// Computes the projection matrix for a quad distortion transformation. + /// + /// The source rectangle. + /// The top-left point of the distorted quad. + /// The top-right point of the distorted quad. + /// The bottom-right point of the distorted quad. + /// The bottom-left point of the distorted quad. + /// The computed projection matrix for the quad distortion. + /// + /// This method is based on the algorithm described in the following article: + /// https://blog.mbedded.ninja/mathematics/geometry/projective-transformations/ + /// + public static Matrix4x4 ComputeQuadDistortMatrix(Rectangle rectangle, PointF topLeft, PointF topRight, PointF bottomRight, PointF bottomLeft) + { + PointF p1 = new(rectangle.X, rectangle.Y); + PointF p2 = new(rectangle.X + rectangle.Width, rectangle.Y); + PointF p3 = new(rectangle.X + rectangle.Width, rectangle.Y + rectangle.Height); + PointF p4 = new(rectangle.X, rectangle.Y + rectangle.Height); + + PointF q1 = topLeft; + PointF q2 = topRight; + PointF q3 = bottomRight; + PointF q4 = bottomLeft; + + // @formatter:off + float[][] matrixData = + [ + [p1.X, p1.Y, 1, 0, 0, 0, -p1.X * q1.X, -p1.Y * q1.X], + [0, 0, 0, p1.X, p1.Y, 1, -p1.X * q1.Y, -p1.Y * q1.Y], + [p2.X, p2.Y, 1, 0, 0, 0, -p2.X * q2.X, -p2.Y * q2.X], + [0, 0, 0, p2.X, p2.Y, 1, -p2.X * q2.Y, -p2.Y * q2.Y], + [p3.X, p3.Y, 1, 0, 0, 0, -p3.X * q3.X, -p3.Y * q3.X], + [0, 0, 0, p3.X, p3.Y, 1, -p3.X * q3.Y, -p3.Y * q3.Y], + [p4.X, p4.Y, 1, 0, 0, 0, -p4.X * q4.X, -p4.Y * q4.X], + [0, 0, 0, p4.X, p4.Y, 1, -p4.X * q4.Y, -p4.Y * q4.Y], + ]; + + float[] b = + [ + q1.X, + q1.Y, + q2.X, + q2.Y, + q3.X, + q3.Y, + q4.X, + q4.Y, + ]; + + GaussianEliminationSolver.Solve(matrixData, b); + + #pragma warning disable SA1117 + Matrix4x4 projectionMatrix = new( + b[0], b[3], 0, b[6], + b[1], b[4], 0, b[7], + 0, 0, 1, 0, + b[2], b[5], 0, 1); + #pragma warning restore SA1117 + + // @formatter:on + return projectionMatrix; + } +} diff --git a/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs b/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs index 0387adebb..037a88329 100644 --- a/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs +++ b/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using System.Numerics; +using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.Processing.Processors.Transforms; namespace SixLabors.ImageSharp.Processing; @@ -270,6 +271,28 @@ public class ProjectiveTransformBuilder public ProjectiveTransformBuilder AppendTranslation(Vector2 position) => this.AppendMatrix(Matrix4x4.CreateTranslation(new Vector3(position, 0))); + /// + /// Prepends a quad distortion matrix using the specified corner points. + /// + /// The top-left corner point of the distorted quad. + /// The top-right corner point of the distorted quad. + /// The bottom-right corner point of the distorted quad. + /// The bottom-left corner point of the distorted quad. + /// The . + public ProjectiveTransformBuilder PrependQuadDistortion(PointF topLeft, PointF topRight, PointF bottomRight, PointF bottomLeft) + => this.Prepend(size => QuadDistortionHelper.ComputeQuadDistortMatrix(new Rectangle(Point.Empty, size), topLeft, topRight, bottomRight, bottomLeft)); + + /// + /// Appends a quad distortion matrix using the specified corner points. + /// + /// The top-left corner point of the distorted quad. + /// The top-right corner point of the distorted quad. + /// The bottom-right corner point of the distorted quad. + /// The bottom-left corner point of the distorted quad. + /// The . + public ProjectiveTransformBuilder AppendQuadDistortion(PointF topLeft, PointF topRight, PointF bottomRight, PointF bottomLeft) + => this.Append(size => QuadDistortionHelper.ComputeQuadDistortMatrix(new Rectangle(Point.Empty, size), topLeft, topRight, bottomRight, bottomLeft)); + /// /// Prepends a raw matrix. /// diff --git a/tests/ImageSharp.Tests/Common/GaussianEliminationSolverTest.cs b/tests/ImageSharp.Tests/Common/GaussianEliminationSolverTest.cs new file mode 100644 index 000000000..85bc68b82 --- /dev/null +++ b/tests/ImageSharp.Tests/Common/GaussianEliminationSolverTest.cs @@ -0,0 +1,66 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Common.Helpers; + +namespace SixLabors.ImageSharp.Tests.Common; + +public class GaussianEliminationSolverTest +{ + [Theory] + [MemberData(nameof(MatrixTestData))] + public void CanSolve(float[][] matrix, float[] result, float[] expected) + { + GaussianEliminationSolver.Solve(matrix, result); + + for (int i = 0; i < expected.Length; i++) + { + Assert.Equal(result[i], expected[i], 4); + } + } + + public static TheoryData MatrixTestData + { + get + { + TheoryData data = []; + { + float[][] matrix = + [ + [2, 3, 4], + [1, 2, 3], + [3, -4, 0], + ]; + float[] result = [6, 4, 10]; + float[] expected = [18 / 11f, -14 / 11f, 18 / 11f]; + data.Add(matrix, result, expected); + } + + { + float[][] matrix = + [ + [1, 4, -1], + [2, 5, 8], + [1, 3, -3], + ]; + float[] result = [4, 15, 1]; + float[] expected = [1, 1, 1]; + data.Add(matrix, result, expected); + } + + { + float[][] matrix = + [ + [-1, 0, 0], + [0, 1, 0], + [0, 0, 1], + ]; + float[] result = [1, 2, 3]; + float[] expected = [-1, 2, 3]; + data.Add(matrix, result, expected); + } + + return data; + } + } +} diff --git a/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformTests.cs b/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformTests.cs index 4aec53036..293d39d66 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformTests.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformTests.cs @@ -55,6 +55,14 @@ public class ProjectiveTransformTests { TaperSide.Right, TaperCorner.RightOrBottom }, }; + public static readonly TheoryData QuadDistortionData = new() + { + { new PointF(0, 0), new PointF(150, 0), new PointF(150, 150), new PointF(0, 150) }, // source == destination + { new PointF(25, 50), new PointF(210, 25), new PointF(140, 210), new PointF(15, 125) }, // Distortion + { new PointF(-50, -50), new PointF(200, -50), new PointF(200, 200), new PointF(-50, 200) }, // Scaling + { new PointF(150, 0), new PointF(150, 150), new PointF(0, 150), new PointF(0, 0) }, // Rotation + }; + public ProjectiveTransformTests(ITestOutputHelper output) => this.Output = output; [Theory] @@ -93,6 +101,24 @@ public class ProjectiveTransformTests } } + [Theory] + [WithTestPatternImages(nameof(QuadDistortionData), 150, 150, PixelTypes.Rgba32)] + public void Transform_WithQuadDistortion(TestImageProvider provider, PointF topLeft, PointF topRight, PointF bottomRight, PointF bottomLeft) + where TPixel : unmanaged, IPixel + { + using (Image image = provider.GetImage()) + { + ProjectiveTransformBuilder builder = new ProjectiveTransformBuilder() + .AppendQuadDistortion(topLeft, topRight, bottomRight, bottomLeft); + + image.Mutate(i => i.Transform(builder)); + + FormattableString testOutputDetails = $"{topLeft}-{topRight}-{bottomRight}-{bottomLeft}"; + image.DebugSave(provider, testOutputDetails); + image.CompareFirstFrameToReferenceOutput(TolerantComparer, provider, testOutputDetails); + } + } + [Theory] [WithSolidFilledImages(100, 100, 0, 0, 255, PixelTypes.Rgba32)] public void RawTransformMatchesDocumentedExample(TestImageProvider provider) diff --git a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithQuadDistortion_Rgba32_TestPattern150x150_PointF [ X=-50, Y=-50 ]-PointF [ X=200, Y=-50 ]-PointF [ X=200, Y=200 ]-PointF [ X=-50, Y=200 ].png b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithQuadDistortion_Rgba32_TestPattern150x150_PointF [ X=-50, Y=-50 ]-PointF [ X=200, Y=-50 ]-PointF [ X=200, Y=200 ]-PointF [ X=-50, Y=200 ].png new file mode 100644 index 000000000..cac4e4ab2 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithQuadDistortion_Rgba32_TestPattern150x150_PointF [ X=-50, Y=-50 ]-PointF [ X=200, Y=-50 ]-PointF [ X=200, Y=200 ]-PointF [ X=-50, Y=200 ].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fb2804f09a350d9889d57bec60972b6a60ac341f179926cbb8361e9809e47883 +size 33139 diff --git a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithQuadDistortion_Rgba32_TestPattern150x150_PointF [ X=0, Y=0 ]-PointF [ X=150, Y=0 ]-PointF [ X=150, Y=150 ]-PointF [ X=0, Y=150 ].png b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithQuadDistortion_Rgba32_TestPattern150x150_PointF [ X=0, Y=0 ]-PointF [ X=150, Y=0 ]-PointF [ X=150, Y=150 ]-PointF [ X=0, Y=150 ].png new file mode 100644 index 000000000..2c4cb321b --- /dev/null +++ b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithQuadDistortion_Rgba32_TestPattern150x150_PointF [ X=0, Y=0 ]-PointF [ X=150, Y=0 ]-PointF [ X=150, Y=150 ]-PointF [ X=0, Y=150 ].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0f468d2def55cda480d1adb077d8d087b4b15229d25bd39cd9e4fe7a203d6747 +size 1982 diff --git a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithQuadDistortion_Rgba32_TestPattern150x150_PointF [ X=150, Y=0 ]-PointF [ X=150, Y=150 ]-PointF [ X=0, Y=150 ]-PointF [ X=0, Y=0 ].png b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithQuadDistortion_Rgba32_TestPattern150x150_PointF [ X=150, Y=0 ]-PointF [ X=150, Y=150 ]-PointF [ X=0, Y=150 ]-PointF [ X=0, Y=0 ].png new file mode 100644 index 000000000..cf48a0dcf --- /dev/null +++ b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithQuadDistortion_Rgba32_TestPattern150x150_PointF [ X=150, Y=0 ]-PointF [ X=150, Y=150 ]-PointF [ X=0, Y=150 ]-PointF [ X=0, Y=0 ].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e5db33fb6cd26e7641370ece52710e3ec3ab2e23da1b4586616f2fa6ce0c4d46 +size 3030 diff --git a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithQuadDistortion_Rgba32_TestPattern150x150_PointF [ X=25, Y=50 ]-PointF [ X=210, Y=25 ]-PointF [ X=140, Y=210 ]-PointF [ X=15, Y=125 ].png b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithQuadDistortion_Rgba32_TestPattern150x150_PointF [ X=25, Y=50 ]-PointF [ X=210, Y=25 ]-PointF [ X=140, Y=210 ]-PointF [ X=15, Y=125 ].png new file mode 100644 index 000000000..e0be7d88d --- /dev/null +++ b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithQuadDistortion_Rgba32_TestPattern150x150_PointF [ X=25, Y=50 ]-PointF [ X=210, Y=25 ]-PointF [ X=140, Y=210 ]-PointF [ X=15, Y=125 ].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9a1ff90af73904b689c8ef1a63f0194c86ec46596a51c9c6cff7483d326b9968 +size 36739 From e244efa77a932b1288d5f3dbc5f2ba584a777c1a Mon Sep 17 00:00:00 2001 From: Socolin Date: Wed, 31 Jul 2024 18:37:56 -0400 Subject: [PATCH 2/8] Adjust GaussianEliminationSolver, replace generic type with float --- .../Helpers/GaussianEliminationSolver.cs | 21 ++++++++----------- .../Common/Helpers/QuadDistortionHelper.cs | 2 +- .../Common/GaussianEliminationSolverTest.cs | 2 +- 3 files changed, 11 insertions(+), 14 deletions(-) diff --git a/src/ImageSharp/Common/Helpers/GaussianEliminationSolver.cs b/src/ImageSharp/Common/Helpers/GaussianEliminationSolver.cs index 3bb9eb395..840bc34e3 100644 --- a/src/ImageSharp/Common/Helpers/GaussianEliminationSolver.cs +++ b/src/ImageSharp/Common/Helpers/GaussianEliminationSolver.cs @@ -1,6 +1,5 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Numerics; namespace SixLabors.ImageSharp.Common.Helpers; @@ -9,9 +8,7 @@ namespace SixLabors.ImageSharp.Common.Helpers; /// This class applies Gaussian Elimination to transform the matrix into row echelon form and then performs back substitution to find the solution vector. /// This implementation is based on: https://www.algorithm-archive.org/contents/gaussian_elimination/gaussian_elimination.html /// -/// The type of numbers used in the matrix and solution vector. Must implement the interface. -internal static class GaussianEliminationSolver - where TNumber : INumber +internal static class GaussianEliminationSolver { /// /// Solves the system of linear equations represented by the given matrix and result vector using Gaussian Elimination. @@ -23,24 +20,24 @@ internal static class GaussianEliminationSolver /// The matrix passed to this method must be a square matrix. /// If the matrix is singular (i.e., has no unique solution), an will be thrown. /// - public static void Solve(TNumber[][] matrix, TNumber[] result) + public static void Solve(float[][] matrix, float[] result) { TransformToRowEchelonForm(matrix, result); ApplyBackSubstitution(matrix, result); } - private static void TransformToRowEchelonForm(TNumber[][] matrix, TNumber[] result) + private static void TransformToRowEchelonForm(float[][] matrix, float[] result) { int colCount = matrix.Length; int rowCount = matrix[0].Length; int pivotRow = 0; for (int pivotCol = 0; pivotCol < colCount; pivotCol++) { - TNumber maxValue = TNumber.Abs(matrix[pivotRow][pivotCol]); + float maxValue = float.Abs(matrix[pivotRow][pivotCol]); int maxIndex = pivotRow; for (int r = pivotRow + 1; r < rowCount; r++) { - TNumber value = TNumber.Abs(matrix[r][pivotCol]); + float value = float.Abs(matrix[r][pivotCol]); if (value > maxValue) { maxIndex = r; @@ -48,7 +45,7 @@ internal static class GaussianEliminationSolver } } - if (matrix[maxIndex][pivotCol] == TNumber.Zero) + if (matrix[maxIndex][pivotCol] == 0) { throw new NotSupportedException("Matrix is singular and cannot be solve"); } @@ -58,21 +55,21 @@ internal static class GaussianEliminationSolver for (int r = pivotRow + 1; r < rowCount; r++) { - TNumber fraction = matrix[r][pivotCol] / matrix[pivotRow][pivotCol]; + float fraction = matrix[r][pivotCol] / matrix[pivotRow][pivotCol]; for (int c = pivotCol + 1; c < colCount; c++) { matrix[r][c] -= matrix[pivotRow][c] * fraction; } result[r] -= result[pivotRow] * fraction; - matrix[r][pivotCol] = TNumber.Zero; + matrix[r][pivotCol] = 0; } pivotRow++; } } - private static void ApplyBackSubstitution(TNumber[][] matrix, TNumber[] result) + private static void ApplyBackSubstitution(float[][] matrix, float[] result) { int rowCount = matrix[0].Length; diff --git a/src/ImageSharp/Common/Helpers/QuadDistortionHelper.cs b/src/ImageSharp/Common/Helpers/QuadDistortionHelper.cs index 9fd6e84c7..1f114633d 100644 --- a/src/ImageSharp/Common/Helpers/QuadDistortionHelper.cs +++ b/src/ImageSharp/Common/Helpers/QuadDistortionHelper.cs @@ -60,7 +60,7 @@ internal static class QuadDistortionHelper q4.Y, ]; - GaussianEliminationSolver.Solve(matrixData, b); + GaussianEliminationSolver.Solve(matrixData, b); #pragma warning disable SA1117 Matrix4x4 projectionMatrix = new( diff --git a/tests/ImageSharp.Tests/Common/GaussianEliminationSolverTest.cs b/tests/ImageSharp.Tests/Common/GaussianEliminationSolverTest.cs index 85bc68b82..3964f4051 100644 --- a/tests/ImageSharp.Tests/Common/GaussianEliminationSolverTest.cs +++ b/tests/ImageSharp.Tests/Common/GaussianEliminationSolverTest.cs @@ -11,7 +11,7 @@ public class GaussianEliminationSolverTest [MemberData(nameof(MatrixTestData))] public void CanSolve(float[][] matrix, float[] result, float[] expected) { - GaussianEliminationSolver.Solve(matrix, result); + GaussianEliminationSolver.Solve(matrix, result); for (int i = 0; i < expected.Length; i++) { From 81026ba73cea0f0b9a3544274c3737f1cdad9781 Mon Sep 17 00:00:00 2001 From: Socolin Date: Wed, 31 Jul 2024 18:38:46 -0400 Subject: [PATCH 3/8] Remove comment to disable Jetbrains formatter from code --- .../Common/Helpers/QuadDistortionHelper.cs | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/src/ImageSharp/Common/Helpers/QuadDistortionHelper.cs b/src/ImageSharp/Common/Helpers/QuadDistortionHelper.cs index 1f114633d..8c621c486 100644 --- a/src/ImageSharp/Common/Helpers/QuadDistortionHelper.cs +++ b/src/ImageSharp/Common/Helpers/QuadDistortionHelper.cs @@ -35,17 +35,16 @@ internal static class QuadDistortionHelper PointF q3 = bottomRight; PointF q4 = bottomLeft; - // @formatter:off float[][] matrixData = [ - [p1.X, p1.Y, 1, 0, 0, 0, -p1.X * q1.X, -p1.Y * q1.X], - [0, 0, 0, p1.X, p1.Y, 1, -p1.X * q1.Y, -p1.Y * q1.Y], - [p2.X, p2.Y, 1, 0, 0, 0, -p2.X * q2.X, -p2.Y * q2.X], - [0, 0, 0, p2.X, p2.Y, 1, -p2.X * q2.Y, -p2.Y * q2.Y], - [p3.X, p3.Y, 1, 0, 0, 0, -p3.X * q3.X, -p3.Y * q3.X], - [0, 0, 0, p3.X, p3.Y, 1, -p3.X * q3.Y, -p3.Y * q3.Y], - [p4.X, p4.Y, 1, 0, 0, 0, -p4.X * q4.X, -p4.Y * q4.X], - [0, 0, 0, p4.X, p4.Y, 1, -p4.X * q4.Y, -p4.Y * q4.Y], + [p1.X, p1.Y, 1, 0, 0, 0, -p1.X * q1.X, -p1.Y * q1.X], + [0, 0, 0, p1.X, p1.Y, 1, -p1.X * q1.Y, -p1.Y * q1.Y], + [p2.X, p2.Y, 1, 0, 0, 0, -p2.X * q2.X, -p2.Y * q2.X], + [0, 0, 0, p2.X, p2.Y, 1, -p2.X * q2.Y, -p2.Y * q2.Y], + [p3.X, p3.Y, 1, 0, 0, 0, -p3.X * q3.X, -p3.Y * q3.X], + [0, 0, 0, p3.X, p3.Y, 1, -p3.X * q3.Y, -p3.Y * q3.Y], + [p4.X, p4.Y, 1, 0, 0, 0, -p4.X * q4.X, -p4.Y * q4.X], + [0, 0, 0, p4.X, p4.Y, 1, -p4.X * q4.Y, -p4.Y * q4.Y], ]; float[] b = @@ -62,15 +61,14 @@ internal static class QuadDistortionHelper GaussianEliminationSolver.Solve(matrixData, b); - #pragma warning disable SA1117 +#pragma warning disable SA1117 Matrix4x4 projectionMatrix = new( b[0], b[3], 0, b[6], b[1], b[4], 0, b[7], 0, 0, 1, 0, b[2], b[5], 0, 1); - #pragma warning restore SA1117 +#pragma warning restore SA1117 - // @formatter:on return projectionMatrix; } } From e7e9eff496ed9fea357f2ced6d7c85156e7e6b02 Mon Sep 17 00:00:00 2001 From: Socolin Date: Wed, 31 Jul 2024 18:48:13 -0400 Subject: [PATCH 4/8] Adjust AppendQuadDistortion and PrependQuadDistortion after rebase --- src/ImageSharp/Processing/ProjectiveTransformBuilder.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs b/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs index 037a88329..c7ed45232 100644 --- a/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs +++ b/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs @@ -280,7 +280,9 @@ public class ProjectiveTransformBuilder /// The bottom-left corner point of the distorted quad. /// The . public ProjectiveTransformBuilder PrependQuadDistortion(PointF topLeft, PointF topRight, PointF bottomRight, PointF bottomLeft) - => this.Prepend(size => QuadDistortionHelper.ComputeQuadDistortMatrix(new Rectangle(Point.Empty, size), topLeft, topRight, bottomRight, bottomLeft)); + => this.Prepend( + size => QuadDistortionHelper.ComputeQuadDistortMatrix(new Rectangle(Point.Empty, size), topLeft, topRight, bottomRight, bottomLeft), + size => QuadDistortionHelper.ComputeQuadDistortMatrix(new Rectangle(Point.Empty, size), topLeft, topRight, bottomRight, bottomLeft)); /// /// Appends a quad distortion matrix using the specified corner points. @@ -291,7 +293,10 @@ public class ProjectiveTransformBuilder /// The bottom-left corner point of the distorted quad. /// The . public ProjectiveTransformBuilder AppendQuadDistortion(PointF topLeft, PointF topRight, PointF bottomRight, PointF bottomLeft) - => this.Append(size => QuadDistortionHelper.ComputeQuadDistortMatrix(new Rectangle(Point.Empty, size), topLeft, topRight, bottomRight, bottomLeft)); + => this.Append( + size => QuadDistortionHelper.ComputeQuadDistortMatrix(new Rectangle(Point.Empty, size), topLeft, topRight, bottomRight, bottomLeft), + size => QuadDistortionHelper.ComputeQuadDistortMatrix(new Rectangle(Point.Empty, size), topLeft, topRight, bottomRight, bottomLeft) + ); /// /// Prepends a raw matrix. From 514d0393d6fa84a6f6f044e43639f7b57c1a6964 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 16 Oct 2024 19:46:51 +1000 Subject: [PATCH 5/8] Fix build issues --- .../Common/Helpers/GaussianEliminationSolver.cs | 2 +- src/ImageSharp/Common/Helpers/QuadDistortionHelper.cs | 4 ++-- src/ImageSharp/Processing/ProjectiveTransformBuilder.cs | 9 ++------- 3 files changed, 5 insertions(+), 10 deletions(-) diff --git a/src/ImageSharp/Common/Helpers/GaussianEliminationSolver.cs b/src/ImageSharp/Common/Helpers/GaussianEliminationSolver.cs index 840bc34e3..fdb479060 100644 --- a/src/ImageSharp/Common/Helpers/GaussianEliminationSolver.cs +++ b/src/ImageSharp/Common/Helpers/GaussianEliminationSolver.cs @@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Common.Helpers; /// /// Represents a solver for systems of linear equations using the Gaussian Elimination method. /// This class applies Gaussian Elimination to transform the matrix into row echelon form and then performs back substitution to find the solution vector. -/// This implementation is based on: https://www.algorithm-archive.org/contents/gaussian_elimination/gaussian_elimination.html +/// This implementation is based on: /// internal static class GaussianEliminationSolver { diff --git a/src/ImageSharp/Common/Helpers/QuadDistortionHelper.cs b/src/ImageSharp/Common/Helpers/QuadDistortionHelper.cs index 8c621c486..27863f773 100644 --- a/src/ImageSharp/Common/Helpers/QuadDistortionHelper.cs +++ b/src/ImageSharp/Common/Helpers/QuadDistortionHelper.cs @@ -21,7 +21,7 @@ internal static class QuadDistortionHelper /// The computed projection matrix for the quad distortion. /// /// This method is based on the algorithm described in the following article: - /// https://blog.mbedded.ninja/mathematics/geometry/projective-transformations/ + /// /// public static Matrix4x4 ComputeQuadDistortMatrix(Rectangle rectangle, PointF topLeft, PointF topRight, PointF bottomRight, PointF bottomLeft) { @@ -65,7 +65,7 @@ internal static class QuadDistortionHelper Matrix4x4 projectionMatrix = new( b[0], b[3], 0, b[6], b[1], b[4], 0, b[7], - 0, 0, 1, 0, + 0, 0, 1, 0, b[2], b[5], 0, 1); #pragma warning restore SA1117 diff --git a/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs b/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs index 06eeff962..d5dedb1a7 100644 --- a/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs +++ b/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs @@ -289,9 +289,7 @@ public class ProjectiveTransformBuilder /// The bottom-left corner point of the distorted quad. /// The . public ProjectiveTransformBuilder PrependQuadDistortion(PointF topLeft, PointF topRight, PointF bottomRight, PointF bottomLeft) - => this.Prepend( - size => QuadDistortionHelper.ComputeQuadDistortMatrix(new Rectangle(Point.Empty, size), topLeft, topRight, bottomRight, bottomLeft), - size => QuadDistortionHelper.ComputeQuadDistortMatrix(new Rectangle(Point.Empty, size), topLeft, topRight, bottomRight, bottomLeft)); + => this.Prepend(size => QuadDistortionHelper.ComputeQuadDistortMatrix(new Rectangle(Point.Empty, size), topLeft, topRight, bottomRight, bottomLeft)); /// /// Appends a quad distortion matrix using the specified corner points. @@ -302,10 +300,7 @@ public class ProjectiveTransformBuilder /// The bottom-left corner point of the distorted quad. /// The . public ProjectiveTransformBuilder AppendQuadDistortion(PointF topLeft, PointF topRight, PointF bottomRight, PointF bottomLeft) - => this.Append( - size => QuadDistortionHelper.ComputeQuadDistortMatrix(new Rectangle(Point.Empty, size), topLeft, topRight, bottomRight, bottomLeft), - size => QuadDistortionHelper.ComputeQuadDistortMatrix(new Rectangle(Point.Empty, size), topLeft, topRight, bottomRight, bottomLeft) - ); + => this.Append(size => QuadDistortionHelper.ComputeQuadDistortMatrix(new Rectangle(Point.Empty, size), topLeft, topRight, bottomRight, bottomLeft)); /// /// Prepends a raw matrix. From 4544742e345977f4b1cad5cb5e29b3cc45852d35 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 16 Oct 2024 20:53:16 +1000 Subject: [PATCH 6/8] Fix identity checks --- src/ImageSharp/Common/Helpers/QuadDistortionHelper.cs | 7 ++++++- .../Processing/Processors/Transforms/TransformUtils.cs | 8 ++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/ImageSharp/Common/Helpers/QuadDistortionHelper.cs b/src/ImageSharp/Common/Helpers/QuadDistortionHelper.cs index 27863f773..dcd89bafb 100644 --- a/src/ImageSharp/Common/Helpers/QuadDistortionHelper.cs +++ b/src/ImageSharp/Common/Helpers/QuadDistortionHelper.cs @@ -23,7 +23,12 @@ internal static class QuadDistortionHelper /// This method is based on the algorithm described in the following article: /// /// - public static Matrix4x4 ComputeQuadDistortMatrix(Rectangle rectangle, PointF topLeft, PointF topRight, PointF bottomRight, PointF bottomLeft) + public static Matrix4x4 ComputeQuadDistortMatrix( + Rectangle rectangle, + PointF topLeft, + PointF topRight, + PointF bottomRight, + PointF bottomLeft) { PointF p1 = new(rectangle.X, rectangle.Y); PointF p2 = new(rectangle.X + rectangle.Width, rectangle.Y); diff --git a/src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs b/src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs index 62ea5e830..fa6ab137b 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs @@ -301,7 +301,7 @@ internal static class TransformUtils { 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 (matrix.IsIdentity || matrix.Equals(default)) { return size; } @@ -376,7 +376,7 @@ internal static class TransformUtils { Guard.IsTrue(size.Width > 0 && size.Height > 0, nameof(size), "Source size dimensions cannot be 0!"); - if (matrix.Equals(default) || matrix.Equals(Matrix3x2.Identity)) + if (matrix.IsIdentity || matrix.Equals(default)) { return size; } @@ -412,7 +412,7 @@ internal static class TransformUtils /// private static bool TryGetTransformedRectangle(RectangleF rectangle, Matrix3x2 matrix, out Rectangle bounds) { - if (rectangle.Equals(default) || Matrix3x2.Identity.Equals(matrix)) + if (matrix.IsIdentity || rectangle.Equals(default)) { bounds = default; return false; @@ -439,7 +439,7 @@ internal static class TransformUtils [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool TryGetTransformedRectangle(RectangleF rectangle, Matrix4x4 matrix, out Rectangle bounds) { - if (rectangle.Equals(default) || Matrix4x4.Identity.Equals(matrix)) + if (matrix.IsIdentity || rectangle.Equals(default)) { bounds = default; return false; From c51d84d79d189b752430e53b10fd56e2c52d09b4 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 16 Oct 2024 21:25:07 +1000 Subject: [PATCH 7/8] Use double precision for accuracy --- .../Helpers/GaussianEliminationSolver.cs | 12 +++++----- .../Common/Helpers/QuadDistortionHelper.cs | 10 ++++---- .../Common/GaussianEliminationSolverTest.cs | 24 +++++++++---------- 3 files changed, 23 insertions(+), 23 deletions(-) diff --git a/src/ImageSharp/Common/Helpers/GaussianEliminationSolver.cs b/src/ImageSharp/Common/Helpers/GaussianEliminationSolver.cs index fdb479060..ee87cdc86 100644 --- a/src/ImageSharp/Common/Helpers/GaussianEliminationSolver.cs +++ b/src/ImageSharp/Common/Helpers/GaussianEliminationSolver.cs @@ -20,24 +20,24 @@ internal static class GaussianEliminationSolver /// The matrix passed to this method must be a square matrix. /// If the matrix is singular (i.e., has no unique solution), an will be thrown. /// - public static void Solve(float[][] matrix, float[] result) + public static void Solve(double[][] matrix, double[] result) { TransformToRowEchelonForm(matrix, result); ApplyBackSubstitution(matrix, result); } - private static void TransformToRowEchelonForm(float[][] matrix, float[] result) + private static void TransformToRowEchelonForm(double[][] matrix, double[] result) { int colCount = matrix.Length; int rowCount = matrix[0].Length; int pivotRow = 0; for (int pivotCol = 0; pivotCol < colCount; pivotCol++) { - float maxValue = float.Abs(matrix[pivotRow][pivotCol]); + double maxValue = double.Abs(matrix[pivotRow][pivotCol]); int maxIndex = pivotRow; for (int r = pivotRow + 1; r < rowCount; r++) { - float value = float.Abs(matrix[r][pivotCol]); + double value = double.Abs(matrix[r][pivotCol]); if (value > maxValue) { maxIndex = r; @@ -55,7 +55,7 @@ internal static class GaussianEliminationSolver for (int r = pivotRow + 1; r < rowCount; r++) { - float fraction = matrix[r][pivotCol] / matrix[pivotRow][pivotCol]; + double fraction = matrix[r][pivotCol] / matrix[pivotRow][pivotCol]; for (int c = pivotCol + 1; c < colCount; c++) { matrix[r][c] -= matrix[pivotRow][c] * fraction; @@ -69,7 +69,7 @@ internal static class GaussianEliminationSolver } } - private static void ApplyBackSubstitution(float[][] matrix, float[] result) + private static void ApplyBackSubstitution(double[][] matrix, double[] result) { int rowCount = matrix[0].Length; diff --git a/src/ImageSharp/Common/Helpers/QuadDistortionHelper.cs b/src/ImageSharp/Common/Helpers/QuadDistortionHelper.cs index dcd89bafb..c1eee3daf 100644 --- a/src/ImageSharp/Common/Helpers/QuadDistortionHelper.cs +++ b/src/ImageSharp/Common/Helpers/QuadDistortionHelper.cs @@ -40,7 +40,7 @@ internal static class QuadDistortionHelper PointF q3 = bottomRight; PointF q4 = bottomLeft; - float[][] matrixData = + double[][] matrixData = [ [p1.X, p1.Y, 1, 0, 0, 0, -p1.X * q1.X, -p1.Y * q1.X], [0, 0, 0, p1.X, p1.Y, 1, -p1.X * q1.Y, -p1.Y * q1.Y], @@ -52,7 +52,7 @@ internal static class QuadDistortionHelper [0, 0, 0, p4.X, p4.Y, 1, -p4.X * q4.Y, -p4.Y * q4.Y], ]; - float[] b = + double[] b = [ q1.X, q1.Y, @@ -68,10 +68,10 @@ internal static class QuadDistortionHelper #pragma warning disable SA1117 Matrix4x4 projectionMatrix = new( - b[0], b[3], 0, b[6], - b[1], b[4], 0, b[7], + (float)b[0], (float)b[3], 0, (float)b[6], + (float)b[1], (float)b[4], 0, (float)b[7], 0, 0, 1, 0, - b[2], b[5], 0, 1); + (float)b[2], (float)b[5], 0, 1); #pragma warning restore SA1117 return projectionMatrix; diff --git a/tests/ImageSharp.Tests/Common/GaussianEliminationSolverTest.cs b/tests/ImageSharp.Tests/Common/GaussianEliminationSolverTest.cs index 3964f4051..03bac732b 100644 --- a/tests/ImageSharp.Tests/Common/GaussianEliminationSolverTest.cs +++ b/tests/ImageSharp.Tests/Common/GaussianEliminationSolverTest.cs @@ -9,7 +9,7 @@ public class GaussianEliminationSolverTest { [Theory] [MemberData(nameof(MatrixTestData))] - public void CanSolve(float[][] matrix, float[] result, float[] expected) + public void CanSolve(double[][] matrix, double[] result, double[] expected) { GaussianEliminationSolver.Solve(matrix, result); @@ -19,44 +19,44 @@ public class GaussianEliminationSolverTest } } - public static TheoryData MatrixTestData + public static TheoryData MatrixTestData { get { - TheoryData data = []; + TheoryData data = []; { - float[][] matrix = + double[][] matrix = [ [2, 3, 4], [1, 2, 3], [3, -4, 0], ]; - float[] result = [6, 4, 10]; - float[] expected = [18 / 11f, -14 / 11f, 18 / 11f]; + double[] result = [6, 4, 10]; + double[] expected = [18 / 11f, -14 / 11f, 18 / 11f]; data.Add(matrix, result, expected); } { - float[][] matrix = + double[][] matrix = [ [1, 4, -1], [2, 5, 8], [1, 3, -3], ]; - float[] result = [4, 15, 1]; - float[] expected = [1, 1, 1]; + double[] result = [4, 15, 1]; + double[] expected = [1, 1, 1]; data.Add(matrix, result, expected); } { - float[][] matrix = + double[][] matrix = [ [-1, 0, 0], [0, 1, 0], [0, 0, 1], ]; - float[] result = [1, 2, 3]; - float[] expected = [-1, 2, 3]; + double[] result = [1, 2, 3]; + double[] expected = [-1, 2, 3]; data.Add(matrix, result, expected); } From 3d86c583eeb4681f33847a9a7e7db8fde4994b56 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 21 Oct 2024 20:08:10 +1000 Subject: [PATCH 8/8] Refactor and fix for transform space --- .../Common/Helpers/QuadDistortionHelper.cs | 79 --------- .../Processing/AffineTransformBuilder.cs | 19 +-- .../AffineTransformProcessor{TPixel}.cs | 29 ++-- .../Linear}/GaussianEliminationSolver.cs | 2 +- .../ProjectiveTransformProcessor{TPixel}.cs | 25 +-- .../Processors/Transforms/TransformUtils.cs | 151 +++++++++++++++--- .../Processing/ProjectiveTransformBuilder.cs | 23 +-- .../Common/GaussianEliminationSolverTest.cs | 2 +- .../Transforms/ProjectiveTransformTests.cs | 10 +- ...X=200, Y=200 ]-PointF [ X=-50, Y=200 ].png | 4 +- ...[ X=150, Y=150 ]-PointF [ X=0, Y=150 ].png | 4 +- ...ntF [ X=0, Y=150 ]-PointF [ X=0, Y=0 ].png | 4 +- ... X=140, Y=210 ]-PointF [ X=15, Y=125 ].png | 4 +- 13 files changed, 184 insertions(+), 172 deletions(-) delete mode 100644 src/ImageSharp/Common/Helpers/QuadDistortionHelper.cs rename src/ImageSharp/{Common/Helpers => Processing/Processors/Transforms/Linear}/GaussianEliminationSolver.cs (97%) diff --git a/src/ImageSharp/Common/Helpers/QuadDistortionHelper.cs b/src/ImageSharp/Common/Helpers/QuadDistortionHelper.cs deleted file mode 100644 index c1eee3daf..000000000 --- a/src/ImageSharp/Common/Helpers/QuadDistortionHelper.cs +++ /dev/null @@ -1,79 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Numerics; - -namespace SixLabors.ImageSharp.Common.Helpers; - -/// -/// Provides helper methods for performing quad distortion transformations. -/// -internal static class QuadDistortionHelper -{ - /// - /// Computes the projection matrix for a quad distortion transformation. - /// - /// The source rectangle. - /// The top-left point of the distorted quad. - /// The top-right point of the distorted quad. - /// The bottom-right point of the distorted quad. - /// The bottom-left point of the distorted quad. - /// The computed projection matrix for the quad distortion. - /// - /// This method is based on the algorithm described in the following article: - /// - /// - public static Matrix4x4 ComputeQuadDistortMatrix( - Rectangle rectangle, - PointF topLeft, - PointF topRight, - PointF bottomRight, - PointF bottomLeft) - { - PointF p1 = new(rectangle.X, rectangle.Y); - PointF p2 = new(rectangle.X + rectangle.Width, rectangle.Y); - PointF p3 = new(rectangle.X + rectangle.Width, rectangle.Y + rectangle.Height); - PointF p4 = new(rectangle.X, rectangle.Y + rectangle.Height); - - PointF q1 = topLeft; - PointF q2 = topRight; - PointF q3 = bottomRight; - PointF q4 = bottomLeft; - - double[][] matrixData = - [ - [p1.X, p1.Y, 1, 0, 0, 0, -p1.X * q1.X, -p1.Y * q1.X], - [0, 0, 0, p1.X, p1.Y, 1, -p1.X * q1.Y, -p1.Y * q1.Y], - [p2.X, p2.Y, 1, 0, 0, 0, -p2.X * q2.X, -p2.Y * q2.X], - [0, 0, 0, p2.X, p2.Y, 1, -p2.X * q2.Y, -p2.Y * q2.Y], - [p3.X, p3.Y, 1, 0, 0, 0, -p3.X * q3.X, -p3.Y * q3.X], - [0, 0, 0, p3.X, p3.Y, 1, -p3.X * q3.Y, -p3.Y * q3.Y], - [p4.X, p4.Y, 1, 0, 0, 0, -p4.X * q4.X, -p4.Y * q4.X], - [0, 0, 0, p4.X, p4.Y, 1, -p4.X * q4.Y, -p4.Y * q4.Y], - ]; - - double[] b = - [ - q1.X, - q1.Y, - q2.X, - q2.Y, - q3.X, - q3.Y, - q4.X, - q4.Y, - ]; - - GaussianEliminationSolver.Solve(matrixData, b); - -#pragma warning disable SA1117 - Matrix4x4 projectionMatrix = new( - (float)b[0], (float)b[3], 0, (float)b[6], - (float)b[1], (float)b[4], 0, (float)b[7], - 0, 0, 1, 0, - (float)b[2], (float)b[5], 0, 1); -#pragma warning restore SA1117 - - return projectionMatrix; - } -} diff --git a/src/ImageSharp/Processing/AffineTransformBuilder.cs b/src/ImageSharp/Processing/AffineTransformBuilder.cs index 4ac9546f3..6d1e8aaa5 100644 --- a/src/ImageSharp/Processing/AffineTransformBuilder.cs +++ b/src/ImageSharp/Processing/AffineTransformBuilder.cs @@ -11,7 +11,7 @@ namespace SixLabors.ImageSharp.Processing; /// public class AffineTransformBuilder { - private readonly List> transformMatrixFactories = new(); + private readonly List> transformMatrixFactories = []; /// /// Initializes a new instance of the class. @@ -301,7 +301,8 @@ public class AffineTransformBuilder /// /// The source image size. /// The . - public Matrix3x2 BuildMatrix(Size sourceSize) => this.BuildMatrix(new Rectangle(Point.Empty, sourceSize)); + public Matrix3x2 BuildMatrix(Size sourceSize) + => this.BuildMatrix(new Rectangle(Point.Empty, sourceSize)); /// /// Returns the combined transform matrix for a given source rectangle. @@ -345,18 +346,8 @@ public class AffineTransformBuilder /// The . public Size GetTransformedSize(Rectangle sourceRectangle) { - Size size = sourceRectangle.Size; - - // Translate the origin matrix to cater for source rectangle offsets. - Matrix3x2 matrix = Matrix3x2.CreateTranslation(-sourceRectangle.Location); - - foreach (Func factory in this.transformMatrixFactories) - { - matrix *= factory(size); - CheckDegenerate(matrix); - } - - return TransformUtils.GetTransformedSize(matrix, size, this.TransformSpace); + Matrix3x2 matrix = this.BuildMatrix(sourceRectangle); + return TransformUtils.GetTransformedSize(matrix, sourceRectangle.Size, this.TransformSpace); } private static void CheckDegenerate(Matrix3x2 matrix) diff --git a/src/ImageSharp/Processing/Processors/Transforms/Linear/AffineTransformProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Transforms/Linear/AffineTransformProcessor{TPixel}.cs index c5c2a778e..888d51320 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Linear/AffineTransformProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Linear/AffineTransformProcessor{TPixel}.cs @@ -61,12 +61,12 @@ internal class AffineTransformProcessor : TransformProcessor, IR if (matrix.Equals(Matrix3x2.Identity)) { // The clone will be blank here copy all the pixel data over - var interest = Rectangle.Intersect(this.SourceRectangle, destination.Bounds()); + Rectangle interest = Rectangle.Intersect(this.SourceRectangle, destination.Bounds()); Buffer2DRegion sourceBuffer = source.PixelBuffer.GetRegion(interest); - Buffer2DRegion destbuffer = destination.PixelBuffer.GetRegion(interest); + Buffer2DRegion destinationBuffer = destination.PixelBuffer.GetRegion(interest); for (int y = 0; y < sourceBuffer.Height; y++) { - sourceBuffer.DangerousGetRowSpan(y).CopyTo(destbuffer.DangerousGetRowSpan(y)); + sourceBuffer.DangerousGetRowSpan(y).CopyTo(destinationBuffer.DangerousGetRowSpan(y)); } return; @@ -77,7 +77,7 @@ internal class AffineTransformProcessor : TransformProcessor, IR if (sampler is NearestNeighborResampler) { - var nnOperation = new NNAffineOperation( + NNAffineOperation nnOperation = new( source.PixelBuffer, Rectangle.Intersect(this.SourceRectangle, source.Bounds()), destination.PixelBuffer, @@ -91,7 +91,7 @@ internal class AffineTransformProcessor : TransformProcessor, IR return; } - var operation = new AffineOperation( + AffineOperation operation = new( configuration, source.PixelBuffer, Rectangle.Intersect(this.SourceRectangle, source.Bounds()), @@ -128,17 +128,17 @@ internal class AffineTransformProcessor : TransformProcessor, IR [MethodImpl(InliningOptions.ShortMethod)] public void Invoke(int y) { - Span destRow = this.destination.DangerousGetRowSpan(y); + Span destinationRowSpan = this.destination.DangerousGetRowSpan(y); - for (int x = 0; x < destRow.Length; x++) + for (int x = 0; x < destinationRowSpan.Length; x++) { - var point = Vector2.Transform(new Vector2(x, y), this.matrix); + Vector2 point = Vector2.Transform(new Vector2(x, y), this.matrix); int px = (int)MathF.Round(point.X); int py = (int)MathF.Round(point.Y); if (this.bounds.Contains(px, py)) { - destRow[x] = this.source.GetElementUnsafe(px, py); + destinationRowSpan[x] = this.source.GetElementUnsafe(px, py); } } } @@ -195,16 +195,16 @@ internal class AffineTransformProcessor : TransformProcessor, IR for (int y = rows.Min; y < rows.Max; y++) { - Span rowSpan = this.destination.DangerousGetRowSpan(y); + Span destinationRowSpan = this.destination.DangerousGetRowSpan(y); PixelOperations.Instance.ToVector4( this.configuration, - rowSpan, + destinationRowSpan, span, PixelConversionModifiers.Scale); for (int x = 0; x < span.Length; x++) { - var point = Vector2.Transform(new Vector2(x, y), matrix); + Vector2 point = Vector2.Transform(new Vector2(x, y), matrix); float pY = point.Y; float pX = point.X; @@ -221,13 +221,14 @@ internal class AffineTransformProcessor : TransformProcessor, IR Vector4 sum = Vector4.Zero; for (int yK = top; yK <= bottom; yK++) { + Span sourceRowSpan = this.source.DangerousGetRowSpan(yK); float yWeight = sampler.GetValue(yK - pY); for (int xK = left; xK <= right; xK++) { float xWeight = sampler.GetValue(xK - pX); - Vector4 current = this.source.GetElementUnsafe(xK, yK).ToScaledVector4(); + Vector4 current = sourceRowSpan[xK].ToScaledVector4(); Numerics.Premultiply(ref current); sum += current * xWeight * yWeight; } @@ -240,7 +241,7 @@ internal class AffineTransformProcessor : TransformProcessor, IR PixelOperations.Instance.FromVector4Destructive( this.configuration, span, - rowSpan, + destinationRowSpan, PixelConversionModifiers.Scale); } } diff --git a/src/ImageSharp/Common/Helpers/GaussianEliminationSolver.cs b/src/ImageSharp/Processing/Processors/Transforms/Linear/GaussianEliminationSolver.cs similarity index 97% rename from src/ImageSharp/Common/Helpers/GaussianEliminationSolver.cs rename to src/ImageSharp/Processing/Processors/Transforms/Linear/GaussianEliminationSolver.cs index ee87cdc86..1190de435 100644 --- a/src/ImageSharp/Common/Helpers/GaussianEliminationSolver.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Linear/GaussianEliminationSolver.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Common.Helpers; +namespace SixLabors.ImageSharp.Processing.Processors.Transforms.Linear; /// /// Represents a solver for systems of linear equations using the Gaussian Elimination method. diff --git a/src/ImageSharp/Processing/Processors/Transforms/Linear/ProjectiveTransformProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Transforms/Linear/ProjectiveTransformProcessor{TPixel}.cs index b741dc4ee..068f69ceb 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Linear/ProjectiveTransformProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Linear/ProjectiveTransformProcessor{TPixel}.cs @@ -61,12 +61,12 @@ internal class ProjectiveTransformProcessor : TransformProcessor if (matrix.Equals(Matrix4x4.Identity)) { // The clone will be blank here copy all the pixel data over - var interest = Rectangle.Intersect(this.SourceRectangle, destination.Bounds()); + Rectangle interest = Rectangle.Intersect(this.SourceRectangle, destination.Bounds()); Buffer2DRegion sourceBuffer = source.PixelBuffer.GetRegion(interest); - Buffer2DRegion destbuffer = destination.PixelBuffer.GetRegion(interest); + Buffer2DRegion destinationBuffer = destination.PixelBuffer.GetRegion(interest); for (int y = 0; y < sourceBuffer.Height; y++) { - sourceBuffer.DangerousGetRowSpan(y).CopyTo(destbuffer.DangerousGetRowSpan(y)); + sourceBuffer.DangerousGetRowSpan(y).CopyTo(destinationBuffer.DangerousGetRowSpan(y)); } return; @@ -77,7 +77,7 @@ internal class ProjectiveTransformProcessor : TransformProcessor if (sampler is NearestNeighborResampler) { - var nnOperation = new NNProjectiveOperation( + NNProjectiveOperation nnOperation = new( source.PixelBuffer, Rectangle.Intersect(this.SourceRectangle, source.Bounds()), destination.PixelBuffer, @@ -91,7 +91,7 @@ internal class ProjectiveTransformProcessor : TransformProcessor return; } - var operation = new ProjectiveOperation( + ProjectiveOperation operation = new( configuration, source.PixelBuffer, Rectangle.Intersect(this.SourceRectangle, source.Bounds()), @@ -128,9 +128,9 @@ internal class ProjectiveTransformProcessor : TransformProcessor [MethodImpl(InliningOptions.ShortMethod)] public void Invoke(int y) { - Span destRow = this.destination.DangerousGetRowSpan(y); + Span destinationRowSpan = this.destination.DangerousGetRowSpan(y); - for (int x = 0; x < destRow.Length; x++) + for (int x = 0; x < destinationRowSpan.Length; x++) { Vector2 point = TransformUtils.ProjectiveTransform2D(x, y, this.matrix); int px = (int)MathF.Round(point.X); @@ -138,7 +138,7 @@ internal class ProjectiveTransformProcessor : TransformProcessor if (this.bounds.Contains(px, py)) { - destRow[x] = this.source.GetElementUnsafe(px, py); + destinationRowSpan[x] = this.source.GetElementUnsafe(px, py); } } } @@ -195,10 +195,10 @@ internal class ProjectiveTransformProcessor : TransformProcessor for (int y = rows.Min; y < rows.Max; y++) { - Span rowSpan = this.destination.DangerousGetRowSpan(y); + Span destinationRowSpan = this.destination.DangerousGetRowSpan(y); PixelOperations.Instance.ToVector4( this.configuration, - rowSpan, + destinationRowSpan, span, PixelConversionModifiers.Scale); @@ -221,13 +221,14 @@ internal class ProjectiveTransformProcessor : TransformProcessor Vector4 sum = Vector4.Zero; for (int yK = top; yK <= bottom; yK++) { + Span sourceRowSpan = this.source.DangerousGetRowSpan(yK); float yWeight = sampler.GetValue(yK - pY); for (int xK = left; xK <= right; xK++) { float xWeight = sampler.GetValue(xK - pX); - Vector4 current = this.source.GetElementUnsafe(xK, yK).ToScaledVector4(); + Vector4 current = sourceRowSpan[xK].ToScaledVector4(); Numerics.Premultiply(ref current); sum += current * xWeight * yWeight; } @@ -240,7 +241,7 @@ internal class ProjectiveTransformProcessor : TransformProcessor PixelOperations.Instance.FromVector4Destructive( this.configuration, span, - rowSpan, + destinationRowSpan, PixelConversionModifiers.Scale); } } diff --git a/src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs b/src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs index fa6ab137b..47b3250b8 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs @@ -3,6 +3,7 @@ using System.Numerics; using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Processing.Processors.Transforms.Linear; namespace SixLabors.ImageSharp.Processing.Processors.Transforms; @@ -278,6 +279,91 @@ internal static class TransformUtils return matrix; } + /// + /// Computes the projection matrix for a quad distortion transformation. + /// + /// The source rectangle. + /// The top-left point of the distorted quad. + /// The top-right point of the distorted quad. + /// The bottom-right point of the distorted quad. + /// The bottom-left point of the distorted quad. + /// The to use when creating the matrix. + /// The computed projection matrix for the quad distortion. + /// + /// This method is based on the algorithm described in the following article: + /// + /// + public static Matrix4x4 CreateQuadDistortionMatrix( + Rectangle rectangle, + PointF topLeft, + PointF topRight, + PointF bottomRight, + PointF bottomLeft, + TransformSpace transformSpace) + { + PointF p1 = new(rectangle.X, rectangle.Y); + PointF p2 = new(rectangle.X + rectangle.Width, rectangle.Y); + PointF p3 = new(rectangle.X + rectangle.Width, rectangle.Y + rectangle.Height); + PointF p4 = new(rectangle.X, rectangle.Y + rectangle.Height); + + PointF q1 = topLeft; + PointF q2 = topRight; + PointF q3 = bottomRight; + PointF q4 = bottomLeft; + + double[][] matrixData = + [ + [p1.X, p1.Y, 1, 0, 0, 0, -p1.X * q1.X, -p1.Y * q1.X], + [0, 0, 0, p1.X, p1.Y, 1, -p1.X * q1.Y, -p1.Y * q1.Y], + [p2.X, p2.Y, 1, 0, 0, 0, -p2.X * q2.X, -p2.Y * q2.X], + [0, 0, 0, p2.X, p2.Y, 1, -p2.X * q2.Y, -p2.Y * q2.Y], + [p3.X, p3.Y, 1, 0, 0, 0, -p3.X * q3.X, -p3.Y * q3.X], + [0, 0, 0, p3.X, p3.Y, 1, -p3.X * q3.Y, -p3.Y * q3.Y], + [p4.X, p4.Y, 1, 0, 0, 0, -p4.X * q4.X, -p4.Y * q4.X], + [0, 0, 0, p4.X, p4.Y, 1, -p4.X * q4.Y, -p4.Y * q4.Y], + ]; + + double[] b = + [ + q1.X, + q1.Y, + q2.X, + q2.Y, + q3.X, + q3.Y, + q4.X, + q4.Y, + ]; + + GaussianEliminationSolver.Solve(matrixData, b); + +#pragma warning disable SA1117 + Matrix4x4 projectionMatrix = new( + (float)b[0], (float)b[3], 0, (float)b[6], + (float)b[1], (float)b[4], 0, (float)b[7], + 0, 0, 1, 0, + (float)b[2], (float)b[5], 0, 1); +#pragma warning restore SA1117 + + // 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. + if (transformSpace == TransformSpace.Pixel && IsAffineRotationOrSkew(projectionMatrix)) + { + if (projectionMatrix.M41 != 0) + { + projectionMatrix.M41--; + } + + if (projectionMatrix.M42 != 0) + { + projectionMatrix.M42--; + } + } + + return projectionMatrix; + } + /// /// Returns the size relative to the source for the given transformation matrix. /// @@ -293,11 +379,12 @@ internal static class TransformUtils /// /// The transformation matrix. /// The source size. + /// The used when generating the matrix. /// /// The . /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Size GetTransformedSize(Matrix4x4 matrix, Size size) + public static Size GetTransformedSize(Matrix4x4 matrix, Size size, TransformSpace transformSpace) { Guard.IsTrue(size.Width > 0 && size.Height > 0, nameof(size), "Source size dimensions cannot be 0!"); @@ -309,27 +396,7 @@ internal static class TransformUtils // 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) && - - // 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); + bool usePixelSpace = transformSpace == TransformSpace.Pixel && IsAffineRotationOrSkew(matrix); // 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. @@ -492,4 +559,44 @@ internal static class TransformUtils (int)Math.Ceiling(right), (int)Math.Ceiling(bottom)); } + + private static bool IsAffineRotationOrSkew(Matrix4x4 matrix) + { + const float epsilon = 1e-6f; + + // Check if the matrix is affine (last column should be [0, 0, 0, 1]) + if (Math.Abs(matrix.M14) > epsilon || + Math.Abs(matrix.M24) > epsilon || + Math.Abs(matrix.M34) > epsilon || + Math.Abs(matrix.M44 - 1f) > epsilon) + { + return false; + } + + // Translation component (M41, m42) are allowed, others are not. + if (Math.Abs(matrix.M43) > epsilon) + { + return false; + } + + // Extract the linear (rotation and skew) part of the matrix + // Upper-left 3x3 matrix + float m11 = matrix.M11, m12 = matrix.M12, m13 = matrix.M13; + float m21 = matrix.M21, m22 = matrix.M22, m23 = matrix.M23; + float m31 = matrix.M31, m32 = matrix.M32, m33 = matrix.M33; + + // Compute the determinant of the linear part + float determinant = (m11 * ((m22 * m33) - (m23 * m32))) - + (m12 * ((m21 * m33) - (m23 * m31))) + + (m13 * ((m21 * m32) - (m22 * m31))); + + // Check if the determinant is approximately ±1 (no scaling) + if (Math.Abs(Math.Abs(determinant) - 1f) > epsilon) + { + return false; + } + + // All checks passed; the matrix represents rotation and/or skew (with possible translation) + return true; + } } diff --git a/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs b/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs index d5dedb1a7..82b897ea5 100644 --- a/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs +++ b/src/ImageSharp/Processing/ProjectiveTransformBuilder.cs @@ -2,7 +2,6 @@ // Licensed under the Six Labors Split License. using System.Numerics; -using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.Processing.Processors.Transforms; namespace SixLabors.ImageSharp.Processing; @@ -12,7 +11,7 @@ namespace SixLabors.ImageSharp.Processing; /// public class ProjectiveTransformBuilder { - private readonly List> transformMatrixFactories = new(); + private readonly List> transformMatrixFactories = []; /// /// Initializes a new instance of the class. @@ -289,7 +288,8 @@ public class ProjectiveTransformBuilder /// The bottom-left corner point of the distorted quad. /// The . public ProjectiveTransformBuilder PrependQuadDistortion(PointF topLeft, PointF topRight, PointF bottomRight, PointF bottomLeft) - => this.Prepend(size => QuadDistortionHelper.ComputeQuadDistortMatrix(new Rectangle(Point.Empty, size), topLeft, topRight, bottomRight, bottomLeft)); + => this.Prepend(size => TransformUtils.CreateQuadDistortionMatrix( + new Rectangle(Point.Empty, size), topLeft, topRight, bottomRight, bottomLeft, this.TransformSpace)); /// /// Appends a quad distortion matrix using the specified corner points. @@ -300,7 +300,8 @@ public class ProjectiveTransformBuilder /// The bottom-left corner point of the distorted quad. /// The . public ProjectiveTransformBuilder AppendQuadDistortion(PointF topLeft, PointF topRight, PointF bottomRight, PointF bottomLeft) - => this.Append(size => QuadDistortionHelper.ComputeQuadDistortMatrix(new Rectangle(Point.Empty, size), topLeft, topRight, bottomRight, bottomLeft)); + => this.Append(size => TransformUtils.CreateQuadDistortionMatrix( + new Rectangle(Point.Empty, size), topLeft, topRight, bottomRight, bottomLeft, this.TransformSpace)); /// /// Prepends a raw matrix. @@ -384,18 +385,8 @@ public class ProjectiveTransformBuilder /// The . public Size GetTransformedSize(Rectangle sourceRectangle) { - Size size = sourceRectangle.Size; - - // Translate the origin matrix to cater for source rectangle offsets. - Matrix4x4 matrix = Matrix4x4.CreateTranslation(new Vector3(-sourceRectangle.Location, 0)); - - foreach (Func factory in this.transformMatrixFactories) - { - matrix *= factory(size); - CheckDegenerate(matrix); - } - - return TransformUtils.GetTransformedSize(matrix, size); + Matrix4x4 matrix = this.BuildMatrix(sourceRectangle); + return TransformUtils.GetTransformedSize(matrix, sourceRectangle.Size, this.TransformSpace); } private static void CheckDegenerate(Matrix4x4 matrix) diff --git a/tests/ImageSharp.Tests/Common/GaussianEliminationSolverTest.cs b/tests/ImageSharp.Tests/Common/GaussianEliminationSolverTest.cs index 03bac732b..95b8d2013 100644 --- a/tests/ImageSharp.Tests/Common/GaussianEliminationSolverTest.cs +++ b/tests/ImageSharp.Tests/Common/GaussianEliminationSolverTest.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.Common.Helpers; +using SixLabors.ImageSharp.Processing.Processors.Transforms.Linear; namespace SixLabors.ImageSharp.Tests.Common; diff --git a/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformTests.cs b/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformTests.cs index a8e05a332..6b6db69c1 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformTests.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformTests.cs @@ -154,11 +154,11 @@ public class ProjectiveTransformTests using (Image image = provider.GetImage()) { #pragma warning disable SA1117 // Parameters should be on same line or separate lines - 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); + 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() diff --git a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithQuadDistortion_Rgba32_TestPattern150x150_PointF [ X=-50, Y=-50 ]-PointF [ X=200, Y=-50 ]-PointF [ X=200, Y=200 ]-PointF [ X=-50, Y=200 ].png b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithQuadDistortion_Rgba32_TestPattern150x150_PointF [ X=-50, Y=-50 ]-PointF [ X=200, Y=-50 ]-PointF [ X=200, Y=200 ]-PointF [ X=-50, Y=200 ].png index cac4e4ab2..38c603855 100644 --- a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithQuadDistortion_Rgba32_TestPattern150x150_PointF [ X=-50, Y=-50 ]-PointF [ X=200, Y=-50 ]-PointF [ X=200, Y=200 ]-PointF [ X=-50, Y=200 ].png +++ b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithQuadDistortion_Rgba32_TestPattern150x150_PointF [ X=-50, Y=-50 ]-PointF [ X=200, Y=-50 ]-PointF [ X=200, Y=200 ]-PointF [ X=-50, Y=200 ].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fb2804f09a350d9889d57bec60972b6a60ac341f179926cbb8361e9809e47883 -size 33139 +oid sha256:abce6af307a81a8ebac8e502142b00b2615403b5570c8dbe7b6895cfdd1a6d60 +size 66879 diff --git a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithQuadDistortion_Rgba32_TestPattern150x150_PointF [ X=0, Y=0 ]-PointF [ X=150, Y=0 ]-PointF [ X=150, Y=150 ]-PointF [ X=0, Y=150 ].png b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithQuadDistortion_Rgba32_TestPattern150x150_PointF [ X=0, Y=0 ]-PointF [ X=150, Y=0 ]-PointF [ X=150, Y=150 ]-PointF [ X=0, Y=150 ].png index 2c4cb321b..f7ea0d006 100644 --- a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithQuadDistortion_Rgba32_TestPattern150x150_PointF [ X=0, Y=0 ]-PointF [ X=150, Y=0 ]-PointF [ X=150, Y=150 ]-PointF [ X=0, Y=150 ].png +++ b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithQuadDistortion_Rgba32_TestPattern150x150_PointF [ X=0, Y=0 ]-PointF [ X=150, Y=0 ]-PointF [ X=150, Y=150 ]-PointF [ X=0, Y=150 ].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0f468d2def55cda480d1adb077d8d087b4b15229d25bd39cd9e4fe7a203d6747 -size 1982 +oid sha256:d4cda265a50aa26711efafdbcd947c9a01eff872611df5298920583f9a3d4224 +size 26458 diff --git a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithQuadDistortion_Rgba32_TestPattern150x150_PointF [ X=150, Y=0 ]-PointF [ X=150, Y=150 ]-PointF [ X=0, Y=150 ]-PointF [ X=0, Y=0 ].png b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithQuadDistortion_Rgba32_TestPattern150x150_PointF [ X=150, Y=0 ]-PointF [ X=150, Y=150 ]-PointF [ X=0, Y=150 ]-PointF [ X=0, Y=0 ].png index cf48a0dcf..78c37cc44 100644 --- a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithQuadDistortion_Rgba32_TestPattern150x150_PointF [ X=150, Y=0 ]-PointF [ X=150, Y=150 ]-PointF [ X=0, Y=150 ]-PointF [ X=0, Y=0 ].png +++ b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithQuadDistortion_Rgba32_TestPattern150x150_PointF [ X=150, Y=0 ]-PointF [ X=150, Y=150 ]-PointF [ X=0, Y=150 ]-PointF [ X=0, Y=0 ].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e5db33fb6cd26e7641370ece52710e3ec3ab2e23da1b4586616f2fa6ce0c4d46 -size 3030 +oid sha256:278a488a858b8eda141493fe00c617eb1f664196853da8341d7e5b7f231ddce4 +size 24645 diff --git a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithQuadDistortion_Rgba32_TestPattern150x150_PointF [ X=25, Y=50 ]-PointF [ X=210, Y=25 ]-PointF [ X=140, Y=210 ]-PointF [ X=15, Y=125 ].png b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithQuadDistortion_Rgba32_TestPattern150x150_PointF [ X=25, Y=50 ]-PointF [ X=210, Y=25 ]-PointF [ X=140, Y=210 ]-PointF [ X=15, Y=125 ].png index e0be7d88d..b4740828d 100644 --- a/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithQuadDistortion_Rgba32_TestPattern150x150_PointF [ X=25, Y=50 ]-PointF [ X=210, Y=25 ]-PointF [ X=140, Y=210 ]-PointF [ X=15, Y=125 ].png +++ b/tests/Images/External/ReferenceOutput/ProjectiveTransformTests/Transform_WithQuadDistortion_Rgba32_TestPattern150x150_PointF [ X=25, Y=50 ]-PointF [ X=210, Y=25 ]-PointF [ X=140, Y=210 ]-PointF [ X=15, Y=125 ].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9a1ff90af73904b689c8ef1a63f0194c86ec46596a51c9c6cff7483d326b9968 -size 36739 +oid sha256:e03e79e6fab3a9e43041e54640a04c7cc3677709e7d879f9f410cf8afc7547a7 +size 42691