Skip to content

Commit

Permalink
Merge pull request #2748 from Socolin/quad-distortion
Browse files Browse the repository at this point in the history
Add QuadDistortion to ProjectiveTransformBuilder
  • Loading branch information
JimBobSquarePants authored Oct 21, 2024
2 parents ed2c2a1 + 3d86c58 commit f61cbe7
Show file tree
Hide file tree
Showing 12 changed files with 388 additions and 84 deletions.
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

0 comments on commit f61cbe7

Please sign in to comment.