Skip to content

Commit 5a79591

Browse files
committed
Add QuadDistortion to ProjectiveTransformBuilder
1 parent 8da27c9 commit 5a79591

9 files changed

+292
-0
lines changed
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
// Copyright (c) Six Labors.
2+
// Licensed under the Six Labors Split License.
3+
using System.Numerics;
4+
5+
namespace SixLabors.ImageSharp.Common.Helpers;
6+
7+
/// <summary>
8+
/// Represents a solver for systems of linear equations using the Gaussian Elimination method.
9+
/// This class applies Gaussian Elimination to transform the matrix into row echelon form and then performs back substitution to find the solution vector.
10+
/// This implementation is based on: https://www.algorithm-archive.org/contents/gaussian_elimination/gaussian_elimination.html
11+
/// </summary>
12+
/// <typeparam name="TNumber">The type of numbers used in the matrix and solution vector. Must implement the <see cref="INumber{TNumber}"/> interface.</typeparam>
13+
internal static class GaussianEliminationSolver<TNumber>
14+
where TNumber : INumber<TNumber>
15+
{
16+
/// <summary>
17+
/// Solves the system of linear equations represented by the given matrix and result vector using Gaussian Elimination.
18+
/// </summary>
19+
/// <param name="matrix">The square matrix representing the coefficients of the linear equations.</param>
20+
/// <param name="result">The vector representing the constants on the right-hand side of the linear equations.</param>
21+
/// <exception cref="Exception">Thrown if the matrix is singular and cannot be solved.</exception>
22+
/// <remarks>
23+
/// The matrix passed to this method must be a square matrix.
24+
/// If the matrix is singular (i.e., has no unique solution), an <see cref="NotSupportedException"/> will be thrown.
25+
/// </remarks>
26+
public static void Solve(TNumber[][] matrix, TNumber[] result)
27+
{
28+
TransformToRowEchelonForm(matrix, result);
29+
ApplyBackSubstitution(matrix, result);
30+
}
31+
32+
private static void TransformToRowEchelonForm(TNumber[][] matrix, TNumber[] result)
33+
{
34+
int colCount = matrix.Length;
35+
int rowCount = matrix[0].Length;
36+
int pivotRow = 0;
37+
for (int pivotCol = 0; pivotCol < colCount; pivotCol++)
38+
{
39+
TNumber maxValue = TNumber.Abs(matrix[pivotRow][pivotCol]);
40+
int maxIndex = pivotRow;
41+
for (int r = pivotRow + 1; r < rowCount; r++)
42+
{
43+
TNumber value = TNumber.Abs(matrix[r][pivotCol]);
44+
if (value > maxValue)
45+
{
46+
maxIndex = r;
47+
maxValue = value;
48+
}
49+
}
50+
51+
if (matrix[maxIndex][pivotCol] == TNumber.Zero)
52+
{
53+
throw new NotSupportedException("Matrix is singular and cannot be solve");
54+
}
55+
56+
(matrix[pivotRow], matrix[maxIndex]) = (matrix[maxIndex], matrix[pivotRow]);
57+
(result[pivotRow], result[maxIndex]) = (result[maxIndex], result[pivotRow]);
58+
59+
for (int r = pivotRow + 1; r < rowCount; r++)
60+
{
61+
TNumber fraction = matrix[r][pivotCol] / matrix[pivotRow][pivotCol];
62+
for (int c = pivotCol + 1; c < colCount; c++)
63+
{
64+
matrix[r][c] -= matrix[pivotRow][c] * fraction;
65+
}
66+
67+
result[r] -= result[pivotRow] * fraction;
68+
matrix[r][pivotCol] = TNumber.Zero;
69+
}
70+
71+
pivotRow++;
72+
}
73+
}
74+
75+
private static void ApplyBackSubstitution(TNumber[][] matrix, TNumber[] result)
76+
{
77+
int rowCount = matrix[0].Length;
78+
79+
for (int row = rowCount - 1; row >= 0; row--)
80+
{
81+
result[row] /= matrix[row][row];
82+
83+
for (int r = 0; r < row; r++)
84+
{
85+
result[r] -= result[row] * matrix[r][row];
86+
}
87+
}
88+
}
89+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
// Copyright (c) Six Labors.
2+
// Licensed under the Six Labors Split License.
3+
4+
using System.Numerics;
5+
6+
namespace SixLabors.ImageSharp.Common.Helpers;
7+
8+
/// <summary>
9+
/// Provides helper methods for performing quad distortion transformations.
10+
/// </summary>
11+
internal static class QuadDistortionHelper
12+
{
13+
/// <summary>
14+
/// Computes the projection matrix for a quad distortion transformation.
15+
/// </summary>
16+
/// <param name="rectangle">The source rectangle.</param>
17+
/// <param name="topLeft">The top-left point of the distorted quad.</param>
18+
/// <param name="topRight">The top-right point of the distorted quad.</param>
19+
/// <param name="bottomRight">The bottom-right point of the distorted quad.</param>
20+
/// <param name="bottomLeft">The bottom-left point of the distorted quad.</param>
21+
/// <returns>The computed projection matrix for the quad distortion.</returns>
22+
/// <remarks>
23+
/// This method is based on the algorithm described in the following article:
24+
/// https://blog.mbedded.ninja/mathematics/geometry/projective-transformations/
25+
/// </remarks>
26+
public static Matrix4x4 ComputeQuadDistortMatrix(Rectangle rectangle, PointF topLeft, PointF topRight, PointF bottomRight, PointF bottomLeft)
27+
{
28+
PointF p1 = new(rectangle.X, rectangle.Y);
29+
PointF p2 = new(rectangle.X + rectangle.Width, rectangle.Y);
30+
PointF p3 = new(rectangle.X + rectangle.Width, rectangle.Y + rectangle.Height);
31+
PointF p4 = new(rectangle.X, rectangle.Y + rectangle.Height);
32+
33+
PointF q1 = topLeft;
34+
PointF q2 = topRight;
35+
PointF q3 = bottomRight;
36+
PointF q4 = bottomLeft;
37+
38+
// @formatter:off
39+
float[][] matrixData =
40+
[
41+
[p1.X, p1.Y, 1, 0, 0, 0, -p1.X * q1.X, -p1.Y * q1.X],
42+
[0, 0, 0, p1.X, p1.Y, 1, -p1.X * q1.Y, -p1.Y * q1.Y],
43+
[p2.X, p2.Y, 1, 0, 0, 0, -p2.X * q2.X, -p2.Y * q2.X],
44+
[0, 0, 0, p2.X, p2.Y, 1, -p2.X * q2.Y, -p2.Y * q2.Y],
45+
[p3.X, p3.Y, 1, 0, 0, 0, -p3.X * q3.X, -p3.Y * q3.X],
46+
[0, 0, 0, p3.X, p3.Y, 1, -p3.X * q3.Y, -p3.Y * q3.Y],
47+
[p4.X, p4.Y, 1, 0, 0, 0, -p4.X * q4.X, -p4.Y * q4.X],
48+
[0, 0, 0, p4.X, p4.Y, 1, -p4.X * q4.Y, -p4.Y * q4.Y],
49+
];
50+
51+
float[] b =
52+
[
53+
q1.X,
54+
q1.Y,
55+
q2.X,
56+
q2.Y,
57+
q3.X,
58+
q3.Y,
59+
q4.X,
60+
q4.Y,
61+
];
62+
63+
GaussianEliminationSolver<float>.Solve(matrixData, b);
64+
65+
#pragma warning disable SA1117
66+
Matrix4x4 projectionMatrix = new(
67+
b[0], b[3], 0, b[6],
68+
b[1], b[4], 0, b[7],
69+
0, 0, 1, 0,
70+
b[2], b[5], 0, 1);
71+
#pragma warning restore SA1117
72+
73+
// @formatter:on
74+
return projectionMatrix;
75+
}
76+
}

src/ImageSharp/Processing/ProjectiveTransformBuilder.cs

Lines changed: 23 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.Numerics;
5+
using SixLabors.ImageSharp.Common.Helpers;
56
using SixLabors.ImageSharp.Processing.Processors.Transforms;
67

78
namespace SixLabors.ImageSharp.Processing;
@@ -270,6 +271,28 @@ public ProjectiveTransformBuilder AppendTranslation(PointF position)
270271
public ProjectiveTransformBuilder AppendTranslation(Vector2 position)
271272
=> this.AppendMatrix(Matrix4x4.CreateTranslation(new Vector3(position, 0)));
272273

274+
/// <summary>
275+
/// Prepends a quad distortion matrix using the specified corner points.
276+
/// </summary>
277+
/// <param name="topLeft">The top-left corner point of the distorted quad.</param>
278+
/// <param name="topRight">The top-right corner point of the distorted quad.</param>
279+
/// <param name="bottomRight">The bottom-right corner point of the distorted quad.</param>
280+
/// <param name="bottomLeft">The bottom-left corner point of the distorted quad.</param>
281+
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
282+
public ProjectiveTransformBuilder PrependQuadDistortion(PointF topLeft, PointF topRight, PointF bottomRight, PointF bottomLeft)
283+
=> this.Prepend(size => QuadDistortionHelper.ComputeQuadDistortMatrix(new Rectangle(Point.Empty, size), topLeft, topRight, bottomRight, bottomLeft));
284+
285+
/// <summary>
286+
/// Appends a quad distortion matrix using the specified corner points.
287+
/// </summary>
288+
/// <param name="topLeft">The top-left corner point of the distorted quad.</param>
289+
/// <param name="topRight">The top-right corner point of the distorted quad.</param>
290+
/// <param name="bottomRight">The bottom-right corner point of the distorted quad.</param>
291+
/// <param name="bottomLeft">The bottom-left corner point of the distorted quad.</param>
292+
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
293+
public ProjectiveTransformBuilder AppendQuadDistortion(PointF topLeft, PointF topRight, PointF bottomRight, PointF bottomLeft)
294+
=> this.Append(size => QuadDistortionHelper.ComputeQuadDistortMatrix(new Rectangle(Point.Empty, size), topLeft, topRight, bottomRight, bottomLeft));
295+
273296
/// <summary>
274297
/// Prepends a raw matrix.
275298
/// </summary>
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
// Copyright (c) Six Labors.
2+
// Licensed under the Six Labors Split License.
3+
4+
using SixLabors.ImageSharp.Common.Helpers;
5+
6+
namespace SixLabors.ImageSharp.Tests.Common;
7+
8+
public class GaussianEliminationSolverTest
9+
{
10+
[Theory]
11+
[MemberData(nameof(MatrixTestData))]
12+
public void CanSolve(float[][] matrix, float[] result, float[] expected)
13+
{
14+
GaussianEliminationSolver<float>.Solve(matrix, result);
15+
16+
for (int i = 0; i < expected.Length; i++)
17+
{
18+
Assert.Equal(result[i], expected[i], 4);
19+
}
20+
}
21+
22+
public static TheoryData<float[][], float[], float[]> MatrixTestData
23+
{
24+
get
25+
{
26+
TheoryData<float[][], float[], float[]> data = [];
27+
{
28+
float[][] matrix =
29+
[
30+
[2, 3, 4],
31+
[1, 2, 3],
32+
[3, -4, 0],
33+
];
34+
float[] result = [6, 4, 10];
35+
float[] expected = [18 / 11f, -14 / 11f, 18 / 11f];
36+
data.Add(matrix, result, expected);
37+
}
38+
39+
{
40+
float[][] matrix =
41+
[
42+
[1, 4, -1],
43+
[2, 5, 8],
44+
[1, 3, -3],
45+
];
46+
float[] result = [4, 15, 1];
47+
float[] expected = [1, 1, 1];
48+
data.Add(matrix, result, expected);
49+
}
50+
51+
{
52+
float[][] matrix =
53+
[
54+
[-1, 0, 0],
55+
[0, 1, 0],
56+
[0, 0, 1],
57+
];
58+
float[] result = [1, 2, 3];
59+
float[] expected = [-1, 2, 3];
60+
data.Add(matrix, result, expected);
61+
}
62+
63+
return data;
64+
}
65+
}
66+
}

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

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,14 @@ public class ProjectiveTransformTests
5555
{ TaperSide.Right, TaperCorner.RightOrBottom },
5656
};
5757

