Browse Source

Merge pull request #1118 from SixLabors/js/faster-transforms

Faster Transforms
af/octree-no-pixelmap
James Jackson-South 6 years ago
committed by GitHub
parent
commit
45a2e24a6e
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 29
      src/ImageSharp/Common/Helpers/Guard.cs
  2. 34
      src/ImageSharp/Common/Helpers/ImageMaths.cs
  3. 12
      src/ImageSharp/Processing/AffineTransformBuilder.cs
  4. 4
      src/ImageSharp/Processing/Extensions/Transforms/TransformExtensions.cs
  5. 36
      src/ImageSharp/Processing/KnownResamplers.cs
  6. 18
      src/ImageSharp/Processing/Processors/CloningImageProcessor{TPixel}.cs
  7. 188
      src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor{TPixel}.cs
  8. 2
      src/ImageSharp/Processing/Processors/Transforms/CropProcessor{TPixel}.cs
  9. 14
      src/ImageSharp/Processing/Processors/Transforms/IResampler.cs
  10. 23
      src/ImageSharp/Processing/Processors/Transforms/IResamplingTransformImageProcessor{TPixel}.cs
  11. 8
      src/ImageSharp/Processing/Processors/Transforms/Linear/AffineTransformProcessor.cs
  12. 234
      src/ImageSharp/Processing/Processors/Transforms/Linear/AffineTransformProcessor{TPixel}.cs
  13. 0
      src/ImageSharp/Processing/Processors/Transforms/Linear/AutoOrientProcessor.cs
  14. 0
      src/ImageSharp/Processing/Processors/Transforms/Linear/AutoOrientProcessor{TPixel}.cs
  15. 0
      src/ImageSharp/Processing/Processors/Transforms/Linear/FlipProcessor.cs
  16. 0
      src/ImageSharp/Processing/Processors/Transforms/Linear/FlipProcessor{TPixel}.cs
  17. 104
      src/ImageSharp/Processing/Processors/Transforms/Linear/LinearTransformUtilities.cs
  18. 8
      src/ImageSharp/Processing/Processors/Transforms/Linear/ProjectiveTransformProcessor.cs
  19. 158
      src/ImageSharp/Processing/Processors/Transforms/Linear/ProjectiveTransformProcessor{TPixel}.cs
  20. 4
      src/ImageSharp/Processing/Processors/Transforms/Linear/RotateProcessor.cs
  21. 0
      src/ImageSharp/Processing/Processors/Transforms/Linear/RotateProcessor{TPixel}.cs
  22. 4
      src/ImageSharp/Processing/Processors/Transforms/Linear/SkewProcessor.cs
  23. 24
      src/ImageSharp/Processing/Processors/Transforms/Resamplers/BicubicResampler.cs
  24. 16
      src/ImageSharp/Processing/Processors/Transforms/Resamplers/BoxResampler.cs
  25. 26
      src/ImageSharp/Processing/Processors/Transforms/Resamplers/CatmullRomResampler.cs
  26. 112
      src/ImageSharp/Processing/Processors/Transforms/Resamplers/CubicResampler.cs
  27. 25
      src/ImageSharp/Processing/Processors/Transforms/Resamplers/HermiteResampler.cs
  28. 32
      src/ImageSharp/Processing/Processors/Transforms/Resamplers/Lanczos2Resampler.cs
  29. 32
      src/ImageSharp/Processing/Processors/Transforms/Resamplers/Lanczos3Resampler.cs
  30. 32
      src/ImageSharp/Processing/Processors/Transforms/Resamplers/Lanczos5Resampler.cs
  31. 32
      src/ImageSharp/Processing/Processors/Transforms/Resamplers/Lanczos8Resampler.cs
  32. 68
      src/ImageSharp/Processing/Processors/Transforms/Resamplers/LanczosResampler.cs
  33. 24
      src/ImageSharp/Processing/Processors/Transforms/Resamplers/MitchellNetravaliResampler.cs
  34. 21
      src/ImageSharp/Processing/Processors/Transforms/Resamplers/NearestNeighborResampler.cs
  35. 24
      src/ImageSharp/Processing/Processors/Transforms/Resamplers/RobidouxResampler.cs
  36. 24
      src/ImageSharp/Processing/Processors/Transforms/Resamplers/RobidouxSharpResampler.cs
  37. 24
      src/ImageSharp/Processing/Processors/Transforms/Resamplers/SplineResampler.cs
  38. 16
      src/ImageSharp/Processing/Processors/Transforms/Resamplers/TriangleResampler.cs
  39. 16
      src/ImageSharp/Processing/Processors/Transforms/Resamplers/WelchResampler.cs
  40. 17
      src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernel.cs
  41. 17
      src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.PeriodicKernelMap.cs
  42. 55
      src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.cs
  43. 17
      src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor.cs
  44. 203
      src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor{TPixel}.cs
  45. 3
      src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWorker.cs
  46. 160
      src/ImageSharp/Processing/Processors/Transforms/TransformKernelMap.cs
  47. 13
      src/ImageSharp/Processing/Processors/Transforms/TransformUtilities.cs
  48. 12
      src/ImageSharp/Processing/ProjectiveTransformBuilder.cs
  49. 26
      tests/ImageSharp.Benchmarks/Samplers/Diffuse.cs
  50. 42
      tests/ImageSharp.Benchmarks/Samplers/Rotate.cs
  51. 43
      tests/ImageSharp.Benchmarks/Samplers/Skew.cs
  52. 3
      tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeKernelMapTests.ReferenceKernelMap.cs
  53. 122
      tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeKernelMapTests.cs
  54. 4
      tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs
  55. 8
      tests/ImageSharp.Tests/Processing/Transforms/PadTest.cs
  56. 16
      tests/ImageSharp.Tests/Processing/Transforms/ResizeTests.cs
  57. 4
      tests/ImageSharp.Tests/Processing/Transforms/TransformBuilderTestBase.cs
  58. 2
      tests/Images/External

29
src/ImageSharp/Common/Helpers/Guard.cs

@ -0,0 +1,29 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Reflection;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp;
namespace SixLabors
{
internal static partial class Guard
{
/// <summary>
/// Ensures that the value is a value type.
/// </summary>
/// <param name="value">The target object, which cannot be null.</param>
/// <param name="parameterName">The name of the parameter that is to be checked.</param>
/// <typeparam name="TValue">The type of the value.</typeparam>
/// <exception cref="ArgumentException"><paramref name="value"/> is not a value type.</exception>
[MethodImpl(InliningOptions.ShortMethod)]
public static void MustBeValueType<TValue>(TValue value, string parameterName)
{
if (!value.GetType().GetTypeInfo().IsValueType)
{
ThrowArgumentException("Type must be a struct.", parameterName);
}
}
}
}

34
src/ImageSharp/Common/Helpers/ImageMaths.cs

@ -242,40 +242,6 @@ namespace SixLabors.ImageSharp
return 1F;
}
/// <summary>
/// Returns the result of a B-C filter against the given value.
/// <see href="http://www.imagemagick.org/Usage/filter/#cubic_bc"/>
/// </summary>
/// <param name="x">The value to process.</param>
/// <param name="b">The B-Spline curve variable.</param>
/// <param name="c">The Cardinal curve variable.</param>
/// <returns>
/// The <see cref="float"/>.
/// </returns>
[MethodImpl(InliningOptions.ShortMethod)]
public static float GetBcValue(float x, float b, float c)
{
if (x < 0F)
{
x = -x;
}
float temp = x * x;
if (x < 1F)
{
x = ((12 - (9 * b) - (6 * c)) * (x * temp)) + ((-18 + (12 * b) + (6 * c)) * temp) + (6 - (2 * b));
return x / 6F;
}
if (x < 2F)
{
x = ((-b - (6 * c)) * (x * temp)) + (((6 * b) + (30 * c)) * temp) + (((-12 * b) - (48 * c)) * x) + ((8 * b) + (24 * c));
return x / 6F;
}
return 0F;
}
/// <summary>
/// Gets the bounding <see cref="Rectangle"/> from the given points.
/// </summary>

12
src/ImageSharp/Processing/AffineTransformBuilder.cs

@ -31,7 +31,7 @@ namespace SixLabors.ImageSharp.Processing
/// <param name="radians">The amount of rotation, in radians.</param>
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
public AffineTransformBuilder PrependRotationRadians(float radians)
=> this.Prepend(size => TransformUtils.CreateRotationMatrixRadians(radians, size));
=> this.Prepend(size => TransformUtilities.CreateRotationMatrixRadians(radians, size));
/// <summary>
/// Prepends a rotation matrix using the given rotation in degrees at the given origin.
@ -67,7 +67,7 @@ namespace SixLabors.ImageSharp.Processing
/// <param name="radians">The amount of rotation, in radians.</param>
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
public AffineTransformBuilder AppendRotationRadians(float radians)
=> this.Append(size => TransformUtils.CreateRotationMatrixRadians(radians, size));
=> this.Append(size => TransformUtilities.CreateRotationMatrixRadians(radians, size));
/// <summary>
/// Appends a rotation matrix using the given rotation in degrees at the given origin.
@ -142,7 +142,7 @@ namespace SixLabors.ImageSharp.Processing
/// <param name="degreesY">The Y angle, in degrees.</param>
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
public AffineTransformBuilder PrependSkewDegrees(float degreesX, float degreesY)
=> this.Prepend(size => TransformUtils.CreateSkewMatrixDegrees(degreesX, degreesY, size));
=> this.Prepend(size => TransformUtilities.CreateSkewMatrixDegrees(degreesX, degreesY, size));
/// <summary>
/// Prepends a centered skew matrix from the give angles in radians.
@ -151,7 +151,7 @@ namespace SixLabors.ImageSharp.Processing
/// <param name="radiansY">The Y angle, in radians.</param>
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
public AffineTransformBuilder PrependSkewRadians(float radiansX, float radiansY)
=> this.Prepend(size => TransformUtils.CreateSkewMatrixRadians(radiansX, radiansY, size));
=> this.Prepend(size => TransformUtilities.CreateSkewMatrixRadians(radiansX, radiansY, size));
/// <summary>
/// Prepends a skew matrix using the given angles in degrees at the given origin.
@ -180,7 +180,7 @@ namespace SixLabors.ImageSharp.Processing
/// <param name="degreesY">The Y angle, in degrees.</param>
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
public AffineTransformBuilder AppendSkewDegrees(float degreesX, float degreesY)
=> this.Append(size => TransformUtils.CreateSkewMatrixDegrees(degreesX, degreesY, size));
=> this.Append(size => TransformUtilities.CreateSkewMatrixDegrees(degreesX, degreesY, size));
/// <summary>
/// Appends a centered skew matrix from the give angles in radians.
@ -189,7 +189,7 @@ namespace SixLabors.ImageSharp.Processing
/// <param name="radiansY">The Y angle, in radians.</param>
/// <returns>The <see cref="AffineTransformBuilder"/>.</returns>
public AffineTransformBuilder AppendSkewRadians(float radiansX, float radiansY)
=> this.Append(size => TransformUtils.CreateSkewMatrixRadians(radiansX, radiansY, size));
=> this.Append(size => TransformUtilities.CreateSkewMatrixRadians(radiansX, radiansY, size));
/// <summary>
/// Appends a skew matrix using the given angles in degrees at the given origin.

4
src/ImageSharp/Processing/Extensions/Transforms/TransformExtensions.cs

@ -52,7 +52,7 @@ namespace SixLabors.ImageSharp.Processing
IResampler sampler)
{
Matrix3x2 transform = builder.BuildMatrix(sourceRectangle);
Size targetDimensions = TransformUtils.GetTransformedSize(sourceRectangle.Size, transform);
Size targetDimensions = TransformUtilities.GetTransformedSize(sourceRectangle.Size, transform);
return ctx.Transform(sourceRectangle, transform, targetDimensions, sampler);
}
@ -116,7 +116,7 @@ namespace SixLabors.ImageSharp.Processing
IResampler sampler)
{
Matrix4x4 transform = builder.BuildMatrix(sourceRectangle);
Size targetDimensions = TransformUtils.GetTransformedSize(sourceRectangle.Size, transform);
Size targetDimensions = TransformUtilities.GetTransformedSize(sourceRectangle.Size, transform);
return ctx.Transform(sourceRectangle, transform, targetDimensions, sampler);
}

36
src/ImageSharp/Processing/KnownResamplers.cs

@ -1,4 +1,4 @@
// Copyright (c) Six Labors and contributors.
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using SixLabors.ImageSharp.Processing.Processors.Transforms;
@ -13,86 +13,86 @@ namespace SixLabors.ImageSharp.Processing
/// <summary>
/// Gets the Bicubic sampler that implements the bicubic kernel algorithm W(x)
/// </summary>
public static IResampler Bicubic { get; } = new BicubicResampler();
public static IResampler Bicubic { get; } = default(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();
public static IResampler Box { get; } = default(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();
public static IResampler CatmullRom { get; } = CubicResampler.CatmullRom;
/// <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();
public static IResampler Hermite { get; } = CubicResampler.Hermite;
/// <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();
public static IResampler Lanczos2 { get; } = LanczosResampler.Lanczos2;
/// <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();
public static IResampler Lanczos3 { get; } = LanczosResampler.Lanczos3;
/// <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();
public static IResampler Lanczos5 { get; } = LanczosResampler.Lanczos5;
/// <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();
public static IResampler Lanczos8 { get; } = LanczosResampler.Lanczos8;
/// <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();
public static IResampler MitchellNetravali { get; } = CubicResampler.MitchellNetravali;
/// <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();
public static IResampler NearestNeighbor { get; } = default(NearestNeighborResampler);
/// <summary>
/// Gets the Robidoux sampler. This algorithm developed by Nicolas Robidoux providing a very good equilibrium between
/// detail preservation (sharpness) and smoothness comparable to <see cref="MitchellNetravali"/>.
/// </summary>
public static IResampler Robidoux { get; } = new RobidouxResampler();
public static IResampler Robidoux { get; } = CubicResampler.Robidoux;
/// <summary>
/// Gets the Robidoux Sharp sampler. A sharpened form of the <see cref="Robidoux"/> sampler
/// </summary>
public static IResampler RobidouxSharp { get; } = new RobidouxSharpResampler();
public static IResampler RobidouxSharp { get; } = CubicResampler.RobidouxSharp;
/// <summary>
/// Gets the Spline sampler. A seperable cubic algorithm similar to <see cref="MitchellNetravali"/> but yielding smoother results.
/// Gets the Spline sampler. A separable cubic algorithm similar to <see cref="MitchellNetravali"/> but yielding smoother results.
/// </summary>
public static IResampler Spline { get; } = new SplineResampler();
public static IResampler Spline { get; } = CubicResampler.Spline;
/// <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();
public static IResampler Triangle { get; } = default(TriangleResampler);
/// <summary>
/// Gets the Welch sampler. A high speed algorithm that delivers very sharpened results.
/// </summary>
public static IResampler Welch { get; } = new WelchResampler();
public static IResampler Welch { get; } = default(WelchResampler);
}
}
}

