Browse Source

Make AffineTransformProcessor and AffineTransformBuilder API more flexible

pull/775/head
Anton Firszov 8 years ago
parent
commit
103d129012
  1. 83
      src/ImageSharp/Processing/AffineTransformBuilder.cs
  2. 6
      src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor.cs
  3. 71
      src/ImageSharp/Processing/TransformExtensions.cs
  4. 2
      tests/ImageSharp.Tests/Drawing/DrawImageTest.cs
  5. 28
      tests/ImageSharp.Tests/Processing/Transforms/AffineTransformBuilderTests.cs
  6. 18
      tests/ImageSharp.Tests/Processing/Transforms/AffineTransformTests.cs
  7. 2
      tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformBuilderTests.cs
  8. 18
      tests/ImageSharp.Tests/Processing/Transforms/TransformBuilderTestBase.cs

83
src/ImageSharp/Processing/AffineTransformBuilder.cs

@ -1,6 +1,7 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Collections.Generic;
using System.Numerics;
using SixLabors.ImageSharp.Processing.Processors.Transforms;
@ -13,34 +14,7 @@ namespace SixLabors.ImageSharp.Processing
/// </summary>
public class AffineTransformBuilder
{
private readonly List<Matrix3x2> matrices = new List<Matrix3x2>();
private readonly Rectangle sourceRectangle;
/// <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(new Rectangle(Point.Empty, sourceSize))
{
}
/// <summary>
/// Initializes a new instance of the <see cref="AffineTransformBuilder"/> class.
/// </summary>
/// <param name="sourceRectangle">The source rectangle.</param>
public AffineTransformBuilder(Rectangle sourceRectangle)
{
Guard.MustBeGreaterThan(sourceRectangle.Width, 0, nameof(sourceRectangle));
Guard.MustBeGreaterThan(sourceRectangle.Height, 0, nameof(sourceRectangle));
this.sourceRectangle = sourceRectangle;
}
/// <summary>
/// Gets the source image size.
/// </summary>
internal Size Size => this.sourceRectangle.Size;
private readonly List<Func<Size, Matrix3x2>> matrixFactories = new List<Func<Size, Matrix3x2>>();
/// <summary>
/// Prepends a centered rotation matrix using the given rotation in radians.
@ -48,7 +22,7 @@ namespace SixLabors.ImageSharp.Processing
/// <param name="radians">The amount of rotation, in radians.</param>
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
public AffineTransformBuilder PrependCenteredRotationRadians(float radians)
=> this.PrependMatrix(TransformUtils.CreateRotationMatrixRadians(radians, this.Size));
=> this.Prepend(size => TransformUtils.CreateRotationMatrixRadians(radians, size));
/// <summary>
/// Appends a centered rotation matrix using the given rotation in radians.
@ -56,7 +30,7 @@ namespace SixLabors.ImageSharp.Processing
/// <param name="radians">The amount of rotation, in radians.</param>
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
public AffineTransformBuilder AppendCenteredRotationRadians(float radians)
=> this.AppendMatrix(TransformUtils.CreateRotationMatrixRadians(radians, this.Size));
=> this.Append(size => TransformUtils.CreateRotationMatrixRadians(radians, size));
/// <summary>
/// Prepends a centered rotation matrix using the given rotation in degrees.
@ -129,7 +103,7 @@ namespace SixLabors.ImageSharp.Processing
/// <param name="degreesY">The Y angle, in degrees.</param>
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
public AffineTransformBuilder PrependSkewDegrees(float degreesX, float degreesY)
=> this.PrependMatrix(TransformUtils.CreateSkewMatrixDegrees(degreesX, degreesY, this.Size));
=> this.Prepend(size => TransformUtils.CreateSkewMatrixDegrees(degreesX, degreesY, size));
/// <summary>
/// Appends a centered skew matrix from the give angles in degrees.
@ -138,7 +112,7 @@ namespace SixLabors.ImageSharp.Processing
/// <param name="degreesY">The Y angle, in degrees.</param>
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
public AffineTransformBuilder AppendSkewDegrees(float degreesX, float degreesY)
=> this.AppendMatrix(TransformUtils.CreateSkewMatrixDegrees(degreesX, degreesY, this.Size));
=> this.Append(size => TransformUtils.CreateSkewMatrixDegrees(degreesX, degreesY, size));
/// <summary>
/// Prepends a translation matrix from the given vector.
@ -179,7 +153,7 @@ namespace SixLabors.ImageSharp.Processing
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
public AffineTransformBuilder PrependMatrix(Matrix3x2 matrix)
{
this.matrices.Insert(0, matrix);
this.matrixFactories.Insert(0, _ => matrix);
return this;
}
@ -190,35 +164,50 @@ namespace SixLabors.ImageSharp.Processing
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
public AffineTransformBuilder AppendMatrix(Matrix3x2 matrix)
{
this.matrices.Add(matrix);
this.matrixFactories.Add(_ => matrix);
return this;
}
/// <summary>
/// Returns the combined matrix.
/// Returns the combined matrix for a given source size.
/// </summary>
/// <param name="sourceSize">The source image size</param>
/// <returns>The <see cref="Matrix3x2"/>.</returns>
public Matrix3x2 BuildMatrix(Size sourceSize) => this.BuildMatrix(new Rectangle(Point.Empty, sourceSize));
/// <summary>
/// Returns the combined matrix for a given source rectangle.
/// </summary>
/// <param name="sourceRectangle">The rectangle in the source image</param>
/// <returns>The <see cref="Matrix3x2"/>.</returns>
public Matrix3x2 BuildMatrix()
public Matrix3x2 BuildMatrix(Rectangle sourceRectangle)
{
Matrix3x2 matrix = Matrix3x2.Identity;
Guard.MustBeGreaterThan(sourceRectangle.Width, 0, nameof(sourceRectangle));
Guard.MustBeGreaterThan(sourceRectangle.Height, 0, nameof(sourceRectangle));
// Translate the origin matrix to cater for source rectangle offsets.
if (!this.sourceRectangle.Equals(default))
{
matrix *= Matrix3x2.CreateTranslation(-this.sourceRectangle.Location);
}
var matrix = Matrix3x2.CreateTranslation(-sourceRectangle.Location);
foreach (Matrix3x2 m in this.matrices)
Size size = sourceRectangle.Size;
foreach (Func<Size, Matrix3x2> factory in this.matrixFactories)
{
matrix *= m;
matrix *= factory(size);
}
return matrix;
}
/// <summary>
/// Removes all matrices from the builder.
/// </summary>
public void Clear() => this.matrices.Clear();
private AffineTransformBuilder Prepend(Func<Size, Matrix3x2> factory)
{
this.matrixFactories.Insert(0, factory);
return this;
}
private AffineTransformBuilder Append(Func<Size, Matrix3x2> factory)
{
this.matrixFactories.Add(factory);
return this;
}
}
}

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

