Browse Source

Normalize matrix transforms across methods

Methods now match w3c expected results.
af/merge-core
James Jackson-South 9 years ago
parent
commit
af819aba07
  1. 52
      src/ImageSharp/Processing/Processors/Transforms/AffineProcessor.cs
  2. 199
      src/ImageSharp/Processing/Processors/Transforms/ResamplingWeightedProcessor.Weights.cs
  3. 2
      src/ImageSharp/Processing/Processors/Transforms/ResamplingWeightedProcessor.cs
  4. 4
      src/ImageSharp/Processing/Processors/Transforms/RotateProcessor.cs
  5. 4
      src/ImageSharp/Processing/Processors/Transforms/SkewProcessor.cs
  6. 1
      src/ImageSharp/Processing/Processors/Transforms/TransformProcessor.cs
  7. 53
      src/ImageSharp/Processing/Processors/Transforms/WeightsBuffer.cs
  8. 150
      src/ImageSharp/Processing/Processors/Transforms/WeightsWindow.cs
  9. 4
      tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeProfilingBenchmarks.cs
  10. 11
      tests/ImageSharp.Tests/Processing/Transforms/TransformTests.cs

52
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;
}
/// <summary>
/// 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.
/// </summary>
/// <param name="min">The minimum sampling offset</param>
/// <param name="max">The maximum sampling offset</param>
/// <param name="sourceMin">The minimum source bounds</param>
/// <param name="sourceMax">The maximum source bounds</param>
/// <param name="point">The transformed point dimension</param>
/// <param name="sampler">The sampler</param>
/// <param name="scale">The transformed image scale relative to the source</param>
/// <param name="weights">The collection of weights</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void CalculateWeightsDown(int top, int bottom, int min, int max, float point, IResampler sampler, float scale, Buffer<float> weights)
private static void CalculateWeightsDown(int min, int max, int sourceMin, int sourceMax, float point, IResampler sampler, float scale, Buffer<float> 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
}
}
/// <summary>
/// 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.
/// </summary>
/// <param name="sourceMin">The minimum source bounds</param>
/// <param name="sourceMax">The maximum source bounds</param>
/// <param name="point">The transformed point dimension</param>
/// <param name="sampler">The sampler</param>
/// <param name="weights">The collection of weights</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void CalculateWeightsScaleUp(int min, int max, float point, IResampler sampler, Buffer<float> weights)
private static void CalculateWeightsScaleUp(int sourceMin, int sourceMax, float point, IResampler sampler, Buffer<float> 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;
}
/// <summary>

199
src/ImageSharp/Processing/Processors/Transforms/ResamplingWeightedProcessor.Weights.cs