18
src/ImageSharp/Processing/Processors/CloningImageProcessor{TPixel}.cs

@ -110,10 +110,10 @@ namespace SixLabors.ImageSharp.Processing.Processors
}
/// <summary>
/// Gets the size of the target image.
/// Gets the size of the destination image.
/// </summary>
/// <returns>The <see cref="Size"/>.</returns>
protected abstract Size GetTargetSize();
protected abstract Size GetDestinationSize();
/// <summary>
/// This method is called before the process is applied to prepare the processor.
@ -168,21 +168,21 @@ namespace SixLabors.ImageSharp.Processing.Processors
private Image<TPixel> CreateTarget()
{
Image<TPixel> source = this.Source;
Size targetSize = this.GetTargetSize();
Size destinationSize = this.GetDestinationSize();
// We will always be creating the clone even for mutate because we may need to resize the canvas.
var targetFrames = new ImageFrame<TPixel>[source.Frames.Count];
for (int i = 0; i < targetFrames.Length; i++)
var destinationFrames = new ImageFrame<TPixel>[source.Frames.Count];
for (int i = 0; i < destinationFrames.Length; i++)
{
targetFrames[i] = new ImageFrame<TPixel>(
destinationFrames[i] = new ImageFrame<TPixel>(
this.Configuration,
targetSize.Width,
targetSize.Height,
destinationSize.Width,
destinationSize.Height,
source.Frames[i].Metadata.DeepClone());
}
// Use the overload to prevent an extra frame being added.
return new Image<TPixel>(this.Configuration, source.Metadata.DeepClone(), targetFrames);
return new Image<TPixel>(this.Configuration, source.Metadata.DeepClone(), destinationFrames);
}
private void CheckFrameCount(Image<TPixel> a, Image<TPixel> b)

188
src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor{TPixel}.cs

@ -1,188 +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.Advanced;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
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 AffineTransformProcessor<TPixel> : TransformProcessor<TPixel>
where TPixel : struct, IPixel<TPixel>
{
private readonly Size targetSize;
private readonly Matrix3x2 transformMatrix;
private readonly IResampler resampler;
/// <summary>
/// Initializes a new instance of the <see cref="AffineTransformProcessor{TPixel}"/> class.
/// </summary>
/// <param name="configuration">The configuration which allows altering default behaviour or extending the library.</param>
/// <param name="definition">The <see cref="AffineTransformProcessor"/> defining the processor parameters.</param>
/// <param name="source">The source <see cref="Image{TPixel}"/> for the current processor instance.</param>
/// <param name="sourceRectangle">The source area to process for the current processor instance.</param>
public AffineTransformProcessor(Configuration configuration, AffineTransformProcessor definition, Image<TPixel> source, Rectangle sourceRectangle)
: base(configuration, source, sourceRectangle)
{
this.targetSize = definition.TargetDimensions;
this.transformMatrix = definition.TransformMatrix;
this.resampler = definition.Sampler;
}
protected override Size GetTargetSize() => this.targetSize;
/// <inheritdoc/>
protected override void OnFrameApply(ImageFrame<TPixel> source, ImageFrame<TPixel> destination)
{
// Handle transforms that result in output identical to the original.
if (this.transformMatrix.Equals(default) || this.transformMatrix.Equals(Matrix3x2.Identity))
{
// The clone will be blank here copy all the pixel data over
source.GetPixelMemoryGroup().CopyTo(destination.GetPixelMemoryGroup());
return;
}
int width = this.targetSize.Width;
var targetBounds = new Rectangle(Point.Empty, this.targetSize);
Configuration configuration = this.Configuration;
// Convert from screen to world space.
Matrix3x2.Invert(this.transformMatrix, out Matrix3x2 matrix);
if (this.resampler is NearestNeighborResampler)
{
var nnOperation = new NearestNeighborRowIntervalOperation(this.SourceRectangle, ref matrix, width, source, destination);
ParallelRowIterator.IterateRows(
configuration,
targetBounds,
in nnOperation);
return;
}
using var kernelMap = new TransformKernelMap(configuration, source.Size(), destination.Size(), this.resampler);
var operation = new RowIntervalOperation(configuration, kernelMap, ref matrix, width, source, destination);
ParallelRowIterator.IterateRows<RowIntervalOperation, Vector4>(
configuration,
targetBounds,
in operation);
}
/// <summary>
/// A <see langword="struct"/> implementing the nearest neighbor resampler logic for <see cref="AffineTransformProcessor{T}"/>.
/// </summary>
private readonly struct NearestNeighborRowIntervalOperation : IRowIntervalOperation
{
private readonly Rectangle bounds;
private readonly Matrix3x2 matrix;
private readonly int maxX;
private readonly ImageFrame<TPixel> source;
private readonly ImageFrame<TPixel> destination;
[MethodImpl(InliningOptions.ShortMethod)]
public NearestNeighborRowIntervalOperation(
Rectangle bounds,
ref Matrix3x2 matrix,
int maxX,
ImageFrame<TPixel> source,
ImageFrame<TPixel> destination)
{
this.bounds = bounds;
this.matrix = matrix;
this.maxX = maxX;
this.source = source;
this.destination = destination;
}
/// <inheritdoc/>
/// <param name="rows"></param>
[MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(in RowInterval rows)
{
for (int y = rows.Min; y < rows.Max; y++)
{
Span<TPixel> destRow = this.destination.GetPixelRowSpan(y);
for (int x = 0; x < this.maxX; x++)
{
var point = Point.Transform(new Point(x, y), this.matrix);
if (this.bounds.Contains(point.X, point.Y))
{
destRow[x] = this.source[point.X, point.Y];
}
}
}
}
}
/// <summary>
/// A <see langword="struct"/> implementing the transformation logic for <see cref="AffineTransformProcessor{T}"/>.
/// </summary>
private readonly struct RowIntervalOperation : IRowIntervalOperation<Vector4>
{
private readonly Configuration configuration;
private readonly TransformKernelMap kernelMap;
private readonly Matrix3x2 matrix;
private readonly int maxX;
private readonly ImageFrame<TPixel> source;
private readonly ImageFrame<TPixel> destination;
[MethodImpl(InliningOptions.ShortMethod)]
public RowIntervalOperation(
Configuration configuration,
TransformKernelMap kernelMap,
ref Matrix3x2 matrix,
int maxX,
ImageFrame<TPixel> source,
ImageFrame<TPixel> destination)
{
this.configuration = configuration;
this.kernelMap = kernelMap;
this.matrix = matrix;
this.maxX = maxX;
this.source = source;
this.destination = destination;
}
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(in RowInterval rows, Span<Vector4> span)
{
for (int y = rows.Min; y < rows.Max; y++)
{
Span<TPixel> targetRowSpan = this.destination.GetPixelRowSpan(y);
PixelOperations<TPixel>.Instance.ToVector4(this.configuration, targetRowSpan, span);
ref float ySpanRef = ref this.kernelMap.GetYStartReference(y);
ref float xSpanRef = ref this.kernelMap.GetXStartReference(y);
for (int x = 0; x < this.maxX; 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), this.matrix);
this.kernelMap.Convolve(
point,
x,
ref ySpanRef,
ref xSpanRef,
this.source.PixelBuffer,
span);
}
PixelOperations<TPixel>.Instance.FromVector4Destructive(
this.configuration,
span,
targetRowSpan);
}
}
}
}
}

2
src/ImageSharp/Processing/Processors/Transforms/CropProcessor{TPixel}.cs

@ -30,7 +30,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
=> this.cropRectangle = definition.CropRectangle;
/// <inheritdoc/>
protected override Size GetTargetSize() => new Size(this.cropRectangle.Width, this.cropRectangle.Height);
protected override Size GetDestinationSize() => new Size(this.cropRectangle.Width, this.cropRectangle.Height);
/// <inheritdoc/>
protected override void OnFrameApply(ImageFrame<TPixel> source, ImageFrame<TPixel> destination)

14
src/ImageSharp/Processing/Processors/Transforms/IResampler.cs

@ -1,6 +1,8 @@
// Copyright (c) Six Labors and contributors.
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Processing.Processors.Transforms
{
/// <summary>
@ -21,5 +23,13 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
/// The <see cref="float"/>
/// </returns>
float GetValue(float x);
/// <summary>
/// Applies a transformation upon an image.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="processor">The transforming image processor.</param>
void ApplyTransform<TPixel>(IResamplingTransformImageProcessor<TPixel> processor)
where TPixel : struct, IPixel<TPixel>;
}
}
}

23
src/ImageSharp/Processing/Processors/Transforms/IResamplingTransformImageProcessor{TPixel}.cs

@ -0,0 +1,23 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Processing.Processors.Transforms
{
/// <summary>
/// Implements an algorithm to alter the pixels of an image via resampling transforms.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
public interface IResamplingTransformImageProcessor<TPixel> : IImageProcessor<TPixel>
where TPixel : struct, IPixel<TPixel>
{
/// <summary>
/// Applies a resampling transform with the given sampler.
/// </summary>
/// <typeparam name="TResampler">The type of sampler.</typeparam>
/// <param name="sampler">The sampler to use.</param>
void ApplyTransform<TResampler>(in TResampler sampler)
where TResampler : struct, IResampler;
}
}

8
src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor.cs → src/ImageSharp/Processing/Processors/Transforms/Linear/AffineTransformProcessor.cs

@ -19,9 +19,11 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
public AffineTransformProcessor(Matrix3x2 matrix, IResampler sampler, Size targetDimensions)
{
Guard.NotNull(sampler, nameof(sampler));
Guard.MustBeValueType(sampler, nameof(sampler));
this.Sampler = sampler;
this.TransformMatrix = matrix;
this.TargetDimensions = targetDimensions;
this.DestinationSize = targetDimensions;
}
/// <summary>
@ -35,9 +37,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
public Matrix3x2 TransformMatrix { get; }
/// <summary>
/// Gets the target dimensions to constrain the transformed image to.
/// Gets the destination size to constrain the transformed image to.
/// </summary>
public Size TargetDimensions { get; }
public Size DestinationSize { get; }
/// <inheritdoc/>
public override ICloningImageProcessor<TPixel> CreatePixelSpecificCloningProcessor<TPixel>(Configuration configuration, Image<TPixel> source, Rectangle sourceRectangle)

234
src/ImageSharp/Processing/Processors/Transforms/Linear/AffineTransformProcessor{TPixel}.cs

@ -0,0 +1,234 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
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 AffineTransformProcessor<TPixel> : TransformProcessor<TPixel>, IResamplingTransformImageProcessor<TPixel>
where TPixel : struct, IPixel<TPixel>
{
private readonly Size destinationSize;
private readonly Matrix3x2 transformMatrix;
private readonly IResampler resampler;
private ImageFrame<TPixel> source;
private ImageFrame<TPixel> destination;
/// <summary>
/// Initializes a new instance of the <see cref="AffineTransformProcessor{TPixel}"/> class.
/// </summary>
/// <param name="configuration">The configuration which allows altering default behaviour or extending the library.</param>
/// <param name="definition">The <see cref="AffineTransformProcessor"/> defining the processor parameters.</param>
/// <param name="source">The source <see cref="Image{TPixel}"/> for the current processor instance.</param>
/// <param name="sourceRectangle">The source area to process for the current processor instance.</param>
public AffineTransformProcessor(Configuration configuration, AffineTransformProcessor definition, Image<TPixel> source, Rectangle sourceRectangle)
: base(configuration, source, sourceRectangle)
{
this.destinationSize = definition.DestinationSize;
this.transformMatrix = definition.TransformMatrix;
this.resampler = definition.Sampler;
}
protected override Size GetDestinationSize() => this.destinationSize;
/// <inheritdoc/>
protected override void OnFrameApply(ImageFrame<TPixel> source, ImageFrame<TPixel> destination)
{
this.source = source;
this.destination = destination;
this.resampler.ApplyTransform(this);
}
/// <inheritdoc/>
public void ApplyTransform<TResampler>(in TResampler sampler)
where TResampler : struct, IResampler
{
Configuration configuration = this.Configuration;
ImageFrame<TPixel> source = this.source;
ImageFrame<TPixel> destination = this.destination;
Matrix3x2 matrix = this.transformMatrix;
// Handle transforms that result in output identical to the original.
if (matrix.Equals(default) || matrix.Equals(Matrix3x2.Identity))
{
// The clone will be blank here copy all the pixel data over
source.GetPixelMemoryGroup().CopyTo(destination.GetPixelMemoryGroup());
return;
}
// Convert from screen to world space.
Matrix3x2.Invert(matrix, out matrix);
if (sampler is NearestNeighborResampler)
{
var nnOperation = new NNAffineOperation(source, destination, matrix);
ParallelRowIterator.IterateRows(
configuration,
destination.Bounds(),
in nnOperation);
return;
}
int yRadius = LinearTransformUtilities.GetSamplingRadius(in sampler, source.Height, destination.Height);
int xRadius = LinearTransformUtilities.GetSamplingRadius(in sampler, source.Width, destination.Width);
var radialExtents = new Vector2(xRadius, yRadius);
int yLength = (yRadius * 2) + 1;
int xLength = (xRadius * 2) + 1;
// We use 2D buffers so that we can access the weight spans per row in parallel.
using Buffer2D<float> yKernelBuffer = configuration.MemoryAllocator.Allocate2D<float>(yLength, destination.Height);
using Buffer2D<float> xKernelBuffer = configuration.MemoryAllocator.Allocate2D<float>(xLength, destination.Height);
int maxX = source.Width - 1;
int maxY = source.Height - 1;
var maxSourceExtents = new Vector4(maxX, maxY, maxX, maxY);
var operation = new AffineOperation<TResampler>(
configuration,
source,
destination,
yKernelBuffer,
xKernelBuffer,
in sampler,
matrix,
radialExtents,
maxSourceExtents);
ParallelRowIterator.IterateRows<AffineOperation<TResampler>, Vector4>(
configuration,
destination.Bounds(),
in operation);
}
private readonly struct NNAffineOperation : IRowIntervalOperation
{
private readonly ImageFrame<TPixel> source;
private readonly ImageFrame<TPixel> destination;
private readonly Rectangle bounds;
private readonly Matrix3x2 matrix;
private readonly int maxX;
[MethodImpl(InliningOptions.ShortMethod)]
public NNAffineOperation(
ImageFrame<TPixel> source,
ImageFrame<TPixel> destination,
Matrix3x2 matrix)
{
this.source = source;
this.destination = destination;
this.bounds = source.Bounds();
this.matrix = matrix;
this.maxX = destination.Width;
}
[MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(in RowInterval rows)
{
for (int y = rows.Min; y < rows.Max; y++)
{
Span<TPixel> destRow = this.destination.GetPixelRowSpan(y);
for (int x = 0; x < this.maxX; x++)
{
var point = Vector2.Transform(new Vector2(x, y), this.matrix);
int px = (int)MathF.Round(point.X);
int py = (int)MathF.Round(point.Y);
if (this.bounds.Contains(px, py))
{
destRow[x] = this.source[px, py];
}
}
}
}
}
private readonly struct AffineOperation<TResampler> : IRowIntervalOperation<Vector4>
where TResampler : struct, IResampler
{
private readonly Configuration configuration;
private readonly ImageFrame<TPixel> source;
private readonly ImageFrame<TPixel> destination;
private readonly Buffer2D<float> yKernelBuffer;
private readonly Buffer2D<float> xKernelBuffer;
private readonly TResampler sampler;
private readonly Matrix3x2 matrix;
private readonly Vector2 radialExtents;
private readonly Vector4 maxSourceExtents;
private readonly int maxX;
[MethodImpl(InliningOptions.ShortMethod)]
public AffineOperation(
Configuration configuration,
ImageFrame<TPixel> source,
ImageFrame<TPixel> destination,
Buffer2D<float> yKernelBuffer,
Buffer2D<float> xKernelBuffer,
in TResampler sampler,
Matrix3x2 matrix,
Vector2 radialExtents,
Vector4 maxSourceExtents)
{
this.configuration = configuration;
this.source = source;
this.destination = destination;
this.yKernelBuffer = yKernelBuffer;
this.xKernelBuffer = xKernelBuffer;
this.sampler = sampler;
this.matrix = matrix;
this.radialExtents = radialExtents;
this.maxSourceExtents = maxSourceExtents;
this.maxX = destination.Width;
}
[MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(in RowInterval rows, Span<Vector4> span)
{
Buffer2D<TPixel> sourceBuffer = this.source.PixelBuffer;
for (int y = rows.Min; y < rows.Max; y++)
{
PixelOperations<TPixel>.Instance.ToVector4(
this.configuration,
this.destination.GetPixelRowSpan(y),
span);
ref float yKernelSpanRef = ref MemoryMarshal.GetReference(this.yKernelBuffer.GetRowSpan(y));
ref float xKernelSpanRef = ref MemoryMarshal.GetReference(this.xKernelBuffer.GetRowSpan(y));
for (int x = 0; x < this.maxX; 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), this.matrix);
LinearTransformUtilities.Convolve(
in this.sampler,
point,
sourceBuffer,
span,
x,
ref yKernelSpanRef,
ref xKernelSpanRef,
this.radialExtents,
this.maxSourceExtents);
}
PixelOperations<TPixel>.Instance.FromVector4Destructive(
this.configuration,
span,
this.destination.GetPixelRowSpan(y));
}
}
}
}
}

