mirror of https://github.com/SixLabors/ImageSharp
29 changed files with 1141 additions and 546 deletions
@ -0,0 +1,263 @@ |
|||
// 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 |
|||
{ |
|||
/// <content>
|
|||
/// Extensions for <see cref="IResampler"/>.
|
|||
/// </content>
|
|||
public static partial class ResamplerExtensions |
|||
{ |
|||
private readonly struct NNAffineOperation<TPixel> : IRowIntervalOperation |
|||
where TPixel : struct, IPixel<TPixel> |
|||
{ |
|||
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 = destination.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 NNProjectiveOperation<TPixel> : IRowIntervalOperation |
|||
where TPixel : struct, IPixel<TPixel> |
|||
{ |
|||
private readonly ImageFrame<TPixel> source; |
|||
private readonly ImageFrame<TPixel> destination; |
|||
private readonly Rectangle bounds; |
|||
private readonly Matrix4x4 matrix; |
|||
private readonly int maxX; |
|||
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public NNProjectiveOperation( |
|||
ImageFrame<TPixel> source, |
|||
ImageFrame<TPixel> destination, |
|||
Matrix4x4 matrix) |
|||
{ |
|||
this.source = source; |
|||
this.destination = destination; |
|||
this.bounds = destination.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++) |
|||
{ |
|||
Vector2 point = TransformUtilities.ProjectiveTransform2D(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, TPixel> : IRowIntervalOperation<Vector4> |
|||
where TResampler : unmanaged, IResampler |
|||
where TPixel : struct, IPixel<TPixel> |
|||
{ |
|||
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); |
|||
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)); |
|||
} |
|||
} |
|||
} |
|||
|
|||
private readonly struct ProjectiveOperation<TResampler, TPixel> : IRowIntervalOperation<Vector4> |
|||
where TResampler : unmanaged, IResampler |
|||
where TPixel : struct, IPixel<TPixel> |
|||
{ |
|||
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 Matrix4x4 matrix; |
|||
private readonly Vector2 radialExtents; |
|||
private readonly Vector4 maxSourceExtents; |
|||
private readonly int maxX; |
|||
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public ProjectiveOperation( |
|||
Configuration configuration, |
|||
ImageFrame<TPixel> source, |
|||
ImageFrame<TPixel> destination, |
|||
Buffer2D<float> yKernelBuffer, |
|||
Buffer2D<float> xKernelBuffer, |
|||
in TResampler sampler, |
|||
Matrix4x4 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.
|
|||
Vector2 point = TransformUtilities.ProjectiveTransform2D(x, y, this.matrix); |
|||
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,0 +1,249 @@ |
|||
// 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>
|
|||
/// Extensions for <see cref="IResampler"/>.
|
|||
/// </summary>
|
|||
public static partial class ResamplerExtensions |
|||
{ |
|||
/// <summary>
|
|||
/// Applies an affine transformation upon an image.
|
|||
/// </summary>
|
|||
/// <typeparam name="TResampler">The type of sampler.</typeparam>
|
|||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
|||
/// <param name="configuration">The configuration.</param>
|
|||
/// <param name="sampler">The pixel sampler.</param>
|
|||
/// <param name="source">The source image frame.</param>
|
|||
/// <param name="destination">The destination image frame.</param>
|
|||
/// <param name="matrix">The transform matrix.</param>
|
|||
public static void ApplyAffineTransform<TResampler, TPixel>( |
|||
Configuration configuration, |
|||
in TResampler sampler, |
|||
ImageFrame<TPixel> source, |
|||
ImageFrame<TPixel> destination, |
|||
Matrix3x2 matrix) |
|||
where TResampler : unmanaged, IResampler |
|||
where TPixel : struct, IPixel<TPixel> |
|||
{ |
|||
// 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.GetPixelSpan().CopyTo(destination.GetPixelSpan()); |
|||
return; |
|||
} |
|||
|
|||
// Convert from screen to world space.
|
|||
Matrix3x2.Invert(matrix, out matrix); |
|||
|
|||
if (sampler is NearestNeighborResampler) |
|||
{ |
|||
var nnOperation = new NNAffineOperation<TPixel>(source, destination, matrix); |
|||
ParallelRowIterator.IterateRows( |
|||
configuration, |
|||
destination.Bounds(), |
|||
in nnOperation); |
|||
|
|||
return; |
|||
} |
|||
|
|||
int yRadius = GetSamplingRadius(in sampler, source.Height, destination.Height); |
|||
int xRadius = 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, TPixel>( |
|||
configuration, |
|||
source, |
|||
destination, |
|||
yKernelBuffer, |
|||
xKernelBuffer, |
|||
in sampler, |
|||
matrix, |
|||
radialExtents, |
|||
maxSourceExtents); |
|||
|
|||
ParallelRowIterator.IterateRows<AffineOperation<TResampler, TPixel>, Vector4>( |
|||
configuration, |
|||
destination.Bounds(), |
|||
in operation); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Applies a projective transformation upon an image.
|
|||
/// </summary>
|
|||
/// <typeparam name="TResampler">The type of sampler.</typeparam>
|
|||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
|||
/// <param name="configuration">The configuration.</param>
|
|||
/// <param name="sampler">The pixel sampler.</param>
|
|||
/// <param name="source">The source image frame.</param>
|
|||
/// <param name="destination">The destination image frame.</param>
|
|||
/// <param name="matrix">The transform matrix.</param>
|
|||
public static void ApplyProjectiveTransform<TResampler, TPixel>( |
|||
Configuration configuration, |
|||
in TResampler sampler, |
|||
ImageFrame<TPixel> source, |
|||
ImageFrame<TPixel> destination, |
|||
Matrix4x4 matrix) |
|||
where TResampler : unmanaged, IResampler |
|||
where TPixel : struct, IPixel<TPixel> |
|||
{ |
|||
// Handle transforms that result in output identical to the original.
|
|||
if (matrix.Equals(default) || matrix.Equals(Matrix4x4.Identity)) |
|||
{ |
|||
// The clone will be blank here copy all the pixel data over
|
|||
source.GetPixelSpan().CopyTo(destination.GetPixelSpan()); |
|||
return; |
|||
} |
|||
|
|||
// Convert from screen to world space.
|
|||
Matrix4x4.Invert(matrix, out matrix); |
|||
|
|||
if (sampler is NearestNeighborResampler) |
|||
{ |
|||
var nnOperation = new NNProjectiveOperation<TPixel>(source, destination, matrix); |
|||
ParallelRowIterator.IterateRows( |
|||
configuration, |
|||
destination.Bounds(), |
|||
in nnOperation); |
|||
|
|||
return; |
|||
} |
|||
|
|||
int yRadius = GetSamplingRadius(in sampler, source.Height, destination.Height); |
|||
int xRadius = 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 ProjectiveOperation<TResampler, TPixel>( |
|||
configuration, |
|||
source, |
|||
destination, |
|||
yKernelBuffer, |
|||
xKernelBuffer, |
|||
in sampler, |
|||
matrix, |
|||
radialExtents, |
|||
maxSourceExtents); |
|||
|
|||
ParallelRowIterator.IterateRows<ProjectiveOperation<TResampler, TPixel>, Vector4>( |
|||
configuration, |
|||
destination.Bounds(), |
|||
in operation); |
|||
} |
|||
|
|||
[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 : unmanaged, 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 : unmanaged, 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; |
|||
} |
|||
} |
|||
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
private static int GetSamplingRadius<TResampler>(in TResampler sampler, int sourceSize, int destinationSize) |
|||
where TResampler : unmanaged, IResampler |
|||
{ |
|||
double scale = sourceSize / destinationSize; |
|||
if (scale < 1) |
|||
{ |
|||
scale = 1; |
|||
} |
|||
|
|||
return (int)Math.Ceiling(scale * sampler.Radius); |
|||
} |
|||
} |
|||
} |
|||
@ -1,21 +1,51 @@ |
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Copyright (c) Six Labors and contributors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
|
|||
using System.Numerics; |
|||
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 ApplyAffineTransform<TPixel>( |
|||
Configuration configuration, |
|||
ImageFrame<TPixel> source, |
|||
ImageFrame<TPixel> destination, |
|||
Matrix3x2 matrix) |
|||
where TPixel : struct, IPixel<TPixel> => ResamplerExtensions.ApplyAffineTransform( |
|||
configuration, |
|||
in this, |
|||
source, |
|||
destination, |
|||
matrix); |
|||
|
|||
/// <inheritdoc/>
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public void ApplyProjectiveTransform<TPixel>( |
|||
Configuration configuration, |
|||
ImageFrame<TPixel> source, |
|||
ImageFrame<TPixel> destination, |
|||
Matrix4x4 matrix) |
|||
where TPixel : struct, IPixel<TPixel> => ResamplerExtensions.ApplyProjectiveTransform( |
|||
configuration, |
|||
in this, |
|||
source, |
|||
destination, |
|||
matrix); |
|||
} |
|||
} |
|||
} |
|||
|
|||
@ -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(); |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue