From 56ad62dc7831603706636e36d338353cf1894d43 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 11 Dec 2018 13:59:04 +1100 Subject: [PATCH] Fix #787 --- .../ProjectiveTransformProcessor.cs | 13 +-- .../Processors/Transforms/TransformUtils.cs | 79 ++++++++++++------- .../Transforms/ProjectiveTransformTests.cs | 27 ++++++- tests/Images/External | 2 +- 4 files changed, 83 insertions(+), 38 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Transforms/ProjectiveTransformProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/ProjectiveTransformProcessor.cs index 98d488a42..0b5627e19 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/ProjectiveTransformProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/ProjectiveTransformProcessor.cs @@ -75,7 +75,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms // Convert from screen to world space. Matrix4x4.Invert(this.TransformMatrix, out Matrix4x4 matrix); - const float Epsilon = 0.0000001F; if (this.Sampler is NearestNeighborResampler) { @@ -90,11 +89,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms for (int x = 0; x < width; x++) { - var v3 = Vector3.Transform(new Vector3(x, y, 1), matrix); - - float z = MathF.Max(v3.Z, Epsilon); - int px = (int)MathF.Round(v3.X / z); - int py = (int)MathF.Round(v3.Y / z); + Vector2 point = TransformUtils.ProjectiveTransform2D(x, y, matrix); + int px = (int)MathF.Round(point.X); + int py = (int)MathF.Round(point.Y); if (sourceRectangle.Contains(px, py)) { @@ -127,9 +124,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms { // Use the single precision position to calculate correct bounding pixels // otherwise we get rogue pixels outside of the bounds. - var v3 = Vector3.Transform(new Vector3(x, y, 1F), matrix); - Vector2 point = new Vector2(v3.X, v3.Y) / MathF.Max(v3.Z, Epsilon); - + Vector2 point = TransformUtils.ProjectiveTransform2D(x, y, matrix); kernel.Convolve(point, x, ref ySpanRef, ref xSpanRef, source.PixelBuffer, vectorSpan); } diff --git a/src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs b/src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs index 24b15d309..0ec8c8393 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs @@ -3,6 +3,7 @@ using System; using System.Numerics; +using System.Runtime.CompilerServices; using SixLabors.Primitives; namespace SixLabors.ImageSharp.Processing.Processors.Transforms @@ -12,6 +13,21 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms /// internal static class TransformUtils { + /// + /// Applies the projective transform against the given coordinates flattened into the 2D space. + /// + /// The "x" vector coordinate. + /// The "y" vector coordinate. + /// The transform matrix. + /// The . + [MethodImpl(InliningOptions.ShortMethod)] + public static Vector2 ProjectiveTransform2D(float x, float y, Matrix4x4 matrix) + { + const float Epsilon = 0.0000001F; + var v4 = Vector4.Transform(new Vector4(x, y, 0, 1F), matrix); + return new Vector2(v4.X, v4.Y) / MathF.Max(v4.W, Epsilon); + } + /// /// Creates a centered rotation matrix using the given rotation in degrees and the source size. /// @@ -94,12 +110,28 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms { Matrix4x4 matrix = Matrix4x4.Identity; + /* + * SkMatrix is layed out in the following manner: + * + * [ ScaleX SkewY Persp0 ] + * [ SkewX ScaleY Persp1 ] + * [ TransX TransY Persp2 ] + * + * When converting from Matrix4x4 to SkMatrix, the third row and + * column is dropped. When converting from SkMatrix to Matrix4x4 + * the third row and column remain as identity: + * + * [ a b c ] [ a b 0 c ] + * [ d e f ] -> [ d e 0 f ] + * [ g h i ] [ 0 0 1 0 ] + * [ g h 0 i ] + */ switch (side) { case TaperSide.Left: matrix.M11 = fraction; matrix.M22 = fraction; - matrix.M13 = (fraction - 1) / size.Width; + matrix.M14 = (fraction - 1) / size.Width; switch (corner) { @@ -107,13 +139,13 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms break; case TaperCorner.LeftOrTop: - matrix.M12 = size.Height * matrix.M13; - matrix.M32 = size.Height * (1 - fraction); + matrix.M12 = size.Height * matrix.M14; + matrix.M42 = size.Height * (1 - fraction); break; case TaperCorner.Both: - matrix.M12 = size.Height * .5F * matrix.M13; - matrix.M32 = size.Height * (1 - fraction) / 2; + matrix.M12 = size.Height * .5F * matrix.M14; + matrix.M42 = size.Height * (1 - fraction) / 2; break; } @@ -122,7 +154,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms case TaperSide.Top: matrix.M11 = fraction; matrix.M22 = fraction; - matrix.M23 = (fraction - 1) / size.Height; + matrix.M24 = (fraction - 1) / size.Height; switch (corner) { @@ -130,13 +162,13 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms break; case TaperCorner.LeftOrTop: - matrix.M21 = size.Width * matrix.M23; - matrix.M31 = size.Width * (1 - fraction); + matrix.M21 = size.Width * matrix.M24; + matrix.M41 = size.Width * (1 - fraction); break; case TaperCorner.Both: - matrix.M21 = size.Width * .5F * matrix.M23; - matrix.M31 = size.Width * (1 - fraction) / 2; + matrix.M21 = size.Width * .5F * matrix.M24; + matrix.M41 = size.Width * (1 - fraction) * .5F; break; } @@ -144,7 +176,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms case TaperSide.Right: matrix.M11 = 1 / fraction; - matrix.M13 = (1 - fraction) / (size.Width * fraction); + matrix.M14 = (1 - fraction) / (size.Width * fraction); switch (corner) { @@ -152,11 +184,11 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms break; case TaperCorner.LeftOrTop: - matrix.M12 = size.Height * matrix.M13; + matrix.M12 = size.Height * matrix.M14; break; case TaperCorner.Both: - matrix.M12 = size.Height * .5F * matrix.M13; + matrix.M12 = size.Height * .5F * matrix.M14; break; } @@ -164,7 +196,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms case TaperSide.Bottom: matrix.M22 = 1 / fraction; - matrix.M23 = (1 - fraction) / (size.Height * fraction); + matrix.M24 = (1 - fraction) / (size.Height * fraction); switch (corner) { @@ -172,11 +204,11 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms break; case TaperCorner.LeftOrTop: - matrix.M21 = size.Width * matrix.M23; + matrix.M21 = size.Width * matrix.M24; break; case TaperCorner.Both: - matrix.M21 = size.Width * .5F * matrix.M23; + matrix.M21 = size.Width * .5F * matrix.M24; break; } @@ -260,17 +292,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms return rectangle; } - Vector2 GetVector(float x, float y) - { - const float Epsilon = 0.0000001F; - var v3 = Vector3.Transform(new Vector3(x, y, 1F), matrix); - return new Vector2(v3.X, v3.Y) / MathF.Max(v3.Z, Epsilon); - } - - Vector2 tl = GetVector(rectangle.Left, rectangle.Top); - Vector2 tr = GetVector(rectangle.Right, rectangle.Top); - Vector2 bl = GetVector(rectangle.Left, rectangle.Bottom); - Vector2 br = GetVector(rectangle.Right, rectangle.Bottom); + Vector2 tl = ProjectiveTransform2D(rectangle.Left, rectangle.Top, matrix); + Vector2 tr = ProjectiveTransform2D(rectangle.Right, rectangle.Top, matrix); + Vector2 bl = ProjectiveTransform2D(rectangle.Left, rectangle.Bottom, matrix); + Vector2 br = ProjectiveTransform2D(rectangle.Right, rectangle.Bottom, matrix); return GetBoundingRectangle(tl, tr, bl, br); } diff --git a/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformTests.cs b/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformTests.cs index 1da660d22..056f66af7 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformTests.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformTests.cs @@ -111,7 +111,32 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms using (Image image = provider.GetImage()) { Matrix4x4 matrix = Matrix4x4.Identity; - matrix.M13 = 0.01F; + matrix.M14 = 0.01F; + + ProjectiveTransformBuilder builder = new ProjectiveTransformBuilder() + .AppendMatrix(matrix); + + image.Mutate(i => i.Transform(builder)); + + image.DebugSave(provider); + image.CompareToReferenceOutput(TolerantComparer, provider); + } + } + + [Theory] + [WithSolidFilledImages(290, 154, 0, 0, 255, PixelTypes.Rgba32)] + public void PerspectiveTransformMatchesCSS(TestImageProvider provider) + where TPixel : struct, IPixel + { + // https://jsfiddle.net/dFrHS/545/ + // https://github.com/SixLabors/ImageSharp/issues/787 + using (Image image = provider.GetImage()) + { + var matrix = new Matrix4x4( + 0.260987f, -0.434909f, 0, -0.0022184f, + 0.373196f, 0.949882f, 0, -0.000312129f, + 0, 0, 1, 0, + 52, 165, 0, 1); ProjectiveTransformBuilder builder = new ProjectiveTransformBuilder() .AppendMatrix(matrix); diff --git a/tests/Images/External b/tests/Images/External index 1edb0f3e0..69603ee5b 160000 --- a/tests/Images/External +++ b/tests/Images/External @@ -1 +1 @@ -Subproject commit 1edb0f3e04c18974821a3012a87f7c2e073c8019 +Subproject commit 69603ee5b6f7dd64114fc44d321e50d9b2d439be