Browse Source

Introduce AffineTransformBuilder

pull/775/head
James Jackson-South 7 years ago
parent
commit
8205216dfc
  1. 177
      src/ImageSharp/Processing/AffineTransformBuilder.cs
  2. 33
      src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor.cs
  3. 239
      src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessorOld.cs
  4. 38
      src/ImageSharp/Processing/Processors/Transforms/CenteredAffineTransformProcessor.cs
  5. 2
      src/ImageSharp/Processing/Processors/Transforms/RotateProcessor.cs
  6. 8
      src/ImageSharp/Processing/Processors/Transforms/SkewProcessor.cs
  7. 20
      src/ImageSharp/Processing/Processors/Transforms/TransformHelpers.cs
  8. 56
      src/ImageSharp/Processing/Processors/Transforms/TransformKernelMap.cs
  9. 78
      src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs
  10. 54
      src/ImageSharp/Processing/TransformExtensions.cs
  11. 10
      tests/ImageSharp.Benchmarks/Samplers/Rotate.cs
  12. 45
      tests/ImageSharp.Benchmarks/Samplers/Skew.cs
  13. 22
      tests/ImageSharp.Tests/Drawing/DrawImageTest.cs
  14. 54
      tests/ImageSharp.Tests/Processing/Transforms/AffineTransformTests.cs

177
src/ImageSharp/Processing/AffineTransformBuilder.cs

@ -0,0 +1,177 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System.Collections.Generic;
using System.Numerics;
using SixLabors.ImageSharp.Processing.Processors.Transforms;
using SixLabors.Primitives;
namespace SixLabors.ImageSharp.Processing
{
/// <summary>
/// A helper class for constructing <see cref="Matrix3x2"/> instances for use in affine transforms.
/// </summary>
public class AffineTransformBuilder
{
private readonly List<Matrix3x2> matrices = new List<Matrix3x2>();
private Rectangle rectangle;
/// <summary>
/// Initializes a new instance of the <see cref="AffineTransformBuilder"/> class.
/// </summary>
/// <param name="sourceSize">The source image size.</param>
public AffineTransformBuilder(Size sourceSize) => this.Size = sourceSize;
/// <summary>
/// Initializes a new instance of the <see cref="AffineTransformBuilder"/> class.
/// </summary>
/// <param name="sourceRectangle">The source rectangle.</param>
public AffineTransformBuilder(Rectangle sourceRectangle)
: this(sourceRectangle.Size)
=> this.rectangle = sourceRectangle;
/// <summary>
/// Prepends a centered rotation matrix using the given rotation in degrees.
/// </summary>
/// <param name="degrees">The amount of rotation, in degrees.</param>
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
public AffineTransformBuilder PrependRotateMatrixDegrees(float degrees)
{
this.matrices.Insert(0, TransformUtils.CreateRotationMatrixDegrees(degrees, this.Size));
return this;
}
/// <summary>
/// Gets the source image size.
/// </summary>
internal Size Size { get; }
/// <summary>
/// Appends a centered rotation matrix using the given rotation in degrees.
/// </summary>
/// <param name="degrees">The amount of rotation, in degrees.</param>
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
public AffineTransformBuilder AppendRotateMatrixDegrees(float degrees)
{
this.matrices.Add(TransformUtils.CreateRotationMatrixDegrees(degrees, this.Size));
return this;
}
/// <summary>
/// Prepends a scale matrix from the given vector scale.
/// </summary>
/// <param name="scales">The horizontal and vertical scale.</param>
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
public AffineTransformBuilder PrependScaleMatrix(SizeF scales)
{
this.matrices.Insert(0, Matrix3x2Extensions.CreateScale(scales));
return this;
}
/// <summary>
/// Appends a scale matrix from the given vector scale.
/// </summary>
/// <param name="scales">The horizontal and vertical scale.</param>
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
public AffineTransformBuilder AppendScaleMatrix(SizeF scales)
{
this.matrices.Add(Matrix3x2Extensions.CreateScale(scales));
return this;
}
/// <summary>
/// Prepends a centered skew matrix from the give angles in degrees.
/// </summary>
/// <param name="degreesX">The X angle, in degrees.</param>
/// <param name="degreesY">The Y angle, in degrees.</param>
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
public AffineTransformBuilder PrependSkewMatrixDegrees(float degreesX, float degreesY)
{
this.matrices.Insert(0, TransformUtils.CreateSkewMatrixDegrees(degreesX, degreesY, this.Size));
return this;
}
/// <summary>
/// Appends a centered skew matrix from the give angles in degrees.
/// </summary>
/// <param name="degreesX">The X angle, in degrees.</param>
/// <param name="degreesY">The Y angle, in degrees.</param>
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
public AffineTransformBuilder AppendSkewMatrixDegrees(float degreesX, float degreesY)
{
this.matrices.Add(TransformUtils.CreateSkewMatrixDegrees(degreesX, degreesY, this.Size));
return this;
}
/// <summary>
/// Prepends a translation matrix from the given vector.
/// </summary>
/// <param name="position">The translation position.</param>
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
public AffineTransformBuilder PrependTranslationMatrix(PointF position)
{
this.matrices.Insert(0, Matrix3x2Extensions.CreateTranslation(position));
return this;
}
/// <summary>
/// Appends a translation matrix from the given vector.
/// </summary>
/// <param name="position">The translation position.</param>
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
public AffineTransformBuilder AppendTranslationMatrix(PointF position)
{
this.matrices.Add(Matrix3x2Extensions.CreateTranslation(position));
return this;
}
/// <summary>
/// Prepends a raw matrix.
/// </summary>
/// <param name="matrix">The matrix to prepend.</param>
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
public AffineTransformBuilder PrependMatrix(Matrix3x2 matrix)
{
this.matrices.Insert(0, matrix);
return this;
}
/// <summary>
/// Appends a raw matrix.
/// </summary>
/// <param name="matrix">The matrix to append.</param>
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
public AffineTransformBuilder AppendMatrix(Matrix3x2 matrix)
{
this.matrices.Add(matrix);
return this;
}
/// <summary>
/// Returns the combined matrix.
/// </summary>
/// <returns>The <see cref="Matrix3x2"/>.</returns>
public Matrix3x2 BuildMatrix()
{
Matrix3x2 matrix = Matrix3x2.Identity;
// Translate the origin matrix to cater for source rectangle offsets.
if (!this.rectangle.Equals(default))
{
matrix *= Matrix3x2.CreateTranslation(-this.rectangle.Location);
}
foreach (Matrix3x2 m in this.matrices)
{
matrix *= m;
}
return matrix;
}
/// <summary>
/// Removes all matrices from the builder.
/// </summary>
public void Clear() => this.matrices.Clear();
}
}