@ -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
{
/// <content>
/// Conains the definition of <see cref="WeightsWindow"/> and <see cref="WeightsBuffer"/>.
/// </content>
internal abstract partial class ResamplingWeightedProcessor<TPixel>
{
/// <summary>
/// Points to a collection of of weights allocated in <see cref="WeightsBuffer"/>.
/// </summary>
internal struct WeightsWindow
{
/// <summary>
/// The local left index position
/// </summary>
public int Left;
/// <summary>
/// The length of the weights window
/// </summary>
public int Length;
/// <summary>
/// The index in the destination buffer
/// </summary>
private readonly int flatStartIndex;
/// <summary>
/// The buffer containing the weights values.
/// </summary>
private readonly Buffer<float> buffer;
/// <summary>
/// Initializes a new instance of the <see cref="WeightsWindow"/> struct.
/// </summary>
/// <param name="index">The destination index in the buffer</param>
/// <param name="left">The local left index</param>
/// <param name="buffer">The span</param>
/// <param name="length">The length of the window</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal WeightsWindow(int index, int left, Buffer2D<float> buffer, int length)
{
this.flatStartIndex = (index * buffer.Width) + left;
this.Left = left;
this.buffer = buffer;
this.Length = length;
}
/// <summary>
/// Gets a reference to the first item of the window.
/// </summary>
/// <returns>The reference to the first item of the window</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ref float GetStartReference()
{
return ref this.buffer[this.flatStartIndex];
}
/// <summary>
/// Gets the span representing the portion of the <see cref="WeightsBuffer"/> that this window covers
/// </summary>
/// <returns>The <see cref="Span{T}"/></returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Span<float> GetWindowSpan() => this.buffer.Slice(this.flatStartIndex, this.Length);
/// <summary>
/// Computes the sum of vectors in 'rowSpan' weighted by weight values, pointed by this <see cref="WeightsWindow"/> instance.
/// </summary>
/// <param name="rowSpan">The input span of vectors</param>
/// <param name="sourceX">The source row position.</param>
/// <returns>The weighted sum</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Vector4 ComputeWeightedRowSum(Span<Vector4> 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;
}
/// <summary>
/// Computes the sum of vectors in 'rowSpan' weighted by weight values, pointed by this <see cref="WeightsWindow"/> instance.
/// Applies <see cref="Vector4Extensions.Expand(float)"/> to all input vectors.
/// </summary>
/// <param name="rowSpan">The input span of vectors</param>
/// <param name="sourceX">The source row position.</param>
/// <returns>The weighted sum</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Vector4 ComputeExpandedWeightedRowSum(Span<Vector4> 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;
}
/// <summary>
/// Computes the sum of vectors in 'firstPassPixels' at a row pointed by 'x',
/// weighted by weight values, pointed by this <see cref="WeightsWindow"/> instance.
/// </summary>
/// <param name="firstPassPixels">The buffer of input vectors in row first order</param>
/// <param name="x">The row position</param>
/// <param name="sourceY">The source column position.</param>
/// <returns>The weighted sum</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Vector4 ComputeWeightedColumnSum(Buffer2D<Vector4> 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;
}
}
/// <summary>
/// Holds the <see cref="WeightsWindow"/> values in an optimized contigous memory region.
/// </summary>
internal class WeightsBuffer : IDisposable
{
private readonly Buffer2D<float> dataBuffer;
/// <summary>
/// Initializes a new instance of the <see cref="WeightsBuffer"/> class.
/// </summary>
/// <param name="sourceSize">The size of the source window</param>
/// <param name="destinationSize">The size of the destination window</param>
public WeightsBuffer(int sourceSize, int destinationSize)
{
this.dataBuffer = Buffer2D<float>.CreateClean(sourceSize, destinationSize);
this.Weights = new WeightsWindow[destinationSize];
}
/// <summary>
/// Gets the calculated <see cref="Weights"/> values.
/// </summary>
public WeightsWindow[] Weights { get; }
/// <summary>
/// Disposes <see cref="WeightsBuffer"/> instance releasing it's backing buffer.
/// </summary>
public void Dispose()
{
this.dataBuffer.Dispose();
}
/// <summary>
/// Slices a weights value at the given positions.
/// </summary>
/// <param name="destIdx">The index in destination buffer</param>
/// <param name="leftIdx">The local left index value</param>
/// <param name="rightIdx">The local right index value</param>
/// <returns>The weights</returns>
public WeightsWindow GetWeightsWindow(int destIdx, int leftIdx, int rightIdx)
{
return new WeightsWindow(destIdx, leftIdx, this.dataBuffer, rightIdx - leftIdx + 1);
}
}
}
}

2
src/ImageSharp/Processing/Processors/Transforms/ResamplingWeightedProcessor.cs

@ -13,7 +13,7 @@ namespace SixLabors.ImageSharp.Processing.Processors
/// Adapted from <see href="http://www.realtimerendering.com/resources/GraphicsGems/gemsiii/filter_rcg.c"/>
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
internal abstract partial class ResamplingWeightedProcessor<TPixel> : CloningImageProcessor<TPixel>
internal abstract class ResamplingWeightedProcessor<TPixel> : CloningImageProcessor<TPixel>
where TPixel : struct, IPixel<TPixel>
{
/// <summary>

4
src/ImageSharp/Processing/Processors/Transforms/RotateProcessor.cs

@ -47,7 +47,9 @@ namespace SixLabors.ImageSharp.Processing.Processors
/// <inheritdoc/>
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;
}
/// <inheritdoc/>

4
src/ImageSharp/Processing/Processors/Transforms/SkewProcessor.cs

@ -50,7 +50,9 @@ namespace SixLabors.ImageSharp.Processing.Processors
/// <inheritdoc/>
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;
}
}
}

1
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;
}

53
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
{
/// <summary>
/// Holds the <see cref="WeightsWindow"/> values in an optimized contigous memory region.
/// </summary>
internal class WeightsBuffer : IDisposable
{
private readonly Buffer2D<float> dataBuffer;
/// <summary>
/// Initializes a new instance of the <see cref="WeightsBuffer"/> class.
/// </summary>
/// <param name="sourceSize">The size of the source window</param>
/// <param name="destinationSize">The size of the destination window</param>
public WeightsBuffer(int sourceSize, int destinationSize)
{
this.dataBuffer = Buffer2D<float>.CreateClean(sourceSize, destinationSize);
this.Weights = new WeightsWindow[destinationSize];
}
/// <summary>
/// Gets the calculated <see cref="Weights"/> values.
/// </summary>
public WeightsWindow[] Weights { get; }
/// <summary>
/// Disposes <see cref="WeightsBuffer"/> instance releasing it's backing buffer.
/// </summary>
public void Dispose()
{
this.dataBuffer.Dispose();
}
/// <summary>
/// Slices a weights value at the given positions.
/// </summary>
/// <param name="destIdx">The index in destination buffer</param>
/// <param name="leftIdx">The local left index value</param>
/// <param name="rightIdx">The local right index value</param>
/// <returns>The weights</returns>
public WeightsWindow GetWeightsWindow(int destIdx, int leftIdx, int rightIdx)
{
return new WeightsWindow(destIdx, leftIdx, this.dataBuffer, rightIdx - leftIdx + 1);
}
}
}