0
src/ImageSharp/Processing/Processors/Transforms/AutoOrientProcessor.cs → src/ImageSharp/Processing/Processors/Transforms/Linear/AutoOrientProcessor.cs

0
src/ImageSharp/Processing/Processors/Transforms/AutoOrientProcessor{TPixel}.cs → src/ImageSharp/Processing/Processors/Transforms/Linear/AutoOrientProcessor{TPixel}.cs

0
src/ImageSharp/Processing/Processors/Transforms/FlipProcessor.cs → src/ImageSharp/Processing/Processors/Transforms/Linear/FlipProcessor.cs

0
src/ImageSharp/Processing/Processors/Transforms/FlipProcessor{TPixel}.cs → src/ImageSharp/Processing/Processors/Transforms/Linear/FlipProcessor{TPixel}.cs

104
src/ImageSharp/Processing/Processors/Transforms/Linear/LinearTransformUtilities.cs

@ -0,0 +1,104 @@
// 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;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Processing.Processors.Transforms
{
/// <summary>
/// Utility methods for affine and projective transforms.
/// </summary>
internal static class LinearTransformUtilities
{
[MethodImpl(InliningOptions.ShortMethod)]
internal static int GetSamplingRadius<TResampler>(in TResampler sampler, int sourceSize, int destinationSize)
where TResampler : struct, IResampler
{
double scale = sourceSize / destinationSize;
if (scale < 1)
{
scale = 1;
}
return (int)Math.Ceiling(scale * sampler.Radius);
}
[MethodImpl(InliningOptions.ShortMethod)]
internal static void Convolve<TResampler, TPixel>(
in TResampler sampler,
Vector2 transformedPoint,
Buffer2D<TPixel> sourcePixels,
Span<Vector4> targetRow,
int column,
ref float yKernelSpanRef,
ref float xKernelSpanRef,
Vector2 radialExtents,
Vector4 maxSourceExtents)
where TResampler : struct, IResampler
where TPixel : struct, IPixel<TPixel>
{
// Clamp sampling pixel radial extents to the source image edges
Vector2 minXY = transformedPoint - radialExtents;
Vector2 maxXY = transformedPoint + radialExtents;
// left, top, right, bottom
var sourceExtents = new Vector4(
MathF.Ceiling(minXY.X),
MathF.Ceiling(minXY.Y),
MathF.Floor(maxXY.X),
MathF.Floor(maxXY.Y));
sourceExtents = Vector4.Clamp(sourceExtents, Vector4.Zero, maxSourceExtents);
int left = (int)sourceExtents.X;
int top = (int)sourceExtents.Y;
int right = (int)sourceExtents.Z;
int bottom = (int)sourceExtents.W;
if (left == right || top == bottom)
{
return;
}
CalculateWeights(in sampler, top, bottom, transformedPoint.Y, ref yKernelSpanRef);
CalculateWeights(in sampler, left, right, transformedPoint.X, ref xKernelSpanRef);
Vector4 sum = Vector4.Zero;
for (int kernelY = 0, y = top; y <= bottom; y++, kernelY++)
{
float yWeight = Unsafe.Add(ref yKernelSpanRef, kernelY);
for (int kernelX = 0, x = left; x <= right; x++, kernelX++)
{
float xWeight = Unsafe.Add(ref xKernelSpanRef, kernelX);
// Values are first premultiplied to prevent darkening of edge pixels.
var current = sourcePixels[x, y].ToVector4();
Vector4Utils.Premultiply(ref current);
sum += current * xWeight * yWeight;
}
}
// Reverse the premultiplication
Vector4Utils.UnPremultiply(ref sum);
targetRow[column] = sum;
}
[MethodImpl(InliningOptions.ShortMethod)]
private static void CalculateWeights<TResampler>(in TResampler sampler, int min, int max, float point, ref float weightsRef)
where TResampler : struct, IResampler
{
float sum = 0;
for (int x = 0, i = min; i <= max; i++, x++)
{
float weight = sampler.GetValue(i - point);
sum += weight;
Unsafe.Add(ref weightsRef, x) = weight;
}
}
}
}

8
src/ImageSharp/Processing/Processors/Transforms/ProjectiveTransformProcessor.cs → src/ImageSharp/Processing/Processors/Transforms/Linear/ProjectiveTransformProcessor.cs

@ -19,9 +19,11 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
public ProjectiveTransformProcessor(Matrix4x4 matrix, IResampler sampler, Size targetDimensions)
{
Guard.NotNull(sampler, nameof(sampler));
Guard.MustBeValueType(sampler, nameof(sampler));
this.Sampler = sampler;
this.TransformMatrix = matrix;
this.TargetDimensions = targetDimensions;
this.DestinationSize = targetDimensions;
}
/// <summary>
@ -35,9 +37,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
public Matrix4x4 TransformMatrix { get; }
/// <summary>
/// Gets the target dimensions to constrain the transformed image to.
/// Gets the destination size to constrain the transformed image to.
/// </summary>
public Size TargetDimensions { get; }
public Size DestinationSize { get; }
/// <inheritdoc />
public override ICloningImageProcessor<TPixel> CreatePixelSpecificCloningProcessor<TPixel>(Configuration configuration, Image<TPixel> source, Rectangle sourceRectangle)

158
src/ImageSharp/Processing/Processors/Transforms/ProjectiveTransformProcessor{TPixel}.cs → src/ImageSharp/Processing/Processors/Transforms/Linear/ProjectiveTransformProcessor{TPixel}.cs

@ -4,6 +4,7 @@
using System;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
@ -14,12 +15,14 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
/// Provides the base methods to perform non-affine transforms on an image.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
internal class ProjectiveTransformProcessor<TPixel> : TransformProcessor<TPixel>
internal class ProjectiveTransformProcessor<TPixel> : TransformProcessor<TPixel>, IResamplingTransformImageProcessor<TPixel>
where TPixel : struct, IPixel<TPixel>
{
private readonly Size targetSize;
private readonly Size destinationSize;
private readonly IResampler resampler;
private readonly Matrix4x4 transformMatrix;
private ImageFrame<TPixel> source;
private ImageFrame<TPixel> destination;
/// <summary>
/// Initializes a new instance of the <see cref="ProjectiveTransformProcessor{TPixel}"/> class.
@ -31,74 +34,102 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
public ProjectiveTransformProcessor(Configuration configuration, ProjectiveTransformProcessor definition, Image<TPixel> source, Rectangle sourceRectangle)
: base(configuration, source, sourceRectangle)
{
this.targetSize = definition.TargetDimensions;
this.destinationSize = definition.DestinationSize;
this.transformMatrix = definition.TransformMatrix;
this.resampler = definition.Sampler;
}
protected override Size GetTargetSize() => this.targetSize;
protected override Size GetDestinationSize() => this.destinationSize;
/// <inheritdoc/>
protected override void OnFrameApply(ImageFrame<TPixel> source, ImageFrame<TPixel> destination)
{
this.source = source;
this.destination = destination;
this.resampler.ApplyTransform(this);
}
/// <inheritdoc/>
public void ApplyTransform<TResampler>(in TResampler sampler)
where TResampler : struct, IResampler
{
Configuration configuration = this.Configuration;
ImageFrame<TPixel> source = this.source;
ImageFrame<TPixel> destination = this.destination;
Matrix4x4 matrix = this.transformMatrix;
// Handle transforms that result in output identical to the original.
if (this.transformMatrix.Equals(default) || this.transformMatrix.Equals(Matrix4x4.Identity))
if (matrix.Equals(default) || matrix.Equals(Matrix4x4.Identity))
{
// The clone will be blank here copy all the pixel data over
source.GetPixelMemoryGroup().CopyTo(destination.GetPixelMemoryGroup());
return;
}
int width = this.targetSize.Width;
var targetBounds = new Rectangle(Point.Empty, this.targetSize);
Configuration configuration = this.Configuration;
// Convert from screen to world space.
Matrix4x4.Invert(this.transformMatrix, out Matrix4x4 matrix);
Matrix4x4.Invert(matrix, out matrix);
if (this.resampler is NearestNeighborResampler)
if (sampler is NearestNeighborResampler)
{
Rectangle sourceBounds = this.SourceRectangle;
var nnOperation = new NearestNeighborRowIntervalOperation(sourceBounds, ref matrix, width, source, destination);
var nnOperation = new NNProjectiveOperation(source, destination, matrix);
ParallelRowIterator.IterateRows(
configuration,
targetBounds,
destination.Bounds(),
in nnOperation);
return;
}
using var kernelMap = new TransformKernelMap(configuration, source.Size(), destination.Size(), this.resampler);
int yRadius = LinearTransformUtilities.GetSamplingRadius(in sampler, source.Height, destination.Height);
int xRadius = LinearTransformUtilities.GetSamplingRadius(in sampler, source.Width, destination.Width);
var radialExtents = new Vector2(xRadius, yRadius);
int yLength = (yRadius * 2) + 1;
int xLength = (xRadius * 2) + 1;
// We use 2D buffers so that we can access the weight spans per row in parallel.
using Buffer2D<float> yKernelBuffer = configuration.MemoryAllocator.Allocate2D<float>(yLength, destination.Height);
using Buffer2D<float> xKernelBuffer = configuration.MemoryAllocator.Allocate2D<float>(xLength, destination.Height);
var operation = new RowIntervalOperation(configuration, kernelMap, ref matrix, width, source, destination);
ParallelRowIterator.IterateRows<RowIntervalOperation, Vector4>(
int maxX = source.Width - 1;
int maxY = source.Height - 1;
var maxSourceExtents = new Vector4(maxX, maxY, maxX, maxY);
var operation = new ProjectiveOperation<TResampler>(
configuration,
source,
destination,
yKernelBuffer,
xKernelBuffer,
in sampler,
matrix,
radialExtents,
maxSourceExtents);
ParallelRowIterator.IterateRows<ProjectiveOperation<TResampler>, Vector4>(
configuration,
targetBounds,
destination.Bounds(),
in operation);
}
private readonly struct NearestNeighborRowIntervalOperation : IRowIntervalOperation
private readonly struct NNProjectiveOperation : IRowIntervalOperation
{
private readonly ImageFrame<TPixel> source;
private readonly ImageFrame<TPixel> destination;
private readonly Rectangle bounds;
private readonly Matrix4x4 matrix;
private readonly int maxX;
private readonly ImageFrame<TPixel> source;
private readonly ImageFrame<TPixel> destination;
[MethodImpl(InliningOptions.ShortMethod)]
public NearestNeighborRowIntervalOperation(
Rectangle bounds,
ref Matrix4x4 matrix,
int maxX,
public NNProjectiveOperation(
ImageFrame<TPixel> source,
ImageFrame<TPixel> destination)
ImageFrame<TPixel> destination,
Matrix4x4 matrix)
{
this.bounds = bounds;
this.matrix = matrix;
this.maxX = maxX;
this.source = source;
this.destination = destination;
this.bounds = source.Bounds();
this.matrix = matrix;
this.maxX = destination.Width;
}
[MethodImpl(InliningOptions.ShortMethod)]
@ -110,7 +141,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
for (int x = 0; x < this.maxX; x++)
{
Vector2 point = TransformUtils.ProjectiveTransform2D(x, y, this.matrix);
Vector2 point = TransformUtilities.ProjectiveTransform2D(x, y, this.matrix);
int px = (int)MathF.Round(point.X);
int py = (int)MathF.Round(point.Y);
@ -123,60 +154,79 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
}
}
private readonly struct RowIntervalOperation : IRowIntervalOperation<Vector4>
private readonly struct ProjectiveOperation<TResampler> : IRowIntervalOperation<Vector4>
where TResampler : struct, IResampler
{
private readonly Configuration configuration;
private readonly TransformKernelMap kernelMap;
private readonly Matrix4x4 matrix;
private readonly int maxX;
private readonly ImageFrame<TPixel> source;
private readonly ImageFrame<TPixel> destination;
private readonly Buffer2D<float> yKernelBuffer;
private readonly Buffer2D<float> xKernelBuffer;
private readonly TResampler sampler;
private readonly Matrix4x4 matrix;
private readonly Vector2 radialExtents;
private readonly Vector4 maxSourceExtents;
private readonly int maxX;
[MethodImpl(InliningOptions.ShortMethod)]
public RowIntervalOperation(
public ProjectiveOperation(
Configuration configuration,
TransformKernelMap kernelMap,
ref Matrix4x4 matrix,
int maxX,
ImageFrame<TPixel> source,
ImageFrame<TPixel> destination)
ImageFrame<TPixel> destination,
Buffer2D<float> yKernelBuffer,
Buffer2D<float> xKernelBuffer,
in TResampler sampler,
Matrix4x4 matrix,
Vector2 radialExtents,
Vector4 maxSourceExtents)
{
this.configuration = configuration;
this.kernelMap = kernelMap;
this.matrix = matrix;
this.maxX = maxX;
this.source = source;
this.destination = destination;
this.yKernelBuffer = yKernelBuffer;
this.xKernelBuffer = xKernelBuffer;
this.sampler = sampler;
this.matrix = matrix;
this.radialExtents = radialExtents;
this.maxSourceExtents = maxSourceExtents;
this.maxX = destination.Width;
}
[MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(in RowInterval rows, Span<Vector4> span)
{
Buffer2D<TPixel> sourceBuffer = this.source.PixelBuffer;
for (int y = rows.Min; y < rows.Max; y++)
{
Span<TPixel> targetRowSpan = this.destination.GetPixelRowSpan(y);
PixelOperations<TPixel>.Instance.ToVector4(this.configuration, targetRowSpan, span);
ref float ySpanRef = ref this.kernelMap.GetYStartReference(y);
ref float xSpanRef = ref this.kernelMap.GetXStartReference(y);
PixelOperations<TPixel>.Instance.ToVector4(
this.configuration,
this.destination.GetPixelRowSpan(y),
span);
ref float yKernelSpanRef = ref MemoryMarshal.GetReference(this.yKernelBuffer.GetRowSpan(y));
ref float xKernelSpanRef = ref MemoryMarshal.GetReference(this.xKernelBuffer.GetRowSpan(y));
for (int x = 0; x < this.maxX; x++)
{
// Use the single precision position to calculate correct bounding pixels
// otherwise we get rogue pixels outside of the bounds.
Vector2 point = TransformUtils.ProjectiveTransform2D(x, y, this.matrix);
this.kernelMap.Convolve(
Vector2 point = TransformUtilities.ProjectiveTransform2D(x, y, this.matrix);
LinearTransformUtilities.Convolve(
in this.sampler,
point,
sourceBuffer,
span,
x,
ref ySpanRef,
ref xSpanRef,
this.source.PixelBuffer,
span);
ref yKernelSpanRef,
ref xKernelSpanRef,
this.radialExtents,
this.maxSourceExtents);
}
PixelOperations<TPixel>.Instance.FromVector4Destructive(
this.configuration,
span,
targetRowSpan);
this.destination.GetPixelRowSpan(y));
}
}
}

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

@ -28,14 +28,14 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
/// <param name="sourceSize">The source image size</param>
public RotateProcessor(float degrees, IResampler sampler, Size sourceSize)
: this(
TransformUtils.CreateRotationMatrixDegrees(degrees, sourceSize),
TransformUtilities.CreateRotationMatrixDegrees(degrees, sourceSize),
sampler,
sourceSize)
=> this.Degrees = degrees;
// Helper constructor
private RotateProcessor(Matrix3x2 rotationMatrix, IResampler sampler, Size sourceSize)
: base(rotationMatrix, sampler, TransformUtils.GetTransformedSize(sourceSize, rotationMatrix))
: base(rotationMatrix, sampler, TransformUtilities.GetTransformedSize(sourceSize, rotationMatrix))
{
}

0
src/ImageSharp/Processing/Processors/Transforms/RotateProcessor{TPixel}.cs → src/ImageSharp/Processing/Processors/Transforms/Linear/RotateProcessor{TPixel}.cs

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

@ -30,7 +30,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
/// <param name="sourceSize">The source image size</param>
public SkewProcessor(float degreesX, float degreesY, IResampler sampler, Size sourceSize)
: this(
TransformUtils.CreateSkewMatrixDegrees(degreesX, degreesY, sourceSize),
TransformUtilities.CreateSkewMatrixDegrees(degreesX, degreesY, sourceSize),
sampler,
sourceSize)
{
@ -40,7 +40,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
// Helper constructor:
private SkewProcessor(Matrix3x2 skewMatrix, IResampler sampler, Size sourceSize)
: base(skewMatrix, sampler, TransformUtils.GetTransformedSize(sourceSize, skewMatrix))
: base(skewMatrix, sampler, TransformUtilities.GetTransformedSize(sourceSize, skewMatrix))
{
}

24
src/ImageSharp/Processing/Processors/Transforms/Resamplers/BicubicResampler.cs

@ -1,6 +1,9 @@
// Copyright (c) Six Labors and contributors.
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Processing.Processors.Transforms
{
/// <summary>
@ -8,12 +11,13 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
/// <see href="https://en.wikipedia.org/wiki/Bicubic_interpolation#Bicubic_convolution_algorithm">Wikipedia</see>
/// A commonly used algorithm within image processing that preserves sharpness better than triangle interpolation.
/// </summary>
public class BicubicResampler : IResampler
public readonly struct BicubicResampler : IResampler
{
/// <inheritdoc/>
public float Radius => 2;
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public float GetValue(float x)
{
if (x < 0F)
@ -21,21 +25,25 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
x = -x;
}
float result = 0;
// Given the coefficient "a" as -0.5F.
if (x <= 1F)
{
// Below simplified result = ((a + 2F) * (x * x * x)) - ((a + 3F) * (x * x)) + 1;
result = (((1.5F * x) - 2.5F) * x * x) + 1;
return (((1.5F * x) - 2.5F) * x * x) + 1;
}
else if (x < 2F)
{
// Below simplified result = (a * (x * x * x)) - ((5F * a) * (x * x)) + ((8F * a) * x) - (4F * a);
result = (((((-0.5F * x) + 2.5F) * x) - 4) * x) + 2;
return (((((-0.5F * x) + 2.5F) * x) - 4) * x) + 2;
}
return result;
return 0;
}
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public void ApplyTransform<TPixel>(IResamplingTransformImageProcessor<TPixel> processor)
where TPixel : struct, IPixel<TPixel>
=> processor.ApplyTransform(in this);
}
}
}

16
src/ImageSharp/Processing/Processors/Transforms/Resamplers/BoxResampler.cs

@ -1,18 +1,22 @@
// Copyright (c) Six Labors and contributors.
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Processing.Processors.Transforms
{
/// <summary>
/// The function implements the box algorithm. Similar to nearest neighbor when upscaling.
/// When downscaling the pixels will average, merging together.
/// </summary>
public class BoxResampler : IResampler
public readonly struct BoxResampler : IResampler
{
/// <inheritdoc/>
public float Radius => 0.5F;
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public float GetValue(float x)
{
if (x > -0.5F && x <= 0.5F)
@ -22,5 +26,11 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
return 0;
}
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public void ApplyTransform<TPixel>(IResamplingTransformImageProcessor<TPixel> processor)
where TPixel : struct, IPixel<TPixel>
=> processor.ApplyTransform(in this);
}
}
}