58+
public static readonly TheoryData<PointF, PointF, PointF, PointF> QuadDistortionData = new()
59+
{
60+
{ new PointF(0, 0), new PointF(150, 0), new PointF(150, 150), new PointF(0, 150) }, // source == destination
61+
{ new PointF(25, 50), new PointF(210, 25), new PointF(140, 210), new PointF(15, 125) }, // Distortion
62+
{ new PointF(-50, -50), new PointF(200, -50), new PointF(200, 200), new PointF(-50, 200) }, // Scaling
63+
{ new PointF(150, 0), new PointF(150, 150), new PointF(0, 150), new PointF(0, 0) }, // Rotation
64+
};
65+
5866
public ProjectiveTransformTests(ITestOutputHelper output) => this.Output = output;
5967

6068
[Theory]
@@ -93,6 +101,24 @@ public void Transform_WithTaperMatrix<TPixel>(TestImageProvider<TPixel> provider
93101
}
94102
}
95103

104+
[Theory]
105+
[WithTestPatternImages(nameof(QuadDistortionData), 150, 150, PixelTypes.Rgba32)]
106+
public void Transform_WithQuadDistortion<TPixel>(TestImageProvider<TPixel> provider, PointF topLeft, PointF topRight, PointF bottomRight, PointF bottomLeft)
107+
where TPixel : unmanaged, IPixel<TPixel>
108+
{
109+
using (Image<TPixel> image = provider.GetImage())
110+
{
111+
ProjectiveTransformBuilder builder = new ProjectiveTransformBuilder()
112+
.AppendQuadDistortion(topLeft, topRight, bottomRight, bottomLeft);
113+
114+
image.Mutate(i => i.Transform(builder));
115+
116+
FormattableString testOutputDetails = $"{topLeft}-{topRight}-{bottomRight}-{bottomLeft}";
117+
image.DebugSave(provider, testOutputDetails);
118+
image.CompareFirstFrameToReferenceOutput(TolerantComparer, provider, testOutputDetails);
119+
}
120+
}
121+
96122
[Theory]
97123
[WithSolidFilledImages(100, 100, 0, 0, 255, PixelTypes.Rgba32)]
98124
public void RawTransformMatchesDocumentedExample<TPixel>(TestImageProvider<TPixel> provider)
Loading
Loading

0 commit comments

Comments
 (0)