Browse Source

Refactor and fix for transform space

pull/2748/head
James Jackson-South 2 years ago
parent
commit
3d86c583ee
  1. 79
      src/ImageSharp/Common/Helpers/QuadDistortionHelper.cs
  2. 19
      src/ImageSharp/Processing/AffineTransformBuilder.cs
  3. 29
      src/ImageSharp/Processing/Processors/Transforms/Linear/AffineTransformProcessor{TPixel}.cs
  4. 2
      src/ImageSharp/Processing/Processors/Transforms/Linear/GaussianEliminationSolver.cs
  5. 25
      src/ImageSharp/Processing/Processors/Transforms/Linear/ProjectiveTransformProcessor{TPixel}.cs
  6. 151
      src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs
  7. 23
      src/ImageSharp/Processing/ProjectiveTransformBuilder.cs
  8. 2
      tests/ImageSharp.Tests/Common/GaussianEliminationSolverTest.cs
  9. 10
      tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformTests.cs
  10. 4
      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
  11. 4
      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
  12. 4
      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
  13. 4
      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

79
src/ImageSharp/Common/Helpers/QuadDistortionHelper.cs

@ -1,79 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
namespace SixLabors.ImageSharp.Common.Helpers;
/// <summary>
/// Provides helper methods for performing quad distortion transformations.
/// </summary>
internal static class QuadDistortionHelper
{
/// <summary>
/// Computes the projection matrix for a quad distortion transformation.
/// </summary>
/// <param name="rectangle">The source rectangle.</param>
/// <param name="topLeft">The top-left point of the distorted quad.</param>
/// <param name="topRight">The top-right point of the distorted quad.</param>
/// <param name="bottomRight">The bottom-right point of the distorted quad.</param>
/// <param name="bottomLeft">The bottom-left point of the distorted quad.</param>
/// <returns>The computed projection matrix for the quad distortion.</returns>
/// <remarks>
/// This method is based on the algorithm described in the following article:
/// <see href="https://blog.mbedded.ninja/mathematics/geometry/projective-transformations/"/>
/// </remarks>
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;
}
}

19
src/ImageSharp/Processing/AffineTransformBuilder.cs

@ -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.
@ -301,7 +301,8 @@ public class AffineTransformBuilder
/// </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.
@ -345,18 +346,8 @@ public class AffineTransformBuilder
/// <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)

29
src/ImageSharp/Processing/Processors/Transforms/Linear/AffineTransformProcessor{TPixel}.cs

@ -61,12 +61,12 @@ internal class AffineTransformProcessor<TPixel> : TransformProcessor<TPixel>, 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<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;
@ -77,7 +77,7 @@ internal class AffineTransformProcessor<TPixel> : TransformProcessor<TPixel>, 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<TPixel> : TransformProcessor<TPixel>, IR
return;
}
var operation = new AffineOperation<TResampler>(
AffineOperation<TResampler> operation = new(
configuration,
source.PixelBuffer,
Rectangle.Intersect(this.SourceRectangle, source.Bounds()),
@ -128,17 +128,17 @@ internal class AffineTransformProcessor<TPixel> : TransformProcessor<TPixel>, IR
[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);
}
}
}
@ -195,16 +195,16 @@ internal class AffineTransformProcessor<TPixel> : TransformProcessor<TPixel>, IR
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;
@ -221,13 +221,14 @@ internal class AffineTransformProcessor<TPixel> : TransformProcessor<TPixel>, IR
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;
}
@ -240,7 +241,7 @@ internal class AffineTransformProcessor<TPixel> : TransformProcessor<TPixel>, IR
PixelOperations<TPixel>.Instance.FromVector4Destructive(
this.configuration,
span,
rowSpan,
destinationRowSpan,
PixelConversionModifiers.Scale);
}
}

2
src/ImageSharp/Common/Helpers/GaussianEliminationSolver.cs → 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;
/// <summary>
/// Represents a solver for systems of linear equations using the Gaussian Elimination method.

25
src/ImageSharp/Processing/Processors/Transforms/Linear/ProjectiveTransformProcessor{TPixel}.cs

@ -61,12 +61,12 @@ internal class ProjectiveTransformProcessor<TPixel> : TransformProcessor<TPixel>
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;
@ -77,7 +77,7 @@ internal class ProjectiveTransformProcessor<TPixel> : TransformProcessor<TPixel>
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<TPixel> : TransformProcessor<TPixel>
return;
}
var operation = new ProjectiveOperation<TResampler>(
ProjectiveOperation<TResampler> operation = new(
configuration,
source.PixelBuffer,
Rectangle.Intersect(this.SourceRectangle, source.Bounds()),
@ -128,9 +128,9 @@ internal class ProjectiveTransformProcessor<TPixel> : TransformProcessor<TPixel>
[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);
@ -138,7 +138,7 @@ internal class ProjectiveTransformProcessor<TPixel> : TransformProcessor<TPixel>
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<TPixel> : TransformProcessor<TPixel>
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);
@ -221,13 +221,14 @@ internal class ProjectiveTransformProcessor<TPixel> : TransformProcessor<TPixel>
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;
}
@ -240,7 +241,7 @@ internal class ProjectiveTransformProcessor<TPixel> : TransformProcessor<TPixel>
PixelOperations<TPixel>.Instance.FromVector4Destructive(
this.configuration,
span,
rowSpan,
destinationRowSpan,
PixelConversionModifiers.Scale);
}
}

151
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;
}
/// <summary>
/// Computes the projection matrix for a quad distortion transformation.
/// </summary>
/// <param name="rectangle">The source rectangle.</param>
/// <param name="topLeft">The top-left point of the distorted quad.</param>
/// <param name="topRight">The top-right point of the distorted quad.</param>
/// <param name="bottomRight">The bottom-right point of the distorted quad.</param>
/// <param name="bottomLeft">The bottom-left point of the distorted quad.</param>
/// <param name="transformSpace">The <see cref="TransformSpace"/> to use when creating the matrix.</param>
/// <returns>The computed projection matrix for the quad distortion.</returns>
/// <remarks>
/// This method is based on the algorithm described in the following article:
/// <see href="https://blog.mbedded.ninja/mathematics/geometry/projective-transformations/"/>
/// </remarks>
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;
}
/// <summary>
/// Returns the size relative to the source for the given transformation matrix.
/// </summary>
@ -293,11 +379,12 @@ internal static class TransformUtils
/// </summary>
/// <param name="matrix">The transformation matrix.</param>
/// <param name="size">The source size.</param>
/// <param name="transformSpace">The <see cref="TransformSpace"/> used when generating the matrix.</param>
/// <returns>
/// The <see cref="Size"/>.
/// </returns>
[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;
}
}

23
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;
/// </summary>
public class ProjectiveTransformBuilder
{
private readonly List<Func<Size, Matrix4x4>> transformMatrixFactories = new();
private readonly List<Func<Size, Matrix4x4>> transformMatrixFactories = [];
/// <summary>
/// Initializes a new instance of the <see cref="ProjectiveTransformBuilder"/> class.
@ -289,7 +288,8 @@ public class ProjectiveTransformBuilder
/// <param name="bottomLeft">The bottom-left corner point of the distorted quad.</param>
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
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));
/// <summary>
/// Appends a quad distortion matrix using the specified corner points.
@ -300,7 +300,8 @@ public class ProjectiveTransformBuilder
/// <param name="bottomLeft">The bottom-left corner point of the distorted quad.</param>
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
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));
/// <summary>
/// Prepends a raw matrix.
@ -384,18 +385,8 @@ public class ProjectiveTransformBuilder
/// <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.
Matrix4x4 matrix = Matrix4x4.CreateTranslation(new Vector3(-sourceRectangle.Location, 0));
foreach (Func<Size, Matrix4x4> 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)

2
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;

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

@ -154,11 +154,11 @@ public class ProjectiveTransformTests
using (Image<TPixel> 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()

4
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

4
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

4
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

4
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

Loading…
Cancel
Save