33
src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor.cs

@ -19,8 +19,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
internal class AffineTransformProcessor<TPixel> : TransformProcessorBase<TPixel>
where TPixel : struct, IPixel<TPixel>
{
private readonly Rectangle transformedRectangle;
/// <summary>
/// Initializes a new instance of the <see cref="AffineTransformProcessor{TPixel}"/> class.
/// </summary>
@ -32,18 +30,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
Guard.NotNull(sampler, nameof(sampler));
this.Sampler = sampler;
this.TransformMatrix = matrix;
this.transformedRectangle = TransformUtils.GetTransformedRectangle(
new Rectangle(Point.Empty, sourceSize),
matrix);
// We want to resize the canvas here taking into account any translations.
this.TargetDimensions = new Size(this.transformedRectangle.Right, this.transformedRectangle.Bottom);
// Handle a negative translation that exceeds the original with of the image.
if (this.TargetDimensions.Width <= 0 || this.TargetDimensions.Height <= 0)
{
this.TargetDimensions = sourceSize;
}
this.TargetDimensions = TransformUtils.GetTransformedSize(sourceSize, matrix);
}
/// <summary>
@ -79,10 +66,17 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
Rectangle sourceRectangle,
Configuration configuration)
{
// Handle tranforms that result in output identical to the original.
if (this.TransformMatrix.Equals(Matrix3x2.Identity))
{
// The cloned will be blank here copy all the pixel data over
source.GetPixelSpan().CopyTo(destination.GetPixelSpan());
return;
}
int height = this.TargetDimensions.Height;
int width = this.TargetDimensions.Width;
Rectangle sourceBounds = source.Bounds();
var targetBounds = new Rectangle(Point.Empty, this.TargetDimensions);
// Convert from screen to world space.
@ -102,7 +96,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
for (int x = 0; x < width; x++)
{
var point = Point.Transform(new Point(x, y), matrix);
if (sourceBounds.Contains(point.X, point.Y))
if (sourceRectangle.Contains(point.X, point.Y))
{
destRow[x] = source[point.X, point.Y];
}
@ -113,7 +107,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
return;
}
using (var kernel = new TransformKernelMap(configuration, source.Size(), destination.Size(), this.Sampler))
var kernel = new TransformKernelMap(configuration, source.Size(), destination.Size(), this.Sampler);
try
{
ParallelHelper.IterateRowsWithTempBuffer<Vector4>(
targetBounds,
@ -140,6 +135,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
}
});
}
finally
{
kernel.Dispose();
}
}
}
}

239
src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessorOld.cs