150
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
{
/// <summary>
/// Points to a collection of of weights allocated in <see cref="WeightsBuffer"/>.
/// </summary>
internal struct WeightsWindow
{
/// <summary>
/// The local left index position
/// </summary>
public int Left;
/// <summary>
/// The length of the weights window
/// </summary>
public int Length;
/// <summary>
/// The index in the destination buffer
/// </summary>
private readonly int flatStartIndex;
/// <summary>
/// The buffer containing the weights values.
/// </summary>
private readonly Buffer<float> buffer;
/// <summary>
/// Initializes a new instance of the <see cref="WeightsWindow"/> struct.
/// </summary>
/// <param name="index">The destination index in the buffer</param>
/// <param name="left">The local left index</param>
/// <param name="buffer">The span</param>
/// <param name="length">The length of the window</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal WeightsWindow(int index, int left, Buffer2D<float> buffer, int length)
{
this.flatStartIndex = (index * buffer.Width) + left;
this.Left = left;
this.buffer = buffer;
this.Length = length;
}
/// <summary>
/// Gets a reference to the first item of the window.
/// </summary>
/// <returns>The reference to the first item of the window</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ref float GetStartReference()
{
return ref this.buffer[this.flatStartIndex];
}
/// <summary>
/// Gets the span representing the portion of the <see cref="WeightsBuffer"/> that this window covers
/// </summary>
/// <returns>The <see cref="Span{T}"/></returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Span<float> GetWindowSpan() => this.buffer.Slice(this.flatStartIndex, this.Length);
/// <summary>
/// Computes the sum of vectors in 'rowSpan' weighted by weight values, pointed by this <see cref="WeightsWindow"/> instance.
/// </summary>
/// <param name="rowSpan">The input span of vectors</param>
/// <param name="sourceX">The source row position.</param>
/// <returns>The weighted sum</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Vector4 ComputeWeightedRowSum(Span<Vector4> 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;
}
/// <summary>
/// Computes the sum of vectors in 'rowSpan' weighted by weight values, pointed by this <see cref="WeightsWindow"/> instance.
/// Applies <see cref="Vector4Extensions.Expand(float)"/> to all input vectors.
/// </summary>
/// <param name="rowSpan">The input span of vectors</param>
/// <param name="sourceX">The source row position.</param>
/// <returns>The weighted sum</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Vector4 ComputeExpandedWeightedRowSum(Span<Vector4> 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;
}
/// <summary>
/// Computes the sum of vectors in 'firstPassPixels' at a row pointed by 'x',
/// weighted by weight values, pointed by this <see cref="WeightsWindow"/> instance.
/// </summary>
/// <param name="firstPassPixels">The buffer of input vectors in row first order</param>
/// <param name="x">The row position</param>
/// <param name="sourceY">The source column position.</param>
/// <returns>The weighted sum</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Vector4 ComputeWeightedColumnSum(Buffer2D<Vector4> 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;
}
}
}

4
tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeProfilingBenchmarks.cs

@ -40,11 +40,11 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
{
var proc = new ResizeProcessor<Rgba32>(KnownResamplers.Bicubic, 200, 200);
ResamplingWeightedProcessor<Rgba32>.WeightsBuffer weights = proc.PrecomputeWeights(200, 500);
WeightsBuffer weights = proc.PrecomputeWeights(200, 500);
var bld = new StringBuilder();
foreach (ResamplingWeightedProcessor<Rgba32>.WeightsWindow window in weights.Weights)
foreach (WeightsWindow window in weights.Weights)
{
Span<float> span = window.GetWindowSpan();
for (int i = 0; i < window.Length; i++)

11
tests/ImageSharp.Tests/Processing/Transforms/TransformTests.cs

@ -17,8 +17,8 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms
public static readonly TheoryData<float, float, float> TransformValues
= new TheoryData<float, float, float>
{
{ 20, 10, 50 },
{ -20, -10, 50 }
{ 20, 10, 45 },
{ -20, -10, 45 }
};
public static readonly List<string> ResamplerNames
@ -52,13 +52,10 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms
IResampler sampler = GetResampler(resamplerName);
using (Image<TPixel> 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));
}
}

Loading…
Cancel
Save