26
src/ImageSharp/Processing/Processors/Transforms/Resamplers/CatmullRomResampler.cs

@ -1,26 +0,0 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
namespace SixLabors.ImageSharp.Processing.Processors.Transforms
{
/// <summary>
/// The Catmull-Rom filter is a well known standard Cubic Filter often used as a interpolation function.
/// This filter produces a reasonably sharp edge, but without a the pronounced gradient change on large
/// scale image enlargements that a 'Lagrange' filter can produce.
/// <see href="http://www.imagemagick.org/Usage/filter/#cubic_bc"/>
/// </summary>
public class CatmullRomResampler : IResampler
{
/// <inheritdoc/>
public float Radius => 2;
/// <inheritdoc/>
public float GetValue(float x)
{
const float B = 0;
const float C = 0.5F;
return ImageMaths.GetBcValue(x, B, C);
}
}
}

112
src/ImageSharp/Processing/Processors/Transforms/Resamplers/CubicResampler.cs

@ -0,0 +1,112 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Processing.Processors.Transforms
{
/// <summary>
/// Cubic filters contain a collection of different filters of varying B-Spline and
/// Cardinal values. With these two values you can generate any smoothly fitting
/// (continuious first derivative) piece-wise cubic filter.
/// <see href="http://www.imagemagick.org/Usage/filter/#cubic_bc"/>
/// <see href="https://www.cs.utexas.edu/~fussell/courses/cs384g-fall2013/lectures/mitchell/Mitchell.pdf"/>
/// </summary>
public readonly struct CubicResampler : IResampler
{
private readonly float bspline;
private readonly float cardinal;
/// <summary>
/// The Catmull-Rom filter is a well known standard Cubic Filter often used as a interpolation function.
/// This filter produces a reasonably sharp edge, but without a the pronounced gradient change on large
/// scale image enlargements that a 'Lagrange' filter can produce.
/// </summary>
public static CubicResampler CatmullRom = new CubicResampler(2, 0, .5F);
/// <summary>
/// The Hermite filter is type of smoothed triangular interpolation Filter,
/// This filter rounds off strong edges while preserving flat 'color levels' in the original image.
/// </summary>
public static CubicResampler Hermite = new CubicResampler(2, 0, 0);
/// <summary>
/// The function implements the Mitchell-Netravali algorithm as described on
/// <see href="https://de.wikipedia.org/wiki/Mitchell-Netravali-Filter">Wikipedia</see>
/// </summary>
public static CubicResampler MitchellNetravali = new CubicResampler(2, .3333333F, .3333333F);
/// <summary>
/// The function implements the Robidoux algorithm.
/// <see href="http://www.imagemagick.org/Usage/filter/#robidoux"/>
/// </summary>
public static CubicResampler Robidoux = new CubicResampler(2, .37821575509399867F, .31089212245300067F);
/// <summary>
/// The function implements the Robidoux Sharp algorithm.
/// <see href="http://www.imagemagick.org/Usage/filter/#robidoux"/>
/// </summary>
public static CubicResampler RobidouxSharp = new CubicResampler(2, .2620145123990142F, .3689927438004929F);
/// <summary>
/// The function implements the spline algorithm.
/// <see href="http://www.imagemagick.org/Usage/filter/#cubic_bc"/>
/// </summary>
/// <summary>
/// The function implements the Robidoux Sharp algorithm.
/// <see href="http://www.imagemagick.org/Usage/filter/#robidoux"/>
/// </summary>
public static CubicResampler Spline = new CubicResampler(2, 1, 0);
/// <summary>
/// Initializes a new instance of the <see cref="CubicResampler"/> struct.
/// </summary>
/// <param name="radius">The sampling radius.</param>
/// <param name="bspline">The B-Spline value.</param>
/// <param name="cardinal">The Cardinal cubic value.</param>
public CubicResampler(float radius, float bspline, float cardinal)
{
this.Radius = radius;
this.bspline = bspline;
this.cardinal = cardinal;
}
/// <inheritdoc/>
public float Radius { get; }
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public float GetValue(float x)
{
float b = this.bspline;
float c = this.cardinal;
if (x < 0F)
{
x = -x;
}
float temp = x * x;
if (x < 1F)
{
x = ((12 - (9 * b) - (6 * c)) * (x * temp)) + ((-18 + (12 * b) + (6 * c)) * temp) + (6 - (2 * b));
return x / 6F;
}
if (x < 2F)
{
x = ((-b - (6 * c)) * (x * temp)) + (((6 * b) + (30 * c)) * temp) + (((-12 * b) - (48 * c)) * x) + ((8 * b) + (24 * c));
return x / 6F;
}
return 0F;
}
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public void ApplyTransform<TPixel>(IResamplingTransformImageProcessor<TPixel> processor)
where TPixel : struct, IPixel<TPixel>
=> processor.ApplyTransform(in this);
}
}

25
src/ImageSharp/Processing/Processors/Transforms/Resamplers/HermiteResampler.cs

@ -1,25 +0,0 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
namespace SixLabors.ImageSharp.Processing.Processors.Transforms
{
/// <summary>
/// The Hermite filter is type of smoothed triangular interpolation Filter,
/// This filter rounds off strong edges while preserving flat 'color levels' in the original image.
/// <see href="http://www.imagemagick.org/Usage/filter/#cubic_bc"/>
/// </summary>
public class HermiteResampler : IResampler
{
/// <inheritdoc/>
public float Radius => 2;
/// <inheritdoc/>
public float GetValue(float x)
{
const float B = 0F;
const float C = 0F;
return ImageMaths.GetBcValue(x, B, C);
}
}
}

32
src/ImageSharp/Processing/Processors/Transforms/Resamplers/Lanczos2Resampler.cs

@ -1,32 +0,0 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
namespace SixLabors.ImageSharp.Processing.Processors.Transforms
{
/// <summary>
/// The function implements the Lanczos kernel algorithm as described on
/// <see href="https://en.wikipedia.org/wiki/Lanczos_resampling#Algorithm">Wikipedia</see>
/// with a radius of 2 pixels.
/// </summary>
public class Lanczos2Resampler : IResampler
{
/// <inheritdoc/>
public float Radius => 2;
/// <inheritdoc/>
public float GetValue(float x)
{
if (x < 0F)
{
x = -x;
}
if (x < 2F)
{
return ImageMaths.SinC(x) * ImageMaths.SinC(x / 2F);
}
return 0F;
}
}
}

32
src/ImageSharp/Processing/Processors/Transforms/Resamplers/Lanczos3Resampler.cs

@ -1,32 +0,0 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
namespace SixLabors.ImageSharp.Processing.Processors.Transforms
{
/// <summary>
/// The function implements the Lanczos kernel algorithm as described on
/// <see href="https://en.wikipedia.org/wiki/Lanczos_resampling#Algorithm">Wikipedia</see>
/// with a radius of 3 pixels.
/// </summary>
public class Lanczos3Resampler : IResampler
{
/// <inheritdoc/>
public float Radius => 3;
/// <inheritdoc/>
public float GetValue(float x)
{
if (x < 0F)
{
x = -x;
}
if (x < 3F)
{
return ImageMaths.SinC(x) * ImageMaths.SinC(x / 3F);
}
return 0F;
}
}
}

32
src/ImageSharp/Processing/Processors/Transforms/Resamplers/Lanczos5Resampler.cs

@ -1,32 +0,0 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
namespace SixLabors.ImageSharp.Processing.Processors.Transforms
{
/// <summary>
/// The function implements the Lanczos kernel algorithm as described on
/// <see href="https://en.wikipedia.org/wiki/Lanczos_resampling#Algorithm">Wikipedia</see>
/// with a radius of 5 pixels.
/// </summary>
public class Lanczos5Resampler : IResampler
{
/// <inheritdoc/>
public float Radius => 5;
/// <inheritdoc/>
public float GetValue(float x)
{
if (x < 0F)
{
x = -x;
}
if (x < 5F)
{
return ImageMaths.SinC(x) * ImageMaths.SinC(x / 5F);
}
return 0F;
}
}
}

32
src/ImageSharp/Processing/Processors/Transforms/Resamplers/Lanczos8Resampler.cs