@ -1,239 +0,0 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.ParallelUtils;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.Memory;
using SixLabors.Primitives;
namespace SixLabors.ImageSharp.Processing.Processors.Transforms
{
/// <summary>
/// Provides the base methods to perform affine transforms on an image.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
internal class AffineTransformProcessorOld<TPixel> : InterpolatedTransformProcessorBase<TPixel>
where TPixel : struct, IPixel<TPixel>
{
/// <summary>
/// Initializes a new instance of the <see cref="AffineTransformProcessorOld{TPixel}"/> class.
/// </summary>
/// <param name="matrix">The transform matrix</param>
/// <param name="sampler">The sampler to perform the transform operation.</param>
/// <param name="targetDimensions">The target dimensions to constrain the transformed image to.</param>
public AffineTransformProcessorOld(Matrix3x2 matrix, IResampler sampler, Size targetDimensions)
: base(sampler)
{
this.TransformMatrix = matrix;
this.TargetDimensions = targetDimensions;
}
/// <summary>
/// Gets the matrix used to supply the affine transform
/// </summary>
public Matrix3x2 TransformMatrix { get; }
/// <summary>
/// Gets the target dimensions to constrain the transformed image to
/// </summary>
public Size TargetDimensions { get; }
/// <inheritdoc/>
protected override Image<TPixel> CreateDestination(Image<TPixel> source, Rectangle sourceRectangle)
{
// We will always be creating the clone even for mutate because we may need to resize the canvas
IEnumerable<ImageFrame<TPixel>> frames =
source.Frames.Select(x => new ImageFrame<TPixel>(source.GetConfiguration(), this.TargetDimensions, x.MetaData.DeepClone()));
// Use the overload to prevent an extra frame being added
return new Image<TPixel>(source.GetConfiguration(), source.MetaData.DeepClone(), frames);
}
/// <inheritdoc/>
protected override void OnFrameApply(
ImageFrame<TPixel> source,
ImageFrame<TPixel> destination,
Rectangle sourceRectangle,
Configuration configuration)
{
int height = this.TargetDimensions.Height;
int width = this.TargetDimensions.Width;
Rectangle sourceBounds = source.Bounds();
var targetBounds = new Rectangle(0, 0, width, height);
// Since could potentially be resizing the canvas we might need to re-calculate the matrix
Matrix3x2 matrix = this.GetProcessingMatrix(sourceBounds, targetBounds);
// Convert from screen to world space.
Matrix3x2.Invert(matrix, out matrix);
if (this.Sampler is NearestNeighborResampler)
{
ParallelHelper.IterateRows(
targetBounds,
configuration,
rows =>
{
for (int y = rows.Min; y < rows.Max; y++)
{
Span<TPixel> destRow = destination.GetPixelRowSpan(y);
for (int x = 0; x < width; x++)
{
var point = Point.Transform(new Point(x, y), matrix);
if (sourceBounds.Contains(point.X, point.Y))
{
destRow[x] = source[point.X, point.Y];
}
}
}
});
return;
}
int maxSourceX = source.Width - 1;
int maxSourceY = source.Height - 1;
(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);
MemoryAllocator memoryAllocator = configuration.MemoryAllocator;
using (Buffer2D<float> yBuffer = memoryAllocator.Allocate2D<float>(yLength, height))
using (Buffer2D<float> xBuffer = memoryAllocator.Allocate2D<float>(xLength, height))
{
ParallelHelper.IterateRows(
targetBounds,
configuration,
rows =>
{
for (int y = rows.Min; y < rows.Max; 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);
// Clamp sampling pixel radial extents to the source image edges
Vector2 maxXY = point + radius;
Vector2 minXY = point - radius;
// max, maxY, minX, minY
var extents = new Vector4(
MathF.Floor(maxXY.X + .5F),
MathF.Floor(maxXY.Y + .5F),
MathF.Ceiling(minXY.X - .5F),
MathF.Ceiling(minXY.Y - .5F));
int right = (int)extents.X;
int bottom = (int)extents.Y;
int left = (int)extents.Z;
int top = (int)extents.W;
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;
if (minX == maxX || minY == maxY)
{
continue;
}
// It appears these have to be calculated on-the-fly.
// Precalculating transformed weights would require prior knowledge of every transformed pixel location
// since they can be at sub-pixel positions on both axis.
// 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,
ref ySpanRef,
yLength);
CalculateWeightsDown(
left,
right,
minX,
maxX,
point.X,
sampler,
xScale,
ref xSpanRef,
xLength);
}
else
{
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 = Unsafe.Add(ref ySpanRef, yy);
for (int xx = 0, i = minX; i <= maxX; i++, xx++)
{
float xWeight = Unsafe.Add(ref xSpanRef, xx);
// Values are first premultiplied to prevent darkening of edge pixels
var current = source[i, j].ToVector4();
Vector4Utils.Premultiply(ref current);
sum += current * xWeight * yWeight;
}
}
ref TPixel dest = ref Unsafe.Add(ref destRowRef, x);
// Reverse the premultiplication
Vector4Utils.UnPremultiply(ref sum);
dest.FromVector4(sum);
}
}
});
}
}
/// <summary>
/// Gets a transform matrix adjusted for final processing based upon the target image bounds.
/// </summary>
/// <param name="sourceRectangle">The source image bounds.</param>
/// <param name="destinationRectangle">The destination image bounds.</param>
/// <returns>
/// The <see cref="Matrix3x2"/>.
/// </returns>
protected virtual Matrix3x2 GetProcessingMatrix(Rectangle sourceRectangle, Rectangle destinationRectangle)
=> this.TransformMatrix;
}
}

38
src/ImageSharp/Processing/Processors/Transforms/CenteredAffineTransformProcessor.cs

@ -1,38 +0,0 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System.Numerics;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.Primitives;
namespace SixLabors.ImageSharp.Processing.Processors.Transforms
{
/// <summary>
/// A base class that provides methods to allow the automatic centering of affine transforms
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
internal abstract class CenteredAffineTransformProcessor<TPixel> : AffineTransformProcessorOld<TPixel>
where TPixel : struct, IPixel<TPixel>
{
/// <summary>
/// Initializes a new instance of the <see cref="CenteredAffineTransformProcessor{TPixel}"/> class.
/// </summary>
/// <param name="matrix">The transform matrix</param>
/// <param name="sampler">The sampler to perform the transform operation.</param>
/// <param name="sourceSize">The source image size</param>
protected CenteredAffineTransformProcessor(Matrix3x2 matrix, IResampler sampler, Size sourceSize)
: base(matrix, sampler, GetTransformedDimensions(sourceSize, matrix))
{
}
/// <inheritdoc/>
protected override Matrix3x2 GetProcessingMatrix(Rectangle sourceRectangle, Rectangle destinationRectangle)
=> TransformHelpers.GetCenteredTransformMatrix(sourceRectangle, destinationRectangle, this.TransformMatrix);
private static Size GetTransformedDimensions(Size sourceDimensions, Matrix3x2 matrix)
{
var sourceRectangle = new Rectangle(0, 0, sourceDimensions.Width, sourceDimensions.Height);
return TransformHelpers.GetTransformedBoundingRectangle(sourceRectangle, matrix).Size;
}
}
}

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

@ -35,7 +35,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
/// <param name="sourceSize">The source image size</param>
public RotateProcessor(float degrees, IResampler sampler, Size sourceSize)
: base(
TransformUtils.CreateCenteredRotationMatrixDegrees(degrees, sourceSize),
TransformUtils.CreateRotationMatrixDegrees(degrees, sourceSize),
sampler,
sourceSize)
=> this.Degrees = degrees;

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

