Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add QuadDistortion to ProjectiveTransformBuilder #2748

Merged
merged 10 commits into from
Oct 21, 2024
19 changes: 5 additions & 14 deletions src/ImageSharp/Processing/AffineTransformBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ namespace SixLabors.ImageSharp.Processing;
/// </summary>
public class AffineTransformBuilder
{
private readonly List<Func<Size, Matrix3x2>> transformMatrixFactories = new();
private readonly List<Func<Size, Matrix3x2>> transformMatrixFactories = [];

/// <summary>
/// Initializes a new instance of the <see cref="AffineTransformBuilder"/> class.
Expand Down Expand Up @@ -301,7 +301,8 @@ public AffineTransformBuilder AppendMatrix(Matrix3x2 matrix)
/// </summary>
/// <param name="sourceSize">The source image size.</param>
/// <returns>The <see cref="Matrix3x2"/>.</returns>
public Matrix3x2 BuildMatrix(Size sourceSize) => this.BuildMatrix(new Rectangle(Point.Empty, sourceSize));
public Matrix3x2 BuildMatrix(Size sourceSize)
=> this.BuildMatrix(new Rectangle(Point.Empty, sourceSize));

/// <summary>
/// Returns the combined transform matrix for a given source rectangle.
Expand Down Expand Up @@ -345,18 +346,8 @@ public Matrix3x2 BuildMatrix(Rectangle sourceRectangle)
/// <returns>The <see cref="Size"/>.</returns>
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<Size, Matrix3x2> 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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,12 @@ public void ApplyTransform<TResampler>(in TResampler sampler)
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<TPixel> sourceBuffer = source.PixelBuffer.GetRegion(interest);
Buffer2DRegion<TPixel> destbuffer = destination.PixelBuffer.GetRegion(interest);
Buffer2DRegion<TPixel> 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;
Expand All @@ -77,7 +77,7 @@ public void ApplyTransform<TResampler>(in TResampler sampler)

if (sampler is NearestNeighborResampler)
{
var nnOperation = new NNAffineOperation(
NNAffineOperation nnOperation = new(
source.PixelBuffer,
Rectangle.Intersect(this.SourceRectangle, source.Bounds()),
destination.PixelBuffer,
Expand All @@ -91,7 +91,7 @@ public void ApplyTransform<TResampler>(in TResampler sampler)
return;
}

var operation = new AffineOperation<TResampler>(
AffineOperation<TResampler> operation = new(
configuration,
source.PixelBuffer,
Rectangle.Intersect(this.SourceRectangle, source.Bounds()),
Expand Down Expand Up @@ -128,17 +128,17 @@ public NNAffineOperation(
[MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(int y)
{
Span<TPixel> destRow = this.destination.DangerousGetRowSpan(y);
Span<TPixel> 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);
}
}
}
Expand Down Expand Up @@ -195,16 +195,16 @@ public void Invoke(in RowInterval rows, Span<Vector4> span)

for (int y = rows.Min; y < rows.Max; y++)
{
Span<TPixel> rowSpan = this.destination.DangerousGetRowSpan(y);
Span<TPixel> destinationRowSpan = this.destination.DangerousGetRowSpan(y);
PixelOperations<TPixel>.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;

Expand All @@ -221,13 +221,14 @@ public void Invoke(in RowInterval rows, Span<Vector4> span)
Vector4 sum = Vector4.Zero;
for (int yK = top; yK <= bottom; yK++)
{
Span<TPixel> 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;
}
Expand All @@ -240,7 +241,7 @@ public void Invoke(in RowInterval rows, Span<Vector4> span)
PixelOperations<TPixel>.Instance.FromVector4Destructive(
this.configuration,
span,
rowSpan,
destinationRowSpan,
PixelConversionModifiers.Scale);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.

namespace SixLabors.ImageSharp.Processing.Processors.Transforms.Linear;

/// <summary>
/// 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: <see href="https://www.algorithm-archive.org/contents/gaussian_elimination/gaussian_elimination.html"/>
/// </summary>
internal static class GaussianEliminationSolver
{
/// <summary>
/// Solves the system of linear equations represented by the given matrix and result vector using Gaussian Elimination.
/// </summary>
/// <param name="matrix">The square matrix representing the coefficients of the linear equations.</param>
/// <param name="result">The vector representing the constants on the right-hand side of the linear equations.</param>
/// <exception cref="Exception">Thrown if the matrix is singular and cannot be solved.</exception>
/// <remarks>
/// The matrix passed to this method must be a square matrix.
/// If the matrix is singular (i.e., has no unique solution), an <see cref="NotSupportedException"/> will be thrown.
/// </remarks>
public static void Solve(double[][] matrix, double[] result)
{
TransformToRowEchelonForm(matrix, result);
ApplyBackSubstitution(matrix, 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++)
{
double maxValue = double.Abs(matrix[pivotRow][pivotCol]);
int maxIndex = pivotRow;
for (int r = pivotRow + 1; r < rowCount; r++)
{
double value = double.Abs(matrix[r][pivotCol]);
if (value > maxValue)
{
maxIndex = r;
maxValue = value;
}
}

if (matrix[maxIndex][pivotCol] == 0)
{
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++)
{
double 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] = 0;
}

pivotRow++;
}
}

private static void ApplyBackSubstitution(double[][] matrix, double[] 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];
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,12 @@ public void ApplyTransform<TResampler>(in TResampler sampler)
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<TPixel> sourceBuffer = source.PixelBuffer.GetRegion(interest);
Buffer2DRegion<TPixel> destbuffer = destination.PixelBuffer.GetRegion(interest);
Buffer2DRegion<TPixel> 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;
Expand All @@ -77,7 +77,7 @@ public void ApplyTransform<TResampler>(in TResampler sampler)

if (sampler is NearestNeighborResampler)
{
var nnOperation = new NNProjectiveOperation(
NNProjectiveOperation nnOperation = new(
source.PixelBuffer,
Rectangle.Intersect(this.SourceRectangle, source.Bounds()),
destination.PixelBuffer,
Expand All @@ -91,7 +91,7 @@ public void ApplyTransform<TResampler>(in TResampler sampler)
return;
}

var operation = new ProjectiveOperation<TResampler>(
ProjectiveOperation<TResampler> operation = new(
configuration,
source.PixelBuffer,
Rectangle.Intersect(this.SourceRectangle, source.Bounds()),
Expand Down Expand Up @@ -128,17 +128,17 @@ public NNProjectiveOperation(
[MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(int y)
{
Span<TPixel> destRow = this.destination.DangerousGetRowSpan(y);
Span<TPixel> 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);
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);
}
}
}
Expand Down Expand Up @@ -195,10 +195,10 @@ public void Invoke(in RowInterval rows, Span<Vector4> span)

for (int y = rows.Min; y < rows.Max; y++)
{
Span<TPixel> rowSpan = this.destination.DangerousGetRowSpan(y);
Span<TPixel> destinationRowSpan = this.destination.DangerousGetRowSpan(y);
PixelOperations<TPixel>.Instance.ToVector4(
this.configuration,
rowSpan,
destinationRowSpan,
span,
PixelConversionModifiers.Scale);

Expand All @@ -221,13 +221,14 @@ public void Invoke(in RowInterval rows, Span<Vector4> span)
Vector4 sum = Vector4.Zero;
for (int yK = top; yK <= bottom; yK++)
{
Span<TPixel> 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;
}
Expand All @@ -240,7 +241,7 @@ public void Invoke(in RowInterval rows, Span<Vector4> span)
PixelOperations<TPixel>.Instance.FromVector4Destructive(
this.configuration,
span,
rowSpan,
destinationRowSpan,
PixelConversionModifiers.Scale);
}
}
Expand Down
Loading
Loading