@ -1,32 +0,0 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
namespace SixLabors.ImageSharp.Processing.Processors.Transforms
{
/// <summary>
/// The function implements the Lanczos kernel algorithm as described on
/// <see href="https://en.wikipedia.org/wiki/Lanczos_resampling#Algorithm">Wikipedia</see>
/// with a radius of 8 pixels.
/// </summary>
public class Lanczos8Resampler : IResampler
{
/// <inheritdoc/>
public float Radius => 8;
/// <inheritdoc/>
public float GetValue(float x)
{
if (x < 0F)
{
x = -x;
}
if (x < 8F)
{
return ImageMaths.SinC(x) * ImageMaths.SinC(x / 8F);
}
return 0F;
}
}
}

68
src/ImageSharp/Processing/Processors/Transforms/Resamplers/LanczosResampler.cs

@ -0,0 +1,68 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Processing.Processors.Transforms
{
/// <summary>
/// The function implements the Lanczos kernel algorithm as described on
/// <see href="https://en.wikipedia.org/wiki/Lanczos_resampling#Algorithm">Wikipedia</see>.
/// </summary>
public readonly struct LanczosResampler : IResampler
{
/// <summary>
/// Implements the Lanczos kernel algorithm with a radius of 2.
/// </summary>
public static LanczosResampler Lanczos2 = new LanczosResampler(2);
/// <summary>
/// Implements the Lanczos kernel algorithm with a radius of 3.
/// </summary>
public static LanczosResampler Lanczos3 = new LanczosResampler(3);
/// <summary>
/// Implements the Lanczos kernel algorithm with a radius of 5.
/// </summary>
public static LanczosResampler Lanczos5 = new LanczosResampler(5);
/// <summary>
/// Implements the Lanczos kernel algorithm with a radius of 8.
/// </summary>
public static LanczosResampler Lanczos8 = new LanczosResampler(8);
/// <summary>
/// Initializes a new instance of the <see cref="LanczosResampler"/> struct.
/// </summary>
/// <param name="radius">The sampling radius.</param>
public LanczosResampler(float radius) => this.Radius = radius;
/// <inheritdoc/>
public float Radius { get; }
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public float GetValue(float x)
{
if (x < 0F)
{
x = -x;
}
float radius = this.Radius;
if (x < radius)
{
return ImageMaths.SinC(x) * ImageMaths.SinC(x / radius);
}
return 0F;
}
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public void ApplyTransform<TPixel>(IResamplingTransformImageProcessor<TPixel> processor)
where TPixel : struct, IPixel<TPixel>
=> processor.ApplyTransform(in this);
}
}

24
src/ImageSharp/Processing/Processors/Transforms/Resamplers/MitchellNetravaliResampler.cs

@ -1,24 +0,0 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
namespace SixLabors.ImageSharp.Processing.Processors.Transforms
{
/// <summary>
/// The function implements the mitchell algorithm as described on
/// <see href="https://de.wikipedia.org/wiki/Mitchell-Netravali-Filter">Wikipedia</see>
/// </summary>
public class MitchellNetravaliResampler : IResampler
{
/// <inheritdoc/>
public float Radius => 2;
/// <inheritdoc/>
public float GetValue(float x)
{
const float B = 0.3333333F;
const float C = 0.3333333F;
return ImageMaths.GetBcValue(x, B, C);
}
}
}

21
src/ImageSharp/Processing/Processors/Transforms/Resamplers/NearestNeighborResampler.cs

@ -1,21 +1,28 @@
// Copyright (c) Six Labors and contributors.
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Processing.Processors.Transforms
{
/// <summary>
/// The function implements the nearest neighbor algorithm. This uses an unscaled filter
/// which will select the closest pixel to the new pixels position.
/// </summary>
public class NearestNeighborResampler : IResampler
public readonly struct NearestNeighborResampler : IResampler
{
/// <inheritdoc/>
public float Radius => 1;
/// <inheritdoc/>
public float GetValue(float x)
{
return x;
}
[MethodImpl(InliningOptions.ShortMethod)]
public float GetValue(float x) => x;
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public void ApplyTransform<TPixel>(IResamplingTransformImageProcessor<TPixel> processor)
where TPixel : struct, IPixel<TPixel>
=> processor.ApplyTransform(in this);
}
}
}

24
src/ImageSharp/Processing/Processors/Transforms/Resamplers/RobidouxResampler.cs

@ -1,24 +0,0 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
namespace SixLabors.ImageSharp.Processing.Processors.Transforms
{
/// <summary>
/// The function implements the Robidoux algorithm.
/// <see href="http://www.imagemagick.org/Usage/filter/#robidoux"/>
/// </summary>
public class RobidouxResampler : IResampler
{
/// <inheritdoc/>
public float Radius => 2;
/// <inheritdoc/>
public float GetValue(float x)
{
const float B = 0.37821575509399867F;
const float C = 0.31089212245300067F;
return ImageMaths.GetBcValue(x, B, C);
}
}
}

24
src/ImageSharp/Processing/Processors/Transforms/Resamplers/RobidouxSharpResampler.cs

@ -1,24 +0,0 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
namespace SixLabors.ImageSharp.Processing.Processors.Transforms
{
/// <summary>
/// The function implements the Robidoux Sharp algorithm.
/// <see href="http://www.imagemagick.org/Usage/filter/#robidoux"/>
/// </summary>
public class RobidouxSharpResampler : IResampler
{
/// <inheritdoc/>
public float Radius => 2;
/// <inheritdoc/>
public float GetValue(float x)
{
const float B = 0.2620145123990142F;
const float C = 0.3689927438004929F;
return ImageMaths.GetBcValue(x, B, C);
}
}
}

24
src/ImageSharp/Processing/Processors/Transforms/Resamplers/SplineResampler.cs

@ -1,24 +0,0 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
namespace SixLabors.ImageSharp.Processing.Processors.Transforms
{
/// <summary>
/// The function implements the spline algorithm.
/// <see href="http://www.imagemagick.org/Usage/filter/#cubic_bc"/>
/// </summary>
public class SplineResampler : IResampler
{
/// <inheritdoc/>
public float Radius => 2;
/// <inheritdoc/>
public float GetValue(float x)
{
const float B = 1F;
const float C = 0F;
return ImageMaths.GetBcValue(x, B, C);
}
}
}

16
src/ImageSharp/Processing/Processors/Transforms/Resamplers/TriangleResampler.cs

@ -1,6 +1,9 @@
// Copyright (c) Six Labors and contributors.
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Processing.Processors.Transforms
{
/// <summary>
@ -8,12 +11,13 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
/// Bilinear interpolation 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 class TriangleResampler : IResampler
public readonly struct TriangleResampler : IResampler
{
/// <inheritdoc/>
public float Radius => 1;
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public float GetValue(float x)
{
if (x < 0F)
@ -28,5 +32,11 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
return 0F;
}
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public void ApplyTransform<TPixel>(IResamplingTransformImageProcessor<TPixel> processor)
where TPixel : struct, IPixel<TPixel>
=> processor.ApplyTransform(in this);
}
}
}

16
src/ImageSharp/Processing/Processors/Transforms/Resamplers/WelchResampler.cs

@ -1,18 +1,22 @@
// Copyright (c) Six Labors and contributors.
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Processing.Processors.Transforms
{
/// <summary>
/// The function implements the welch algorithm.
/// <see href="http://www.imagemagick.org/Usage/filter/"/>
/// </summary>
public class WelchResampler : IResampler
public readonly struct WelchResampler : IResampler
{
/// <inheritdoc/>
public float Radius => 3;
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public float GetValue(float x)
{
if (x < 0F)
@ -27,5 +31,11 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
return 0F;
}
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public void ApplyTransform<TPixel>(IResamplingTransformImageProcessor<TPixel> processor)
where TPixel : struct, IPixel<TPixel>
=> processor.ApplyTransform(in this);
}
}
}

17
src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernel.cs

@ -1,4 +1,4 @@
// Copyright (c) Six Labors and contributors.
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
@ -28,12 +28,20 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
/// <summary>
/// Gets the start index for the destination row.
/// </summary>
public int StartIndex { get; }
public int StartIndex
{
[MethodImpl(InliningOptions.ShortMethod)]
get;
}
/// <summary>
/// Gets the the length of the kernel.
/// </summary>
public int Length { get; }
public int Length
{
[MethodImpl(InliningOptions.ShortMethod)]
get;
}
/// <summary>
/// Gets the span representing the portion of the <see cref="ResizeKernelMap"/> that this window covers.
@ -81,6 +89,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
/// Copy the contents of <see cref="ResizeKernel"/> altering <see cref="StartIndex"/>
/// to the value <paramref name="left"/>.
/// </summary>
[MethodImpl(InliningOptions.ShortMethod)]
internal ResizeKernel AlterLeftValue(int left)
{
return new ResizeKernel(left, this.bufferPtr, this.Length);
@ -96,4 +105,4 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
}
}
}
}
}

17
src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.PeriodicKernelMap.cs

@ -1,13 +1,10 @@
// Copyright (c) Six Labors and contributors.
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Processing.Processors.Transforms
{
/// <content>
/// Contains <see cref="PeriodicKernelMap"/>
/// </content>
internal partial class ResizeKernelMap
{
/// <summary>
@ -21,7 +18,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
public PeriodicKernelMap(
MemoryAllocator memoryAllocator,
IResampler sampler,
int sourceLength,
int destinationLength,
double ratio,
@ -31,7 +27,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
int cornerInterval)
: base(
memoryAllocator,
sampler,
sourceLength,
destinationLength,
(cornerInterval * 2) + period,
@ -45,15 +40,14 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
internal override string Info => base.Info + $"|period:{this.period}|cornerInterval:{this.cornerInterval}";
protected override void Initialize()
protected internal override void Initialize<TResampler>(in TResampler sampler)
{
// Build top corner data + one period of the mosaic data:
int startOfFirstRepeatedMosaic = this.cornerInterval + this.period;
for (int i = 0; i < startOfFirstRepeatedMosaic; i++)
{
ResizeKernel kernel = this.BuildKernel(i, i);
this.kernels[i] = kernel;
this.kernels[i] = this.BuildKernel(in sampler, i, i);
}
// Copy the mosaics:
@ -70,10 +64,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
int bottomStartData = this.cornerInterval + this.period;
for (int i = 0; i < this.cornerInterval; i++)
{
ResizeKernel kernel = this.BuildKernel(bottomStartDest + i, bottomStartData + i);
this.kernels[bottomStartDest + i] = kernel;
this.kernels[bottomStartDest + i] = this.BuildKernel(in sampler, bottomStartDest + i, bottomStartData + i);
}
}
}
}
}
}

55
src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.cs

@ -5,21 +5,17 @@ using System;
using System.Buffers;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Processing.Processors.Transforms
{
/// <summary>
/// Provides <see cref="ResizeKernel"/> values from an optimized,
/// contiguous memory region.
/// Provides resize kernel values from an optimized contiguous memory region.
/// </summary>
internal partial class ResizeKernelMap : IDisposable
{
private static readonly TolerantMath TolerantMath = TolerantMath.Default;
private readonly IResampler sampler;
private readonly int sourceLength;
private readonly double ratio;
@ -34,12 +30,13 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
private readonly ResizeKernel[] kernels;
private bool isDisposed;
// To avoid both GC allocations, and MemoryAllocator ceremony:
private readonly double[] tempValues;
private ResizeKernelMap(
MemoryAllocator memoryAllocator,
IResampler sampler,
int sourceLength,
int destinationLength,
int bufferHeight,
@ -47,7 +44,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
double scale,
int radius)
{
this.sampler = sampler;
this.ratio = ratio;
this.scale = scale;
this.radius = radius;
@ -80,30 +76,47 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
/// Disposes <see cref="ResizeKernelMap"/> instance releasing it's backing buffer.
/// </summary>
public void Dispose()
=> this.Dispose(true);
/// <summary>
/// Disposes the object and frees resources for the Garbage Collector.
/// </summary>
/// <param name="disposing">Whether to dispose of managed and unmanaged objects.</param>
protected virtual void Dispose(bool disposing)
{
this.pinHandle.Dispose();
this.data.Dispose();
if (!this.isDisposed)
{
this.isDisposed = true;
if (disposing)
{
this.pinHandle.Dispose();
this.data.Dispose();
}
}
}
/// <summary>
/// Returns a <see cref="ResizeKernel"/> for an index value between 0 and DestinationSize - 1.
/// </summary>
[MethodImpl(InliningOptions.ShortMethod)]
public ref ResizeKernel GetKernel(int destIdx) => ref this.kernels[destIdx];
internal ref ResizeKernel GetKernel(int destIdx) => ref this.kernels[destIdx];
/// <summary>
/// Computes the weights to apply at each pixel when resizing.
/// </summary>
/// <typeparam name="TResampler">The type of sampler.</typeparam>
/// <param name="sampler">The <see cref="IResampler"/></param>
/// <param name="destinationSize">The destination size</param>
/// <param name="sourceSize">The source size</param>
/// <param name="memoryAllocator">The <see cref="MemoryAllocator"/> to use for buffer allocations</param>
/// <returns>The <see cref="ResizeKernelMap"/></returns>
public static ResizeKernelMap Calculate(
IResampler sampler,
public static ResizeKernelMap Calculate<TResampler>(
in TResampler sampler,
int destinationSize,
int sourceSize,
MemoryAllocator memoryAllocator)
where TResampler : struct, IResampler
{
double ratio = (double)sourceSize / destinationSize;
double scale = ratio;
@ -144,7 +157,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
ResizeKernelMap result = hasAtLeast2Periods
? new PeriodicKernelMap(
memoryAllocator,
sampler,
sourceSize,
destinationSize,
ratio,
@ -154,7 +166,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
cornerInterval)
: new ResizeKernelMap(
memoryAllocator,
sampler,
sourceSize,
destinationSize,
destinationSize,
@ -162,17 +173,20 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
scale,
radius);
result.Initialize();
result.Initialize(in sampler);
return result;
}
protected virtual void Initialize()
/// <summary>
/// Initializes the kernel map.
/// </summary>
protected internal virtual void Initialize<TResampler>(in TResampler sampler)
where TResampler : struct, IResampler
{
for (int i = 0; i < this.DestinationLength; i++)
{
ResizeKernel kernel = this.BuildKernel(i, i);
this.kernels[i] = kernel;
this.kernels[i] = this.BuildKernel(in sampler, i, i);
}
}
@ -181,7 +195,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
/// referencing the data at row <paramref name="dataRowIndex"/> within <see cref="data"/>,
/// so the data reusable by other data rows.
/// </summary>
private ResizeKernel BuildKernel(int destRowIndex, int dataRowIndex)
private ResizeKernel BuildKernel<TResampler>(in TResampler sampler, int destRowIndex, int dataRowIndex)
where TResampler : struct, IResampler
{
double center = ((destRowIndex + .5) * this.ratio) - .5;
@ -205,7 +220,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
for (int j = left; j <= right; j++)
{
double value = this.sampler.GetValue((float)((j - center) / this.scale));
double value = sampler.GetValue((float)((j - center) / this.scale));
sum += value;
kernelValues[j - left] = value;

17
src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor.cs

@ -17,13 +17,14 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
{
Guard.NotNull(options, nameof(options));
Guard.NotNull(options.Sampler, nameof(options.Sampler));
Guard.MustBeValueType(options.Sampler, nameof(options.Sampler));
(Size size, Rectangle rectangle) = ResizeHelper.CalculateTargetLocationAndBounds(sourceSize, options);
this.Sampler = options.Sampler;
this.TargetWidth = size.Width;
this.TargetHeight = size.Height;
this.TargetRectangle = rectangle;
this.DestinationWidth = size.Width;
this.DestinationHeight = size.Height;
this.DestinationRectangle = rectangle;
this.Compand = options.Compand;
}
@ -33,19 +34,19 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
public IResampler Sampler { get; }
/// <summary>
/// Gets the target width.
/// Gets the destination width.
/// </summary>
public int TargetWidth { get; }
public int DestinationWidth { get; }
/// <summary>
/// Gets the target height.
/// Gets the destination height.
/// </summary>
public int TargetHeight { get; }
public int DestinationHeight { get; }
/// <summary>
/// Gets the resize rectangle.
/// </summary>
public Rectangle TargetRectangle { get; }
public Rectangle DestinationRectangle { get; }
/// <summary>
/// Gets a value indicating whether to compress or expand individual pixel color values on processing.

203
src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor{TPixel}.cs

@ -12,59 +12,35 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
/// <summary>
/// Implements resizing of images using various resamplers.
/// </summary>
/// <remarks>
/// The original code has been adapted from <see href="http://www.realtimerendering.com/resources/GraphicsGems/gemsiii/filter_rcg.c"/>.
/// </remarks>
/// <typeparam name="TPixel">The pixel format.</typeparam>
internal class ResizeProcessor<TPixel> : TransformProcessor<TPixel>
internal partial class ResizeProcessor<TPixel> : TransformProcessor<TPixel>, IResamplingTransformImageProcessor<TPixel>
where TPixel : struct, IPixel<TPixel>
{
private bool isDisposed;
private readonly int targetWidth;
private readonly int targetHeight;
private readonly int destinationWidth;
private readonly int destinationHeight;
private readonly IResampler resampler;
private readonly Rectangle targetRectangle;
private readonly Rectangle destinationRectangle;
private readonly bool compand;
// The following fields are not immutable but are optionally created on demand.
private ResizeKernelMap horizontalKernelMap;
private ResizeKernelMap verticalKernelMap;
private Image<TPixel> destination;
public ResizeProcessor(Configuration configuration, ResizeProcessor definition, Image<TPixel> source, Rectangle sourceRectangle)
: base(configuration, source, sourceRectangle)
{
this.targetWidth = definition.TargetWidth;
this.targetHeight = definition.TargetHeight;
this.targetRectangle = definition.TargetRectangle;
this.destinationWidth = definition.DestinationWidth;
this.destinationHeight = definition.DestinationHeight;
this.destinationRectangle = definition.DestinationRectangle;
this.resampler = definition.Sampler;
this.compand = definition.Compand;
}
/// <inheritdoc/>
protected override Size GetTargetSize() => new Size(this.targetWidth, this.targetHeight);
protected override Size GetDestinationSize() => new Size(this.destinationWidth, this.destinationHeight);
/// <inheritdoc/>
protected override void BeforeImageApply(Image<TPixel> destination)
{
if (!(this.resampler is NearestNeighborResampler))
{
Image<TPixel> source = this.Source;
Rectangle sourceRectangle = this.SourceRectangle;
// Since all image frame dimensions have to be the same we can calculate this for all frames.
MemoryAllocator memoryAllocator = source.GetMemoryAllocator();
this.horizontalKernelMap = ResizeKernelMap.Calculate(
this.resampler,
this.targetRectangle.Width,
sourceRectangle.Width,
memoryAllocator);
this.verticalKernelMap = ResizeKernelMap.Calculate(
this.resampler,
this.targetRectangle.Height,
sourceRectangle.Height,
memoryAllocator);
}
this.destination = destination;
this.resampler.ApplyTransform(this);
base.BeforeImageApply(destination);
}
@ -72,54 +48,143 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
/// <inheritdoc/>
protected override void OnFrameApply(ImageFrame<TPixel> source, ImageFrame<TPixel> destination)
{
Rectangle sourceRectangle = this.SourceRectangle;
// Everything happens in BeforeImageApply.
}
public void ApplyTransform<TResampler>(in TResampler sampler)
where TResampler : struct, IResampler
{
Configuration configuration = this.Configuration;
Image<TPixel> source = this.Source;
Image<TPixel> destination = this.destination;
Rectangle sourceRectangle = this.SourceRectangle;
Rectangle destinationRectangle = this.destinationRectangle;
bool compand = this.compand;
// Handle resize dimensions identical to the original
if (source.Width == destination.Width
&& source.Height == destination.Height
&& sourceRectangle == this.targetRectangle)
&& sourceRectangle == destinationRectangle)
{
// The cloned will be blank here copy all the pixel data over
source.GetPixelMemoryGroup().CopyTo(destination.GetPixelMemoryGroup());
for (int i = 0; i < source.Frames.Count; i++)
{
ImageFrame<TPixel> sourceFrame = source.Frames[i];
ImageFrame<TPixel> destinationFrame = destination.Frames[i];
// The cloned will be blank here copy all the pixel data over
sourceFrame.GetPixelMemoryGroup().CopyTo(destinationFrame.GetPixelMemoryGroup());
}
return;
}
int width = this.targetWidth;
int height = this.targetHeight;
var interest = Rectangle.Intersect(this.targetRectangle, new Rectangle(0, 0, width, height));
var interest = Rectangle.Intersect(destinationRectangle, destination.Bounds());
if (sampler is NearestNeighborResampler)
{
for (int i = 0; i < source.Frames.Count; i++)
{
ImageFrame<TPixel> sourceFrame = source.Frames[i];
ImageFrame<TPixel> destinationFrame = destination.Frames[i];
ApplyNNResizeFrameTransform(
configuration,
sourceFrame,
destinationFrame,
sourceRectangle,
destinationRectangle,
interest);
}
return;
}
if (this.resampler is NearestNeighborResampler)
// Since all image frame dimensions have to be the same we can calculate
// the kernel maps and reuse for all frames.
MemoryAllocator allocator = configuration.MemoryAllocator;
using var horizontalKernelMap = ResizeKernelMap.Calculate(
in sampler,
destinationRectangle.Width,
sourceRectangle.Width,
allocator);
using var verticalKernelMap = ResizeKernelMap.Calculate(
in sampler,
destinationRectangle.Height,
sourceRectangle.Height,
allocator);
for (int i = 0; i < source.Frames.Count; i++)
{
// Scaling factors
float widthFactor = sourceRectangle.Width / (float)this.targetRectangle.Width;
float heightFactor = sourceRectangle.Height / (float)this.targetRectangle.Height;
ImageFrame<TPixel> sourceFrame = source.Frames[i];
ImageFrame<TPixel> destinationFrame = destination.Frames[i];
var operation = new RowIntervalOperation(sourceRectangle, this.targetRectangle, widthFactor, heightFactor, source, destination);
ParallelRowIterator.IterateRows(
ApplyResizeFrameTransform(
configuration,
sourceFrame,
destinationFrame,
horizontalKernelMap,
verticalKernelMap,
sourceRectangle,
destinationRectangle,
interest,
in operation);
return;
compand);
}
}
private static void ApplyNNResizeFrameTransform(
Configuration configuration,
ImageFrame<TPixel> source,
ImageFrame<TPixel> destination,
Rectangle sourceRectangle,
Rectangle destinationRectangle,
Rectangle interest)
{
// Scaling factors
float widthFactor = sourceRectangle.Width / (float)destinationRectangle.Width;
float heightFactor = sourceRectangle.Height / (float)destinationRectangle.Height;
var operation = new NNRowIntervalOperation(
sourceRectangle,
destinationRectangle,
widthFactor,
heightFactor,
source,
destination);
ParallelRowIterator.IterateRows(
configuration,
interest,
in operation);
}
private static void ApplyResizeFrameTransform(
Configuration configuration,
ImageFrame<TPixel> source,
ImageFrame<TPixel> destination,
ResizeKernelMap horizontalKernelMap,
ResizeKernelMap verticalKernelMap,
Rectangle sourceRectangle,
Rectangle destinationRectangle,
Rectangle interest,
bool compand)
{
PixelConversionModifiers conversionModifiers =
PixelConversionModifiers.Premultiply.ApplyCompanding(this.compand);
PixelConversionModifiers.Premultiply.ApplyCompanding(compand);
BufferArea<TPixel> sourceArea = source.PixelBuffer.GetArea(sourceRectangle);
// To reintroduce parallel processing, we to launch multiple workers
// To reintroduce parallel processing, we would launch multiple workers
// for different row intervals of the image.
using (var worker = new ResizeWorker<TPixel>(
configuration,
sourceArea,
conversionModifiers,
this.horizontalKernelMap,
this.verticalKernelMap,
width,
horizontalKernelMap,
verticalKernelMap,
destination.Width,
interest,
this.targetRectangle.Location))
destinationRectangle.Location))
{
worker.Initialize();
@ -128,27 +193,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
}
}
/// <inheritdoc/>
protected override void Dispose(bool disposing)
{
if (this.isDisposed)
{
return;
}
if (disposing)
{
this.horizontalKernelMap?.Dispose();
this.horizontalKernelMap = null;
this.verticalKernelMap?.Dispose();
this.verticalKernelMap = null;
}
this.isDisposed = true;
base.Dispose(disposing);
}
private readonly struct RowIntervalOperation : IRowIntervalOperation
private readonly struct NNRowIntervalOperation : IRowIntervalOperation
{
private readonly Rectangle sourceBounds;
private readonly Rectangle destinationBounds;
@ -158,7 +203,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
private readonly ImageFrame<TPixel> destination;
[MethodImpl(InliningOptions.ShortMethod)]
public RowIntervalOperation(
public NNRowIntervalOperation(
Rectangle sourceBounds,
Rectangle destinationBounds,
float widthFactor,

3
src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWorker.cs

@ -6,7 +6,6 @@ using System.Buffers;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
@ -104,7 +103,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
this.tempColumnBuffer.Dispose();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
[MethodImpl(InliningOptions.ShortMethod)]
public Span<Vector4> GetColumnSpan(int x, int startY)
{
return this.transposedFirstPassBuffer.GetRowSpan(x).Slice(startY - this.currentWindow.Min);

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

@ -1,160 +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 System.Runtime.InteropServices;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Processing.Processors.Transforms
{
/// <summary>
/// Contains the methods required to calculate transform kernel convolution.
/// </summary>
internal class TransformKernelMap : IDisposable
{
private readonly Buffer2D<float> yBuffer;
private readonly Buffer2D<float> xBuffer;
private readonly Vector2 extents;
private Vector4 maxSourceExtents;
private readonly IResampler sampler;
/// <summary>
/// Initializes a new instance of the <see cref="TransformKernelMap"/> class.
/// </summary>
/// <param name="configuration">The configuration.</param>
/// <param name="source">The source size.</param>
/// <param name="destination">The destination size.</param>
/// <param name="sampler">The sampler.</param>
public TransformKernelMap(Configuration configuration, Size source, Size destination, IResampler sampler)
{
this.sampler = sampler;
float yRadius = this.GetSamplingRadius(source.Height, destination.Height);
float xRadius = this.GetSamplingRadius(source.Width, destination.Width);
this.extents = new Vector2(xRadius, yRadius);
int xLength = (int)MathF.Ceiling((this.extents.X * 2) + 2);
int yLength = (int)MathF.Ceiling((this.extents.Y * 2) + 2);
// We use 2D buffers so that we can access the weight spans per row in parallel.
this.yBuffer = configuration.MemoryAllocator.Allocate2D<float>(yLength, destination.Height);
this.xBuffer = configuration.MemoryAllocator.Allocate2D<float>(xLength, destination.Height);
int maxX = source.Width - 1;
int maxY = source.Height - 1;
this.maxSourceExtents = new Vector4(maxX, maxY, maxX, maxY);
}
/// <summary>
/// Gets a reference to the first item of the y window.
/// </summary>
/// <returns>The reference to the first item of the window.</returns>
[MethodImpl(InliningOptions.ShortMethod)]
public ref float GetYStartReference(int y)
=> ref MemoryMarshal.GetReference(this.yBuffer.GetRowSpan(y));
/// <summary>
/// Gets a reference to the first item of the x window.
/// </summary>
/// <returns>The reference to the first item of the window.</returns>
[MethodImpl(InliningOptions.ShortMethod)]
public ref float GetXStartReference(int y)
=> ref MemoryMarshal.GetReference(this.xBuffer.GetRowSpan(y));
public void Convolve<TPixel>(
Vector2 transformedPoint,
int column,
ref float ySpanRef,
ref float xSpanRef,
Buffer2D<TPixel> sourcePixels,
Span<Vector4> targetRow)
where TPixel : struct, IPixel<TPixel>
{
// Clamp sampling pixel radial extents to the source image edges
Vector2 minXY = transformedPoint - this.extents;
Vector2 maxXY = transformedPoint + this.extents;
// left, top, right, bottom
var extents = new Vector4(
MathF.Ceiling(minXY.X - .5F),
MathF.Ceiling(minXY.Y - .5F),
MathF.Floor(maxXY.X + .5F),
MathF.Floor(maxXY.Y + .5F));
extents = Vector4.Clamp(extents, Vector4.Zero, this.maxSourceExtents);
int left = (int)extents.X;
int top = (int)extents.Y;
int right = (int)extents.Z;
int bottom = (int)extents.W;
if (left == right || top == bottom)
{
return;
}
this.CalculateWeights(top, bottom, transformedPoint.Y, ref ySpanRef);
this.CalculateWeights(left, right, transformedPoint.X, ref xSpanRef);
Vector4 sum = Vector4.Zero;
for (int kernelY = 0, y = top; y <= bottom; y++, kernelY++)
{
float yWeight = Unsafe.Add(ref ySpanRef, kernelY);
for (int kernelX = 0, x = left; x <= right; x++, kernelX++)
{
float xWeight = Unsafe.Add(ref xSpanRef, kernelX);
// Values are first premultiplied to prevent darkening of edge pixels.
var current = sourcePixels[x, y].ToVector4();
Vector4Utils.Premultiply(ref current);
sum += current * xWeight * yWeight;
}
}
// Reverse the premultiplication
Vector4Utils.UnPremultiply(ref sum);
targetRow[column] = sum;
}
/// <summary>
/// Calculated the normalized weights for the given point.
/// </summary>
/// <param name="min">The minimum sampling offset</param>
/// <param name="max">The maximum sampling offset</param>
/// <param name="point">The transformed point dimension</param>
/// <param name="weightsRef">The reference to the collection of weights</param>
[MethodImpl(InliningOptions.ShortMethod)]
private void CalculateWeights(int min, int max, float point, ref float weightsRef)
{
float sum = 0;
for (int x = 0, i = min; i <= max; i++, x++)
{
float weight = this.sampler.GetValue(i - point);
sum += weight;
Unsafe.Add(ref weightsRef, x) = weight;
}
}
[MethodImpl(InliningOptions.ShortMethod)]
private float GetSamplingRadius(int sourceSize, int destinationSize)
{
float scale = (float)sourceSize / destinationSize;
if (scale < 1F)
{
scale = 1F;
}
return MathF.Ceiling(scale * this.sampler.Radius);
}
public void Dispose()
{
this.yBuffer?.Dispose();
this.xBuffer?.Dispose();
}
}
}

13
src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs → src/ImageSharp/Processing/Processors/Transforms/TransformUtilities.cs

@ -10,7 +10,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
/// <summary>
/// Contains utility methods for working with transforms.
/// </summary>
internal static class TransformUtils
internal static class TransformUtilities
{
/// <summary>
/// Applies the projective transform against the given coordinates flattened into the 2D space.
@ -33,6 +33,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
/// <param name="degrees">The amount of rotation, in degrees.</param>
/// <param name="size">The source image size.</param>
/// <returns>The <see cref="Matrix3x2"/>.</returns>
[MethodImpl(InliningOptions.ShortMethod)]
public static Matrix3x2 CreateRotationMatrixDegrees(float degrees, Size size)
=> CreateCenteredTransformMatrix(
new Rectangle(Point.Empty, size),
@ -44,6 +45,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
/// <param name="radians">The amount of rotation, in radians.</param>
/// <param name="size">The source image size.</param>
/// <returns>The <see cref="Matrix3x2"/>.</returns>
[MethodImpl(InliningOptions.ShortMethod)]
public static Matrix3x2 CreateRotationMatrixRadians(float radians, Size size)
=> CreateCenteredTransformMatrix(
new Rectangle(Point.Empty, size),
@ -56,6 +58,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
/// <param name="degreesY">The Y angle, in degrees.</param>
/// <param name="size">The source image size.</param>
/// <returns>The <see cref="Matrix3x2"/>.</returns>
[MethodImpl(InliningOptions.ShortMethod)]
public static Matrix3x2 CreateSkewMatrixDegrees(float degreesX, float degreesY, Size size)
=> CreateCenteredTransformMatrix(
new Rectangle(Point.Empty, size),
@ -68,6 +71,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
/// <param name="radiansY">The Y angle, in radians.</param>
/// <param name="size">The source image size.</param>
/// <returns>The <see cref="Matrix3x2"/>.</returns>
[MethodImpl(InliningOptions.ShortMethod)]
public static Matrix3x2 CreateSkewMatrixRadians(float radiansX, float radiansY, Size size)
=> CreateCenteredTransformMatrix(
new Rectangle(Point.Empty, size),
@ -79,6 +83,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
/// <param name="sourceRectangle">The source image bounds.</param>
/// <param name="matrix">The transformation matrix.</param>
/// <returns>The <see cref="Matrix3x2"/></returns>
[MethodImpl(InliningOptions.ShortMethod)]
public static Matrix3x2 CreateCenteredTransformMatrix(Rectangle sourceRectangle, Matrix3x2 matrix)
{
Rectangle destinationRectangle = GetTransformedBoundingRectangle(sourceRectangle, matrix);
@ -105,6 +110,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
/// <param name="corner">An enumeration that indicates on which corners to taper the rectangle.</param>
/// <param name="fraction">The amount to taper.</param>
/// <returns>The <see cref="Matrix4x4"/></returns>
[MethodImpl(InliningOptions.ShortMethod)]
public static Matrix4x4 CreateTaperMatrix(Size size, TaperSide side, TaperCorner corner, float fraction)
{
Matrix4x4 matrix = Matrix4x4.Identity;
@ -225,6 +231,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
/// <returns>
/// The <see cref="Rectangle"/>.
/// </returns>
[MethodImpl(InliningOptions.ShortMethod)]
public static Rectangle GetTransformedBoundingRectangle(Rectangle rectangle, Matrix3x2 matrix)
{
Rectangle transformed = GetTransformedRectangle(rectangle, matrix);
@ -284,6 +291,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
/// <returns>
/// The <see cref="Rectangle"/>.
/// </returns>
[MethodImpl(InliningOptions.ShortMethod)]
public static Rectangle GetTransformedRectangle(Rectangle rectangle, Matrix4x4 matrix)
{
if (rectangle.Equals(default) || Matrix4x4.Identity.Equals(matrix))
@ -307,6 +315,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
/// <returns>
/// The <see cref="Size"/>.
/// </returns>
[MethodImpl(InliningOptions.ShortMethod)]
public static Size GetTransformedSize(Size size, Matrix4x4 matrix)
{
Guard.IsTrue(size.Width > 0 && size.Height > 0, nameof(size), "Source size dimensions cannot be 0!");
@ -321,6 +330,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
return ConstrainSize(rectangle);
}
[MethodImpl(InliningOptions.ShortMethod)]
private static Size ConstrainSize(Rectangle rectangle)
{
// We want to resize the canvas here taking into account any translations.
@ -342,6 +352,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
return new Size(width, height);
}
[MethodImpl(InliningOptions.ShortMethod)]
private static Rectangle GetBoundingRectangle(Vector2 tl, Vector2 tr, Vector2 bl, Vector2 br)
{
// Find the minimum and maximum "corners" based on the given vectors

12
src/ImageSharp/Processing/ProjectiveTransformBuilder.cs

@ -23,7 +23,7 @@ namespace SixLabors.ImageSharp.Processing
/// <param name="fraction">The amount to taper.</param>
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
public ProjectiveTransformBuilder PrependTaper(TaperSide side, TaperCorner corner, float fraction)
=> this.Prepend(size => TransformUtils.CreateTaperMatrix(size, side, corner, fraction));
=> this.Prepend(size => TransformUtilities.CreateTaperMatrix(size, side, corner, fraction));
/// <summary>
/// Appends a matrix that performs a tapering projective transform.
@ -33,7 +33,7 @@ namespace SixLabors.ImageSharp.Processing
/// <param name="fraction">The amount to taper.</param>
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
public ProjectiveTransformBuilder AppendTaper(TaperSide side, TaperCorner corner, float fraction)
=> this.Append(size => TransformUtils.CreateTaperMatrix(size, side, corner, fraction));
=> this.Append(size => TransformUtilities.CreateTaperMatrix(size, side, corner, fraction));
/// <summary>
/// Prepends a centered rotation matrix using the given rotation in degrees.
@ -49,7 +49,7 @@ namespace SixLabors.ImageSharp.Processing
/// <param name="radians">The amount of rotation, in radians.</param>
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
public ProjectiveTransformBuilder PrependRotationRadians(float radians)
=> this.Prepend(size => new Matrix4x4(TransformUtils.CreateRotationMatrixRadians(radians, size)));
=> this.Prepend(size => new Matrix4x4(TransformUtilities.CreateRotationMatrixRadians(radians, size)));
/// <summary>
/// Prepends a centered rotation matrix using the given rotation in degrees at the given origin.
@ -83,7 +83,7 @@ namespace SixLabors.ImageSharp.Processing
/// <param name="radians">The amount of rotation, in radians.</param>
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
public ProjectiveTransformBuilder AppendRotationRadians(float radians)
=> this.Append(size => new Matrix4x4(TransformUtils.CreateRotationMatrixRadians(radians, size)));
=> this.Append(size => new Matrix4x4(TransformUtilities.CreateRotationMatrixRadians(radians, size)));
/// <summary>
/// Appends a centered rotation matrix using the given rotation in degrees at the given origin.
@ -167,7 +167,7 @@ namespace SixLabors.ImageSharp.Processing
/// <param name="radiansY">The Y angle, in radians.</param>
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
public ProjectiveTransformBuilder PrependSkewRadians(float radiansX, float radiansY)
=> this.Prepend(size => new Matrix4x4(TransformUtils.CreateSkewMatrixRadians(radiansX, radiansY, size)));
=> this.Prepend(size => new Matrix4x4(TransformUtilities.CreateSkewMatrixRadians(radiansX, radiansY, size)));
/// <summary>
/// Prepends a skew matrix using the given angles in degrees at the given origin.
@ -205,7 +205,7 @@ namespace SixLabors.ImageSharp.Processing
/// <param name="radiansY">The Y angle, in radians.</param>
/// <returns>The <see cref="ProjectiveTransformBuilder"/>.</returns>
public ProjectiveTransformBuilder AppendSkewRadians(float radiansX, float radiansY)
=> this.Append(size => new Matrix4x4(TransformUtils.CreateSkewMatrixRadians(radiansX, radiansY, size)));
=> this.Append(size => new Matrix4x4(TransformUtilities.CreateSkewMatrixRadians(radiansX, radiansY, size)));
/// <summary>
/// Appends a skew matrix using the given angles in degrees at the given origin.

26
tests/ImageSharp.Benchmarks/Samplers/Diffuse.cs

@ -34,32 +34,6 @@ namespace SixLabors.ImageSharp.Benchmarks.Samplers
}
}
// #### 25th October 2019 ####
//
// BenchmarkDotNet=v0.11.5, OS=Windows 10.0.18362
// Intel Core i7-8650U CPU 1.90GHz(Kaby Lake R), 1 CPU, 8 logical and 4 physical cores
// .NET Core SDK = 3.0.100
//
// [Host] : .NET Core 2.1.13 (CoreCLR 4.6.28008.01, CoreFX 4.6.28008.01), 64bit RyuJIT
// Clr : .NET Framework 4.7.2 (CLR 4.0.30319.42000), 64bit RyuJIT-v4.8.4018.0
// Core : .NET Core 2.1.13 (CoreCLR 4.6.28008.01, CoreFX 4.6.28008.01), 64bit RyuJIT
//
// IterationCount=3 LaunchCount=1 WarmupCount=3
//
// #### Before ####
//
// | Method | Job | Runtime | Mean | Error | StdDev | Gen 0 | Gen 1 | Gen 2 | Allocated |
// |---------- |----- |-------- |----------:|---------:|---------:|------:|------:|------:|----------:|
// | DoDiffuse | Clr | Clr | 129.58 ms | 24.60 ms | 1.349 ms | - | - | - | 6 KB |
// | DoDiffuse | Core | Core | 92.63 ms | 89.78 ms | 4.921 ms | - | - | - | 4.58 KB |
//
// #### After ####
//
// | Method | Job | Runtime | Mean | Error | StdDev | Gen 0 | Gen 1 | Gen 2 | Allocated |
// |---------- |----- |-------- |----------:|----------:|----------:|------:|------:|------:|----------:|
// | DoDiffuse | Clr | Clr | 124.93 ms | 33.297 ms | 1.8251 ms | - | - | - | 2 KB |
// | DoDiffuse | Core | Core | 89.63 ms | 9.895 ms | 0.5424 ms | - | - | - | 1.91 KB |
// #### 20th February 2020 ####
//
// BenchmarkDotNet=v0.12.0, OS=Windows 10.0.18363

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

@ -23,27 +23,21 @@ namespace SixLabors.ImageSharp.Benchmarks.Samplers
}
}
/*
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 |
--------- |-------- |---------:|----------:|----------:|----------:|
DoRotate | Clr | 85.19 ms | 13.379 ms | 0.7560 ms | 6 KB |
DoRotate | Core | 53.51 ms | 9.512 ms | 0.5375 ms | 4.29 KB |
#### AFTER ####:
Method | Runtime | Mean | Error | StdDev | Allocated |
--------- |-------- |---------:|---------:|---------:|----------:|
DoRotate | Clr | 77.08 ms | 23.97 ms | 1.354 ms | 6 KB |
DoRotate | Core | 40.36 ms | 47.43 ms | 2.680 ms | 4.36 KB |
*/
// #### 21th February 2020 ####
//
// BenchmarkDotNet=v0.12.0, OS=Windows 10.0.18363
// Intel Core i7-8650U CPU 1.90GHz(Kaby Lake R), 1 CPU, 8 logical and 4 physical cores
// .NET Core SDK = 3.1.101
//
// [Host] : .NET Core 3.1.1 (CoreCLR 4.700.19.60701, CoreFX 4.700.19.60801), X64 RyuJIT
// Job-HOGSNT : .NET Framework 4.8 (4.8.4121.0), X64 RyuJIT
// Job-FKDHXC : .NET Core 2.1.15 (CoreCLR 4.6.28325.01, CoreFX 4.6.28327.02), X64 RyuJIT
// Job-ODABAZ : .NET Core 3.1.1 (CoreCLR 4.700.19.60701, CoreFX 4.700.19.60801), X64 RyuJIT
//
// IterationCount=3 LaunchCount=1 WarmupCount=3
//
// | Method | Runtime | Mean | Error | StdDev | Gen 0 | Gen 1 | Gen 2 | Allocated |
// |--------- |-------------- |---------:|---------:|---------:|------:|------:|------:|----------:|
// | DoRotate | .NET 4.7.2 | 28.77 ms | 3.304 ms | 0.181 ms | - | - | - | 6.5 KB |
// | DoRotate | .NET Core 2.1 | 16.27 ms | 1.044 ms | 0.057 ms | - | - | - | 5.25 KB |
// | DoRotate | .NET Core 3.1 | 17.12 ms | 4.352 ms | 0.239 ms | - | - | - | 6.57 KB |

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

@ -2,7 +2,6 @@
// Licensed under the Apache License, Version 2.0.
using BenchmarkDotNet.Attributes;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
@ -24,27 +23,21 @@ namespace SixLabors.ImageSharp.Benchmarks.Samplers
}
}
/*
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 |
*/
// #### 21th February 2020 ####
//
// BenchmarkDotNet=v0.12.0, OS=Windows 10.0.18363
// Intel Core i7-8650U CPU 1.90GHz(Kaby Lake R), 1 CPU, 8 logical and 4 physical cores
// .NET Core SDK = 3.1.101
//
// [Host] : .NET Core 3.1.1 (CoreCLR 4.700.19.60701, CoreFX 4.700.19.60801), X64 RyuJIT
// Job-VKKTMF : .NET Framework 4.8 (4.8.4121.0), X64 RyuJIT
// Job-KTVRKR : .NET Core 2.1.15 (CoreCLR 4.6.28325.01, CoreFX 4.6.28327.02), X64 RyuJIT
// Job-EONWDB : .NET Core 3.1.1 (CoreCLR 4.700.19.60701, CoreFX 4.700.19.60801), X64 RyuJIT
//
// IterationCount=3 LaunchCount=1 WarmupCount=3
//
// | Method | Runtime | Mean | Error | StdDev | Gen 0 | Gen 1 | Gen 2 | Allocated |
// |------- |-------------- |---------:|----------:|---------:|------:|------:|------:|----------:|
// | DoSkew | .NET 4.7.2 | 24.60 ms | 33.971 ms | 1.862 ms | - | - | - | 6.5 KB |
// | DoSkew | .NET Core 2.1 | 12.13 ms | 2.256 ms | 0.124 ms | - | - | - | 5.21 KB |
// | DoSkew | .NET Core 3.1 | 12.83 ms | 1.442 ms | 0.079 ms | - | - | - | 6.57 KB |

3
tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeKernelMapTests.ReferenceKernelMap.cs

@ -26,7 +26,8 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
public ReferenceKernel GetKernel(int destinationIndex) => this.kernels[destinationIndex];
public static ReferenceKernelMap Calculate(IResampler sampler, int destinationSize, int sourceSize, bool normalize = true)
public static ReferenceKernelMap Calculate<TResampler>(in TResampler sampler, int destinationSize, int sourceSize, bool normalize = true)
where TResampler : struct, IResampler
{
double ratio = (double)sourceSize / destinationSize;
double scale = ratio;

122
tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeKernelMapTests.cs

@ -25,59 +25,60 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
/// <summary>
/// resamplerName, srcSize, destSize
/// </summary>
public static readonly TheoryData<string, int, int> KernelMapData = new TheoryData<string, int, int>
public static readonly TheoryData<IResampler, int, int> KernelMapData
= new TheoryData<IResampler, int, int>
{
{ nameof(KnownResamplers.Bicubic), 15, 10 },
{ nameof(KnownResamplers.Bicubic), 10, 15 },
{ nameof(KnownResamplers.Bicubic), 20, 20 },
{ nameof(KnownResamplers.Bicubic), 50, 40 },
{ nameof(KnownResamplers.Bicubic), 40, 50 },
{ nameof(KnownResamplers.Bicubic), 500, 200 },
{ nameof(KnownResamplers.Bicubic), 200, 500 },
{ nameof(KnownResamplers.Bicubic), 3032, 400 },
{ nameof(KnownResamplers.Bicubic), 10, 25 },
{ nameof(KnownResamplers.Lanczos3), 16, 12 },
{ nameof(KnownResamplers.Lanczos3), 12, 16 },
{ nameof(KnownResamplers.Lanczos3), 12, 9 },
{ nameof(KnownResamplers.Lanczos3), 9, 12 },
{ nameof(KnownResamplers.Lanczos3), 6, 8 },
{ nameof(KnownResamplers.Lanczos3), 8, 6 },
{ nameof(KnownResamplers.Lanczos3), 20, 12 },
{ nameof(KnownResamplers.Lanczos3), 5, 25 },
{ nameof(KnownResamplers.Lanczos3), 5, 50 },
{ nameof(KnownResamplers.Lanczos3), 25, 5 },
{ nameof(KnownResamplers.Lanczos3), 50, 5 },
{ nameof(KnownResamplers.Lanczos3), 49, 5 },
{ nameof(KnownResamplers.Lanczos3), 31, 5 },
{ nameof(KnownResamplers.Lanczos8), 500, 200 },
{ nameof(KnownResamplers.Lanczos8), 100, 10 },
{ nameof(KnownResamplers.Lanczos8), 100, 80 },
{ nameof(KnownResamplers.Lanczos8), 10, 100 },
{ KnownResamplers.Bicubic, 15, 10 },
{ KnownResamplers.Bicubic, 10, 15 },
{ KnownResamplers.Bicubic, 20, 20 },
{ KnownResamplers.Bicubic, 50, 40 },
{ KnownResamplers.Bicubic, 40, 50 },
{ KnownResamplers.Bicubic, 500, 200 },
{ KnownResamplers.Bicubic, 200, 500 },
{ KnownResamplers.Bicubic, 3032, 400 },
{ KnownResamplers.Bicubic, 10, 25 },
{ KnownResamplers.Lanczos3, 16, 12 },
{ KnownResamplers.Lanczos3, 12, 16 },
{ KnownResamplers.Lanczos3, 12, 9 },
{ KnownResamplers.Lanczos3, 9, 12 },
{ KnownResamplers.Lanczos3, 6, 8 },
{ KnownResamplers.Lanczos3, 8, 6 },
{ KnownResamplers.Lanczos3, 20, 12 },
{ KnownResamplers.Lanczos3, 5, 25 },
{ KnownResamplers.Lanczos3, 5, 50 },
{ KnownResamplers.Lanczos3, 25, 5 },
{ KnownResamplers.Lanczos3, 50, 5 },
{ KnownResamplers.Lanczos3, 49, 5 },
{ KnownResamplers.Lanczos3, 31, 5 },
{ KnownResamplers.Lanczos8, 500, 200 },
{ KnownResamplers.Lanczos8, 100, 10 },
{ KnownResamplers.Lanczos8, 100, 80 },
{ KnownResamplers.Lanczos8, 10, 100 },
// Resize_WorksWithAllResamplers_Rgba32_CalliphoraPartial_Box-0.5:
{ nameof(KnownResamplers.Box), 378, 149 },
{ nameof(KnownResamplers.Box), 349, 174 },
{ KnownResamplers.Box, 378, 149 },
{ KnownResamplers.Box, 349, 174 },
// Accuracy-related regression-test cases cherry-picked from GeneratedImageResizeData
{ nameof(KnownResamplers.Box), 201, 100 },
{ nameof(KnownResamplers.Box), 199, 99 },
{ nameof(KnownResamplers.Box), 10, 299 },
{ nameof(KnownResamplers.Box), 299, 10 },
{ nameof(KnownResamplers.Box), 301, 300 },
{ nameof(KnownResamplers.Box), 1180, 480 },
{ nameof(KnownResamplers.Lanczos2), 3264, 3032 },
{ nameof(KnownResamplers.Bicubic), 1280, 2240 },
{ nameof(KnownResamplers.Bicubic), 1920, 1680 },
{ nameof(KnownResamplers.Bicubic), 3072, 2240 },
{ nameof(KnownResamplers.Welch), 300, 2008 },
{ KnownResamplers.Box, 201, 100 },
{ KnownResamplers.Box, 199, 99 },
{ KnownResamplers.Box, 10, 299 },
{ KnownResamplers.Box, 299, 10 },
{ KnownResamplers.Box, 301, 300 },
{ KnownResamplers.Box, 1180, 480 },
{ KnownResamplers.Lanczos2, 3264, 3032 },
{ KnownResamplers.Bicubic, 1280, 2240 },
{ KnownResamplers.Bicubic, 1920, 1680 },
{ KnownResamplers.Bicubic, 3072, 2240 },
{ KnownResamplers.Welch, 300, 2008 },
// ResizeKernel.Length -related regression tests cherry-picked from GeneratedImageResizeData
{ nameof(KnownResamplers.Bicubic), 10, 50 },
{ nameof(KnownResamplers.Bicubic), 49, 301 },
{ nameof(KnownResamplers.Bicubic), 301, 49 },
{ nameof(KnownResamplers.Bicubic), 1680, 1200 },
{ nameof(KnownResamplers.Box), 13, 299 },
{ nameof(KnownResamplers.Lanczos5), 3032, 600 },
{ KnownResamplers.Bicubic, 10, 50 },
{ KnownResamplers.Bicubic, 49, 301 },
{ KnownResamplers.Bicubic, 301, 49 },
{ KnownResamplers.Bicubic, 1680, 1200 },
{ KnownResamplers.Box, 13, 299 },
{ KnownResamplers.Lanczos5, 3032, 600 },
};
public static TheoryData<string, int, int> GeneratedImageResizeData =
@ -85,20 +86,20 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
[Theory(Skip = "Only for debugging and development")]
[MemberData(nameof(KernelMapData))]
public void PrintNonNormalizedKernelMap(string resamplerName, int srcSize, int destSize)
public void PrintNonNormalizedKernelMap<TResampler>(TResampler resampler, int srcSize, int destSize)
where TResampler : struct, IResampler
{
IResampler resampler = TestUtils.GetResampler(resamplerName);
var kernelMap = ReferenceKernelMap.Calculate(resampler, destSize, srcSize, false);
var kernelMap = ReferenceKernelMap.Calculate<TResampler>(in resampler, destSize, srcSize, false);
this.Output.WriteLine($"Actual KernelMap:\n{PrintKernelMap(kernelMap)}\n");
}
[Theory]
[MemberData(nameof(KernelMapData))]
public void KernelMapContentIsCorrect(string resamplerName, int srcSize, int destSize)
public void KernelMapContentIsCorrect<TResampler>(TResampler resampler, int srcSize, int destSize)
where TResampler : struct, IResampler
{
this.VerifyKernelMapContentIsCorrect(resamplerName, srcSize, destSize);
this.VerifyKernelMapContentIsCorrect(resampler, srcSize, destSize);
}
// Comprehensive but expensive tests, for ResizeKernelMap.
@ -113,12 +114,11 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
}
#endif
private void VerifyKernelMapContentIsCorrect(string resamplerName, int srcSize, int destSize)
private void VerifyKernelMapContentIsCorrect<TResampler>(TResampler resampler, int srcSize, int destSize)
where TResampler : struct, IResampler
{
IResampler resampler = TestUtils.GetResampler(resamplerName);
var referenceMap = ReferenceKernelMap.Calculate(resampler, destSize, srcSize);
var kernelMap = ResizeKernelMap.Calculate(resampler, destSize, srcSize, Configuration.Default.MemoryAllocator);
var referenceMap = ReferenceKernelMap.Calculate(in resampler, destSize, srcSize);
var kernelMap = ResizeKernelMap.Calculate(in resampler, destSize, srcSize, Configuration.Default.MemoryAllocator);
#if DEBUG
this.Output.WriteLine(kernelMap.Info);
@ -153,11 +153,11 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
}
}
private static string PrintKernelMap(ResizeKernelMap kernelMap) =>
PrintKernelMap(kernelMap, km => km.DestinationLength, (km, i) => km.GetKernel(i));
private static string PrintKernelMap(ResizeKernelMap kernelMap)
=> PrintKernelMap(kernelMap, km => km.DestinationLength, (km, i) => km.GetKernel(i));
private static string PrintKernelMap(ReferenceKernelMap kernelMap) =>
PrintKernelMap(kernelMap, km => km.DestinationSize, (km, i) => km.GetKernel(i));
private static string PrintKernelMap(ReferenceKernelMap kernelMap)
=> PrintKernelMap(kernelMap, km => km.DestinationSize, (km, i) => km.GetKernel(i));
private static string PrintKernelMap<TKernelMap>(
TKernelMap kernelMap,

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

@ -121,8 +121,8 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms
configuration.MemoryAllocator = allocator;
configuration.WorkingBufferSizeHintInBytes = workingBufferSizeHintInBytes;
var verticalKernelMap = ResizeKernelMap.Calculate(
KnownResamplers.Bicubic,
var verticalKernelMap = ResizeKernelMap.Calculate<BicubicResampler>(
default,
destSize.Height,
image0.Height,
Configuration.Default.MemoryAllocator);

8
tests/ImageSharp.Tests/Processing/Transforms/PadTest.cs

@ -1,4 +1,4 @@
// Copyright (c) Six Labors and contributors.
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using SixLabors.ImageSharp.Processing;
@ -20,9 +20,9 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms
this.operations.Pad(width, height);
ResizeProcessor resizeProcessor = this.Verify<ResizeProcessor>();
Assert.Equal(width, resizeProcessor.TargetWidth);
Assert.Equal(height, resizeProcessor.TargetHeight);
Assert.Equal(width, resizeProcessor.DestinationWidth);
Assert.Equal(height, resizeProcessor.DestinationHeight);
Assert.Equal(sampler, resizeProcessor.Sampler);
}
}
}
}

16
tests/ImageSharp.Tests/Processing/Transforms/ResizeTests.cs

@ -17,8 +17,8 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms
this.operations.Resize(width, height);
ResizeProcessor resizeProcessor = this.Verify<ResizeProcessor>();
Assert.Equal(width, resizeProcessor.TargetWidth);
Assert.Equal(height, resizeProcessor.TargetHeight);
Assert.Equal(width, resizeProcessor.DestinationWidth);
Assert.Equal(height, resizeProcessor.DestinationHeight);
}
[Fact]
@ -30,8 +30,8 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms
this.operations.Resize(width, height, sampler);
ResizeProcessor resizeProcessor = this.Verify<ResizeProcessor>();
Assert.Equal(width, resizeProcessor.TargetWidth);
Assert.Equal(height, resizeProcessor.TargetHeight);
Assert.Equal(width, resizeProcessor.DestinationWidth);
Assert.Equal(height, resizeProcessor.DestinationHeight);
Assert.Equal(sampler, resizeProcessor.Sampler);
}
@ -47,8 +47,8 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms
this.operations.Resize(width, height, sampler, compand);
ResizeProcessor resizeProcessor = this.Verify<ResizeProcessor>();
Assert.Equal(width, resizeProcessor.TargetWidth);
Assert.Equal(height, resizeProcessor.TargetHeight);
Assert.Equal(width, resizeProcessor.DestinationWidth);
Assert.Equal(height, resizeProcessor.DestinationHeight);
Assert.Equal(sampler, resizeProcessor.Sampler);
Assert.Equal(compand, resizeProcessor.Compand);
}
@ -73,8 +73,8 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms
this.operations.Resize(resizeOptions);
ResizeProcessor resizeProcessor = this.Verify<ResizeProcessor>();
Assert.Equal(width, resizeProcessor.TargetWidth);
Assert.Equal(height, resizeProcessor.TargetHeight);
Assert.Equal(width, resizeProcessor.DestinationWidth);
Assert.Equal(height, resizeProcessor.DestinationHeight);
Assert.Equal(sampler, resizeProcessor.Sampler);
Assert.Equal(compand, resizeProcessor.Compand);

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

@ -99,7 +99,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms
this.AppendRotationDegrees(builder, degrees);
// TODO: We should also test CreateRotationMatrixDegrees() (and all TransformUtils stuff!) for correctness
Matrix3x2 matrix = TransformUtils.CreateRotationMatrixDegrees(degrees, size);
Matrix3x2 matrix = TransformUtilities.CreateRotationMatrixDegrees(degrees, size);
var position = new Vector2(x, y);
var expected = Vector2.Transform(position, matrix);
@ -153,7 +153,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms
this.AppendSkewDegrees(builder, degreesX, degreesY);
Matrix3x2 matrix = TransformUtils.CreateSkewMatrixDegrees(degreesX, degreesY, size);
Matrix3x2 matrix = TransformUtilities.CreateSkewMatrixDegrees(degreesX, degreesY, size);
var position = new Vector2(x, y);
var expected = Vector2.Transform(position, matrix);

2
tests/Images/External

@ -1 +1 @@
Subproject commit f9b4bfe42cacb3eefab02ada92ac771a9b93c080
Subproject commit f8a76fd3a900b90c98df67ac896574383a4d09f3
Loading…
Cancel
Save