From af819aba07a6581c950c41d4106d583db165b601 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 4 Dec 2017 15:33:10 +1100 Subject: [PATCH] Normalize matrix transforms across methods Methods now match w3c expected results. --- .../Processors/Transforms/AffineProcessor.cs | 52 +++-- .../ResamplingWeightedProcessor.Weights.cs | 199 ------------------ .../Transforms/ResamplingWeightedProcessor.cs | 2 +- .../Processors/Transforms/RotateProcessor.cs | 4 +- .../Processors/Transforms/SkewProcessor.cs | 4 +- .../Transforms/TransformProcessor.cs | 1 + .../Processors/Transforms/WeightsBuffer.cs | 53 +++++ .../Processors/Transforms/WeightsWindow.cs | 150 +++++++++++++ .../Transforms/ResizeProfilingBenchmarks.cs | 4 +- .../Processing/Transforms/TransformTests.cs | 11 +- 10 files changed, 253 insertions(+), 227 deletions(-) delete mode 100644 src/ImageSharp/Processing/Processors/Transforms/ResamplingWeightedProcessor.Weights.cs create mode 100644 src/ImageSharp/Processing/Processors/Transforms/WeightsBuffer.cs create mode 100644 src/ImageSharp/Processing/Processors/Transforms/WeightsWindow.cs diff --git a/src/ImageSharp/Processing/Processors/Transforms/AffineProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/AffineProcessor.cs index f7ba651778..767c6feed1 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/AffineProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/AffineProcessor.cs @@ -65,7 +65,7 @@ namespace SixLabors.ImageSharp.Processing.Processors int width = this.targetRectangle.Width; Rectangle sourceBounds = source.Bounds(); - // Since could potentially be resizing the canvas we need to recenter the matrix + // Since could potentially be resizing the canvas we need to re-center the matrix Matrix3x2 matrix = this.GetCenteredMatrix(source); if (this.Sampler is NearestNeighborResampler) @@ -146,10 +146,10 @@ namespace SixLabors.ImageSharp.Processing.Processors continue; } - // 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. - // It might not be possible to do so with the resizer weights but perhaps we can fashion something similar for here. + // Precalulating transformed weights would require prior knowledge of every transformed pixel location + // since they can be at sub-pixel positions. + // I've optimized where I can but am always open to suggestions. // // Create and normalize the y-weights if (yScale > 1) @@ -171,7 +171,7 @@ namespace SixLabors.ImageSharp.Processing.Processors CalculateWeightsScaleUp(minX, maxX, point.X, sampler, xBuffer); } - // Now multiply the normalized results against the offsets + // Now multiply the results against the offsets Vector4 sum = Vector4.Zero; for (int yy = 0, j = minY; j <= maxY; j++, yy++) { @@ -205,24 +205,36 @@ namespace SixLabors.ImageSharp.Processing.Processors return translationToTargetCenter * this.transformMatrix * translateToSourceCenter; } + /// + /// Calculated the weights for the given point. This method uses more samples than the upscaled version to ensure edge pixels are correctly rendered. + /// Additionally the weights are nomalized. + /// + /// The minimum sampling offset + /// The maximum sampling offset + /// The minimum source bounds + /// The maximum source bounds + /// The transformed point dimension + /// The sampler + /// The transformed image scale relative to the source + /// The collection of weights [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void CalculateWeightsDown(int top, int bottom, int min, int max, float point, IResampler sampler, float scale, Buffer weights) + private static void CalculateWeightsDown(int min, int max, int sourceMin, int sourceMax, float point, IResampler sampler, float scale, Buffer weights) { 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 = top; i <= bottom; i++, x++) + for (int x = 0, i = min; i <= max; i++, x++) { int index = i; - if (index < min) + if (index < sourceMin) { - index = min; + index = sourceMin; } - if (index > max) + if (index > sourceMax) { - index = max; + index = sourceMax; } float weight = sampler.GetValue((index - point) / scale); @@ -240,11 +252,20 @@ namespace SixLabors.ImageSharp.Processing.Processors } } + /// + /// Calculated the weights for the given point. This method uses more samples than the upscaled version to ensure edge pixels are correctly rendered. + /// Additionally the weights are nomalized. + /// + /// The minimum source bounds + /// The maximum source bounds + /// The transformed point dimension + /// The sampler + /// The collection of weights [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void CalculateWeightsScaleUp(int min, int max, float point, IResampler sampler, Buffer weights) + private static void CalculateWeightsScaleUp(int sourceMin, int sourceMax, float point, IResampler sampler, Buffer weights) { ref float weightsBaseRef = ref weights[0]; - for (int x = 0, i = min; i <= max; i++, x++) + for (int x = 0, i = sourceMin; i <= sourceMax; i++, x++) { float weight = sampler.GetValue(i - point); Unsafe.Add(ref weightsBaseRef, x) = weight; @@ -259,10 +280,9 @@ namespace SixLabors.ImageSharp.Processing.Processors { this.transformMatrix = this.GetTransformMatrix(); - // this.targetRectangle = ImageMaths.GetBoundingRectangle(sourceRectangle, this.transformMatrix); this.targetRectangle = Matrix3x2.Invert(this.transformMatrix, out Matrix3x2 sizeMatrix) - ? ImageMaths.GetBoundingRectangle(sourceRectangle, sizeMatrix) - : sourceRectangle; + ? ImageMaths.GetBoundingRectangle(sourceRectangle, sizeMatrix) + : sourceRectangle; } /// diff --git a/src/ImageSharp/Processing/Processors/Transforms/ResamplingWeightedProcessor.Weights.cs b/src/ImageSharp/Processing/Processors/Transforms/ResamplingWeightedProcessor.Weights.cs deleted file mode 100644 index 1169d2eadc..0000000000 --- a/src/ImageSharp/Processing/Processors/Transforms/ResamplingWeightedProcessor.Weights.cs +++ /dev/null @@ -1,199 +0,0 @@ -// Copyright (c) Six Labors and contributors. -// Licensed under the Apache License, Version 2.0. - -using System; -using System.Numerics; -using System.Runtime.CompilerServices; -using SixLabors.ImageSharp.Memory; - -namespace SixLabors.ImageSharp.Processing.Processors -{ - /// - /// Conains the definition of and . - /// - internal abstract partial class ResamplingWeightedProcessor - { - /// - /// Points to a collection of of weights allocated in . - /// - internal struct WeightsWindow - { - /// - /// The local left index position - /// - public int Left; - - /// - /// The length of the weights window - /// - public int Length; - - /// - /// The index in the destination buffer - /// - private readonly int flatStartIndex; - - /// - /// The buffer containing the weights values. - /// - private readonly Buffer buffer; - - /// - /// Initializes a new instance of the struct. - /// - /// The destination index in the buffer - /// The local left index - /// The span - /// The length of the window - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal WeightsWindow(int index, int left, Buffer2D buffer, int length) - { - this.flatStartIndex = (index * buffer.Width) + left; - this.Left = left; - this.buffer = buffer; - this.Length = length; - } - - /// - /// Gets a reference to the first item of the window. - /// - /// The reference to the first item of the window - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public ref float GetStartReference() - { - return ref this.buffer[this.flatStartIndex]; - } - - /// - /// Gets the span representing the portion of the that this window covers - /// - /// The - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public Span GetWindowSpan() => this.buffer.Slice(this.flatStartIndex, this.Length); - - /// - /// Computes the sum of vectors in 'rowSpan' weighted by weight values, pointed by this instance. - /// - /// The input span of vectors - /// The source row position. - /// The weighted sum - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public Vector4 ComputeWeightedRowSum(Span rowSpan, int sourceX) - { - ref float horizontalValues = ref this.GetStartReference(); - int left = this.Left; - ref Vector4 vecPtr = ref Unsafe.Add(ref rowSpan.DangerousGetPinnableReference(), left + sourceX); - - // Destination color components - Vector4 result = Vector4.Zero; - - for (int i = 0; i < this.Length; i++) - { - float weight = Unsafe.Add(ref horizontalValues, i); - Vector4 v = Unsafe.Add(ref vecPtr, i); - result += v * weight; - } - - return result; - } - - /// - /// Computes the sum of vectors in 'rowSpan' weighted by weight values, pointed by this instance. - /// Applies to all input vectors. - /// - /// The input span of vectors - /// The source row position. - /// The weighted sum - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public Vector4 ComputeExpandedWeightedRowSum(Span rowSpan, int sourceX) - { - ref float horizontalValues = ref this.GetStartReference(); - int left = this.Left; - ref Vector4 vecPtr = ref Unsafe.Add(ref rowSpan.DangerousGetPinnableReference(), left + sourceX); - - // Destination color components - Vector4 result = Vector4.Zero; - - for (int i = 0; i < this.Length; i++) - { - float weight = Unsafe.Add(ref horizontalValues, i); - Vector4 v = Unsafe.Add(ref vecPtr, i); - result += v.Expand() * weight; - } - - return result; - } - - /// - /// Computes the sum of vectors in 'firstPassPixels' at a row pointed by 'x', - /// weighted by weight values, pointed by this instance. - /// - /// The buffer of input vectors in row first order - /// The row position - /// The source column position. - /// The weighted sum - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public Vector4 ComputeWeightedColumnSum(Buffer2D firstPassPixels, int x, int sourceY) - { - ref float verticalValues = ref this.GetStartReference(); - int left = this.Left; - - // Destination color components - Vector4 result = Vector4.Zero; - - for (int i = 0; i < this.Length; i++) - { - float yw = Unsafe.Add(ref verticalValues, i); - int index = left + i + sourceY; - result += firstPassPixels[x, index] * yw; - } - - return result; - } - } - - /// - /// Holds the values in an optimized contigous memory region. - /// - internal class WeightsBuffer : IDisposable - { - private readonly Buffer2D dataBuffer; - - /// - /// Initializes a new instance of the class. - /// - /// The size of the source window - /// The size of the destination window - public WeightsBuffer(int sourceSize, int destinationSize) - { - this.dataBuffer = Buffer2D.CreateClean(sourceSize, destinationSize); - this.Weights = new WeightsWindow[destinationSize]; - } - - /// - /// Gets the calculated values. - /// - public WeightsWindow[] Weights { get; } - - /// - /// Disposes instance releasing it's backing buffer. - /// - public void Dispose() - { - this.dataBuffer.Dispose(); - } - - /// - /// Slices a weights value at the given positions. - /// - /// The index in destination buffer - /// The local left index value - /// The local right index value - /// The weights - public WeightsWindow GetWeightsWindow(int destIdx, int leftIdx, int rightIdx) - { - return new WeightsWindow(destIdx, leftIdx, this.dataBuffer, rightIdx - leftIdx + 1); - } - } - } -} \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Transforms/ResamplingWeightedProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/ResamplingWeightedProcessor.cs index 781f3a01c7..cb65559daa 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/ResamplingWeightedProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/ResamplingWeightedProcessor.cs @@ -13,7 +13,7 @@ namespace SixLabors.ImageSharp.Processing.Processors /// Adapted from /// /// The pixel format. - internal abstract partial class ResamplingWeightedProcessor : CloningImageProcessor + internal abstract class ResamplingWeightedProcessor : CloningImageProcessor where TPixel : struct, IPixel { /// diff --git a/src/ImageSharp/Processing/Processors/Transforms/RotateProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/RotateProcessor.cs index be49ec321b..e3fd36ce69 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/RotateProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/RotateProcessor.cs @@ -47,7 +47,9 @@ namespace SixLabors.ImageSharp.Processing.Processors /// protected override Matrix3x2 GetTransformMatrix() { - return Matrix3x2Extensions.CreateRotationDegrees(-this.Degrees, PointF.Empty); + Matrix3x2 matrix = Matrix3x2Extensions.CreateRotationDegrees(this.Degrees, PointF.Empty); + Matrix3x2.Invert(matrix, out matrix); + return matrix; } /// diff --git a/src/ImageSharp/Processing/Processors/Transforms/SkewProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/SkewProcessor.cs index 8da8b1e57b..eb9f068db0 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/SkewProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/SkewProcessor.cs @@ -50,7 +50,9 @@ namespace SixLabors.ImageSharp.Processing.Processors /// protected override Matrix3x2 GetTransformMatrix() { - return Matrix3x2Extensions.CreateSkewDegrees(-this.DegreesX, -this.DegreesY, PointF.Empty); + Matrix3x2 matrix = Matrix3x2Extensions.CreateSkewDegrees(this.DegreesX, this.DegreesY, PointF.Empty); + Matrix3x2.Invert(matrix, out matrix); + return matrix; } } } \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Transforms/TransformProcessor.cs b/src/ImageSharp/Processing/Processors/Transforms/TransformProcessor.cs index 62bdc4a1eb..45e9d4dd5e 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/TransformProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/TransformProcessor.cs @@ -30,6 +30,7 @@ namespace SixLabors.ImageSharp.Processing.Processors public TransformProcessor(Matrix3x2 matrix, IResampler sampler) : base(sampler) { + Matrix3x2.Invert(matrix, out matrix); this.TransformMatrix = matrix; } diff --git a/src/ImageSharp/Processing/Processors/Transforms/WeightsBuffer.cs b/src/ImageSharp/Processing/Processors/Transforms/WeightsBuffer.cs new file mode 100644 index 0000000000..0e91087f90 --- /dev/null +++ b/src/ImageSharp/Processing/Processors/Transforms/WeightsBuffer.cs @@ -0,0 +1,53 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; + +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Processing.Processors +{ + /// + /// Holds the values in an optimized contigous memory region. + /// + internal class WeightsBuffer : IDisposable + { + private readonly Buffer2D dataBuffer; + + /// + /// Initializes a new instance of the class. + /// + /// The size of the source window + /// The size of the destination window + public WeightsBuffer(int sourceSize, int destinationSize) + { + this.dataBuffer = Buffer2D.CreateClean(sourceSize, destinationSize); + this.Weights = new WeightsWindow[destinationSize]; + } + + /// + /// Gets the calculated values. + /// + public WeightsWindow[] Weights { get; } + + /// + /// Disposes instance releasing it's backing buffer. + /// + public void Dispose() + { + this.dataBuffer.Dispose(); + } + + /// + /// Slices a weights value at the given positions. + /// + /// The index in destination buffer + /// The local left index value + /// The local right index value + /// The weights + public WeightsWindow GetWeightsWindow(int destIdx, int leftIdx, int rightIdx) + { + return new WeightsWindow(destIdx, leftIdx, this.dataBuffer, rightIdx - leftIdx + 1); + } + } +} \ No newline at end of file diff --git a/src/ImageSharp/Processing/Processors/Transforms/WeightsWindow.cs b/src/ImageSharp/Processing/Processors/Transforms/WeightsWindow.cs new file mode 100644 index 0000000000..d23ee5a060 --- /dev/null +++ b/src/ImageSharp/Processing/Processors/Transforms/WeightsWindow.cs @@ -0,0 +1,150 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Numerics; +using System.Runtime.CompilerServices; + +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Processing.Processors +{ + /// + /// Points to a collection of of weights allocated in . + /// + internal struct WeightsWindow + { + /// + /// The local left index position + /// + public int Left; + + /// + /// The length of the weights window + /// + public int Length; + + /// + /// The index in the destination buffer + /// + private readonly int flatStartIndex; + + /// + /// The buffer containing the weights values. + /// + private readonly Buffer buffer; + + /// + /// Initializes a new instance of the struct. + /// + /// The destination index in the buffer + /// The local left index + /// The span + /// The length of the window + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal WeightsWindow(int index, int left, Buffer2D buffer, int length) + { + this.flatStartIndex = (index * buffer.Width) + left; + this.Left = left; + this.buffer = buffer; + this.Length = length; + } + + /// + /// Gets a reference to the first item of the window. + /// + /// The reference to the first item of the window + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ref float GetStartReference() + { + return ref this.buffer[this.flatStartIndex]; + } + + /// + /// Gets the span representing the portion of the that this window covers + /// + /// The + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Span GetWindowSpan() => this.buffer.Slice(this.flatStartIndex, this.Length); + + /// + /// Computes the sum of vectors in 'rowSpan' weighted by weight values, pointed by this instance. + /// + /// The input span of vectors + /// The source row position. + /// The weighted sum + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector4 ComputeWeightedRowSum(Span rowSpan, int sourceX) + { + ref float horizontalValues = ref this.GetStartReference(); + int left = this.Left; + ref Vector4 vecPtr = ref Unsafe.Add(ref rowSpan.DangerousGetPinnableReference(), left + sourceX); + + // Destination color components + Vector4 result = Vector4.Zero; + + for (int i = 0; i < this.Length; i++) + { + float weight = Unsafe.Add(ref horizontalValues, i); + Vector4 v = Unsafe.Add(ref vecPtr, i); + result += v * weight; + } + + return result; + } + + /// + /// Computes the sum of vectors in 'rowSpan' weighted by weight values, pointed by this instance. + /// Applies to all input vectors. + /// + /// The input span of vectors + /// The source row position. + /// The weighted sum + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector4 ComputeExpandedWeightedRowSum(Span rowSpan, int sourceX) + { + ref float horizontalValues = ref this.GetStartReference(); + int left = this.Left; + ref Vector4 vecPtr = ref Unsafe.Add(ref rowSpan.DangerousGetPinnableReference(), left + sourceX); + + // Destination color components + Vector4 result = Vector4.Zero; + + for (int i = 0; i < this.Length; i++) + { + float weight = Unsafe.Add(ref horizontalValues, i); + Vector4 v = Unsafe.Add(ref vecPtr, i); + result += v.Expand() * weight; + } + + return result; + } + + /// + /// Computes the sum of vectors in 'firstPassPixels' at a row pointed by 'x', + /// weighted by weight values, pointed by this instance. + /// + /// The buffer of input vectors in row first order + /// The row position + /// The source column position. + /// The weighted sum + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector4 ComputeWeightedColumnSum(Buffer2D firstPassPixels, int x, int sourceY) + { + ref float verticalValues = ref this.GetStartReference(); + int left = this.Left; + + // Destination color components + Vector4 result = Vector4.Zero; + + for (int i = 0; i < this.Length; i++) + { + float yw = Unsafe.Add(ref verticalValues, i); + int index = left + i + sourceY; + result += firstPassPixels[x, index] * yw; + } + + return result; + } + } +} \ No newline at end of file diff --git a/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeProfilingBenchmarks.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeProfilingBenchmarks.cs index 6dd6369802..98dbbadaba 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeProfilingBenchmarks.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeProfilingBenchmarks.cs @@ -40,11 +40,11 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms { var proc = new ResizeProcessor(KnownResamplers.Bicubic, 200, 200); - ResamplingWeightedProcessor.WeightsBuffer weights = proc.PrecomputeWeights(200, 500); + WeightsBuffer weights = proc.PrecomputeWeights(200, 500); var bld = new StringBuilder(); - foreach (ResamplingWeightedProcessor.WeightsWindow window in weights.Weights) + foreach (WeightsWindow window in weights.Weights) { Span span = window.GetWindowSpan(); for (int i = 0; i < window.Length; i++) diff --git a/tests/ImageSharp.Tests/Processing/Transforms/TransformTests.cs b/tests/ImageSharp.Tests/Processing/Transforms/TransformTests.cs index 30cb626155..4562d7de73 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/TransformTests.cs +++ b/tests/ImageSharp.Tests/Processing/Transforms/TransformTests.cs @@ -17,8 +17,8 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms public static readonly TheoryData TransformValues = new TheoryData { - { 20, 10, 50 }, - { -20, -10, 50 } + { 20, 10, 45 }, + { -20, -10, 45 } }; public static readonly List ResamplerNames @@ -52,13 +52,10 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms 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 rotate = Matrix3x2Extensions.CreateRotationDegrees(z); Matrix3x2 scale = Matrix3x2Extensions.CreateScale(new SizeF(2F, 2F)); - - image.Mutate(i => i.Transform(scale * rotate, sampler)); + image.Mutate(i => i.Transform(rotate * scale, sampler)); image.DebugSave(provider, string.Join("_", x, y, resamplerName)); } }