mirror of https://github.com/SixLabors/ImageSharp
14 changed files with 360 additions and 476 deletions
@ -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(); |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
@ -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 |
|
|||
Loading…
Reference in new issue