@ -24,13 +24,13 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
/// </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>
public AffineTransformProcessor(Matrix3x2 matrix, IResampler sampler, Size sourceSize)
/// <param name="targetDimensions">The target dimensions.</param>
public AffineTransformProcessor(Matrix3x2 matrix, IResampler sampler, Size targetDimensions)
{
Guard.NotNull(sampler, nameof(sampler));
this.Sampler = sampler;
this.TransformMatrix = matrix;
this.TargetDimensions = TransformUtils.GetTransformedSize(sourceSize, matrix);
this.TargetDimensions = targetDimensions;;
}
/// <summary>

71
src/ImageSharp/Processing/TransformExtensions.cs

@ -1,8 +1,11 @@
// 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.Processors.Transforms;
using SixLabors.Primitives;
namespace SixLabors.ImageSharp.Processing
{
@ -11,6 +14,29 @@ namespace SixLabors.ImageSharp.Processing
/// </summary>
public static class TransformExtensions
{
/// <summary>
/// Performs an affine transform of an image using the specified sampling algorithm.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="ctx">The <see cref="IImageProcessingContext{TPixel}"/>.</param>
/// <param name="sourceRectangle">The source rectangle</param>
/// <param name="transform">The transformation matrix.</param>
/// <param name="targetDimensions">The size of the result image.</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> ctx,
Rectangle sourceRectangle,
Matrix3x2 transform,
Size targetDimensions,
IResampler sampler)
where TPixel : struct, IPixel<TPixel>
{
return ctx.ApplyProcessor(
new AffineTransformProcessor<TPixel>(transform, sampler, targetDimensions),
sourceRectangle);
}
/// <summary>
/// Performs an affine transform of an image.
/// </summary>
@ -18,7 +44,9 @@ namespace SixLabors.ImageSharp.Processing
/// <param name="source">The image to transform.</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, AffineTransformBuilder builder)
public static IImageProcessingContext<TPixel> Transform<TPixel>(
this IImageProcessingContext<TPixel> source,
AffineTransformBuilder builder)
where TPixel : struct, IPixel<TPixel>
=> Transform(source, builder, KnownResamplers.Bicubic);
@ -26,13 +54,39 @@ namespace SixLabors.ImageSharp.Processing
/// Performs an affine transform of an image using the specified sampling algorithm.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="source">The image to transform.</param>
/// <param name="ctx">The <see cref="IImageProcessingContext{TPixel}"/>.</param>
/// <param name="sourceRectangle">The source rectangle</param>
/// <param name="builder">The affine transform builder.</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> ctx,
Rectangle sourceRectangle,
AffineTransformBuilder builder,
IResampler sampler)
where TPixel : struct, IPixel<TPixel>
{
Matrix3x2 transform = builder.BuildMatrix(sourceRectangle);
Size targetDimensions = TransformUtils.GetTransformedSize(sourceRectangle.Size, transform);
return ctx.Transform(sourceRectangle, transform, targetDimensions, sampler);
}
/// <summary>
/// Performs an affine transform of an image using the specified sampling algorithm.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="ctx">The <see cref="IImageProcessingContext{TPixel}"/>.</param>
/// <param name="builder">The affine transform builder.</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, AffineTransformBuilder builder, IResampler sampler)
public static IImageProcessingContext<TPixel> Transform<TPixel>(
this IImageProcessingContext<TPixel> ctx,
AffineTransformBuilder builder,
IResampler sampler)
where TPixel : struct, IPixel<TPixel>
=> source.ApplyProcessor(new AffineTransformProcessor<TPixel>(builder.BuildMatrix(), sampler, builder.Size));
{
return ctx.Transform(new Rectangle(Point.Empty, ctx.GetCurrentSize()), builder, sampler);
}
/// <summary>
/// Performs a projective transform of an image.
@ -41,7 +95,9 @@ namespace SixLabors.ImageSharp.Processing
/// <param name="source">The image to transform.</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, ProjectiveTransformBuilder builder)
public static IImageProcessingContext<TPixel> Transform<TPixel>(
this IImageProcessingContext<TPixel> source,
ProjectiveTransformBuilder builder)
where TPixel : struct, IPixel<TPixel>
=> Transform(source, builder, KnownResamplers.Bicubic);
@ -53,7 +109,10 @@ namespace SixLabors.ImageSharp.Processing
/// <param name="builder">The projective transform builder.</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, ProjectiveTransformBuilder builder, IResampler sampler)
public static IImageProcessingContext<TPixel> Transform<TPixel>(
this IImageProcessingContext<TPixel> source,
ProjectiveTransformBuilder builder,
IResampler sampler)
where TPixel : struct, IPixel<TPixel>
=> source.ApplyProcessor(new ProjectiveTransformProcessor<TPixel>(builder.BuildMatrix(), sampler, builder.Size));
}

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

