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