diff --git a/src/ImageSharp/Processing/Processors/Transforms/AffineProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/AffineProcessor.cs index 843d07f4b5..26b8e3a124 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/AffineProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/AffineProcessor.cs @@ -8,6 +8,7 @@ using System.Numerics; using System.Threading.Tasks; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Helpers; +using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; using SixLabors.Primitives; @@ -26,7 +27,7 @@ namespace SixLabors.ImageSharp.Processing.Processors /// /// Initializes a new instance of the class. /// - /// The sampler to perform the resize operation. + /// The sampler to perform the transform operation. protected AffineProcessor(IResampler sampler) { this.Sampler = sampler; @@ -91,13 +92,15 @@ namespace SixLabors.ImageSharp.Processing.Processors int maxSourceX = source.Width - 1; int maxSourceY = source.Height - 1; - (float radius, float scale) xRadiusScale = this.GetSamplingRadius(source.Width, destination.Width); - (float radius, float scale) yRadiusScale = this.GetSamplingRadius(source.Height, destination.Height); + (float radius, float scale, float ratio) xRadiusScale = this.GetSamplingRadius(source.Width, destination.Width); + (float radius, float scale, float ratio) yRadiusScale = this.GetSamplingRadius(source.Height, destination.Height); float xScale = xRadiusScale.scale; float yScale = yRadiusScale.scale; var radius = new Vector2(xRadiusScale.radius, yRadiusScale.radius); IResampler sampler = this.Sampler; var maxSource = new Vector4(maxSourceX, maxSourceY, maxSourceX, maxSourceY); + int xLength = (int)MathF.Ceiling((radius.X * 2) + 2); + int yLength = (int)MathF.Ceiling((radius.Y * 2) + 2); Parallel.For( 0, @@ -106,50 +109,90 @@ namespace SixLabors.ImageSharp.Processing.Processors y => { Span destRow = destination.GetPixelRowSpan(y); - for (int x = 0; x < width; x++) + using (var yBuffer = new Buffer(yLength)) + using (var xBuffer = new Buffer(xLength)) { - // 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); - - // Clamp sampling pixel radial extents to the source image edges - Vector2 maxXY = point + radius; - Vector2 minXY = point - radius; - - var extents = new Vector4( - MathF.Ceiling(maxXY.X), - MathF.Ceiling(maxXY.Y), - MathF.Floor(minXY.X), - MathF.Floor(minXY.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); + + // Clamp sampling pixel radial extents to the source image edges + Vector2 maxXY = point + radius; + Vector2 minXY = point - radius; + + var extents = new Vector4( + MathF.Ceiling(maxXY.X), + MathF.Ceiling(maxXY.Y), + MathF.Floor(minXY.X), + MathF.Floor(minXY.Y)); + + extents = Vector4.Clamp(extents, Vector4.Zero, maxSource); + + int maxX = (int)extents.X; + int maxY = (int)extents.Y; + int minX = (int)extents.Z; + int minY = (int)extents.W; + + // TODO: Find a way to speed this up if we can we precalculated weights!!! + // It appears these have to be calculated on-the-fly. + // Check with Anton to figure out why indexing from the precalculated weights was wrong. + // + // Create and normalize the y-weights + float ySum = 0; + for (int yy = 0, i = minY; i <= maxY; i++, yy++) + { + float weight = sampler.GetValue((i - point.Y) / yScale); + ySum += weight; + yBuffer[yy] = weight; + } - extents = Vector4.Clamp(extents, Vector4.Zero, maxSource); + // TODO: + // Normalizing the weights fixes scaled transfrom where we scale down but breaks edge pixel belnding + // We end up with too much weight on pixels that should be blended. + // We could, maybe, move the division into the loop and not divide when we hit 0 or maxN but that seems clunky. + if (ySum > 0) + { + for (int i = 0; i < yBuffer.Length; i++) + { + yBuffer[i] = yBuffer[i] / ySum; + } + } - int maxX = (int)extents.X; - int maxY = (int)extents.Y; - int minX = (int)extents.Z; - int minY = (int)extents.W; + // Create and normalize the x-weights + float xSum = 0; + for (int xx = 0, i = minX; i <= maxX; i++, xx++) + { + float weight = sampler.GetValue((i - point.X) / xScale); + xSum += weight; + xBuffer[xx] = weight; + } - if (minX == maxX || minY == maxY) - { - continue; - } + if (xSum > 0) + { + for (int i = 0; i < xBuffer.Length; i++) + { + xBuffer[i] = xBuffer[i] / xSum; + } + } - // It appears these have to be calculated on-the-fly. - // Using the precalculated weights give the wrong values. - // TODO: Find a way to speed this up if we can. - Vector4 sum = Vector4.Zero; - for (int i = minX; i <= maxX; i++) - { - float weightX = sampler.GetValue((i - point.X) / xScale); - for (int j = minY; j <= maxY; j++) + // Now multiply the normalized results against the offsets + Vector4 sum = Vector4.Zero; + for (int yy = 0, j = minY; j <= maxY; j++, yy++) { - float weightY = sampler.GetValue((j - point.Y) / yScale); - sum += source[i, j].ToVector4() * weightX * weightY; + float yWeight = yBuffer[yy]; + + for (int xx = 0, i = minX; i <= maxX; i++, xx++) + { + float xWeight = xBuffer[xx]; + sum += source[i, j].ToVector4() * xWeight * yWeight; + } } - } - ref TPixel dest = ref destRow[x]; - dest.PackFromVector4(sum); + ref TPixel dest = ref destRow[x]; + dest.PackFromVector4(sum); + } } }); } @@ -186,7 +229,7 @@ namespace SixLabors.ImageSharp.Processing.Processors /// The source dimension size /// The destination dimension size /// The radius, and scaling factor - private (float radius, float scale) GetSamplingRadius(int sourceSize, int destinationSize) + private (float radius, float scale, float ratio) GetSamplingRadius(int sourceSize, int destinationSize) { float ratio = (float)sourceSize / destinationSize; float scale = ratio; @@ -196,7 +239,7 @@ namespace SixLabors.ImageSharp.Processing.Processors scale = 1F; } - return (MathF.Ceiling(scale * this.Sampler.Radius), scale); + return (MathF.Ceiling(scale * this.Sampler.Radius), scale, ratio); } } } \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Transforms/TransformProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/TransformProcessor.cs new file mode 100644 index 0000000000..62bdc4a1eb --- /dev/null +++ b/src/ImageSharp/Processing/Processors/Transforms/TransformProcessor.cs @@ -0,0 +1,47 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System.Numerics; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Processing.Processors +{ + /// + /// Provides methods that allow the tranformation of images using various algorithms. + /// + /// The pixel format. + internal class TransformProcessor : AffineProcessor + where TPixel : struct, IPixel + { + /// + /// Initializes a new instance of the class. + /// + /// The transformation matrix + public TransformProcessor(Matrix3x2 matrix) + : this(matrix, KnownResamplers.NearestNeighbor) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The transformation matrix + /// The sampler to perform the transform operation. + public TransformProcessor(Matrix3x2 matrix, IResampler sampler) + : base(sampler) + { + this.TransformMatrix = matrix; + } + + /// + /// Gets the transform matrix + /// + public Matrix3x2 TransformMatrix { get; } + + /// + protected override Matrix3x2 GetTransformMatrix() + { + return this.TransformMatrix; + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Processing/Transforms/Transform.cs b/src/ImageSharp/Processing/Transforms/Transform.cs new file mode 100644 index 0000000000..f3478e32d5 --- /dev/null +++ b/src/ImageSharp/Processing/Transforms/Transform.cs @@ -0,0 +1,39 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System.Numerics; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; +using SixLabors.ImageSharp.Processing.Processors; + +namespace SixLabors.ImageSharp +{ + /// + /// Extension methods for the type. + /// + public static partial class ImageExtensions + { + /// + /// Transforms an image by the given matrix. + /// + /// The pixel format. + /// The image to skew. + /// The transformation matrix. + /// The + public static IImageProcessingContext Transform(this IImageProcessingContext source, Matrix3x2 matrix) + where TPixel : struct, IPixel + => Transform(source, matrix, KnownResamplers.NearestNeighbor); + + /// + /// Transforms an image by the given matrix using the specified sampling algorithm. + /// + /// The pixel format. + /// The image to skew. + /// The transformation matrix. + /// The to perform the resampling. + /// The + public static IImageProcessingContext Transform(this IImageProcessingContext source, Matrix3x2 matrix, IResampler sampler) + where TPixel : struct, IPixel + => source.ApplyProcessor(new TransformProcessor(matrix, sampler)); + } +} \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Processing/Transforms/TransformTests.cs b/tests/ImageSharp.Tests/Processing/Transforms/TransformTests.cs new file mode 100644 index 0000000000..30cb626155 --- /dev/null +++ b/tests/ImageSharp.Tests/Processing/Transforms/TransformTests.cs @@ -0,0 +1,79 @@ +using System; +using System.Collections.Generic; + +namespace SixLabors.ImageSharp.Tests.Processing.Transforms +{ + using System.Numerics; + using System.Reflection; + + using SixLabors.ImageSharp.PixelFormats; + using SixLabors.ImageSharp.Processing; + using SixLabors.Primitives; + + using Xunit; + + public class TransformTests : FileTestBase + { + public static readonly TheoryData TransformValues + = new TheoryData + { + { 20, 10, 50 }, + { -20, -10, 50 } + }; + + public static readonly List ResamplerNames + = new List + { + 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), + }; + + [Theory] + [WithFileCollection(nameof(DefaultFiles), nameof(TransformValues), DefaultPixelType)] + public void ImageShouldTransformWithSampler(TestImageProvider provider, float x, float y, float z) + where TPixel : struct, IPixel + { + + foreach (string resamplerName in ResamplerNames) + { + IResampler sampler = GetResampler(resamplerName); + using (Image image = provider.GetImage()) + { + Matrix3x2 rotate = Matrix3x2Extensions.CreateRotationDegrees(-z); + + // TODO, how does scale work? 2 means half just now, + Matrix3x2 scale = Matrix3x2Extensions.CreateScale(new SizeF(2F, 2F)); + + + image.Mutate(i => i.Transform(scale * rotate, sampler)); + image.DebugSave(provider, string.Join("_", x, y, resamplerName)); + } + } + } + + 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