@ -73,7 +73,7 @@ namespace SixLabors.ImageSharp.Tests
using (Image<TPixel> image = provider.GetImage())
using (var blend = Image.Load<TPixel>(TestFile.Create(TestImages.Bmp.Car).Bytes))
{
AffineTransformBuilder builder = new AffineTransformBuilder(blend.Size())
AffineTransformBuilder builder = new AffineTransformBuilder()
.AppendCenteredRotationDegrees(45F)
.AppendScale(new SizeF(.25F, .25F))
.AppendTranslation(new PointF(10, 10));

28
tests/ImageSharp.Tests/Processing/Transforms/AffineTransformBuilderTests.cs

@ -12,30 +12,6 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms
{
public class AffineTransformBuilderTests : TransformBuilderTestBase<AffineTransformBuilder>
{
[Fact]
public void ConstructorAssignsProperties()
{
var s = new Size(1, 1);
var builder = new AffineTransformBuilder(new Rectangle(Point.Empty, s));
Assert.Equal(s, builder.Size);
}
[Fact]
public void ConstructorThrowsInvalid()
{
Assert.Throws<ArgumentOutOfRangeException>(() =>
{
var s = new Size(0, 1);
var builder = new AffineTransformBuilder(new Rectangle(Point.Empty, s));
});
Assert.Throws<ArgumentOutOfRangeException>(() =>
{
var s = new Size(1, 0);
var builder = new AffineTransformBuilder(new Rectangle(Point.Empty, s));
});
}
protected override void AppendTranslation(AffineTransformBuilder builder, PointF translate) => builder.AppendTranslation(translate);
protected override void AppendScale(AffineTransformBuilder builder, SizeF scale) => builder.AppendScale(scale);
protected override void AppendRotationRadians(AffineTransformBuilder builder, float radians) => builder.AppendCenteredRotationRadians(radians);
@ -44,12 +20,14 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms
protected override void PrependScale(AffineTransformBuilder builder, SizeF scale) => builder.PrependScale(scale);
protected override void PrependRotationRadians(AffineTransformBuilder builder, float radians) => builder.PrependCenteredRotationRadians(radians);
protected override AffineTransformBuilder CreateBuilder(Rectangle rectangle) => new AffineTransformBuilder();
protected override Vector2 Execute(
AffineTransformBuilder builder,
Rectangle rectangle,
Vector2 sourcePoint)
{
Matrix3x2 matrix = builder.BuildMatrix();
Matrix3x2 matrix = builder.BuildMatrix(rectangle);
return Vector2.Transform(sourcePoint, matrix);
}
}

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

@ -79,7 +79,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms
IResampler resampler = GetResampler(resamplerName);
using (Image<TPixel> image = provider.GetImage())
{
AffineTransformBuilder builder = new AffineTransformBuilder(image.Size())
AffineTransformBuilder builder = new AffineTransformBuilder()
.AppendCenteredRotationDegrees(30);
image.Mutate(c => c.Transform(builder, resampler));
@ -101,12 +101,12 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms
using (Image<TPixel> image = provider.GetImage())
{
image.DebugSave(provider, $"_original");
AffineTransformBuilder builder = new AffineTransformBuilder(image.Size())
AffineTransformBuilder builder = new AffineTransformBuilder()
.AppendCenteredRotationDegrees(angleDeg)
.AppendScale(new SizeF(sx, sy))
.AppendTranslation(new PointF(tx, ty));
this.PrintMatrix(builder.BuildMatrix());
this.PrintMatrix(builder.BuildMatrix(image.Size()));
image.Mutate(i => i.Transform(builder, KnownResamplers.Bicubic));
@ -123,7 +123,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms
{
using (Image<TPixel> image = provider.GetImage())
{
AffineTransformBuilder builder = new AffineTransformBuilder(image.Size())
AffineTransformBuilder builder = new AffineTransformBuilder()
.AppendCenteredRotationDegrees(angleDeg)
.AppendScale(new SizeF(s, s));
@ -159,10 +159,10 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms
using (Image<TPixel> image = provider.GetImage())
{
image.DebugSave(provider, $"_original");
AffineTransformBuilder builder = new AffineTransformBuilder(rectangle)
AffineTransformBuilder builder = new AffineTransformBuilder()
.AppendScale(new SizeF(2, 1.5F));
image.Mutate(i => i.Transform(builder, KnownResamplers.Spline));
image.Mutate(i => i.Transform(rectangle, builder, KnownResamplers.Spline));
image.DebugSave(provider);
image.CompareToReferenceOutput(ValidatorComparer, provider);
@ -178,10 +178,10 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms
using (Image<TPixel> image = provider.GetImage())
{
AffineTransformBuilder builder = new AffineTransformBuilder(rectangle)
AffineTransformBuilder builder = new AffineTransformBuilder()
.AppendScale(new SizeF(1F, 2F));
image.Mutate(i => i.Transform(builder, KnownResamplers.Spline));
image.Mutate(i => i.Transform(rectangle, builder, KnownResamplers.Spline));
image.DebugSave(provider);
image.CompareToReferenceOutput(ValidatorComparer, provider);
@ -196,7 +196,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms
IResampler sampler = GetResampler(resamplerName);
using (Image<TPixel> image = provider.GetImage())
{
AffineTransformBuilder builder = new AffineTransformBuilder(image.Size())
AffineTransformBuilder builder = new AffineTransformBuilder()
.AppendCenteredRotationDegrees(50)
.AppendScale(new SizeF(.6F, .6F));

2
tests/ImageSharp.Tests/Processing/Transforms/ProjectiveTransformBuilderTests.cs

@ -35,6 +35,8 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms
});
}
protected override ProjectiveTransformBuilder CreateBuilder(Rectangle rectangle) => new ProjectiveTransformBuilder(rectangle);
protected override void AppendTranslation(ProjectiveTransformBuilder builder, PointF translate) => builder.AppendTranslation(translate);
protected override void AppendScale(ProjectiveTransformBuilder builder, SizeF scale) => builder.AppendScale(scale);
protected override void AppendRotationRadians(ProjectiveTransformBuilder builder, float radians) => builder.AppendCenteredRotationRadians(radians);

18
tests/ImageSharp.Tests/Processing/Transforms/TransformBuilderTestBase.cs

@ -129,9 +129,25 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms
Assert.Equal(p1, p2, Comparer);
}
[Theory]
[InlineData(0, 1)]
[InlineData(1, 0)]
[InlineData(-1, 0)]
public void ThrowsForInvalidSizes(int width, int height)
{
var size = new Size(width, height);
Assert.ThrowsAny<ArgumentOutOfRangeException>(
() =>
{
TBuilder builder = this.CreateBuilder(size);
this.Execute(builder, new Rectangle(Point.Empty, size), Vector2.Zero);
});
}
protected TBuilder CreateBuilder(Size size) => this.CreateBuilder(new Rectangle(Point.Empty, size));
protected virtual TBuilder CreateBuilder(Rectangle rectangle) => (TBuilder)Activator.CreateInstance(typeof(TBuilder), rectangle);
protected abstract TBuilder CreateBuilder(Rectangle rectangle);
protected abstract void AppendTranslation(TBuilder builder, PointF translate);
protected abstract void AppendScale(TBuilder builder, SizeF scale);

Loading…
Cancel
Save