diff --git a/src/ImageSharp/Processing/Transforms/Processors/AffineTransformProcessor.cs b/src/ImageSharp/Processing/Transforms/Processors/AffineTransformProcessor.cs index b9f3dc4bf5..2d6083e55f 100644 --- a/src/ImageSharp/Processing/Transforms/Processors/AffineTransformProcessor.cs +++ b/src/ImageSharp/Processing/Transforms/Processors/AffineTransformProcessor.cs @@ -5,6 +5,8 @@ using System.Collections.Generic; using System.Linq; using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using System.Threading.Tasks; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Memory; @@ -120,9 +122,9 @@ protected override void OnFrameApply( configuration.ParallelOptions, y => { - Span destRow = destination.GetPixelRowSpan(y); - Span ySpan = yBuffer.GetRowSpan(y); - Span xSpan = xBuffer.GetRowSpan(y); + ref TPixel destRowRef = ref MemoryMarshal.GetReference(destination.GetPixelRowSpan(y)); + ref float ySpanRef = ref MemoryMarshal.GetReference(yBuffer.GetRowSpan(y)); + ref float xSpanRef = ref MemoryMarshal.GetReference(xBuffer.GetRowSpan(y)); for (int x = 0; x < width; x++) { @@ -164,24 +166,24 @@ protected override void OnFrameApply( // I've optimized where I can but am always open to suggestions. if (yScale > 1 && xScale > 1) { - CalculateWeightsDown(top, bottom, minY, maxY, point.Y, sampler, yScale, ySpan); - CalculateWeightsDown(left, right, minX, maxX, point.X, sampler, xScale, xSpan); + CalculateWeightsDown(top, bottom, minY, maxY, point.Y, sampler, yScale, ref ySpanRef, yLength); + CalculateWeightsDown(left, right, minX, maxX, point.X, sampler, xScale, ref xSpanRef, xLength); } else { - CalculateWeightsScaleUp(minY, maxY, point.Y, sampler, ySpan); - CalculateWeightsScaleUp(minX, maxX, point.X, sampler, xSpan); + CalculateWeightsScaleUp(minY, maxY, point.Y, sampler, ref ySpanRef); + CalculateWeightsScaleUp(minX, maxX, point.X, sampler, ref xSpanRef); } // Now multiply the results against the offsets Vector4 sum = Vector4.Zero; for (int yy = 0, j = minY; j <= maxY; j++, yy++) { - float yWeight = ySpan[yy]; + float yWeight = Unsafe.Add(ref ySpanRef, yy); for (int xx = 0, i = minX; i <= maxX; i++, xx++) { - float xWeight = xSpan[xx]; + float xWeight = Unsafe.Add(ref xSpanRef, xx); var vector = source[i, j].ToVector4(); // Values are first premultiplied to prevent darkening of edge pixels @@ -190,7 +192,7 @@ protected override void OnFrameApply( } } - ref TPixel dest = ref destRow[x]; + ref TPixel dest = ref Unsafe.Add(ref destRowRef, x); // Reverse the premultiplication dest.PackFromVector4(sum.UnPremultiply()); diff --git a/src/ImageSharp/Processing/Transforms/Processors/InterpolatedTransformProcessorBase.cs b/src/ImageSharp/Processing/Transforms/Processors/InterpolatedTransformProcessorBase.cs index 6e663f1e12..8f57f3ba34 100644 --- a/src/ImageSharp/Processing/Transforms/Processors/InterpolatedTransformProcessorBase.cs +++ b/src/ImageSharp/Processing/Transforms/Processors/InterpolatedTransformProcessorBase.cs @@ -42,12 +42,12 @@ protected InterpolatedTransformProcessorBase(IResampler sampler) /// The transformed point dimension /// The sampler /// The transformed image scale relative to the source - /// The collection of weights + /// The reference to the collection of weights + /// The length of the weights collection [MethodImpl(MethodImplOptions.AggressiveInlining)] - protected static void CalculateWeightsDown(int min, int max, int sourceMin, int sourceMax, float point, IResampler sampler, float scale, Span weights) + protected static void CalculateWeightsDown(int min, int max, int sourceMin, int sourceMax, float point, IResampler sampler, float scale, ref float weightsRef, int length) { float sum = 0; - ref float weightsBaseRef = ref weights[0]; // Downsampling weights requires more edge sampling plus normalization of the weights for (int x = 0, i = min; i <= max; i++, x++) @@ -65,14 +65,14 @@ protected static void CalculateWeightsDown(int min, int max, int sourceMin, int float weight = sampler.GetValue((index - point) / scale); sum += weight; - Unsafe.Add(ref weightsBaseRef, x) = weight; + Unsafe.Add(ref weightsRef, x) = weight; } if (sum > 0) { - for (int i = 0; i < weights.Length; i++) + for (int i = 0; i < length; i++) { - ref float wRef = ref Unsafe.Add(ref weightsBaseRef, i); + ref float wRef = ref Unsafe.Add(ref weightsRef, i); wRef = wRef / sum; } } @@ -85,15 +85,14 @@ protected static void CalculateWeightsDown(int min, int max, int sourceMin, int /// The maximum source bounds /// The transformed point dimension /// The sampler - /// The collection of weights + /// The reference to the collection of weights [MethodImpl(MethodImplOptions.AggressiveInlining)] - protected static void CalculateWeightsScaleUp(int sourceMin, int sourceMax, float point, IResampler sampler, Span weights) + protected static void CalculateWeightsScaleUp(int sourceMin, int sourceMax, float point, IResampler sampler, ref float weightsRef) { - ref float weightsBaseRef = ref weights[0]; for (int x = 0, i = sourceMin; i <= sourceMax; i++, x++) { float weight = sampler.GetValue(i - point); - Unsafe.Add(ref weightsBaseRef, x) = weight; + Unsafe.Add(ref weightsRef, x) = weight; } } diff --git a/src/ImageSharp/Processing/Transforms/Processors/ProjectiveTransformProcessor.cs b/src/ImageSharp/Processing/Transforms/Processors/ProjectiveTransformProcessor.cs index eb40c3f87f..9f76540378 100644 --- a/src/ImageSharp/Processing/Transforms/Processors/ProjectiveTransformProcessor.cs +++ b/src/ImageSharp/Processing/Transforms/Processors/ProjectiveTransformProcessor.cs @@ -5,6 +5,8 @@ using System.Collections.Generic; using System.Linq; using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using System.Threading.Tasks; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Memory; @@ -70,6 +72,7 @@ protected override void OnFrameApply(ImageFrame source, ImageFrame source, ImageFrame source, ImageFrame source, ImageFrame { - Span destRow = destination.GetPixelRowSpan(y); - Span ySpan = yBuffer.GetRowSpan(y); - Span xSpan = xBuffer.GetRowSpan(y); + ref TPixel destRowRef = ref MemoryMarshal.GetReference(destination.GetPixelRowSpan(y)); + ref float ySpanRef = ref MemoryMarshal.GetReference(yBuffer.GetRowSpan(y)); + ref float xSpanRef = ref MemoryMarshal.GetReference(xBuffer.GetRowSpan(y)); for (int x = 0; x < width; x++) { // Use the single precision position to calculate correct bounding pixels // otherwise we get rogue pixels outside of the bounds. - var point = Vector2.Transform(new Vector2(x, y), matrix); + var v3 = Vector3.Transform(new Vector3(x, y, 1), matrix); + float z = MathF.Max(v3.Z, Epsilon); + + // Using Vector4 with dummy 0-s, because Vector2 SIMD implementation is not reliable: + Vector4 point = new Vector4(v3.X, v3.Y, 0, 0) / z; // Clamp sampling pixel radial extents to the source image edges - Vector2 maxXY = point + radius; - Vector2 minXY = point - radius; + Vector4 maxXY = point + radius; + Vector4 minXY = point - radius; // max, maxY, minX, minY var extents = new Vector4( @@ -161,24 +176,24 @@ protected override void OnFrameApply(ImageFrame source, ImageFrame 1 && xScale > 1) { - CalculateWeightsDown(top, bottom, minY, maxY, point.Y, sampler, yScale, ySpan); - CalculateWeightsDown(left, right, minX, maxX, point.X, sampler, xScale, xSpan); + CalculateWeightsDown(top, bottom, minY, maxY, point.Y, sampler, yScale, ref ySpanRef, yLength); + CalculateWeightsDown(left, right, minX, maxX, point.X, sampler, xScale, ref xSpanRef, xLength); } else { - CalculateWeightsScaleUp(minY, maxY, point.Y, sampler, ySpan); - CalculateWeightsScaleUp(minX, maxX, point.X, sampler, xSpan); + CalculateWeightsScaleUp(minY, maxY, point.Y, sampler, ref ySpanRef); + CalculateWeightsScaleUp(minX, maxX, point.X, sampler, ref xSpanRef); } // Now multiply the results against the offsets Vector4 sum = Vector4.Zero; for (int yy = 0, j = minY; j <= maxY; j++, yy++) { - float yWeight = ySpan[yy]; + float yWeight = Unsafe.Add(ref ySpanRef, yy); for (int xx = 0, i = minX; i <= maxX; i++, xx++) { - float xWeight = xSpan[xx]; + float xWeight = Unsafe.Add(ref xSpanRef, xx); var vector = source[i, j].ToVector4(); // Values are first premultiplied to prevent darkening of edge pixels @@ -187,7 +202,7 @@ protected override void OnFrameApply(ImageFrame source, ImageFrame + /// Enumerates the various options which determine which side to taper + /// + public enum TaperSide + { + /// + /// Taper the left side + /// + Left, + + /// + /// Taper the top side + /// + Top, + + /// + /// Taper the right side + /// + Right, + + /// + /// Taper the bottom side + /// + Bottom + } + + /// + /// Enumerates the various options which determine how to taper corners + /// + public enum TaperCorner + { + /// + /// Taper the left or top corner + /// + LeftOrTop, + + /// + /// Taper the right or bottom corner + /// + RightOrBottom, + + /// + /// Taper the both sets of corners + /// + Both + } + + /// + /// Provides helper methods for working with generalized projective transforms. + /// + public static class ProjectiveTransformHelper + { + /// + /// Creates a matrix that performs a tapering projective transform. + /// + /// + /// The rectangular size of the image being transformed. + /// An enumeration that indicates the side of the rectangle that tapers. + /// An enumeration that indicates on which corners to taper the rectangle. + /// The amount to taper. + /// The + public static Matrix4x4 CreateTaperMatrix(Size size, TaperSide taperSide, TaperCorner taperCorner, float taperFraction) + { + Matrix4x4 matrix = Matrix4x4.Identity; + + switch (taperSide) + { + case TaperSide.Left: + matrix.M11 = taperFraction; + matrix.M22 = taperFraction; + matrix.M13 = (taperFraction - 1) / size.Width; + + switch (taperCorner) + { + case TaperCorner.RightOrBottom: + break; + + case TaperCorner.LeftOrTop: + matrix.M12 = size.Height * matrix.M13; + matrix.M32 = size.Height * (1 - taperFraction); + break; + + case TaperCorner.Both: + matrix.M12 = (size.Height * 0.5f) * matrix.M13; + matrix.M32 = size.Height * (1 - taperFraction) / 2; + break; + } + + break; + + case TaperSide.Top: + matrix.M11 = taperFraction; + matrix.M22 = taperFraction; + matrix.M23 = (taperFraction - 1) / size.Height; + + switch (taperCorner) + { + case TaperCorner.RightOrBottom: + break; + + case TaperCorner.LeftOrTop: + matrix.M21 = size.Width * matrix.M23; + matrix.M31 = size.Width * (1 - taperFraction); + break; + + case TaperCorner.Both: + matrix.M21 = (size.Width * 0.5f) * matrix.M23; + matrix.M31 = size.Width * (1 - taperFraction) / 2; + break; + } + + break; + + case TaperSide.Right: + matrix.M11 = 1 / taperFraction; + matrix.M13 = (1 - taperFraction) / (size.Width * taperFraction); + + switch (taperCorner) + { + case TaperCorner.RightOrBottom: + break; + + case TaperCorner.LeftOrTop: + matrix.M12 = size.Height * matrix.M13; + break; + + case TaperCorner.Both: + matrix.M12 = (size.Height * 0.5f) * matrix.M13; + break; + } + + break; + + case TaperSide.Bottom: + matrix.M22 = 1 / taperFraction; + matrix.M23 = (1 - taperFraction) / (size.Height * taperFraction); + + switch (taperCorner) + { + case TaperCorner.RightOrBottom: + break; + + case TaperCorner.LeftOrTop: + matrix.M21 = size.Width * matrix.M23; + break; + + case TaperCorner.Both: + matrix.M21 = (size.Width * 0.5f) * matrix.M23; + break; + } + + break; + } + + return matrix; + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Processing/Transforms/TransformExtensions.cs b/src/ImageSharp/Processing/Transforms/TransformExtensions.cs index 585288d8a8..2607c102bd 100644 --- a/src/ImageSharp/Processing/Transforms/TransformExtensions.cs +++ b/src/ImageSharp/Processing/Transforms/TransformExtensions.cs @@ -86,26 +86,25 @@ public static IImageProcessingContext Transform( /// The image to transform. /// The transformation matrix. /// The - internal static IImageProcessingContext Transform(this IImageProcessingContext source, Matrix4x4 matrix) + public static IImageProcessingContext Transform(this IImageProcessingContext source, Matrix4x4 matrix) where TPixel : struct, IPixel => Transform(source, matrix, KnownResamplers.Bicubic); /// /// Applies a projective transform to the image by the given matrix using the specified sampling algorithm. - /// TODO: Doesn't work yet! Implement tests + Finish implementation + Document Matrix4x4 behavior /// /// The pixel format. /// The image to transform. /// The transformation matrix. /// The to perform the resampling. /// The - internal static IImageProcessingContext Transform(this IImageProcessingContext source, Matrix4x4 matrix, IResampler sampler) + public static IImageProcessingContext Transform(this IImageProcessingContext source, Matrix4x4 matrix, IResampler sampler) where TPixel : struct, IPixel => source.ApplyProcessor(new ProjectiveTransformProcessor(matrix, sampler, source.GetCurrentSize())); /// /// Applies a projective transform to the image by the given matrix using the specified sampling algorithm. - /// TODO: Doesn't work yet! Implement tests + Finish implementation + Document Matrix4x4 behavior + /// TODO: Should we be offsetting the matrix here? /// /// The pixel format. /// The image to transform. diff --git a/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformTests.cs b/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformTests.cs index c9354049d1..9380d4e185 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformTests.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformTests.cs @@ -39,25 +39,24 @@ public static readonly TheoryData TransformVa { 0, 1f, 2f, 0, 0 }, }; - public static readonly TheoryData ResamplerNames = - new TheoryData - { - nameof(KnownResamplers.Bicubic), - nameof(KnownResamplers.Box), - nameof(KnownResamplers.CatmullRom), - nameof(KnownResamplers.Hermite), - nameof(KnownResamplers.Lanczos2), - nameof(KnownResamplers.Lanczos3), - nameof(KnownResamplers.Lanczos5), - nameof(KnownResamplers.Lanczos8), - nameof(KnownResamplers.MitchellNetravali), - nameof(KnownResamplers.NearestNeighbor), - nameof(KnownResamplers.Robidoux), - nameof(KnownResamplers.RobidouxSharp), - nameof(KnownResamplers.Spline), - nameof(KnownResamplers.Triangle), - nameof(KnownResamplers.Welch), - }; + public static readonly TheoryData ResamplerNames = new TheoryData + { + nameof(KnownResamplers.Bicubic), + nameof(KnownResamplers.Box), + nameof(KnownResamplers.CatmullRom), + nameof(KnownResamplers.Hermite), + nameof(KnownResamplers.Lanczos2), + nameof(KnownResamplers.Lanczos3), + nameof(KnownResamplers.Lanczos5), + nameof(KnownResamplers.Lanczos8), + nameof(KnownResamplers.MitchellNetravali), + nameof(KnownResamplers.NearestNeighbor), + nameof(KnownResamplers.Robidoux), + nameof(KnownResamplers.RobidouxSharp), + nameof(KnownResamplers.Spline), + nameof(KnownResamplers.Triangle), + nameof(KnownResamplers.Welch), + }; public static readonly TheoryData Transform_DoesNotCreateEdgeArtifacts_ResamplerNames = new TheoryData @@ -124,7 +123,7 @@ public void Transform_RotateScaleTranslate( image.CompareToReferenceOutput(ValidatorComparer, provider, testOutputDetails); } } - + [Theory] [WithTestPatternImages(96, 96, PixelTypes.Rgba32, 50, 0.8f)] public void Transform_RotateScale_ManuallyCentered(TestImageProvider provider, float angleDeg, float s) @@ -166,7 +165,7 @@ public void Transform_FromSourceRectangle1(TestImageProvider pro using (Image image = provider.GetImage()) { var m = Matrix3x2.CreateScale(2.0F, 1.5F); - + image.Mutate(i => i.Transform(m, KnownResamplers.Spline, rectangle)); image.DebugSave(provider); diff --git a/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformTests.cs b/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformTests.cs new file mode 100644 index 0000000000..3053572018 --- /dev/null +++ b/tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformTests.cs @@ -0,0 +1,137 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Numerics; +using System.Reflection; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; +using SixLabors.ImageSharp.Processing.Transforms; +using SixLabors.ImageSharp.Processing.Transforms.Resamplers; +using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; +using Xunit; +// ReSharper disable InconsistentNaming + +namespace SixLabors.ImageSharp.Tests.Processing.Transforms +{ + using Xunit.Abstractions; + + public class ProjectiveTransformTests + { + private static readonly ImageComparer ValidatorComparer = ImageComparer.TolerantPercentage(0.03f, 3); + private static readonly ImageComparer TolerantComparer = ImageComparer.TolerantPercentage(0.5f, 3); + + private ITestOutputHelper Output { get; } + + public static readonly TheoryData ResamplerNames = new TheoryData + { + nameof(KnownResamplers.Bicubic), + nameof(KnownResamplers.Box), + nameof(KnownResamplers.CatmullRom), + nameof(KnownResamplers.Hermite), + nameof(KnownResamplers.Lanczos2), + nameof(KnownResamplers.Lanczos3), + nameof(KnownResamplers.Lanczos5), + nameof(KnownResamplers.Lanczos8), + nameof(KnownResamplers.MitchellNetravali), + nameof(KnownResamplers.NearestNeighbor), + nameof(KnownResamplers.Robidoux), + nameof(KnownResamplers.RobidouxSharp), + nameof(KnownResamplers.Spline), + nameof(KnownResamplers.Triangle), + nameof(KnownResamplers.Welch), + }; + + public static readonly TheoryData TaperMatrixData = new TheoryData + { + { TaperSide.Bottom, TaperCorner.Both }, + { TaperSide.Bottom, TaperCorner.LeftOrTop }, + { TaperSide.Bottom, TaperCorner.RightOrBottom }, + + { TaperSide.Top, TaperCorner.Both }, + { TaperSide.Top, TaperCorner.LeftOrTop }, + { TaperSide.Top, TaperCorner.RightOrBottom }, + + { TaperSide.Left, TaperCorner.Both }, + { TaperSide.Left, TaperCorner.LeftOrTop }, + { TaperSide.Left, TaperCorner.RightOrBottom }, + + { TaperSide.Right, TaperCorner.Both }, + { TaperSide.Right, TaperCorner.LeftOrTop }, + { TaperSide.Right, TaperCorner.RightOrBottom }, + + }; + + public ProjectiveTransformTests(ITestOutputHelper output) + { + this.Output = output; + } + + [Theory] + [WithTestPatternImages(nameof(ResamplerNames), 150, 150, PixelTypes.Rgba32)] + public void Transform_WithSampler(TestImageProvider provider, string resamplerName) + where TPixel : struct, IPixel + { + IResampler sampler = GetResampler(resamplerName); + using (Image image = provider.GetImage()) + { + Matrix4x4 m = ProjectiveTransformHelper.CreateTaperMatrix(image.Size(), TaperSide.Right, TaperCorner.Both, .5F); + + image.Mutate(i => { i.Transform(m, sampler); }); + + image.DebugSave(provider, resamplerName); + image.CompareToReferenceOutput(ValidatorComparer, provider, resamplerName); + } + } + + [Theory] + [WithSolidFilledImages(nameof(TaperMatrixData), 30, 30, nameof(Rgba32.Red), PixelTypes.Rgba32)] + public void Transform_WithTaperMatrix(TestImageProvider provider, TaperSide taperSide, TaperCorner taperCorner) + where TPixel : struct, IPixel + { + using (Image image = provider.GetImage()) + { + Matrix4x4 m = ProjectiveTransformHelper.CreateTaperMatrix(image.Size(), taperSide, taperCorner, .5F); + image.Mutate(i => { i.Transform(m); }); + + string testOutputDetails = $"{taperSide}-{taperCorner}"; + image.DebugSave(provider, testOutputDetails); + image.CompareFirstFrameToReferenceOutput(TolerantComparer, provider, testOutputDetails); + } + } + + [Theory] + [WithSolidFilledImages(100, 100, 0, 0, 255, PixelTypes.Rgba32)] + public void RawTransformMatchesDocumentedExample(TestImageProvider provider) + where TPixel : struct, IPixel + { + // Printing some extra output to help investigating roundoff errors: + this.Output.WriteLine($"Vector.IsHardwareAccelerated: {Vector.IsHardwareAccelerated}"); + + // This test matches the output described in the example at + // https://docs.microsoft.com/en-us/xamarin/xamarin-forms/user-interface/graphics/skiasharp/transforms/non-affine + using (Image image = provider.GetImage()) + { + Matrix4x4 m = Matrix4x4.Identity; + m.M13 = 0.01F; + + image.Mutate(i => { i.Transform(m); }); + + image.DebugSave(provider); + image.CompareToReferenceOutput(TolerantComparer, provider); + } + } + + private static IResampler GetResampler(string name) + { + PropertyInfo property = typeof(KnownResamplers).GetTypeInfo().GetProperty(name); + + if (property == null) + { + throw new Exception("Invalid property name!"); + } + + return (IResampler)property.GetValue(null); + } + } +} \ No newline at end of file diff --git a/tests/Images/External b/tests/Images/External index 5a9a883801..f641620eb5 160000 --- a/tests/Images/External +++ b/tests/Images/External @@ -1 +1 @@ -Subproject commit 5a9a88380166a87521d10048f53cda7f5f761d66 +Subproject commit f641620eb5378db49d6153bbf1443ad13bda2379