mirror of https://github.com/SixLabors/ImageSharp
committed by
GitHub
42 changed files with 1882 additions and 599 deletions
@ -0,0 +1,245 @@ |
|||||
|
// 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.Threading.Tasks; |
||||
|
using SixLabors.ImageSharp.Advanced; |
||||
|
using SixLabors.ImageSharp.Helpers; |
||||
|
using SixLabors.ImageSharp.Memory; |
||||
|
using SixLabors.ImageSharp.PixelFormats; |
||||
|
using SixLabors.Primitives; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Processing.Processors |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Provides the base methods to perform affine transforms on an image.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
||||
|
internal class AffineTransformProcessor<TPixel> : InterpolatedTransformProcessorBase<TPixel> |
||||
|
where TPixel : struct, IPixel<TPixel> |
||||
|
{ |
||||
|
private Size targetDimensions; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Initializes a new instance of the <see cref="AffineTransformProcessor{TPixel}"/> class.
|
||||
|
/// </summary>
|
||||
|
/// <param name="matrix">The transform matrix</param>
|
||||
|
public AffineTransformProcessor(Matrix3x2 matrix) |
||||
|
: this(matrix, KnownResamplers.Bicubic) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Initializes a new instance of the <see cref="AffineTransformProcessor{TPixel}"/> class.
|
||||
|
/// </summary>
|
||||
|
/// <param name="matrix">The transform matrix</param>
|
||||
|
/// <param name="sampler">The sampler to perform the transform operation.</param>
|
||||
|
public AffineTransformProcessor(Matrix3x2 matrix, IResampler sampler) |
||||
|
: this(matrix, sampler, Size.Empty) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Initializes a new instance of the <see cref="AffineTransformProcessor{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 AffineTransformProcessor(Matrix3x2 matrix, IResampler sampler, Size targetDimensions) |
||||
|
: base(sampler) |
||||
|
{ |
||||
|
// Tansforms are inverted else the output is the opposite of the expected.
|
||||
|
Matrix3x2.Invert(matrix, out matrix); |
||||
|
this.TransformMatrix = matrix; |
||||
|
this.targetDimensions = targetDimensions; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the matrix used to supply the affine transform
|
||||
|
/// </summary>
|
||||
|
public Matrix3x2 TransformMatrix { get; } |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
protected override Image<TPixel> CreateDestination(Image<TPixel> source, Rectangle sourceRectangle) |
||||
|
{ |
||||
|
if (this.targetDimensions == Size.Empty) |
||||
|
{ |
||||
|
// TODO: CreateDestination() should not modify the processors state! (kinda CQRS)
|
||||
|
this.targetDimensions = this.GetTransformedDimensions(sourceRectangle.Size, this.TransformMatrix); |
||||
|
} |
||||
|
|
||||
|
// 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>(this.targetDimensions, x.MetaData.Clone())); |
||||
|
|
||||
|
// Use the overload to prevent an extra frame being added
|
||||
|
return new Image<TPixel>(source.GetConfiguration(), source.MetaData.Clone(), frames); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
protected override void OnApply( |
||||
|
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); |
||||
|
|
||||
|
if (this.Sampler is NearestNeighborResampler) |
||||
|
{ |
||||
|
Parallel.For( |
||||
|
0, |
||||
|
height, |
||||
|
configuration.ParallelOptions, |
||||
|
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); |
||||
|
|
||||
|
using (var yBuffer = new Buffer2D<float>(yLength, height)) |
||||
|
using (var xBuffer = new Buffer2D<float>(xLength, height)) |
||||
|
{ |
||||
|
Parallel.For( |
||||
|
0, |
||||
|
height, |
||||
|
configuration.ParallelOptions, |
||||
|
y => |
||||
|
{ |
||||
|
Span<TPixel> destRow = destination.GetPixelRowSpan(y); |
||||
|
Span<float> ySpan = yBuffer.GetRowSpan(y); |
||||
|
Span<float> xSpan = 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.
|
||||
|
// Precalulating 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, ySpan); |
||||
|
CalculateWeightsDown(left, right, minX, maxX, point.X, sampler, xScale, xSpan); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
CalculateWeightsScaleUp(minY, maxY, point.Y, sampler, ySpan); |
||||
|
CalculateWeightsScaleUp(minX, maxX, point.X, sampler, xSpan); |
||||
|
} |
||||
|
|
||||
|
// Now multiply the results against the offsets
|
||||
|
Vector4 sum = Vector4.Zero; |
||||
|
for (int yy = 0, j = minY; j <= maxY; j++, yy++) |
||||
|
{ |
||||
|
float yWeight = ySpan[yy]; |
||||
|
|
||||
|
for (int xx = 0, i = minX; i <= maxX; i++, xx++) |
||||
|
{ |
||||
|
float xWeight = xSpan[xx]; |
||||
|
var vector = source[i, j].ToVector4(); |
||||
|
|
||||
|
// Values are first premultiplied to prevent darkening of edge pixels
|
||||
|
var mupltiplied = new Vector4(new Vector3(vector.X, vector.Y, vector.Z) * vector.W, vector.W); |
||||
|
sum += mupltiplied * xWeight * yWeight; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
ref TPixel dest = ref destRow[x]; |
||||
|
|
||||
|
// Reverse the premultiplication
|
||||
|
dest.PackFromVector4(new Vector4(new Vector3(sum.X, sum.Y, sum.Z) / sum.W, sum.W)); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <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) |
||||
|
{ |
||||
|
return this.TransformMatrix; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the bounding <see cref="Rectangle"/> relative to the source for the given transformation matrix.
|
||||
|
/// </summary>
|
||||
|
/// <param name="sourceDimensions">The source rectangle.</param>
|
||||
|
/// <param name="matrix">The transformation matrix.</param>
|
||||
|
/// <returns>The <see cref="Rectangle"/></returns>
|
||||
|
protected virtual Size GetTransformedDimensions(Size sourceDimensions, Matrix3x2 matrix) |
||||
|
{ |
||||
|
return sourceDimensions; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,49 @@ |
|||||
|
// 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 |
||||
|
{ |
||||
|
/// <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> : AffineTransformProcessor<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>
|
||||
|
protected CenteredAffineTransformProcessor(Matrix3x2 matrix, IResampler sampler) |
||||
|
: base(matrix, sampler) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
protected override Matrix3x2 GetProcessingMatrix(Rectangle sourceRectangle, Rectangle destinationRectangle) |
||||
|
{ |
||||
|
var translationToTargetCenter = Matrix3x2.CreateTranslation(-destinationRectangle.Width * .5F, -destinationRectangle.Height * .5F); |
||||
|
var translateToSourceCenter = Matrix3x2.CreateTranslation(sourceRectangle.Width * .5F, sourceRectangle.Height * .5F); |
||||
|
return translationToTargetCenter * this.TransformMatrix * translateToSourceCenter; |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
protected override Size GetTransformedDimensions(Size sourceDimensions, Matrix3x2 matrix) |
||||
|
{ |
||||
|
var sourceRectangle = new Rectangle(0, 0, sourceDimensions.Width, sourceDimensions.Height); |
||||
|
|
||||
|
if (!Matrix3x2.Invert(this.TransformMatrix, out Matrix3x2 sizeMatrix)) |
||||
|
{ |
||||
|
// TODO: Shouldn't we throw an exception instead?
|
||||
|
return sourceDimensions; |
||||
|
} |
||||
|
|
||||
|
return TransformHelpers.GetTransformedBoundingRectangle(sourceRectangle, sizeMatrix).Size; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,43 @@ |
|||||
|
// 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 |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// A base class that provides methods to allow the automatic centering of non-affine transforms
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
||||
|
internal abstract class CenteredProjectiveTransformProcessor<TPixel> : ProjectiveTransformProcessor<TPixel> |
||||
|
where TPixel : struct, IPixel<TPixel> |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Initializes a new instance of the <see cref="CenteredProjectiveTransformProcessor{TPixel}"/> class.
|
||||
|
/// </summary>
|
||||
|
/// <param name="matrix">The transform matrix</param>
|
||||
|
/// <param name="sampler">The sampler to perform the transform operation.</param>
|
||||
|
protected CenteredProjectiveTransformProcessor(Matrix4x4 matrix, IResampler sampler) |
||||
|
: base(matrix, sampler) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
protected override Matrix4x4 GetProcessingMatrix(Rectangle sourceRectangle, Rectangle destinationRectangle) |
||||
|
{ |
||||
|
var translationToTargetCenter = Matrix4x4.CreateTranslation(-destinationRectangle.Width * .5F, -destinationRectangle.Height * .5F, 0); |
||||
|
var translateToSourceCenter = Matrix4x4.CreateTranslation(sourceRectangle.Width * .5F, sourceRectangle.Height * .5F, 0); |
||||
|
return translationToTargetCenter * this.TransformMatrix * translateToSourceCenter; |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
protected override Rectangle GetTransformedBoundingRectangle(Rectangle sourceRectangle, Matrix4x4 matrix) |
||||
|
{ |
||||
|
return Matrix4x4.Invert(this.TransformMatrix, out Matrix4x4 sizeMatrix) |
||||
|
? TransformHelpers.GetTransformedBoundingRectangle(sourceRectangle, sizeMatrix) |
||||
|
: sourceRectangle; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,139 @@ |
|||||
|
// Copyright (c) Six Labors and contributors.
|
||||
|
// Licensed under the Apache License, Version 2.0.
|
||||
|
|
||||
|
using System; |
||||
|
using System.Runtime.CompilerServices; |
||||
|
using SixLabors.ImageSharp.MetaData.Profiles.Exif; |
||||
|
using SixLabors.ImageSharp.PixelFormats; |
||||
|
using SixLabors.Primitives; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Processing.Processors |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// The base class for performing interpolated affine and non-affine transforms.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
||||
|
internal abstract class InterpolatedTransformProcessorBase<TPixel> : CloningImageProcessor<TPixel> |
||||
|
where TPixel : struct, IPixel<TPixel> |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Initializes a new instance of the <see cref="InterpolatedTransformProcessorBase{TPixel}"/> class.
|
||||
|
/// </summary>
|
||||
|
/// <param name="sampler">The sampler to perform the transform operation.</param>
|
||||
|
protected InterpolatedTransformProcessorBase(IResampler sampler) |
||||
|
{ |
||||
|
this.Sampler = sampler; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the sampler to perform interpolation of the transform operation.
|
||||
|
/// </summary>
|
||||
|
public IResampler Sampler { get; } |
||||
|
|
||||
|
/// <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)] |
||||
|
protected static void CalculateWeightsDown(int min, int max, int sourceMin, int sourceMax, float point, IResampler sampler, float scale, Span<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 = min; i <= max; i++, x++) |
||||
|
{ |
||||
|
int index = i; |
||||
|
if (index < sourceMin) |
||||
|
{ |
||||
|
index = sourceMin; |
||||
|
} |
||||
|
|
||||
|
if (index > sourceMax) |
||||
|
{ |
||||
|
index = sourceMax; |
||||
|
} |
||||
|
|
||||
|
float weight = sampler.GetValue((index - point) / scale); |
||||
|
sum += weight; |
||||
|
Unsafe.Add(ref weightsBaseRef, x) = weight; |
||||
|
} |
||||
|
|
||||
|
if (sum > 0) |
||||
|
{ |
||||
|
for (int i = 0; i < weights.Length; i++) |
||||
|
{ |
||||
|
ref float wRef = ref Unsafe.Add(ref weightsBaseRef, i); |
||||
|
wRef = wRef / sum; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Calculated the weights for the given point.
|
||||
|
/// </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)] |
||||
|
protected static void CalculateWeightsScaleUp(int sourceMin, int sourceMax, float point, IResampler sampler, Span<float> weights) |
||||
|
{ |
||||
|
ref float weightsBaseRef = ref weights[0]; |
||||
|
for (int x = 0, i = sourceMin; i <= sourceMax; i++, x++) |
||||
|
{ |
||||
|
float weight = sampler.GetValue(i - point); |
||||
|
Unsafe.Add(ref weightsBaseRef, x) = weight; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Calculates the sampling radius for the current sampler
|
||||
|
/// </summary>
|
||||
|
/// <param name="sourceSize">The source dimension size</param>
|
||||
|
/// <param name="destinationSize">The destination dimension size</param>
|
||||
|
/// <returns>The radius, and scaling factor</returns>
|
||||
|
protected (float radius, float scale, float ratio) GetSamplingRadius(int sourceSize, int destinationSize) |
||||
|
{ |
||||
|
float ratio = (float)sourceSize / destinationSize; |
||||
|
float scale = ratio; |
||||
|
|
||||
|
if (scale < 1F) |
||||
|
{ |
||||
|
scale = 1F; |
||||
|
} |
||||
|
|
||||
|
return (MathF.Ceiling(scale * this.Sampler.Radius), scale, ratio); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
protected override void AfterImageApply(Image<TPixel> source, Image<TPixel> destination, Rectangle sourceRectangle) |
||||
|
{ |
||||
|
ExifProfile profile = destination.MetaData.ExifProfile; |
||||
|
if (profile == null) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
if (profile.GetValue(ExifTag.PixelXDimension) != null) |
||||
|
{ |
||||
|
profile.SetValue(ExifTag.PixelXDimension, destination.Width); |
||||
|
} |
||||
|
|
||||
|
if (profile.GetValue(ExifTag.PixelYDimension) != null) |
||||
|
{ |
||||
|
profile.SetValue(ExifTag.PixelYDimension, destination.Height); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,50 +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 |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// Provides methods to transform an image using a <see cref="Matrix3x2"/>.
|
|
||||
/// </summary>
|
|
||||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
|
||||
internal abstract class Matrix3x2Processor<TPixel> : ImageProcessor<TPixel> |
|
||||
where TPixel : struct, IPixel<TPixel> |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// Gets the rectangle designating the target canvas.
|
|
||||
/// </summary>
|
|
||||
protected Rectangle CanvasRectangle { get; private set; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Creates a new target canvas to contain the results of the matrix transform.
|
|
||||
/// </summary>
|
|
||||
/// <param name="sourceRectangle">The source rectangle.</param>
|
|
||||
/// <param name="processMatrix">The processing matrix.</param>
|
|
||||
protected void CreateNewCanvas(Rectangle sourceRectangle, Matrix3x2 processMatrix) |
|
||||
{ |
|
||||
Matrix3x2 sizeMatrix; |
|
||||
this.CanvasRectangle = Matrix3x2.Invert(processMatrix, out sizeMatrix) |
|
||||
? ImageMaths.GetBoundingRectangle(sourceRectangle, sizeMatrix) |
|
||||
: sourceRectangle; |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets a transform matrix adjusted to center upon the target image bounds.
|
|
||||
/// </summary>
|
|
||||
/// <param name="source">The source image.</param>
|
|
||||
/// <param name="matrix">The transform matrix.</param>
|
|
||||
/// <returns>
|
|
||||
/// The <see cref="Matrix3x2"/>.
|
|
||||
/// </returns>
|
|
||||
protected Matrix3x2 GetCenteredMatrix(ImageFrame<TPixel> source, Matrix3x2 matrix) |
|
||||
{ |
|
||||
var translationToTargetCenter = Matrix3x2.CreateTranslation(-this.CanvasRectangle.Width * .5F, -this.CanvasRectangle.Height * .5F); |
|
||||
var translateToSourceCenter = Matrix3x2.CreateTranslation(source.Width * .5F, source.Height * .5F); |
|
||||
return (translationToTargetCenter * matrix) * translateToSourceCenter; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,240 @@ |
|||||
|
// 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.Threading.Tasks; |
||||
|
using SixLabors.ImageSharp.Advanced; |
||||
|
using SixLabors.ImageSharp.Helpers; |
||||
|
using SixLabors.ImageSharp.Memory; |
||||
|
using SixLabors.ImageSharp.PixelFormats; |
||||
|
using SixLabors.Primitives; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Processing.Processors |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Provides the base methods to perform non-affine transforms on an image.
|
||||
|
/// TODO: Doesn't work yet! Implement tests + Finish implementation + Document Matrix4x4 behavior
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
||||
|
internal class ProjectiveTransformProcessor<TPixel> : InterpolatedTransformProcessorBase<TPixel> |
||||
|
where TPixel : struct, IPixel<TPixel> |
||||
|
{ |
||||
|
// TODO: We should use a Size instead! (See AffineTransformProcessor<T>)
|
||||
|
private Rectangle targetRectangle; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Initializes a new instance of the <see cref="ProjectiveTransformProcessor{TPixel}"/> class.
|
||||
|
/// </summary>
|
||||
|
/// <param name="matrix">The transform matrix</param>
|
||||
|
public ProjectiveTransformProcessor(Matrix4x4 matrix) |
||||
|
: this(matrix, KnownResamplers.Bicubic) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Initializes a new instance of the <see cref="ProjectiveTransformProcessor{TPixel}"/> class.
|
||||
|
/// </summary>
|
||||
|
/// <param name="matrix">The transform matrix</param>
|
||||
|
/// <param name="sampler">The sampler to perform the transform operation.</param>
|
||||
|
public ProjectiveTransformProcessor(Matrix4x4 matrix, IResampler sampler) |
||||
|
: this(matrix, sampler, Rectangle.Empty) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Initializes a new instance of the <see cref="ProjectiveTransformProcessor{TPixel}"/> class.
|
||||
|
/// </summary>
|
||||
|
/// <param name="matrix">The transform matrix</param>
|
||||
|
/// <param name="sampler">The sampler to perform the transform operation.</param>
|
||||
|
/// <param name="rectangle">The rectangle to constrain the transformed image to.</param>
|
||||
|
public ProjectiveTransformProcessor(Matrix4x4 matrix, IResampler sampler, Rectangle rectangle) |
||||
|
: base(sampler) |
||||
|
{ |
||||
|
// Tansforms are inverted else the output is the opposite of the expected.
|
||||
|
Matrix4x4.Invert(matrix, out matrix); |
||||
|
this.TransformMatrix = matrix; |
||||
|
this.targetRectangle = rectangle; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the matrix used to supply the non-affine transform
|
||||
|
/// </summary>
|
||||
|
public Matrix4x4 TransformMatrix { get; } |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
protected override Image<TPixel> CreateDestination(Image<TPixel> source, Rectangle sourceRectangle) |
||||
|
{ |
||||
|
if (this.targetRectangle == Rectangle.Empty) |
||||
|
{ |
||||
|
this.targetRectangle = this.GetTransformedBoundingRectangle(sourceRectangle, this.TransformMatrix); |
||||
|
} |
||||
|
|
||||
|
// 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>(this.targetRectangle.Width, this.targetRectangle.Height, x.MetaData.Clone())); |
||||
|
|
||||
|
// Use the overload to prevent an extra frame being added
|
||||
|
return new Image<TPixel>(source.GetConfiguration(), source.MetaData.Clone(), frames); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
protected override void OnApply(ImageFrame<TPixel> source, ImageFrame<TPixel> destination, Rectangle sourceRectangle, Configuration configuration) |
||||
|
{ |
||||
|
int height = this.targetRectangle.Height; |
||||
|
int width = this.targetRectangle.Width; |
||||
|
Rectangle sourceBounds = source.Bounds(); |
||||
|
|
||||
|
// Since could potentially be resizing the canvas we might need to re-calculate the matrix
|
||||
|
Matrix4x4 matrix = this.GetProcessingMatrix(sourceBounds, this.targetRectangle); |
||||
|
|
||||
|
if (this.Sampler is NearestNeighborResampler) |
||||
|
{ |
||||
|
Parallel.For( |
||||
|
0, |
||||
|
height, |
||||
|
configuration.ParallelOptions, |
||||
|
y => |
||||
|
{ |
||||
|
Span<TPixel> destRow = destination.GetPixelRowSpan(y); |
||||
|
|
||||
|
for (int x = 0; x < width; x++) |
||||
|
{ |
||||
|
var point = Point.Round(Vector2.Transform(new Vector2(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); |
||||
|
|
||||
|
using (var yBuffer = new Buffer2D<float>(yLength, height)) |
||||
|
using (var xBuffer = new Buffer2D<float>(xLength, height)) |
||||
|
{ |
||||
|
Parallel.For( |
||||
|
0, |
||||
|
height, |
||||
|
configuration.ParallelOptions, |
||||
|
y => |
||||
|
{ |
||||
|
Span<TPixel> destRow = destination.GetPixelRowSpan(y); |
||||
|
Span<float> ySpan = yBuffer.GetRowSpan(y); |
||||
|
Span<float> xSpan = 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.
|
||||
|
// Precalulating 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, ySpan); |
||||
|
CalculateWeightsDown(left, right, minX, maxX, point.X, sampler, xScale, xSpan); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
CalculateWeightsScaleUp(minY, maxY, point.Y, sampler, ySpan); |
||||
|
CalculateWeightsScaleUp(minX, maxX, point.X, sampler, xSpan); |
||||
|
} |
||||
|
|
||||
|
// Now multiply the results against the offsets
|
||||
|
Vector4 sum = Vector4.Zero; |
||||
|
for (int yy = 0, j = minY; j <= maxY; j++, yy++) |
||||
|
{ |
||||
|
float yWeight = ySpan[yy]; |
||||
|
|
||||
|
for (int xx = 0, i = minX; i <= maxX; i++, xx++) |
||||
|
{ |
||||
|
float xWeight = xSpan[xx]; |
||||
|
var vector = source[i, j].ToVector4(); |
||||
|
|
||||
|
// Values are first premultiplied to prevent darkening of edge pixels
|
||||
|
var mupltiplied = new Vector4(new Vector3(vector.X, vector.Y, vector.Z) * vector.W, vector.W); |
||||
|
sum += mupltiplied * xWeight * yWeight; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
ref TPixel dest = ref destRow[x]; |
||||
|
|
||||
|
// Reverse the premultiplication
|
||||
|
dest.PackFromVector4(new Vector4(new Vector3(sum.X, sum.Y, sum.Z) / sum.W, sum.W)); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <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="Matrix4x4"/>.
|
||||
|
/// </returns>
|
||||
|
protected virtual Matrix4x4 GetProcessingMatrix(Rectangle sourceRectangle, Rectangle destinationRectangle) |
||||
|
{ |
||||
|
return this.TransformMatrix; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the bounding <see cref="Rectangle"/> relative to the source for the given transformation matrix.
|
||||
|
/// </summary>
|
||||
|
/// <param name="sourceRectangle">The source rectangle.</param>
|
||||
|
/// <param name="matrix">The transformation matrix.</param>
|
||||
|
/// <returns>The <see cref="Rectangle"/></returns>
|
||||
|
protected virtual Rectangle GetTransformedBoundingRectangle(Rectangle sourceRectangle, Matrix4x4 matrix) |
||||
|
{ |
||||
|
return sourceRectangle; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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 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 contiguous 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); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,96 @@ |
|||||
|
// Copyright (c) Six Labors and contributors.
|
||||
|
// Licensed under the Apache License, Version 2.0.
|
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Processing |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Contains reusable static instances of known resampling algorithms
|
||||
|
/// </summary>
|
||||
|
public static class KnownResamplers |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Gets the Bicubic sampler that implements the bicubic kernel algorithm W(x)
|
||||
|
/// </summary>
|
||||
|
public static IResampler Bicubic { get; } = new BicubicResampler(); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the Box sampler that implements the box algorithm. Similar to nearest neighbor when upscaling.
|
||||
|
/// When downscaling the pixels will average, merging pixels together.
|
||||
|
/// </summary>
|
||||
|
public static IResampler Box { get; } = new BoxResampler(); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the Catmull-Rom sampler, a well known standard Cubic Filter often used as a interpolation function
|
||||
|
/// </summary>
|
||||
|
public static IResampler CatmullRom { get; } = new CatmullRomResampler(); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the Hermite sampler. A type of smoothed triangular interpolation filter that rounds off strong edges while
|
||||
|
/// preserving flat 'color levels' in the original image.
|
||||
|
/// </summary>
|
||||
|
public static IResampler Hermite { get; } = new HermiteResampler(); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the Lanczos kernel sampler that implements smooth interpolation with a radius of 2 pixels.
|
||||
|
/// This algorithm provides sharpened results when compared to others when downsampling.
|
||||
|
/// </summary>
|
||||
|
public static IResampler Lanczos2 { get; } = new Lanczos2Resampler(); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the Lanczos kernel sampler that implements smooth interpolation with a radius of 3 pixels
|
||||
|
/// This algorithm provides sharpened results when compared to others when downsampling.
|
||||
|
/// </summary>
|
||||
|
public static IResampler Lanczos3 { get; } = new Lanczos3Resampler(); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the Lanczos kernel sampler that implements smooth interpolation with a radius of 5 pixels
|
||||
|
/// This algorithm provides sharpened results when compared to others when downsampling.
|
||||
|
/// </summary>
|
||||
|
public static IResampler Lanczos5 { get; } = new Lanczos5Resampler(); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the Lanczos kernel sampler that implements smooth interpolation with a radius of 8 pixels
|
||||
|
/// This algorithm provides sharpened results when compared to others when downsampling.
|
||||
|
/// </summary>
|
||||
|
public static IResampler Lanczos8 { get; } = new Lanczos8Resampler(); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the Mitchell-Netravali sampler. This seperable cubic algorithm yields a very good equilibrium between
|
||||
|
/// detail preservation (sharpness) and smoothness.
|
||||
|
/// </summary>
|
||||
|
public static IResampler MitchellNetravali { get; } = new MitchellNetravaliResampler(); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the Nearest-Neighbour sampler that implements the nearest neighbor algorithm. This uses a very fast, unscaled filter
|
||||
|
/// which will select the closest pixel to the new pixels position.
|
||||
|
/// </summary>
|
||||
|
public static IResampler NearestNeighbor { get; } = new NearestNeighborResampler(); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the Robidoux sampler. This algorithm developed by Nicolas Robidoux providing a very good equilibrium between
|
||||
|
/// detail preservation (sharpness) and smoothness comprable to <see cref="MitchellNetravali"/>.
|
||||
|
/// </summary>
|
||||
|
public static IResampler Robidoux { get; } = new RobidouxResampler(); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the Robidoux Sharp sampler. A sharpend form of the <see cref="Robidoux"/> sampler
|
||||
|
/// </summary>
|
||||
|
public static IResampler RobidouxSharp { get; } = new RobidouxSharpResampler(); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the Spline sampler. A seperable cubic algorithm similar to <see cref="MitchellNetravali"/> but yielding smoother results.
|
||||
|
/// </summary>
|
||||
|
public static IResampler Spline { get; } = new SplineResampler(); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the Triangle sampler, otherwise known as Bilinear. This interpolation algorithm can be used where perfect image transformation
|
||||
|
/// with pixel matching is impossible, so that one can calculate and assign appropriate intensity values to pixels
|
||||
|
/// </summary>
|
||||
|
public static IResampler Triangle { get; } = new TriangleResampler(); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the Welch sampler. A high speed algorthm that delivers very sharpened results.
|
||||
|
/// </summary>
|
||||
|
public static IResampler Welch { get; } = new WelchResampler(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,122 @@ |
|||||
|
// 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; |
||||
|
using SixLabors.ImageSharp.Processing.Processors; |
||||
|
using SixLabors.Primitives; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Extension methods for the <see cref="Image{TPixel}"/> type.
|
||||
|
/// </summary>
|
||||
|
public static partial class ImageExtensions |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Transforms an image by the given matrix.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
||||
|
/// <param name="source">The image to transform.</param>
|
||||
|
/// <param name="matrix">The transformation matrix.</param>
|
||||
|
/// <returns>The <see cref="Image{TPixel}"/></returns>
|
||||
|
public static IImageProcessingContext<TPixel> Transform<TPixel>(this IImageProcessingContext<TPixel> source, Matrix3x2 matrix) |
||||
|
where TPixel : struct, IPixel<TPixel> |
||||
|
=> Transform(source, matrix, KnownResamplers.NearestNeighbor); |
||||
|
|
||||
|
/// <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> |
||||
|
=> Transform(source, matrix, sampler, Size.Empty); |
||||
|
|
||||
|
/// <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 AffineTransformProcessor<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="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) |
||||
|
where TPixel : struct, IPixel<TPixel> |
||||
|
{ |
||||
|
return source.ApplyProcessor(new AffineTransformProcessor<TPixel>(matrix, sampler, destinationSize)); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Transforms an image by the given matrix.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
||||
|
/// <param name="source">The image to transform.</param>
|
||||
|
/// <param name="matrix">The transformation matrix.</param>
|
||||
|
/// <returns>The <see cref="Image{TPixel}"/></returns>
|
||||
|
internal static IImageProcessingContext<TPixel> Transform<TPixel>(this IImageProcessingContext<TPixel> source, Matrix4x4 matrix) |
||||
|
where TPixel : struct, IPixel<TPixel> |
||||
|
=> Transform(source, matrix, KnownResamplers.NearestNeighbor); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Applies a projective transform to the image by the given matrix using the specified sampling algorithm.
|
||||
|
/// TODO: Doesn't work yet! Implement tests + Finish implementation + Document Matrix4x4 behavior
|
||||
|
/// </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>
|
||||
|
internal static IImageProcessingContext<TPixel> Transform<TPixel>(this IImageProcessingContext<TPixel> source, Matrix4x4 matrix, IResampler sampler) |
||||
|
where TPixel : struct, IPixel<TPixel> |
||||
|
=> Transform(source, matrix, sampler, Rectangle.Empty); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Applies a projective transform to the image by the given matrix using the specified sampling algorithm.
|
||||
|
/// TODO: Doesn't work yet! Implement tests + Finish implementation + Document Matrix4x4 behavior
|
||||
|
/// </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 to constrain the transformed image to.</param>
|
||||
|
/// <returns>The <see cref="Image{TPixel}"/></returns>
|
||||
|
internal static IImageProcessingContext<TPixel> Transform<TPixel>(this IImageProcessingContext<TPixel> source, Matrix4x4 matrix, IResampler sampler, Rectangle rectangle) |
||||
|
where TPixel : struct, IPixel<TPixel> |
||||
|
=> source.ApplyProcessor(new ProjectiveTransformProcessor<TPixel>(matrix, sampler, rectangle)); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,68 @@ |
|||||
|
// Copyright (c) Six Labors and contributors.
|
||||
|
// Licensed under the Apache License, Version 2.0.
|
||||
|
|
||||
|
using System; |
||||
|
using System.Numerics; |
||||
|
using SixLabors.Primitives; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Contains helper methods for working with affine and non-affine transforms
|
||||
|
/// </summary>
|
||||
|
internal class TransformHelpers |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Returns the bounding <see cref="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 <see cref="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, Matrix4x4 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); |
||||
|
} |
||||
|
|
||||
|
private static Rectangle GetBoundingRectangle(Vector2 tl, Vector2 tr, Vector2 bl, Vector2 br) |
||||
|
{ |
||||
|
// Find the minimum and maximum "corners" based on the given vectors
|
||||
|
float minX = MathF.Min(tl.X, MathF.Min(tr.X, MathF.Min(bl.X, br.X))); |
||||
|
float maxX = MathF.Max(tl.X, MathF.Max(tr.X, MathF.Max(bl.X, br.X))); |
||||
|
float minY = MathF.Min(tl.Y, MathF.Min(tr.Y, MathF.Min(bl.Y, br.Y))); |
||||
|
float maxY = MathF.Max(tl.Y, MathF.Max(tr.Y, MathF.Max(bl.Y, br.Y))); |
||||
|
float sizeX = maxX - minX + .5F; |
||||
|
float sizeY = maxY - minY + .5F; |
||||
|
|
||||
|
return new Rectangle((int)(MathF.Ceiling(minX) - .5F), (int)(MathF.Ceiling(minY) - .5F), (int)MathF.Floor(sizeX), (int)MathF.Floor(sizeY)); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,258 @@ |
|||||
|
using System; |
||||
|
using System.Numerics; |
||||
|
using System.Reflection; |
||||
|
using SixLabors.ImageSharp.PixelFormats; |
||||
|
using SixLabors.ImageSharp.Processing; |
||||
|
using SixLabors.Primitives; |
||||
|
using Xunit; |
||||
|
using Xunit.Abstractions; |
||||
|
using SixLabors.ImageSharp.Helpers; |
||||
|
// ReSharper disable InconsistentNaming
|
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Tests.Processing.Transforms |
||||
|
{ |
||||
|
public class AffineTransformTests |
||||
|
{ |
||||
|
private readonly ITestOutputHelper Output; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// angleDeg, sx, sy, tx, ty
|
||||
|
/// </summary>
|
||||
|
public static readonly TheoryData<float, float, float, float, float> TransformValues |
||||
|
= new TheoryData<float, float, float, float, float> |
||||
|
{ |
||||
|
{ 0, 1, 1, 0, 0 }, |
||||
|
{ 50, 1, 1, 0, 0 }, |
||||
|
{ 0, 1, 1, 20, 10 }, |
||||
|
{ 50, 1, 1, 20, 10 }, |
||||
|
{ 0, 1, 1, -20, -10 }, |
||||
|
{ 50, 1, 1, -20, -10 }, |
||||
|
{ 50, 1.5f, 1.5f, 0, 0 }, |
||||
|
{ 50, 1.1F, 1.3F, 30, -20 }, |
||||
|
{ 0, 2f, 1f, 0, 0 }, |
||||
|
{ 0, 1f, 2f, 0, 0 }, |
||||
|
}; |
||||
|
|
||||
|
public static readonly TheoryData<string> ResamplerNames = |
||||
|
new TheoryData<string> |
||||
|
{ |
||||
|
nameof(KnownResamplers.Bicubic), |
||||
|
nameof(KnownResamplers.Box), |
||||
|
nameof(KnownResamplers.CatmullRom), |
||||
|
nameof(KnownResamplers.Hermite), |
||||
|
nameof(KnownResamplers.Lanczos2), |
||||
|
nameof(KnownResamplers.Lanczos3), |
||||
|
nameof(KnownResamplers.Lanczos5), |
||||
|
nameof(KnownResamplers.Lanczos8), |
||||
|
nameof(KnownResamplers.MitchellNetravali), |
||||
|
nameof(KnownResamplers.NearestNeighbor), |
||||
|
nameof(KnownResamplers.Robidoux), |
||||
|
nameof(KnownResamplers.RobidouxSharp), |
||||
|
nameof(KnownResamplers.Spline), |
||||
|
nameof(KnownResamplers.Triangle), |
||||
|
nameof(KnownResamplers.Welch), |
||||
|
}; |
||||
|
|
||||
|
public static readonly TheoryData<string> Transform_DoesNotCreateEdgeArtifacts_ResamplerNames = |
||||
|
new TheoryData<string> |
||||
|
{ |
||||
|
nameof(KnownResamplers.NearestNeighbor), |
||||
|
nameof(KnownResamplers.Triangle), |
||||
|
nameof(KnownResamplers.Bicubic), |
||||
|
nameof(KnownResamplers.Lanczos8), |
||||
|
}; |
||||
|
|
||||
|
public AffineTransformTests(ITestOutputHelper output) |
||||
|
{ |
||||
|
this.Output = output; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The output of an "all white" image should be "all white" or transparent, regardless of the transformation and the resampler.
|
||||
|
/// </summary>
|
||||
|
[Theory] |
||||
|
[WithSolidFilledImages(nameof(Transform_DoesNotCreateEdgeArtifacts_ResamplerNames), 5, 5, 255, 255, 255, 255, PixelTypes.Rgba32)] |
||||
|
public void Transform_DoesNotCreateEdgeArtifacts<TPixel>(TestImageProvider<TPixel> provider, string resamplerName) |
||||
|
where TPixel : struct, IPixel<TPixel> |
||||
|
{ |
||||
|
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); |
||||
|
|
||||
|
Rectangle sourceRectangle = image.Bounds(); |
||||
|
Matrix3x2 matrix = rotate * translate; |
||||
|
|
||||
|
Rectangle destRectangle = TransformHelpers.GetTransformedBoundingRectangle(sourceRectangle, matrix); |
||||
|
|
||||
|
image.Mutate(c => c.Transform(matrix, resampler, destRectangle)); |
||||
|
image.DebugSave(provider, resamplerName); |
||||
|
|
||||
|
VerifyAllPixelsAreWhiteOrTransparent(image); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[WithTestPatternImages(nameof(TransformValues), 100, 50, PixelTypes.Rgba32)] |
||||
|
public void Transform_RotateScaleTranslate<TPixel>( |
||||
|
TestImageProvider<TPixel> provider, |
||||
|
float angleDeg, |
||||
|
float sx, float sy, |
||||
|
float tx, float ty) |
||||
|
where TPixel : struct, IPixel<TPixel> |
||||
|
{ |
||||
|
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; |
||||
|
|
||||
|
this.PrintMatrix(m); |
||||
|
|
||||
|
image.Mutate(i => i.Transform(m, KnownResamplers.Bicubic)); |
||||
|
|
||||
|
string testOutputDetails = $"R({angleDeg})_S({sx},{sy})_T({tx},{ty})"; |
||||
|
image.DebugSave(provider, testOutputDetails); |
||||
|
image.CompareToReferenceOutput(provider, testOutputDetails); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[WithTestPatternImages(96, 96, PixelTypes.Rgba32, 50, 0.8f)] |
||||
|
public void Transform_RotateScale_ManuallyCentered<TPixel>(TestImageProvider<TPixel> provider, float angleDeg, float s) |
||||
|
where TPixel : struct, IPixel<TPixel> |
||||
|
{ |
||||
|
using (Image<TPixel> image = provider.GetImage()) |
||||
|
{ |
||||
|
Matrix3x2 m = this.MakeManuallyCenteredMatrix(angleDeg, s, image); |
||||
|
|
||||
|
image.Mutate(i => i.Transform(m, KnownResamplers.Bicubic)); |
||||
|
|
||||
|
string testOutputDetails = $"R({angleDeg})_S({s})"; |
||||
|
image.DebugSave(provider, testOutputDetails); |
||||
|
image.CompareToReferenceOutput(provider, testOutputDetails); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public static readonly TheoryData<int, int, int, int> Transform_IntoRectangle_Data = |
||||
|
new TheoryData<int, int, int, int> |
||||
|
{ |
||||
|
{ 0, 0, 10, 10 }, |
||||
|
{ 0, 0, 5, 10 }, |
||||
|
{ 0, 0, 10, 5 }, |
||||
|
{ 5, 0, 5, 10 }, |
||||
|
{-5,-5, 20, 20 } |
||||
|
}; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Testing transforms using custom source rectangles:
|
||||
|
/// https://github.com/SixLabors/ImageSharp/pull/386#issuecomment-357104963
|
||||
|
/// </summary>
|
||||
|
[Theory] |
||||
|
[WithTestPatternImages(96, 48, PixelTypes.Rgba32)] |
||||
|
public void Transform_FromSourceRectangle1<TPixel>(TestImageProvider<TPixel> provider) |
||||
|
where TPixel : struct, IPixel<TPixel> |
||||
|
{ |
||||
|
var rectangle = new Rectangle(48, 0, 96, 36); |
||||
|
|
||||
|
using (Image<TPixel> image = provider.GetImage()) |
||||
|
{ |
||||
|
var m = Matrix3x2.CreateScale(2.0F, 1.5F); |
||||
|
|
||||
|
image.Mutate(i => i.Transform(m, KnownResamplers.Spline, rectangle)); |
||||
|
|
||||
|
image.DebugSave(provider); |
||||
|
image.CompareToReferenceOutput(provider); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[WithTestPatternImages(96, 48, PixelTypes.Rgba32)] |
||||
|
public void Transform_FromSourceRectangle2<TPixel>(TestImageProvider<TPixel> provider) |
||||
|
where TPixel : struct, IPixel<TPixel> |
||||
|
{ |
||||
|
var rectangle = new Rectangle(0, 24, 48, 48); |
||||
|
|
||||
|
using (Image<TPixel> image = provider.GetImage()) |
||||
|
{ |
||||
|
var m = Matrix3x2.CreateScale(1.0F, 2.0F); |
||||
|
|
||||
|
image.Mutate(i => i.Transform(m, KnownResamplers.Spline, rectangle)); |
||||
|
|
||||
|
image.DebugSave(provider); |
||||
|
image.CompareToReferenceOutput(provider); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[WithTestPatternImages(nameof(ResamplerNames), 150, 150, PixelTypes.Rgba32)] |
||||
|
public void Transform_WithSampler<TPixel>(TestImageProvider<TPixel> provider, string resamplerName) |
||||
|
where TPixel : struct, IPixel<TPixel> |
||||
|
{ |
||||
|
IResampler sampler = GetResampler(resamplerName); |
||||
|
using (Image<TPixel> image = provider.GetImage()) |
||||
|
{ |
||||
|
Matrix3x2 m = this.MakeManuallyCenteredMatrix(50, 0.6f, image); |
||||
|
|
||||
|
image.Mutate(i => |
||||
|
{ |
||||
|
i.Transform(m, sampler); |
||||
|
}); |
||||
|
|
||||
|
image.DebugSave(provider, resamplerName); |
||||
|
image.CompareToReferenceOutput(provider, resamplerName); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private Matrix3x2 MakeManuallyCenteredMatrix<TPixel>(float angleDeg, float s, Image<TPixel> image) |
||||
|
where TPixel : struct, IPixel<TPixel> |
||||
|
{ |
||||
|
Matrix3x2 rotate = Matrix3x2Extensions.CreateRotationDegrees(angleDeg); |
||||
|
Vector2 toCenter = 0.5f * new Vector2(image.Width, image.Height); |
||||
|
var translate = Matrix3x2.CreateTranslation(-toCenter); |
||||
|
var translateBack = Matrix3x2.CreateTranslation(toCenter); |
||||
|
var scale = Matrix3x2.CreateScale(s); |
||||
|
|
||||
|
Matrix3x2 m = translate * rotate * scale * translateBack; |
||||
|
|
||||
|
this.PrintMatrix(m); |
||||
|
return m; |
||||
|
} |
||||
|
|
||||
|
private static IResampler GetResampler(string name) |
||||
|
{ |
||||
|
PropertyInfo property = typeof(KnownResamplers).GetTypeInfo().GetProperty(name); |
||||
|
|
||||
|
if (property == null) |
||||
|
{ |
||||
|
throw new Exception("Invalid property name!"); |
||||
|
} |
||||
|
|
||||
|
return (IResampler)property.GetValue(null); |
||||
|
} |
||||
|
|
||||
|
private static void VerifyAllPixelsAreWhiteOrTransparent<TPixel>(Image<TPixel> image) |
||||
|
where TPixel : struct, IPixel<TPixel> |
||||
|
{ |
||||
|
TPixel[] data = new TPixel[image.Width * image.Height]; |
||||
|
image.Frames.RootFrame.SavePixelData(data); |
||||
|
var rgba = default(Rgba32); |
||||
|
var white = new Rgb24(255, 255, 255); |
||||
|
foreach (TPixel pixel in data) |
||||
|
{ |
||||
|
pixel.ToRgba32(ref rgba); |
||||
|
if (rgba.A == 0) continue; |
||||
|
|
||||
|
Assert.Equal(white, rgba.Rgb); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void PrintMatrix(Matrix3x2 a) |
||||
|
{ |
||||
|
string s = $"{a.M11:F10},{a.M12:F10},{a.M21:F10},{a.M22:F10},{a.M31:F10},{a.M32:F10}"; |
||||
|
this.Output.WriteLine(s); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue