Browse Source

Better Rotate

pull/775/head
James Jackson-South 7 years ago
parent
commit
dfc926e569
  1. 204
      src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor.cs
  2. 239
      src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessorOld.cs
  3. 2
      src/ImageSharp/Processing/Processors/Transforms/CenteredAffineTransformProcessor.cs
  4. 16
      src/ImageSharp/Processing/Processors/Transforms/RotateProcessor.cs
  5. 188
      src/ImageSharp/Processing/Processors/Transforms/TransformKernelMap.cs
  6. 121
      src/ImageSharp/Processing/Processors/Transforms/TransformUtils.cs
  7. 6
      src/ImageSharp/Processing/TransformExtensions.cs

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

@ -5,13 +5,9 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Numerics; using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.ParallelUtils; using SixLabors.ImageSharp.ParallelUtils;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using SixLabors.Memory;
using SixLabors.Primitives; using SixLabors.Primitives;
namespace SixLabors.ImageSharp.Processing.Processors.Transforms namespace SixLabors.ImageSharp.Processing.Processors.Transforms
@ -20,29 +16,42 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
/// Provides the base methods to perform affine transforms on an image. /// Provides the base methods to perform affine transforms on an image.
/// </summary> /// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam> /// <typeparam name="TPixel">The pixel format.</typeparam>
internal class AffineTransformProcessor<TPixel> : InterpolatedTransformProcessorBase<TPixel> internal class AffineTransformProcessor<TPixel> : TransformProcessorBase<TPixel>
where TPixel : struct, IPixel<TPixel> where TPixel : struct, IPixel<TPixel>
{ {
private readonly Rectangle transformedRectangle;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="AffineTransformProcessor{TPixel}"/> class. /// Initializes a new instance of the <see cref="AffineTransformProcessor{TPixel}"/> class.
/// </summary> /// </summary>
/// <param name="matrix">The transform matrix</param> /// <param name="matrix">The transform matrix</param>
/// <param name="sampler">The sampler to perform the transform operation.</param> /// <param name="sampler">The sampler to perform the transform operation.</param>
/// <param name="targetDimensions">The target dimensions to constrain the transformed image to.</param> /// <param name="sourceSize">The source image size</param>
public AffineTransformProcessor(Matrix3x2 matrix, IResampler sampler, Size targetDimensions) public AffineTransformProcessor(Matrix3x2 matrix, IResampler sampler, Size sourceSize)
: base(sampler)
{ {
Guard.NotNull(sampler, nameof(sampler));
this.Sampler = sampler;
this.TransformMatrix = matrix; this.TransformMatrix = matrix;
this.TargetDimensions = targetDimensions; this.transformedRectangle = TransformUtils.GetTransformedRectangle(
new Rectangle(Point.Empty, sourceSize),
matrix);
// We want to resize the canvas here taking into account any translations.
this.TargetDimensions = new Size(this.transformedRectangle.Right, this.transformedRectangle.Bottom);
} }
/// <summary> /// <summary>
/// Gets the matrix used to supply the affine transform /// Gets the sampler to perform interpolation of the transform operation.
/// </summary>
public IResampler Sampler { get; }
/// <summary>
/// Gets the matrix used to supply the affine transform.
/// </summary> /// </summary>
public Matrix3x2 TransformMatrix { get; } public Matrix3x2 TransformMatrix { get; }
/// <summary> /// <summary>
/// Gets the target dimensions to constrain the transformed image to /// Gets the target dimensions to constrain the transformed image to.
/// </summary> /// </summary>
public Size TargetDimensions { get; } public Size TargetDimensions { get; }
@ -68,13 +77,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
int width = this.TargetDimensions.Width; int width = this.TargetDimensions.Width;
Rectangle sourceBounds = source.Bounds(); Rectangle sourceBounds = source.Bounds();
var targetBounds = new Rectangle(0, 0, width, height); var targetBounds = new Rectangle(Point.Empty, this.TargetDimensions);
// Since could potentially be resizing the canvas we might need to re-calculate the matrix
Matrix3x2 matrix = this.GetProcessingMatrix(sourceBounds, targetBounds);
// Convert from screen to world space. // Convert from screen to world space.
Matrix3x2.Invert(matrix, out matrix); Matrix3x2.Invert(this.TransformMatrix, out Matrix3x2 matrix);
if (this.Sampler is NearestNeighborResampler) if (this.Sampler is NearestNeighborResampler)
{ {
@ -82,158 +88,52 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
targetBounds, targetBounds,
configuration, configuration,
rows => rows =>
{
for (int y = rows.Min; y < rows.Max; y++)
{ {
for (int y = rows.Min; y < rows.Max; y++) Span<TPixel> destRow = destination.GetPixelRowSpan(y);
{
Span<TPixel> destRow = destination.GetPixelRowSpan(y);
for (int x = 0; x < width; x++) for (int x = 0; x < width; x++)
{
var point = Point.Transform(new Point(x, y), matrix);
if (sourceBounds.Contains(point.X, point.Y))
{ {
var point = Point.Transform(new Point(x, y), matrix); destRow[x] = source[point.X, point.Y];
if (sourceBounds.Contains(point.X, point.Y))
{
destRow[x] = source[point.X, point.Y];
}
} }
} }
}); }
});
return; return;
} }
int maxSourceX = source.Width - 1; using (var kernel = new TransformKernelMap(configuration, source.Size(), destination.Size(), this.Sampler))
int maxSourceY = source.Height - 1;
(float radius, float scale, float ratio) xRadiusScale = this.GetSamplingRadius(source.Width, destination.Width);
(float radius, float scale, float ratio) yRadiusScale = this.GetSamplingRadius(source.Height, destination.Height);
float xScale = xRadiusScale.scale;
float yScale = yRadiusScale.scale;
var radius = new Vector2(xRadiusScale.radius, yRadiusScale.radius);
IResampler sampler = this.Sampler;
var maxSource = new Vector4(maxSourceX, maxSourceY, maxSourceX, maxSourceY);
int xLength = (int)MathF.Ceiling((radius.X * 2) + 2);
int yLength = (int)MathF.Ceiling((radius.Y * 2) + 2);
MemoryAllocator memoryAllocator = configuration.MemoryAllocator;
using (Buffer2D<float> yBuffer = memoryAllocator.Allocate2D<float>(yLength, height))
using (Buffer2D<float> xBuffer = memoryAllocator.Allocate2D<float>(xLength, height))
{ {
ParallelHelper.IterateRows( ParallelHelper.IterateRowsWithTempBuffer<Vector4>(
targetBounds, targetBounds,
configuration, configuration,
rows => (rows, vectorBuffer) =>
{
Span<Vector4> vectorSpan = vectorBuffer.Span;
for (int y = rows.Min; y < rows.Max; y++)
{ {
for (int y = rows.Min; y < rows.Max; y++) Span<TPixel> targetRowSpan = destination.GetPixelRowSpan(y);
{ PixelOperations<TPixel>.Instance.ToVector4(configuration, targetRowSpan, vectorSpan);
ref TPixel destRowRef = ref MemoryMarshal.GetReference(destination.GetPixelRowSpan(y)); ref float ySpanRef = ref kernel.GetYStartReference(y);
ref float ySpanRef = ref MemoryMarshal.GetReference(yBuffer.GetRowSpan(y)); ref float xSpanRef = ref kernel.GetXStartReference(y);
ref float xSpanRef = ref MemoryMarshal.GetReference(xBuffer.GetRowSpan(y));
for (int x = 0; x < width; x++) for (int x = 0; x < width; x++)
{ {
// Use the single precision position to calculate correct bounding pixels // Use the single precision position to calculate correct bounding pixels
// otherwise we get rogue pixels outside of the bounds. // otherwise we get rogue pixels outside of the bounds.
var point = Vector2.Transform(new Vector2(x, y), matrix); var point = Vector2.Transform(new Vector2(x, y), matrix);
kernel.Convolve(point, x, ref ySpanRef, ref xSpanRef, source.PixelBuffer, vectorSpan);
// Clamp sampling pixel radial extents to the source image edges
Vector2 maxXY = point + radius;
Vector2 minXY = point - radius;
// max, maxY, minX, minY
var extents = new Vector4(
MathF.Floor(maxXY.X + .5F),
MathF.Floor(maxXY.Y + .5F),
MathF.Ceiling(minXY.X - .5F),
MathF.Ceiling(minXY.Y - .5F));
int right = (int)extents.X;
int bottom = (int)extents.Y;
int left = (int)extents.Z;
int top = (int)extents.W;
extents = Vector4.Clamp(extents, Vector4.Zero, maxSource);
int maxX = (int)extents.X;
int maxY = (int)extents.Y;
int minX = (int)extents.Z;
int minY = (int)extents.W;
if (minX == maxX || minY == maxY)
{
continue;
}
// It appears these have to be calculated on-the-fly.
// Precalculating transformed weights would require prior knowledge of every transformed pixel location
// since they can be at sub-pixel positions on both axis.
// I've optimized where I can but am always open to suggestions.
if (yScale > 1 && xScale > 1)
{
CalculateWeightsDown(
top,
bottom,
minY,
maxY,
point.Y,
sampler,
yScale,
ref ySpanRef,
yLength);
CalculateWeightsDown(
left,
right,
minX,
maxX,
point.X,
sampler,
xScale,
ref xSpanRef,
xLength);
}
else
{
CalculateWeightsScaleUp(minY, maxY, point.Y, sampler, ref ySpanRef);
CalculateWeightsScaleUp(minX, maxX, point.X, sampler, ref xSpanRef);
}
// Now multiply the results against the offsets
Vector4 sum = Vector4.Zero;
for (int yy = 0, j = minY; j <= maxY; j++, yy++)
{
float yWeight = Unsafe.Add(ref ySpanRef, yy);
for (int xx = 0, i = minX; i <= maxX; i++, xx++)
{
float xWeight = Unsafe.Add(ref xSpanRef, xx);
// Values are first premultiplied to prevent darkening of edge pixels
var current = source[i, j].ToVector4();
Vector4Utils.Premultiply(ref current);
sum += current * xWeight * yWeight;
}
}
ref TPixel dest = ref Unsafe.Add(ref destRowRef, x);
// Reverse the premultiplication
Vector4Utils.UnPremultiply(ref sum);
dest.FromVector4(sum);
}
} }
});
PixelOperations<TPixel>.Instance.FromVector4(configuration, vectorSpan, targetRowSpan);
}
});
} }
} }
/// <summary>
/// Gets a transform matrix adjusted for final processing based upon the target image bounds.
/// </summary>
/// <param name="sourceRectangle">The source image bounds.</param>
/// <param name="destinationRectangle">The destination image bounds.</param>
/// <returns>
/// The <see cref="Matrix3x2"/>.
/// </returns>
protected virtual Matrix3x2 GetProcessingMatrix(Rectangle sourceRectangle, Rectangle destinationRectangle)
=> this.TransformMatrix;
} }
} }

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

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

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

@ -11,7 +11,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
/// A base class that provides methods to allow the automatic centering of affine transforms /// A base class that provides methods to allow the automatic centering of affine transforms
/// </summary> /// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam> /// <typeparam name="TPixel">The pixel format.</typeparam>
internal abstract class CenteredAffineTransformProcessor<TPixel> : AffineTransformProcessor<TPixel> internal abstract class CenteredAffineTransformProcessor<TPixel> : AffineTransformProcessorOld<TPixel>
where TPixel : struct, IPixel<TPixel> where TPixel : struct, IPixel<TPixel>
{ {
/// <summary> /// <summary>

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

@ -2,12 +2,10 @@
// Licensed under the Apache License, Version 2.0. // Licensed under the Apache License, Version 2.0.
using System; using System;
using System.Threading.Tasks;
using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.MetaData.Profiles.Exif; using SixLabors.ImageSharp.MetaData.Profiles.Exif;
using SixLabors.ImageSharp.ParallelUtils; using SixLabors.ImageSharp.ParallelUtils;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Processors.Transforms;
using SixLabors.Primitives; using SixLabors.Primitives;
namespace SixLabors.ImageSharp.Processing.Processors.Transforms namespace SixLabors.ImageSharp.Processing.Processors.Transforms
@ -16,7 +14,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
/// Provides methods that allow the rotating of images. /// Provides methods that allow the rotating of images.
/// </summary> /// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam> /// <typeparam name="TPixel">The pixel format.</typeparam>
internal class RotateProcessor<TPixel> : CenteredAffineTransformProcessor<TPixel> internal class RotateProcessor<TPixel> : AffineTransformProcessor<TPixel>
where TPixel : struct, IPixel<TPixel> where TPixel : struct, IPixel<TPixel>
{ {
/// <summary> /// <summary>
@ -36,10 +34,11 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
/// <param name="sampler">The sampler to perform the rotating operation.</param> /// <param name="sampler">The sampler to perform the rotating operation.</param>
/// <param name="sourceSize">The source image size</param> /// <param name="sourceSize">The source image size</param>
public RotateProcessor(float degrees, IResampler sampler, Size sourceSize) public RotateProcessor(float degrees, IResampler sampler, Size sourceSize)
: base(Matrix3x2Extensions.CreateRotationDegrees(degrees, PointF.Empty), sampler, sourceSize) : base(
{ TransformUtils.CreateCenteredRotationMatrixDegrees(degrees, sourceSize),
this.Degrees = degrees; sampler,
} sourceSize)
=> this.Degrees = degrees;
/// <summary> /// <summary>
/// Gets the angle of rotation in degrees. /// Gets the angle of rotation in degrees.
@ -84,7 +83,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
/// <returns>The <see cref="float"/></returns> /// <returns>The <see cref="float"/></returns>
private static float WrapDegrees(float degrees) private static float WrapDegrees(float degrees)
{ {
degrees = degrees % 360; degrees %= 360;
while (degrees < 0) while (degrees < 0)
{ {
@ -223,7 +222,6 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms
int newX = height - y - 1; int newX = height - y - 1;
for (int x = 0; x < width; x++) for (int x = 0; x < width; x++)
{ {
// TODO: Optimize this:
if (destinationBounds.Contains(newX, x)) if (destinationBounds.Contains(newX, x))
{ {
destination[newX, x] = sourceRow[x]; destination[newX, x] = sourceRow[x];

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

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

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

@ -0,0 +1,121 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Numerics;
using SixLabors.Primitives;
namespace SixLabors.ImageSharp.Processing.Processors.Transforms
{
/// <summary>
/// Contains utility methods for working with transforms.
/// </summary>
public static class TransformUtils
{
/// <summary>
/// Creates a centered rotation matrix using the given rotation in degrees and the source size.
/// </summary>
/// <param name="degrees">The amount of rotation, in degrees.</param>
/// <param name="size">The source image size.</param>
/// <returns>The <see cref="Matrix3x2"/>.</returns>
public static Matrix3x2 CreateCenteredRotationMatrixDegrees(float degrees, Size size)
=> CreateCenteredTransformMatrix(
new Rectangle(Point.Empty, size),
Matrix3x2Extensions.CreateRotationDegrees(degrees, PointF.Empty));
/// <summary>
/// Gets the centered transform matrix based upon the source and destination rectangles.
/// </summary>
/// <param name="sourceRectangle">The source image bounds.</param>
/// <param name="matrix">The transformation matrix.</param>
/// <returns>The <see cref="Matrix3x2"/></returns>
public static Matrix3x2 CreateCenteredTransformMatrix(Rectangle sourceRectangle, Matrix3x2 matrix)
{
Rectangle destinationRectangle = GetTransformedBoundingRectangle(sourceRectangle, matrix);
// We invert the matrix to handle the transformation from screen to world space.
// This ensures scaling matrices are correct.
Matrix3x2.Invert(matrix, out Matrix3x2 inverted);
var translationToTargetCenter = Matrix3x2.CreateTranslation(new Vector2(-destinationRectangle.Width, -destinationRectangle.Height) * .5F);
var translateToSourceCenter = Matrix3x2.CreateTranslation(new Vector2(sourceRectangle.Width, sourceRectangle.Height) * .5F);
// Translate back to world space.
Matrix3x2.Invert(translationToTargetCenter * inverted * translateToSourceCenter, out Matrix3x2 centered);
return centered;
}
/// <summary>
/// Gets the centered transform matrix based upon the source and destination rectangles.
/// </summary>
/// <param name="sourceRectangle">The source image bounds.</param>
/// <param name="destinationRectangle">The destination image bounds.</param>
/// <param name="matrix">The transformation matrix.</param>
/// <returns>The <see cref="Matrix3x2"/></returns>
public static Matrix3x2 GetCenteredTransformMatrix(Rectangle sourceRectangle, Rectangle destinationRectangle, Matrix3x2 matrix)
{
// We invert the matrix to handle the transformation from screen to world space.
// This ensures scaling matrices are correct.
Matrix3x2.Invert(matrix, out Matrix3x2 inverted);
var translationToTargetCenter = Matrix3x2.CreateTranslation(new Vector2(-destinationRectangle.Width, -destinationRectangle.Height) * .5F);
var translateToSourceCenter = Matrix3x2.CreateTranslation(new Vector2(sourceRectangle.Width, sourceRectangle.Height) * .5F);
// Translate back to world space.
Matrix3x2.Invert(translationToTargetCenter * inverted * translateToSourceCenter, out Matrix3x2 centered);
return centered;
}
/// <summary>
/// Returns the rectangle bounds relative to the source for the given transformation matrix.
/// </summary>
/// <param name="rectangle">The source rectangle.</param>
/// <param name="matrix">The transformation matrix.</param>
/// <returns>
/// The <see cref="Rectangle"/>.
/// </returns>
public static Rectangle GetTransformedBoundingRectangle(Rectangle rectangle, Matrix3x2 matrix)
{
Rectangle transformed = GetTransformedRectangle(rectangle, matrix);
// TODO: Check this.
return new Rectangle(0, 0, transformed.Width, transformed.Height);
}
/// <summary>
/// Returns the rectangle relative to the source for the given transformation matrix.
/// </summary>
/// <param name="rectangle">The source rectangle.</param>
/// <param name="matrix">The transformation matrix.</param>
/// <returns>
/// The <see cref="Rectangle"/>.
/// </returns>
public static Rectangle GetTransformedRectangle(Rectangle rectangle, Matrix3x2 matrix)
{
if (rectangle.Equals(default) || Matrix3x2.Identity.Equals(matrix))
{
return rectangle;
}
var tl = Vector2.Transform(new Vector2(rectangle.Left, rectangle.Top), matrix);
var tr = Vector2.Transform(new Vector2(rectangle.Right, rectangle.Top), matrix);
var bl = Vector2.Transform(new Vector2(rectangle.Left, rectangle.Bottom), matrix);
var br = Vector2.Transform(new Vector2(rectangle.Right, rectangle.Bottom), matrix);
return GetBoundingRectangle(tl, tr, bl, br);
}
private static Rectangle GetBoundingRectangle(Vector2 tl, Vector2 tr, Vector2 bl, Vector2 br)
{
// Find the minimum and maximum "corners" based on the given vectors
float left = MathF.Min(tl.X, MathF.Min(tr.X, MathF.Min(bl.X, br.X)));
float top = MathF.Min(tl.Y, MathF.Min(tr.Y, MathF.Min(bl.Y, br.Y)));
float right = MathF.Max(tl.X, MathF.Max(tr.X, MathF.Max(bl.X, br.X)));
float bottom = MathF.Max(tl.Y, MathF.Max(tr.Y, MathF.Max(bl.Y, br.Y)));
return Rectangle.Round(RectangleF.FromLTRB(left, top, right, bottom));
}
}
}

6
src/ImageSharp/Processing/TransformExtensions.cs

@ -34,7 +34,7 @@ namespace SixLabors.ImageSharp.Processing
/// <returns>The <see cref="Image{TPixel}"/></returns> /// <returns>The <see cref="Image{TPixel}"/></returns>
public static IImageProcessingContext<TPixel> Transform<TPixel>(this IImageProcessingContext<TPixel> source, Matrix3x2 matrix, IResampler sampler) public static IImageProcessingContext<TPixel> Transform<TPixel>(this IImageProcessingContext<TPixel> source, Matrix3x2 matrix, IResampler sampler)
where TPixel : struct, IPixel<TPixel> where TPixel : struct, IPixel<TPixel>
=> source.ApplyProcessor(new AffineTransformProcessor<TPixel>(matrix, sampler, source.GetCurrentSize())); => source.ApplyProcessor(new AffineTransformProcessorOld<TPixel>(matrix, sampler, source.GetCurrentSize()));
/// <summary> /// <summary>
/// Transforms an image by the given matrix using the specified sampling algorithm /// Transforms an image by the given matrix using the specified sampling algorithm
@ -57,7 +57,7 @@ namespace SixLabors.ImageSharp.Processing
{ {
var t = Matrix3x2.CreateTranslation(-rectangle.Location); var t = Matrix3x2.CreateTranslation(-rectangle.Location);
Matrix3x2 combinedMatrix = t * matrix; Matrix3x2 combinedMatrix = t * matrix;
return source.ApplyProcessor(new AffineTransformProcessor<TPixel>(combinedMatrix, sampler, rectangle.Size)); return source.ApplyProcessor(new AffineTransformProcessorOld<TPixel>(combinedMatrix, sampler, rectangle.Size));
} }
/// <summary> /// <summary>
@ -76,7 +76,7 @@ namespace SixLabors.ImageSharp.Processing
IResampler sampler, IResampler sampler,
Size destinationSize) Size destinationSize)
where TPixel : struct, IPixel<TPixel> where TPixel : struct, IPixel<TPixel>
=> source.ApplyProcessor(new AffineTransformProcessor<TPixel>(matrix, sampler, destinationSize)); => source.ApplyProcessor(new AffineTransformProcessorOld<TPixel>(matrix, sampler, destinationSize));
/// <summary> /// <summary>
/// Transforms an image by the given matrix. /// Transforms an image by the given matrix.

Loading…
Cancel
Save