@ -2,7 +2,6 @@
// Licensed under the Apache License, Version 2.0.
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Processors.Transforms;
using SixLabors.Primitives;
namespace SixLabors.ImageSharp.Processing.Processors.Transforms
@ -11,7 +10,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
/// Provides methods that allow the skewing of images.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
internal class SkewProcessor<TPixel> : CenteredAffineTransformProcessor<TPixel>
internal class SkewProcessor<TPixel> : AffineTransformProcessor<TPixel>
where TPixel : struct, IPixel<TPixel>
{
/// <summary>
@ -33,7 +32,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
/// <param name="sampler">The sampler to perform the skew operation.</param>
/// <param name="sourceSize">The source image size</param>
public SkewProcessor(float degreesX, float degreesY, IResampler sampler, Size sourceSize)
: base(Matrix3x2Extensions.CreateSkewDegrees(degreesX, degreesY, PointF.Empty), sampler, sourceSize)
: base(
TransformUtils.CreateSkewMatrixDegrees(degreesX, degreesY, sourceSize),
sampler,
sourceSize)
{
this.DegreesX = degreesX;
this.DegreesY = degreesY;

20
src/ImageSharp/Processing/Processors/Transforms/TransformHelpers.cs

@ -102,26 +102,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
return centered;
}
/// <summary>
/// Returns the bounding rectangle relative to the source for the given transformation matrix.
/// </summary>
/// <param name="rectangle">The source rectangle.</param>
/// <param name="matrix">The transformation matrix.</param>
/// <returns>
/// The <see cref="Rectangle"/>.
/// </returns>
public static Rectangle GetTransformedBoundingRectangle(Rectangle rectangle, Matrix3x2 matrix)
{
// Calculate the position of the four corners in world space by applying
// The world matrix to the four corners in object space (0, 0, width, height)
var tl = Vector2.Transform(Vector2.Zero, matrix);
var tr = Vector2.Transform(new Vector2(rectangle.Width, 0), matrix);
var bl = Vector2.Transform(new Vector2(0, rectangle.Height), matrix);
var br = Vector2.Transform(new Vector2(rectangle.Width, rectangle.Height), matrix);
return GetBoundingRectangle(tl, tr, bl, br);
}
/// <summary>
/// Returns the bounding rectangle relative to the source for the given transformation matrix.
/// </summary>

56
src/ImageSharp/Processing/Processors/Transforms/TransformKernelMap.cs

@ -9,20 +9,15 @@ using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.Primitives;
// TODO: It would be great if we could somehow optimize this to calculate the weights once.
// currently we cannot do that as we are calulating the weight of the transformed point dimension
// not the point in the original image.
namespace SixLabors.ImageSharp.Processing.Processors.Transforms
{
/// <summary>
/// Contains the methods required to calculate kernel sampling weights on-the-fly.
/// Contains the methods required to calculate transform kernel convolution.
/// </summary>
internal class TransformKernelMap : IDisposable
{
private readonly Buffer2D<float> yBuffer;
private readonly Buffer2D<float> xBuffer;
private readonly int yLength;
private readonly int xLength;
private readonly Vector2 extents;
private Vector4 maxSourceExtents;
private readonly IResampler sampler;
@ -41,12 +36,12 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
float xRadius = this.GetSamplingRadius(source.Width, destination.Width);
this.extents = new Vector2(xRadius, yRadius);
this.xLength = (int)MathF.Ceiling((this.extents.X * 2) + 2);
this.yLength = (int)MathF.Ceiling((this.extents.Y * 2) + 2);
int xLength = (int)MathF.Ceiling((this.extents.X * 2) + 2);
int yLength = (int)MathF.Ceiling((this.extents.Y * 2) + 2);
// We use 2D buffers so that we can access the weight spans in parallel.
this.yBuffer = configuration.MemoryAllocator.Allocate2D<float>(this.yLength, destination.Height);
this.xBuffer = configuration.MemoryAllocator.Allocate2D<float>(this.xLength, destination.Height);
// We use 2D buffers so that we can access the weight spans per row in parallel.
this.yBuffer = configuration.MemoryAllocator.Allocate2D<float>(yLength, destination.Height);
this.xBuffer = configuration.MemoryAllocator.Allocate2D<float>(xLength, destination.Height);
int maxX = source.Width - 1;
int maxY = source.Height - 1;
@ -82,42 +77,34 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
Vector2 minXY = transformedPoint - this.extents;
Vector2 maxXY = transformedPoint + this.extents;
// minX, minY, maxX, maxY
// left, top, right, bottom
var extents = new Vector4(
MathF.Ceiling(minXY.X - .5F),
MathF.Ceiling(minXY.Y - .5F),
MathF.Floor(maxXY.X + .5F),
MathF.Floor(maxXY.Y + .5F));
extents = Vector4.Clamp(extents, Vector4.Zero, this.maxSourceExtents);
int left = (int)extents.X;
int top = (int)extents.Y;
int right = (int)extents.Z;
int bottom = (int)extents.W;
extents = Vector4.Clamp(extents, Vector4.Zero, this.maxSourceExtents);
int minX = (int)extents.X;
int minY = (int)extents.Y;
int maxX = (int)extents.Z;
int maxY = (int)extents.W;
if (minX == maxX || minY == maxY)
if (left == right || top == bottom)
{
return;
}
// TODO: Get Anton to use his superior brain on this one.
// It looks to me like we're calculating the same weights over and over again
// since min(X+Y) and max(X+Y) are the same distance apart.
this.CalculateWeights(minY, maxY, maxY - minY, transformedPoint.Y, ref ySpanRef);
this.CalculateWeights(minX, maxX, maxX - minX, transformedPoint.X, ref xSpanRef);
this.CalculateWeights(top, bottom, transformedPoint.Y, ref ySpanRef);
this.CalculateWeights(left, right, transformedPoint.X, ref xSpanRef);
Vector4 sum = Vector4.Zero;
for (int kernelY = 0, y = minY; y <= maxY; y++, kernelY++)
for (int kernelY = 0, y = top; y <= bottom; y++, kernelY++)
{
float yWeight = Unsafe.Add(ref ySpanRef, kernelY);
for (int kernelX = 0, x = minX; x <= maxX; x++, kernelX++)
for (int kernelX = 0, x = left; x <= right; x++, kernelX++)
{
float xWeight = Unsafe.Add(ref xSpanRef, kernelX);
@ -138,29 +125,18 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
/// </summary>
/// <param name="min">The minimum sampling offset</param>
/// <param name="max">The maximum sampling offset</param>
/// <param name="length">The length of the weights collection</param>
/// <param name="point">The transformed point dimension</param>
/// <param name="weightsRef">The reference to the collection of weights</param>
[MethodImpl(InliningOptions.ShortMethod)]
private void CalculateWeights(int min, int max, int length, float point, ref float weightsRef)
private void CalculateWeights(int min, int max, float point, ref float weightsRef)
{
float sum = 0;
for (int x = 0, i = min; i <= max; i++, x++)
{
float weight = this.sampler.GetValue(i - point);
sum += weight;
Unsafe.Add(ref weightsRef, x) = this.sampler.GetValue(i - point);
Unsafe.Add(ref weightsRef, x) = weight;
}
// TODO: Do we need this? Check what happens when we scale an image down.
// if (sum > 0)
// {
// for (int i = 0; i < length; i++)
// {
// ref float wRef = ref Unsafe.Add(ref weightsRef, i);
// wRef /= sum;
// }
// }
}
[MethodImpl(InliningOptions.ShortMethod)]

78
src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs

@ -10,7 +10,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
/// <summary>
/// Contains utility methods for working with transforms.
/// </summary>
public static class TransformUtils
internal static class TransformUtils
{
/// <summary>
/// Creates a centered rotation matrix using the given rotation in degrees and the source size.
@ -18,43 +18,33 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
/// <param name="degrees">The amount of rotation, in degrees.</param>
/// <param name="size">The source image size.</param>
/// <returns>The <see cref="Matrix3x2"/>.</returns>
public static Matrix3x2 CreateCenteredRotationMatrixDegrees(float degrees, Size size)
public static Matrix3x2 CreateRotationMatrixDegrees(float degrees, Size size)
=> CreateCenteredTransformMatrix(
new Rectangle(Point.Empty, size),
Matrix3x2Extensions.CreateRotationDegrees(degrees, PointF.Empty));
/// <summary>
/// Gets the centered transform matrix based upon the source and destination rectangles.
/// Creates a centered skew matrix from the give angles in degrees and the source size.
/// </summary>
/// <param name="sourceRectangle">The source image bounds.</param>
/// <param name="matrix">The transformation matrix.</param>
/// <returns>The <see cref="Matrix3x2"/></returns>
public static Matrix3x2 CreateCenteredTransformMatrix(Rectangle sourceRectangle, Matrix3x2 matrix)
{
Rectangle destinationRectangle = GetTransformedBoundingRectangle(sourceRectangle, matrix);
// We invert the matrix to handle the transformation from screen to world space.
// This ensures scaling matrices are correct.
Matrix3x2.Invert(matrix, out Matrix3x2 inverted);
var translationToTargetCenter = Matrix3x2.CreateTranslation(new Vector2(-destinationRectangle.Width, -destinationRectangle.Height) * .5F);
var translateToSourceCenter = Matrix3x2.CreateTranslation(new Vector2(sourceRectangle.Width, sourceRectangle.Height) * .5F);
// Translate back to world space.
Matrix3x2.Invert(translationToTargetCenter * inverted * translateToSourceCenter, out Matrix3x2 centered);
return centered;
}
/// <param name="degreesX">The X angle, in degrees.</param>
/// <param name="degreesY">The Y angle, in degrees.</param>
/// <param name="size">The source image size.</param>
/// <returns>The <see cref="Matrix3x2"/>.</returns>
public static Matrix3x2 CreateSkewMatrixDegrees(float degreesX, float degreesY, Size size)
=> CreateCenteredTransformMatrix(
new Rectangle(Point.Empty, size),
Matrix3x2Extensions.CreateSkewDegrees(degreesX, degreesY, PointF.Empty));
/// <summary>
/// Gets the centered transform matrix based upon the source and destination rectangles.
/// </summary>
/// <param name="sourceRectangle">The source image bounds.</param>
/// <param name="destinationRectangle">The destination image bounds.</param>
/// <param name="matrix">The transformation matrix.</param>
/// <returns>The <see cref="Matrix3x2"/></returns>
public static Matrix3x2 GetCenteredTransformMatrix(Rectangle sourceRectangle, Rectangle destinationRectangle, Matrix3x2 matrix)
public static Matrix3x2 CreateCenteredTransformMatrix(Rectangle sourceRectangle, Matrix3x2 matrix)
{
Rectangle destinationRectangle = GetTransformedBoundingRectangle(sourceRectangle, matrix);
// We invert the matrix to handle the transformation from screen to world space.
// This ensures scaling matrices are correct.
Matrix3x2.Invert(matrix, out Matrix3x2 inverted);
@ -79,8 +69,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
public static Rectangle GetTransformedBoundingRectangle(Rectangle rectangle, Matrix3x2 matrix)
{
Rectangle transformed = GetTransformedRectangle(rectangle, matrix);
// TODO: Check this.
return new Rectangle(0, 0, transformed.Width, transformed.Height);
}
@ -107,6 +95,44 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
return GetBoundingRectangle(tl, tr, bl, br);
}
/// <summary>
/// Returns the size relative to the source for the given transformation matrix.
/// </summary>
/// <param name="size">The source size.</param>
/// <param name="matrix">The transformation matrix.</param>
/// <returns>
/// The <see cref="Size"/>.
/// </returns>
public static Size GetTransformedSize(Size size, Matrix3x2 matrix)
{
Guard.IsTrue(size.Width > 0 && size.Height > 0, nameof(size), "Source size dimensions cannot be 0!");
if (matrix.Equals(default) || matrix.Equals(Matrix3x2.Identity))
{
return size;
}
Rectangle rectangle = GetTransformedRectangle(new Rectangle(Point.Empty, size), matrix);
// We want to resize the canvas here taking into account any translations.
int height = rectangle.Top < 0 ? rectangle.Bottom : Math.Max(rectangle.Height, rectangle.Bottom);
int width = rectangle.Left < 0 ? rectangle.Right : Math.Max(rectangle.Width, rectangle.Right);
// If location in either direction is translated to a negative value equal to or exceeding the
// dimensions in eith direction we need to reassign the dimension.
if (height <= 0)
{
height = rectangle.Height;
}
if (width <= 0)
{
width = rectangle.Width;
}
return new Size(width, height);
}
private static Rectangle GetBoundingRectangle(Vector2 tl, Vector2 tr, Vector2 bl, Vector2 br)
{
// Find the minimum and maximum "corners" based on the given vectors

54
src/ImageSharp/Processing/TransformExtensions.cs

@ -18,65 +18,23 @@ namespace SixLabors.ImageSharp.Processing
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="source">The image to transform.</param>
/// <param name="matrix">The transformation matrix.</param>
/// <param name="builder">The affine transform builder.</param>
/// <returns>The <see cref="Image{TPixel}"/></returns>
public static IImageProcessingContext<TPixel> Transform<TPixel>(this IImageProcessingContext<TPixel> source, Matrix3x2 matrix)
public static IImageProcessingContext<TPixel> Transform<TPixel>(this IImageProcessingContext<TPixel> source, AffineTransformBuilder builder)
where TPixel : struct, IPixel<TPixel>
=> Transform(source, matrix, KnownResamplers.Bicubic);
=> Transform(source, builder, KnownResamplers.Bicubic);
/// <summary>
/// Transforms an image by the given matrix using the specified sampling algorithm.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="source">The image to transform.</param>
/// <param name="matrix">The transformation matrix.</param>
/// <param name="sampler">The <see cref="IResampler"/> to perform the resampling.</param>
/// <returns>The <see cref="Image{TPixel}"/></returns>
public static IImageProcessingContext<TPixel> Transform<TPixel>(this IImageProcessingContext<TPixel> source, Matrix3x2 matrix, IResampler sampler)
where TPixel : struct, IPixel<TPixel>
=> source.ApplyProcessor(new AffineTransformProcessorOld<TPixel>(matrix, sampler, source.GetCurrentSize()));
/// <summary>
/// Transforms an image by the given matrix using the specified sampling algorithm
/// and a rectangle defining the transform origin in the source image and the size of the result image.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="source">The image to transform.</param>
/// <param name="matrix">The transformation matrix.</param>
/// <param name="sampler">The <see cref="IResampler"/> to perform the resampling.</param>
/// <param name="rectangle">
/// The rectangle defining the transform origin in the source image, and the size of the result image.
/// </param>
/// <returns>The <see cref="Image{TPixel}"/></returns>
public static IImageProcessingContext<TPixel> Transform<TPixel>(
this IImageProcessingContext<TPixel> source,
Matrix3x2 matrix,
IResampler sampler,
Rectangle rectangle)
where TPixel : struct, IPixel<TPixel>
{
var t = Matrix3x2.CreateTranslation(-rectangle.Location);
Matrix3x2 combinedMatrix = t * matrix;
return source.ApplyProcessor(new AffineTransformProcessorOld<TPixel>(combinedMatrix, sampler, rectangle.Size));
}
/// <summary>
/// Transforms an image by the given matrix using the specified sampling algorithm,
/// cropping or extending the image according to <paramref name="destinationSize"/>.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="source">The image to transform.</param>
/// <param name="matrix">The transformation matrix.</param>
/// <param name="builder">The affine transform builder.</param>
/// <param name="sampler">The <see cref="IResampler"/> to perform the resampling.</param>
/// <param name="destinationSize">The size of the destination image.</param>
/// <returns>The <see cref="Image{TPixel}"/></returns>
public static IImageProcessingContext<TPixel> Transform<TPixel>(
this IImageProcessingContext<TPixel> source,
Matrix3x2 matrix,
IResampler sampler,
Size destinationSize)
public static IImageProcessingContext<TPixel> Transform<TPixel>(this IImageProcessingContext<TPixel> source, AffineTransformBuilder builder, IResampler sampler)
where TPixel : struct, IPixel<TPixel>
=> source.ApplyProcessor(new AffineTransformProcessorOld<TPixel>(matrix, sampler, destinationSize));
=> source.ApplyProcessor(new AffineTransformProcessor<TPixel>(builder.BuildMatrix(), sampler, builder.Size));
/// <summary>
/// Transforms an image by the given matrix.

10
tests/ImageSharp.Benchmarks/Samplers/Rotate.cs

@ -21,7 +21,7 @@ namespace SixLabors.ImageSharp.Benchmarks.Samplers
}
}
// Nov 4 2018
// Nov 7 2018
//BenchmarkDotNet=v0.10.14, OS=Windows 10.0.17763
//Intel Core i7-6600U CPU 2.60GHz(Skylake), 1 CPU, 4 logical and 2 physical cores
//.NET Core SDK = 2.1.403
@ -36,4 +36,10 @@ namespace SixLabors.ImageSharp.Benchmarks.Samplers
// Method | Runtime | Mean | Error | StdDev | Allocated |
//--------- |-------- |---------:|----------:|----------:|----------:|
// DoRotate | Clr | 85.19 ms | 13.379 ms | 0.7560 ms | 6 KB |
// DoRotate | Core | 53.51 ms | 9.512 ms | 0.5375 ms | 4.29 KB |
// DoRotate | Core | 53.51 ms | 9.512 ms | 0.5375 ms | 4.29 KB |
// #### AFTER ####:
//Method | Runtime | Mean | Error | StdDev | Allocated |
//--------- |-------- |---------:|---------:|---------:|----------:|
// DoRotate | Clr | 77.08 ms | 23.97 ms | 1.354 ms | 6 KB |
// DoRotate | Core | 40.36 ms | 47.43 ms | 2.680 ms | 4.36 KB |

45
tests/ImageSharp.Benchmarks/Samplers/Skew.cs

@ -0,0 +1,45 @@
using BenchmarkDotNet.Attributes;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using SixLabors.Primitives;
namespace SixLabors.ImageSharp.Benchmarks.Samplers
{
[Config(typeof(Config.ShortClr))]
public class Skew
{
[Benchmark]
public Size DoSkew()
{
using (var image = new Image<Rgba32>(Configuration.Default, 400, 400, Rgba32.BlanchedAlmond))
{
image.Mutate(x => x.Skew(20, 10));
return image.Size();
}
}
}
}
// Nov 7 2018
//BenchmarkDotNet=v0.10.14, OS=Windows 10.0.17763
//Intel Core i7-6600U CPU 2.60GHz(Skylake), 1 CPU, 4 logical and 2 physical cores
//.NET Core SDK = 2.1.403
// [Host] : .NET Core 2.1.5 (CoreCLR 4.6.26919.02, CoreFX 4.6.26919.02), 64bit RyuJIT
// Job-KKDIMW : .NET Framework 4.7.1 (CLR 4.0.30319.42000), 64bit RyuJIT-v4.7.3190.0
// Job-IUZRFA : .NET Core 2.1.5 (CoreCLR 4.6.26919.02, CoreFX 4.6.26919.02), 64bit RyuJIT
//LaunchCount=1 TargetCount=3 WarmupCount=3
// #### BEFORE ####:
//Method | Runtime | Mean | Error | StdDev | Allocated |
//------- |-------- |---------:|---------:|----------:|----------:|
// DoSkew | Clr | 78.14 ms | 8.383 ms | 0.4736 ms | 6 KB |
// DoSkew | Core | 44.22 ms | 4.109 ms | 0.2322 ms | 4.28 KB |
// #### AFTER ####:
//Method | Runtime | Mean | Error | StdDev | Allocated |
//------- |-------- |---------:|----------:|----------:|----------:|
// DoSkew | Clr | 71.63 ms | 25.589 ms | 1.4458 ms | 6 KB |
// DoSkew | Core | 38.99 ms | 8.640 ms | 0.4882 ms | 4.36 KB |

22
tests/ImageSharp.Tests/Drawing/DrawImageTest.cs

@ -2,10 +2,8 @@
// Licensed under the Apache License, Version 2.0.
using System;
using System.Numerics;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Processors.Transforms;
using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison;
using SixLabors.Primitives;
using Xunit;
@ -75,21 +73,15 @@ namespace SixLabors.ImageSharp.Tests
using (Image<TPixel> image = provider.GetImage())
using (var blend = Image.Load<TPixel>(TestFile.Create(TestImages.Bmp.Car).Bytes))
{
Matrix3x2 rotate = Matrix3x2Extensions.CreateRotationDegrees(45F);
Matrix3x2 scale = Matrix3x2Extensions.CreateScale(new SizeF(.25F, .25F));
Matrix3x2 matrix = rotate * scale;
AffineTransformBuilder builder = new AffineTransformBuilder(blend.Size())
.AppendRotateMatrixDegrees(45F)
.AppendScaleMatrix(new SizeF(.25F, .25F))
.AppendTranslationMatrix(new PointF(10, 10));
// Lets center the matrix so we can tell whether any cut-off issues we may have belong to the drawing processor
Rectangle srcBounds = blend.Bounds();
Rectangle destBounds = TransformHelpers.GetTransformedBoundingRectangle(srcBounds, matrix);
Matrix3x2 centeredMatrix = TransformHelpers.GetCenteredTransformMatrix(srcBounds, destBounds, matrix);
// We pass a new rectangle here based on the dest bounds since we've offset the matrix
blend.Mutate(x => x.Transform(
centeredMatrix,
KnownResamplers.Bicubic,
new Rectangle(0, 0, destBounds.Width, destBounds.Height)));
// Apply a background color so we can see the translation.
blend.Mutate(x => x.Transform(builder).BackgroundColor(NamedColors<TPixel>.HotPink));
// Lets center the matrix so we can tell whether any cut-off issues we may have belong to the drawing processor
var position = new Point((image.Width - blend.Width) / 2, (image.Height - blend.Height) / 2);
image.Mutate(x => x.DrawImage(blend, position, mode, .75F));
image.DebugSave(provider, new[] { "Transformed" });

54
tests/ImageSharp.Tests/Processing/Transforms/AffineTransformTests.cs

@ -78,15 +78,10 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms
IResampler resampler = GetResampler(resamplerName);
using (Image<TPixel> image = provider.GetImage())
{
var rotate = Matrix3x2.CreateRotation((float)Math.PI / 4F, new Vector2(5 / 2F, 5 / 2F));
var translate = Matrix3x2.CreateTranslation((7 - 5) / 2F, (7 - 5) / 2F);
AffineTransformBuilder builder = new AffineTransformBuilder(image.Size())
.AppendRotateMatrixDegrees((float)Math.PI / 4F);
Rectangle sourceRectangle = image.Bounds();
Matrix3x2 matrix = rotate * translate;
Rectangle destRectangle = TransformHelpers.GetTransformedBoundingRectangle(sourceRectangle, matrix);
image.Mutate(c => c.Transform(matrix, resampler, destRectangle));
image.Mutate(c => c.Transform(builder, resampler));
image.DebugSave(provider, resamplerName);
VerifyAllPixelsAreWhiteOrTransparent(image);
@ -104,14 +99,15 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms
{
using (Image<TPixel> image = provider.GetImage())
{
Matrix3x2 rotate = Matrix3x2Extensions.CreateRotationDegrees(angleDeg);
var translate = Matrix3x2.CreateTranslation(tx, ty);
var scale = Matrix3x2.CreateScale(sx, sy);
Matrix3x2 m = rotate * scale * translate;
image.DebugSave(provider, $"_original");
AffineTransformBuilder builder = new AffineTransformBuilder(image.Size())
.AppendRotateMatrixDegrees(angleDeg)
.AppendScaleMatrix(new SizeF(sx, sy))
.AppendTranslationMatrix(new PointF(tx, ty));
this.PrintMatrix(m);
this.PrintMatrix(builder.BuildMatrix());
image.Mutate(i => i.Transform(m, KnownResamplers.Bicubic));
image.Mutate(i => i.Transform(builder, KnownResamplers.Bicubic));
FormattableString testOutputDetails = $"R({angleDeg})_S({sx},{sy})_T({tx},{ty})";
image.DebugSave(provider, testOutputDetails);
@ -126,9 +122,11 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms
{
using (Image<TPixel> image = provider.GetImage())
{
Matrix3x2 m = this.MakeManuallyCenteredMatrix(angleDeg, s, image);
AffineTransformBuilder builder = new AffineTransformBuilder(image.Size())
.AppendRotateMatrixDegrees(angleDeg)
.AppendScaleMatrix(new SizeF(s, s));
image.Mutate(i => i.Transform(m, KnownResamplers.Bicubic));
image.Mutate(i => i.Transform(builder, KnownResamplers.Bicubic));
FormattableString testOutputDetails = $"R({angleDeg})_S({s})";
image.DebugSave(provider, testOutputDetails);
@ -155,13 +153,15 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms
public void Transform_FromSourceRectangle1<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : struct, IPixel<TPixel>
{
var rectangle = new Rectangle(48, 0, 96, 36);
var rectangle = new Rectangle(48, 0, 48, 24);
using (Image<TPixel> image = provider.GetImage())
{
var m = Matrix3x2.CreateScale(2.0F, 1.5F);
image.DebugSave(provider, $"_original");
AffineTransformBuilder builder = new AffineTransformBuilder(rectangle)
.AppendScaleMatrix(new SizeF(2, 1.5F));
image.Mutate(i => i.Transform(m, KnownResamplers.Spline, rectangle));
image.Mutate(i => i.Transform(builder, KnownResamplers.Spline));
image.DebugSave(provider);
image.CompareToReferenceOutput(ValidatorComparer, provider);
@ -173,13 +173,14 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms
public void Transform_FromSourceRectangle2<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : struct, IPixel<TPixel>
{
var rectangle = new Rectangle(0, 24, 48, 48);
var rectangle = new Rectangle(0, 24, 48, 24);
using (Image<TPixel> image = provider.GetImage())
{
var m = Matrix3x2.CreateScale(1.0F, 2.0F);
AffineTransformBuilder builder = new AffineTransformBuilder(rectangle)
.AppendScaleMatrix(new SizeF(1F, 2F));
image.Mutate(i => i.Transform(m, KnownResamplers.Spline, rectangle));
image.Mutate(i => i.Transform(builder, KnownResamplers.Spline));
image.DebugSave(provider);
image.CompareToReferenceOutput(ValidatorComparer, provider);
@ -194,12 +195,11 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms
IResampler sampler = GetResampler(resamplerName);
using (Image<TPixel> image = provider.GetImage())
{
Matrix3x2 m = this.MakeManuallyCenteredMatrix(50, 0.6f, image);
AffineTransformBuilder builder = new AffineTransformBuilder(image.Size())
.AppendRotateMatrixDegrees(50)
.AppendScaleMatrix(new SizeF(.6F, .6F));
image.Mutate(i =>
{
i.Transform(m, sampler);
});
image.Mutate(i => i.Transform(builder, sampler));
image.DebugSave(provider, resamplerName);
image.CompareToReferenceOutput(ValidatorComparer, provider, resamplerName);

Loading…
Cancel
Save