mirror of https://github.com/SixLabors/ImageSharp
committed by
GitHub
36 changed files with 1035 additions and 282 deletions
@ -0,0 +1,101 @@ |
|||||
|
// Copyright (c) Six Labors and contributors.
|
||||
|
// Licensed under the Apache License, Version 2.0.
|
||||
|
|
||||
|
using System; |
||||
|
using System.Runtime.CompilerServices; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Implements basic math operations using tolerant comparison
|
||||
|
/// whenever an equality check is needed.
|
||||
|
/// </summary>
|
||||
|
internal readonly struct TolerantMath |
||||
|
{ |
||||
|
private readonly double epsilon; |
||||
|
|
||||
|
private readonly double negEpsilon; |
||||
|
|
||||
|
public TolerantMath(double epsilon) |
||||
|
{ |
||||
|
DebugGuard.MustBeGreaterThan(epsilon, 0, nameof(epsilon)); |
||||
|
|
||||
|
this.epsilon = epsilon; |
||||
|
this.negEpsilon = -epsilon; |
||||
|
} |
||||
|
|
||||
|
public static TolerantMath Default { get; } = new TolerantMath(1e-8); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// <paramref name="a"/> == 0
|
||||
|
/// </summary>
|
||||
|
[MethodImpl(InliningOptions.ShortMethod)] |
||||
|
public bool IsZero(double a) => a > this.negEpsilon && a < this.epsilon; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// <paramref name="a"/> > 0
|
||||
|
/// </summary>
|
||||
|
[MethodImpl(InliningOptions.ShortMethod)] |
||||
|
public bool IsPositive(double a) => a > this.epsilon; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// <paramref name="a"/> < 0
|
||||
|
/// </summary>
|
||||
|
[MethodImpl(InliningOptions.ShortMethod)] |
||||
|
public bool IsNegative(double a) => a < this.negEpsilon; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// <paramref name="a"/> == <paramref name="b"/>
|
||||
|
/// </summary>
|
||||
|
[MethodImpl(InliningOptions.ShortMethod)] |
||||
|
public bool AreEqual(double a, double b) => this.IsZero(a - b); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// <paramref name="a"/> > <paramref name="b"/>
|
||||
|
/// </summary>
|
||||
|
[MethodImpl(InliningOptions.ShortMethod)] |
||||
|
public bool IsGreater(double a, double b) => a > b + this.epsilon; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// <paramref name="a"/> < <paramref name="b"/>
|
||||
|
/// </summary>
|
||||
|
[MethodImpl(InliningOptions.ShortMethod)] |
||||
|
public bool IsLess(double a, double b) => a < b - this.epsilon; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// <paramref name="a"/> >= <paramref name="b"/>
|
||||
|
/// </summary>
|
||||
|
[MethodImpl(InliningOptions.ShortMethod)] |
||||
|
public bool IsGreaterOrEqual(double a, double b) => a >= b - this.epsilon; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// <paramref name="a"/> <= <paramref name="b"/>
|
||||
|
/// </summary>
|
||||
|
[MethodImpl(InliningOptions.ShortMethod)] |
||||
|
public bool IsLessOrEqual(double a, double b) => b >= a - this.epsilon; |
||||
|
|
||||
|
[MethodImpl(InliningOptions.ShortMethod)] |
||||
|
public double Ceiling(double a) |
||||
|
{ |
||||
|
double rem = Math.IEEERemainder(a, 1); |
||||
|
if (this.IsZero(rem)) |
||||
|
{ |
||||
|
return Math.Round(a); |
||||
|
} |
||||
|
|
||||
|
return Math.Ceiling(a); |
||||
|
} |
||||
|
|
||||
|
[MethodImpl(InliningOptions.ShortMethod)] |
||||
|
public double Floor(double a) |
||||
|
{ |
||||
|
double rem = Math.IEEERemainder(a, 1); |
||||
|
if (this.IsZero(rem)) |
||||
|
{ |
||||
|
return Math.Round(a); |
||||
|
} |
||||
|
|
||||
|
return Math.Floor(a); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,130 +0,0 @@ |
|||||
// Copyright (c) Six Labors and contributors.
|
|
||||
// Licensed under the Apache License, Version 2.0.
|
|
||||
|
|
||||
using System; |
|
||||
using System.Runtime.CompilerServices; |
|
||||
|
|
||||
using SixLabors.ImageSharp.Memory; |
|
||||
using SixLabors.Memory; |
|
||||
|
|
||||
namespace SixLabors.ImageSharp.Processing.Processors.Transforms |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// Holds the <see cref="ResizeKernel"/> values in an optimized contigous memory region.
|
|
||||
/// </summary>
|
|
||||
internal class KernelMap : IDisposable |
|
||||
{ |
|
||||
private readonly Buffer2D<float> data; |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Initializes a new instance of the <see cref="KernelMap"/> class.
|
|
||||
/// </summary>
|
|
||||
/// <param name="memoryAllocator">The <see cref="MemoryAllocator"/> to use for allocations.</param>
|
|
||||
/// <param name="destinationSize">The size of the destination window</param>
|
|
||||
/// <param name="kernelRadius">The radius of the kernel</param>
|
|
||||
public KernelMap(MemoryAllocator memoryAllocator, int destinationSize, float kernelRadius) |
|
||||
{ |
|
||||
int width = (int)Math.Ceiling(kernelRadius * 2); |
|
||||
this.data = memoryAllocator.Allocate2D<float>(width, destinationSize, AllocationOptions.Clean); |
|
||||
this.Kernels = new ResizeKernel[destinationSize]; |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets the calculated <see cref="Kernels"/> values.
|
|
||||
/// </summary>
|
|
||||
public ResizeKernel[] Kernels { get; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Disposes <see cref="KernelMap"/> instance releasing it's backing buffer.
|
|
||||
/// </summary>
|
|
||||
public void Dispose() |
|
||||
{ |
|
||||
this.data.Dispose(); |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Computes the weights to apply at each pixel when resizing.
|
|
||||
/// </summary>
|
|
||||
/// <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="KernelMap"/></returns>
|
|
||||
public static KernelMap Calculate( |
|
||||
IResampler sampler, |
|
||||
int destinationSize, |
|
||||
int sourceSize, |
|
||||
MemoryAllocator memoryAllocator) |
|
||||
{ |
|
||||
float ratio = (float)sourceSize / destinationSize; |
|
||||
float scale = ratio; |
|
||||
|
|
||||
if (scale < 1F) |
|
||||
{ |
|
||||
scale = 1F; |
|
||||
} |
|
||||
|
|
||||
float radius = MathF.Ceiling(scale * sampler.Radius); |
|
||||
var result = new KernelMap(memoryAllocator, destinationSize, radius); |
|
||||
|
|
||||
for (int i = 0; i < destinationSize; i++) |
|
||||
{ |
|
||||
float center = ((i + .5F) * ratio) - .5F; |
|
||||
|
|
||||
// Keep inside bounds.
|
|
||||
int left = (int)MathF.Ceiling(center - radius); |
|
||||
if (left < 0) |
|
||||
{ |
|
||||
left = 0; |
|
||||
} |
|
||||
|
|
||||
int right = (int)MathF.Floor(center + radius); |
|
||||
if (right > sourceSize - 1) |
|
||||
{ |
|
||||
right = sourceSize - 1; |
|
||||
} |
|
||||
|
|
||||
float sum = 0; |
|
||||
|
|
||||
ResizeKernel ws = result.CreateKernel(i, left, right); |
|
||||
result.Kernels[i] = ws; |
|
||||
|
|
||||
ref float weightsBaseRef = ref ws.GetStartReference(); |
|
||||
|
|
||||
for (int j = left; j <= right; j++) |
|
||||
{ |
|
||||
float weight = sampler.GetValue((j - center) / scale); |
|
||||
sum += weight; |
|
||||
|
|
||||
// weights[j - left] = weight:
|
|
||||
Unsafe.Add(ref weightsBaseRef, j - left) = weight; |
|
||||
} |
|
||||
|
|
||||
// Normalize, best to do it here rather than in the pixel loop later on.
|
|
||||
if (sum > 0) |
|
||||
{ |
|
||||
for (int w = 0; w < ws.Length; w++) |
|
||||
{ |
|
||||
// weights[w] = weights[w] / sum:
|
|
||||
ref float wRef = ref Unsafe.Add(ref weightsBaseRef, w); |
|
||||
wRef /= sum; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
return result; |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Slices a weights value at the given positions.
|
|
||||
/// </summary>
|
|
||||
/// <param name="destIdx">The index in destination buffer</param>
|
|
||||
/// <param name="leftIdx">The local left index value</param>
|
|
||||
/// <param name="rightIdx">The local right index value</param>
|
|
||||
/// <returns>The weights</returns>
|
|
||||
private ResizeKernel CreateKernel(int destIdx, int leftIdx, int rightIdx) |
|
||||
{ |
|
||||
return new ResizeKernel(destIdx, leftIdx, this.data, rightIdx - leftIdx + 1); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,81 @@ |
|||||
|
// Copyright (c) Six Labors and contributors.
|
||||
|
// Licensed under the Apache License, Version 2.0.
|
||||
|
|
||||
|
using System; |
||||
|
|
||||
|
using SixLabors.Memory; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Processing.Processors.Transforms |
||||
|
{ |
||||
|
/// <content>
|
||||
|
/// Contains <see cref="PeriodicKernelMap"/>
|
||||
|
/// </content>
|
||||
|
internal partial class ResizeKernelMap |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Memory-optimized <see cref="ResizeKernelMap"/> where repeating rows are stored only once.
|
||||
|
/// </summary>
|
||||
|
private sealed class PeriodicKernelMap : ResizeKernelMap |
||||
|
{ |
||||
|
private readonly int period; |
||||
|
|
||||
|
private readonly int cornerInterval; |
||||
|
|
||||
|
public PeriodicKernelMap( |
||||
|
MemoryAllocator memoryAllocator, |
||||
|
IResampler sampler, |
||||
|
int sourceLength, |
||||
|
int destinationLength, |
||||
|
double ratio, |
||||
|
double scale, |
||||
|
int radius, |
||||
|
int period, |
||||
|
int cornerInterval) |
||||
|
: base( |
||||
|
memoryAllocator, |
||||
|
sampler, |
||||
|
sourceLength, |
||||
|
destinationLength, |
||||
|
(cornerInterval * 2) + period, |
||||
|
ratio, |
||||
|
scale, |
||||
|
radius) |
||||
|
{ |
||||
|
this.cornerInterval = cornerInterval; |
||||
|
this.period = period; |
||||
|
} |
||||
|
|
||||
|
internal override string Info => base.Info + $"|period:{this.period}|cornerInterval:{this.cornerInterval}"; |
||||
|
|
||||
|
protected override void Initialize() |
||||
|
{ |
||||
|
// 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; |
||||
|
} |
||||
|
|
||||
|
// Copy the mosaics:
|
||||
|
int bottomStartDest = this.DestinationLength - this.cornerInterval; |
||||
|
for (int i = startOfFirstRepeatedMosaic; i < bottomStartDest; i++) |
||||
|
{ |
||||
|
double center = ((i + .5) * this.ratio) - .5; |
||||
|
int left = (int)TolerantMath.Ceiling(center - this.radius); |
||||
|
ResizeKernel kernel = this.kernels[i - this.period]; |
||||
|
this.kernels[i] = kernel.AlterLeftValue(left); |
||||
|
} |
||||
|
|
||||
|
// Build bottom corner data:
|
||||
|
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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,247 @@ |
|||||
|
// Copyright (c) Six Labors and contributors.
|
||||
|
// Licensed under the Apache License, Version 2.0.
|
||||
|
|
||||
|
using System; |
||||
|
using System.Buffers; |
||||
|
using System.Runtime.CompilerServices; |
||||
|
using System.Runtime.InteropServices; |
||||
|
|
||||
|
using SixLabors.ImageSharp.Memory; |
||||
|
using SixLabors.Memory; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Processing.Processors.Transforms |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Provides <see cref="ResizeKernel"/> 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; |
||||
|
|
||||
|
private readonly double scale; |
||||
|
|
||||
|
private readonly int radius; |
||||
|
|
||||
|
private readonly MemoryHandle pinHandle; |
||||
|
|
||||
|
private readonly Buffer2D<float> data; |
||||
|
|
||||
|
private readonly ResizeKernel[] kernels; |
||||
|
|
||||
|
// To avoid both GC allocations, and MemoryAllocator ceremony:
|
||||
|
private readonly double[] tempValues; |
||||
|
|
||||
|
private ResizeKernelMap( |
||||
|
MemoryAllocator memoryAllocator, |
||||
|
IResampler sampler, |
||||
|
int sourceLength, |
||||
|
int destinationLength, |
||||
|
int bufferHeight, |
||||
|
double ratio, |
||||
|
double scale, |
||||
|
int radius) |
||||
|
{ |
||||
|
this.sampler = sampler; |
||||
|
this.ratio = ratio; |
||||
|
this.scale = scale; |
||||
|
this.radius = radius; |
||||
|
this.sourceLength = sourceLength; |
||||
|
this.DestinationLength = destinationLength; |
||||
|
int maxWidth = (radius * 2) + 1; |
||||
|
this.data = memoryAllocator.Allocate2D<float>(maxWidth, bufferHeight, AllocationOptions.Clean); |
||||
|
this.pinHandle = this.data.Memory.Pin(); |
||||
|
this.kernels = new ResizeKernel[destinationLength]; |
||||
|
this.tempValues = new double[maxWidth]; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the length of the destination row/column
|
||||
|
/// </summary>
|
||||
|
public int DestinationLength { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets a string of information to help debugging
|
||||
|
/// </summary>
|
||||
|
internal virtual string Info => |
||||
|
$"radius:{this.radius}|sourceSize:{this.sourceLength}|destinationSize:{this.DestinationLength}|ratio:{this.ratio}|scale:{this.scale}"; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Disposes <see cref="ResizeKernelMap"/> instance releasing it's backing buffer.
|
||||
|
/// </summary>
|
||||
|
public void Dispose() |
||||
|
{ |
||||
|
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]; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Computes the weights to apply at each pixel when resizing.
|
||||
|
/// </summary>
|
||||
|
/// <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, |
||||
|
int destinationSize, |
||||
|
int sourceSize, |
||||
|
MemoryAllocator memoryAllocator) |
||||
|
{ |
||||
|
double ratio = (double)sourceSize / destinationSize; |
||||
|
double scale = ratio; |
||||
|
|
||||
|
if (scale < 1) |
||||
|
{ |
||||
|
scale = 1; |
||||
|
} |
||||
|
|
||||
|
int radius = (int)TolerantMath.Ceiling(scale * sampler.Radius); |
||||
|
|
||||
|
// 'ratio' is a rational number.
|
||||
|
// Multiplying it by LCM(sourceSize, destSize)/sourceSize will result in a whole number "again".
|
||||
|
// This value is determining the length of the periods in repeating kernel map rows.
|
||||
|
int period = ImageMaths.LeastCommonMultiple(sourceSize, destinationSize) / sourceSize; |
||||
|
|
||||
|
// the center position at i == 0:
|
||||
|
double center0 = (ratio - 1) * 0.5; |
||||
|
double firstNonNegativeLeftVal = (radius - center0 - 1) / ratio; |
||||
|
|
||||
|
// The number of rows building a "stairway" at the top and the bottom of the kernel map
|
||||
|
// corresponding to the corners of the image.
|
||||
|
// If we do not normalize the kernel values, these rows also fit the periodic logic,
|
||||
|
// however, it's just simpler to calculate them separately.
|
||||
|
int cornerInterval = (int)TolerantMath.Ceiling(firstNonNegativeLeftVal); |
||||
|
|
||||
|
// If firstNonNegativeLeftVal was an integral value, we need firstNonNegativeLeftVal+1
|
||||
|
// instead of Ceiling:
|
||||
|
if (TolerantMath.AreEqual(firstNonNegativeLeftVal, cornerInterval)) |
||||
|
{ |
||||
|
cornerInterval++; |
||||
|
} |
||||
|
|
||||
|
// If 'cornerInterval' is too big compared to 'period', we can't apply the periodic optimization.
|
||||
|
// If we don't have at least 2 periods, we go with the basic implementation:
|
||||
|
bool hasAtLeast2Periods = 2 * (cornerInterval + period) < destinationSize; |
||||
|
|
||||
|
ResizeKernelMap result = hasAtLeast2Periods |
||||
|
? new PeriodicKernelMap( |
||||
|
memoryAllocator, |
||||
|
sampler, |
||||
|
sourceSize, |
||||
|
destinationSize, |
||||
|
ratio, |
||||
|
scale, |
||||
|
radius, |
||||
|
period, |
||||
|
cornerInterval) |
||||
|
: new ResizeKernelMap( |
||||
|
memoryAllocator, |
||||
|
sampler, |
||||
|
sourceSize, |
||||
|
destinationSize, |
||||
|
destinationSize, |
||||
|
ratio, |
||||
|
scale, |
||||
|
radius); |
||||
|
|
||||
|
result.Initialize(); |
||||
|
|
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
protected virtual void Initialize() |
||||
|
{ |
||||
|
for (int i = 0; i < this.DestinationLength; i++) |
||||
|
{ |
||||
|
ResizeKernel kernel = this.BuildKernel(i, i); |
||||
|
this.kernels[i] = kernel; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Builds a <see cref="ResizeKernel"/> for the row <paramref name="destRowIndex"/> (in <see cref="kernels"/>)
|
||||
|
/// 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) |
||||
|
{ |
||||
|
double center = ((destRowIndex + .5) * this.ratio) - .5; |
||||
|
|
||||
|
// Keep inside bounds.
|
||||
|
int left = (int)TolerantMath.Ceiling(center - this.radius); |
||||
|
if (left < 0) |
||||
|
{ |
||||
|
left = 0; |
||||
|
} |
||||
|
|
||||
|
int right = (int)TolerantMath.Floor(center + this.radius); |
||||
|
if (right > this.sourceLength - 1) |
||||
|
{ |
||||
|
right = this.sourceLength - 1; |
||||
|
} |
||||
|
|
||||
|
ResizeKernel kernel = this.CreateKernel(dataRowIndex, left, right); |
||||
|
|
||||
|
Span<double> kernelValues = this.tempValues.AsSpan().Slice(0, kernel.Length); |
||||
|
double sum = 0; |
||||
|
|
||||
|
for (int j = left; j <= right; j++) |
||||
|
{ |
||||
|
double value = this.sampler.GetValue((float)((j - center) / this.scale)); |
||||
|
sum += value; |
||||
|
|
||||
|
kernelValues[j - left] = value; |
||||
|
} |
||||
|
|
||||
|
// Normalize, best to do it here rather than in the pixel loop later on.
|
||||
|
if (sum > 0) |
||||
|
{ |
||||
|
for (int j = 0; j < kernel.Length; j++) |
||||
|
{ |
||||
|
// weights[w] = weights[w] / sum:
|
||||
|
ref double kRef = ref kernelValues[j]; |
||||
|
kRef /= sum; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
kernel.Fill(kernelValues); |
||||
|
|
||||
|
return kernel; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Returns a <see cref="ResizeKernel"/> referencing values of <see cref="data"/>
|
||||
|
/// at row <paramref name="dataRowIndex"/>.
|
||||
|
/// </summary>
|
||||
|
private unsafe ResizeKernel CreateKernel(int dataRowIndex, int left, int right) |
||||
|
{ |
||||
|
int length = right - left + 1; |
||||
|
|
||||
|
if (length > this.data.Width) |
||||
|
{ |
||||
|
throw new InvalidOperationException( |
||||
|
$"Error in KernelMap.CreateKernel({dataRowIndex},{left},{right}): left > this.data.Width"); |
||||
|
} |
||||
|
|
||||
|
Span<float> rowSpan = this.data.GetRowSpan(dataRowIndex); |
||||
|
|
||||
|
ref float rowReference = ref MemoryMarshal.GetReference(rowSpan); |
||||
|
float* rowPtr = (float*)Unsafe.AsPointer(ref rowReference); |
||||
|
return new ResizeKernel(left, rowPtr, length); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,168 @@ |
|||||
|
// Copyright (c) Six Labors and contributors.
|
||||
|
// Licensed under the Apache License, Version 2.0.
|
||||
|
|
||||
|
using System; |
||||
|
|
||||
|
using Xunit; |
||||
|
// ReSharper disable InconsistentNaming
|
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Tests.Helpers |
||||
|
{ |
||||
|
public class TolerantMathTests |
||||
|
{ |
||||
|
private readonly TolerantMath tolerantMath = new TolerantMath(0.1); |
||||
|
|
||||
|
[Theory] |
||||
|
[InlineData(0)] |
||||
|
[InlineData(0.01)] |
||||
|
[InlineData(-0.05)] |
||||
|
public void IsZero_WhenTrue(double a) |
||||
|
{ |
||||
|
Assert.True(this.tolerantMath.IsZero(a)); |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[InlineData(0.11)] |
||||
|
[InlineData(-0.101)] |
||||
|
[InlineData(42)] |
||||
|
public void IsZero_WhenFalse(double a) |
||||
|
{ |
||||
|
Assert.False(this.tolerantMath.IsZero(a)); |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[InlineData(0.11)] |
||||
|
[InlineData(100)] |
||||
|
public void IsPositive_WhenTrue(double a) |
||||
|
{ |
||||
|
Assert.True(this.tolerantMath.IsPositive(a)); |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[InlineData(0.09)] |
||||
|
[InlineData(-0.1)] |
||||
|
[InlineData(-1000)] |
||||
|
public void IsPositive_WhenFalse(double a) |
||||
|
{ |
||||
|
Assert.False(this.tolerantMath.IsPositive(a)); |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[InlineData(-0.11)] |
||||
|
[InlineData(-100)] |
||||
|
public void IsNegative_WhenTrue(double a) |
||||
|
{ |
||||
|
Assert.True(this.tolerantMath.IsNegative(a)); |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[InlineData(-0.09)] |
||||
|
[InlineData(0.1)] |
||||
|
[InlineData(1000)] |
||||
|
public void IsNegative_WhenFalse(double a) |
||||
|
{ |
||||
|
Assert.False(this.tolerantMath.IsNegative(a)); |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[InlineData(4.2, 4.2)] |
||||
|
[InlineData(4.2, 4.25)] |
||||
|
[InlineData(-Math.PI, -Math.PI + 0.05)] |
||||
|
[InlineData(999999.2, 999999.25)] |
||||
|
public void AreEqual_WhenTrue(double a, double b) |
||||
|
{ |
||||
|
Assert.True(this.tolerantMath.AreEqual(a, b)); |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[InlineData(1, 2)] |
||||
|
[InlineData(-1000000, -1000000.2)] |
||||
|
public void AreEqual_WhenFalse(double a, double b) |
||||
|
{ |
||||
|
Assert.False(this.tolerantMath.AreEqual(a, b)); |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[InlineData(2, 1.8)] |
||||
|
[InlineData(-20, -20.2)] |
||||
|
[InlineData(0.1, -0.1)] |
||||
|
[InlineData(100, 10)] |
||||
|
public void IsGreater_IsLess_WhenTrue(double a, double b) |
||||
|
{ |
||||
|
Assert.True(this.tolerantMath.IsGreater(a, b)); |
||||
|
Assert.True(this.tolerantMath.IsLess(b, a)); |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[InlineData(2, 1.95)] |
||||
|
[InlineData(-20, -20.02)] |
||||
|
[InlineData(0.01, -0.01)] |
||||
|
[InlineData(999999, 999999.09)] |
||||
|
public void IsGreater_IsLess_WhenFalse(double a, double b) |
||||
|
{ |
||||
|
Assert.False(this.tolerantMath.IsGreater(a, b)); |
||||
|
Assert.False(this.tolerantMath.IsLess(b, a)); |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[InlineData(3, 2)] |
||||
|
[InlineData(3, 2.99)] |
||||
|
[InlineData(2.99, 3)] |
||||
|
[InlineData(-5, -6)] |
||||
|
[InlineData(-5, -5.05)] |
||||
|
[InlineData(-5.05, -5)] |
||||
|
public void IsGreaterOrEqual_IsLessOrEqual_WhenTrue(double a, double b) |
||||
|
{ |
||||
|
Assert.True(this.tolerantMath.IsGreaterOrEqual(a, b)); |
||||
|
Assert.True(this.tolerantMath.IsLessOrEqual(b, a)); |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[InlineData(2, 3)] |
||||
|
[InlineData(2.89, 3)] |
||||
|
[InlineData(-3, -2.89)] |
||||
|
public void IsGreaterOrEqual_IsLessOrEqual_WhenFalse(double a, double b) |
||||
|
{ |
||||
|
Assert.False(this.tolerantMath.IsGreaterOrEqual(a, b)); |
||||
|
Assert.False(this.tolerantMath.IsLessOrEqual(b, a)); |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[InlineData(3.5, 4.0)] |
||||
|
[InlineData(3.89, 4.0)] |
||||
|
[InlineData(4.09, 4.0)] |
||||
|
[InlineData(4.11, 5.0)] |
||||
|
[InlineData(0.11, 1)] |
||||
|
[InlineData(0.05, 0)] |
||||
|
[InlineData(-0.5, 0)] |
||||
|
[InlineData(-0.95, -1)] |
||||
|
[InlineData(-1.05, -1)] |
||||
|
[InlineData(-1.5, -1)] |
||||
|
public void Ceiling(double value, double expected) |
||||
|
{ |
||||
|
double actual = this.tolerantMath.Ceiling(value); |
||||
|
Assert.Equal(expected, actual); |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[InlineData(1, 1)] |
||||
|
[InlineData(0.99, 1)] |
||||
|
[InlineData(0.5, 0)] |
||||
|
[InlineData(0.01, 0)] |
||||
|
[InlineData(-0.09, 0)] |
||||
|
[InlineData(-0.11, -1)] |
||||
|
[InlineData(-100.11, -101)] |
||||
|
[InlineData(-100.09, -100)] |
||||
|
public void Floor(double value, double expected) |
||||
|
{ |
||||
|
double plz1 = Math.IEEERemainder(1.1, 1); |
||||
|
double plz2 = Math.IEEERemainder(0.9, 1); |
||||
|
|
||||
|
double plz3 = Math.IEEERemainder(-1.1, 1); |
||||
|
double plz4 = Math.IEEERemainder(-0.9, 1); |
||||
|
|
||||
|
double actual = this.tolerantMath.Floor(value); |
||||
|
Assert.Equal(expected, actual); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,61 +0,0 @@ |
|||||
using System; |
|
||||
using System.IO; |
|
||||
using System.Text; |
|
||||
|
|
||||
using SixLabors.ImageSharp.PixelFormats; |
|
||||
using SixLabors.ImageSharp.Processing; |
|
||||
using SixLabors.ImageSharp.Processing.Processors.Transforms; |
|
||||
using SixLabors.Primitives; |
|
||||
|
|
||||
using Xunit; |
|
||||
using Xunit.Abstractions; |
|
||||
|
|
||||
namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms |
|
||||
{ |
|
||||
public class KernelMapTests |
|
||||
{ |
|
||||
private ITestOutputHelper Output { get; } |
|
||||
|
|
||||
public KernelMapTests(ITestOutputHelper output) |
|
||||
{ |
|
||||
this.Output = output; |
|
||||
} |
|
||||
|
|
||||
[Theory(Skip = "TODO: Add asserionts")] |
|
||||
[InlineData(500, 200, nameof(KnownResamplers.Bicubic))] |
|
||||
[InlineData(50, 40, nameof(KnownResamplers.Bicubic))] |
|
||||
[InlineData(40, 30, nameof(KnownResamplers.Bicubic))] |
|
||||
[InlineData(500, 200, nameof(KnownResamplers.Lanczos8))] |
|
||||
[InlineData(100, 80, nameof(KnownResamplers.Lanczos8))] |
|
||||
[InlineData(100, 10, nameof(KnownResamplers.Lanczos8))] |
|
||||
[InlineData(10, 100, nameof(KnownResamplers.Lanczos8))] |
|
||||
public void PrintKernelMap(int srcSize, int destSize, string resamplerName) |
|
||||
{ |
|
||||
var resampler = (IResampler)typeof(KnownResamplers).GetProperty(resamplerName).GetValue(null); |
|
||||
|
|
||||
var kernelMap = KernelMap.Calculate(resampler, destSize, srcSize, Configuration.Default.MemoryAllocator); |
|
||||
|
|
||||
var bld = new StringBuilder(); |
|
||||
|
|
||||
foreach (ResizeKernel window in kernelMap.Kernels) |
|
||||
{ |
|
||||
Span<float> span = window.GetSpan(); |
|
||||
for (int i = 0; i < window.Length; i++) |
|
||||
{ |
|
||||
float value = span[i]; |
|
||||
bld.Append($"{value,7:F4}"); |
|
||||
bld.Append("| "); |
|
||||
} |
|
||||
|
|
||||
bld.AppendLine(); |
|
||||
} |
|
||||
|
|
||||
string outDir = TestEnvironment.CreateOutputDirectory("." + nameof(this.PrintKernelMap)); |
|
||||
string fileName = $@"{outDir}\{resamplerName}_{srcSize}_{destSize}.MD"; |
|
||||
|
|
||||
File.WriteAllText(fileName, bld.ToString()); |
|
||||
|
|
||||
this.Output.WriteLine(bld.ToString()); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,111 @@ |
|||||
|
// Copyright (c) Six Labors and contributors.
|
||||
|
// Licensed under the Apache License, Version 2.0.
|
||||
|
|
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
|
||||
|
using SixLabors.ImageSharp.Processing.Processors.Transforms; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms |
||||
|
{ |
||||
|
public partial class ResizeKernelMapTests |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Simplified reference implementation for <see cref="ResizeKernelMap"/> functionality.
|
||||
|
/// </summary>
|
||||
|
internal class ReferenceKernelMap |
||||
|
{ |
||||
|
private readonly ReferenceKernel[] kernels; |
||||
|
|
||||
|
public ReferenceKernelMap(ReferenceKernel[] kernels) |
||||
|
{ |
||||
|
this.kernels = kernels; |
||||
|
} |
||||
|
|
||||
|
public int DestinationSize => this.kernels.Length; |
||||
|
|
||||
|
public ReferenceKernel GetKernel(int destinationIndex) => this.kernels[destinationIndex]; |
||||
|
|
||||
|
public static ReferenceKernelMap Calculate(IResampler sampler, int destinationSize, int sourceSize, bool normalize = true) |
||||
|
{ |
||||
|
double ratio = (double)sourceSize / destinationSize; |
||||
|
double scale = ratio; |
||||
|
|
||||
|
if (scale < 1F) |
||||
|
{ |
||||
|
scale = 1F; |
||||
|
} |
||||
|
|
||||
|
TolerantMath tolerantMath = TolerantMath.Default; |
||||
|
|
||||
|
double radius = tolerantMath.Ceiling(scale * sampler.Radius); |
||||
|
|
||||
|
var result = new List<ReferenceKernel>(); |
||||
|
|
||||
|
for (int i = 0; i < destinationSize; i++) |
||||
|
{ |
||||
|
double center = ((i + .5) * ratio) - .5; |
||||
|
|
||||
|
// Keep inside bounds.
|
||||
|
int left = (int)tolerantMath.Ceiling(center - radius); |
||||
|
if (left < 0) |
||||
|
{ |
||||
|
left = 0; |
||||
|
} |
||||
|
|
||||
|
int right = (int)tolerantMath.Floor(center + radius); |
||||
|
if (right > sourceSize - 1) |
||||
|
{ |
||||
|
right = sourceSize - 1; |
||||
|
} |
||||
|
|
||||
|
double sum = 0; |
||||
|
|
||||
|
double[] values = new double[right - left + 1]; |
||||
|
|
||||
|
for (int j = left; j <= right; j++) |
||||
|
{ |
||||
|
double weight = sampler.GetValue((float)((j - center) / scale)); |
||||
|
sum += weight; |
||||
|
|
||||
|
values[j - left] = weight; |
||||
|
} |
||||
|
|
||||
|
if (sum > 0 && normalize) |
||||
|
{ |
||||
|
for (int w = 0; w < values.Length; w++) |
||||
|
{ |
||||
|
values[w] /= sum; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
float[] floatVals = values.Select(v => (float)v).ToArray(); |
||||
|
|
||||
|
result.Add(new ReferenceKernel(left, floatVals)); |
||||
|
} |
||||
|
|
||||
|
return new ReferenceKernelMap(result.ToArray()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
internal struct ReferenceKernel |
||||
|
{ |
||||
|
public ReferenceKernel(int left, float[] values) |
||||
|
{ |
||||
|
this.Left = left; |
||||
|
this.Values = values; |
||||
|
} |
||||
|
|
||||
|
public int Left { get; } |
||||
|
|
||||
|
public float[] Values { get; } |
||||
|
|
||||
|
public int Length => this.Values.Length; |
||||
|
|
||||
|
public static implicit operator ReferenceKernel(ResizeKernel orig) |
||||
|
{ |
||||
|
return new ReferenceKernel(orig.Left, orig.Values.ToArray()); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,241 @@ |
|||||
|
// 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.Reflection; |
||||
|
using System.Text; |
||||
|
|
||||
|
using SixLabors.ImageSharp.Processing; |
||||
|
using SixLabors.ImageSharp.Processing.Processors.Transforms; |
||||
|
|
||||
|
using Xunit; |
||||
|
using Xunit.Abstractions; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms |
||||
|
{ |
||||
|
public partial class ResizeKernelMapTests |
||||
|
{ |
||||
|
private ITestOutputHelper Output { get; } |
||||
|
|
||||
|
public ResizeKernelMapTests(ITestOutputHelper output) |
||||
|
{ |
||||
|
this.Output = output; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// resamplerName, srcSize, destSize
|
||||
|
/// </summary>
|
||||
|
public static readonly TheoryData<string, int, int> KernelMapData = new TheoryData<string, 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), 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 }, |
||||
|
|
||||
|
// Resize_WorksWithAllResamplers_Rgba32_CalliphoraPartial_Box-0.5:
|
||||
|
{ nameof(KnownResamplers.Box), 378, 149 }, |
||||
|
{ nameof(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 }, |
||||
|
|
||||
|
// 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 }, |
||||
|
}; |
||||
|
|
||||
|
public static TheoryData<string, int, int> GeneratedImageResizeData = |
||||
|
GenerateImageResizeData(); |
||||
|
|
||||
|
|
||||
|
[Theory(Skip = "Only for debugging and development")] |
||||
|
[MemberData(nameof(KernelMapData))] |
||||
|
public void PrintNonNormalizedKernelMap(string resamplerName, int srcSize, int destSize) |
||||
|
{ |
||||
|
IResampler resampler = TestUtils.GetResampler(resamplerName); |
||||
|
|
||||
|
var kernelMap = ReferenceKernelMap.Calculate(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) |
||||
|
{ |
||||
|
this.VerifyKernelMapContentIsCorrect(resamplerName, srcSize, destSize); |
||||
|
} |
||||
|
|
||||
|
// Comprehensive but expensive tests, for ResizeKernelMap.
|
||||
|
// Enabling them can kill you, but sometimes you have to wear the burden!
|
||||
|
// AppVeyor will never follow you to these shadows of Mordor.
|
||||
|
#if false
|
||||
|
[Theory] |
||||
|
[MemberData(nameof(GeneratedImageResizeData))] |
||||
|
public void KernelMapContentIsCorrect_ExtendedGeneratedValues(string resamplerName, int srcSize, int destSize) |
||||
|
{ |
||||
|
this.VerifyKernelMapContentIsCorrect(resamplerName, srcSize, destSize); |
||||
|
} |
||||
|
#endif
|
||||
|
|
||||
|
private void VerifyKernelMapContentIsCorrect(string resamplerName, int srcSize, int destSize) |
||||
|
{ |
||||
|
IResampler resampler = TestUtils.GetResampler(resamplerName); |
||||
|
|
||||
|
var referenceMap = ReferenceKernelMap.Calculate(resampler, destSize, srcSize); |
||||
|
var kernelMap = ResizeKernelMap.Calculate(resampler, destSize, srcSize, Configuration.Default.MemoryAllocator); |
||||
|
|
||||
|
#if DEBUG
|
||||
|
this.Output.WriteLine($"Expected KernelMap:\n{PrintKernelMap(referenceMap)}\n"); |
||||
|
this.Output.WriteLine($"Actual KernelMap:\n{PrintKernelMap(kernelMap)}\n"); |
||||
|
#endif
|
||||
|
var comparer = new ApproximateFloatComparer(1e-6f); |
||||
|
|
||||
|
for (int i = 0; i < kernelMap.DestinationLength; i++) |
||||
|
{ |
||||
|
ResizeKernel kernel = kernelMap.GetKernel(i); |
||||
|
|
||||
|
ReferenceKernel referenceKernel = referenceMap.GetKernel(i); |
||||
|
|
||||
|
Assert.True( |
||||
|
referenceKernel.Length == kernel.Length, |
||||
|
$"referenceKernel.Length != kernel.Length: {referenceKernel.Length} != {kernel.Length}"); |
||||
|
Assert.True( |
||||
|
referenceKernel.Left == kernel.Left, |
||||
|
$"referenceKernel.Left != kernel.Left: {referenceKernel.Left} != {kernel.Left}"); |
||||
|
float[] expectedValues = referenceKernel.Values; |
||||
|
Span<float> actualValues = kernel.Values; |
||||
|
|
||||
|
Assert.Equal(expectedValues.Length, actualValues.Length); |
||||
|
|
||||
|
|
||||
|
|
||||
|
for (int x = 0; x < expectedValues.Length; x++) |
||||
|
{ |
||||
|
Assert.True( |
||||
|
comparer.Equals(expectedValues[x], actualValues[x]), |
||||
|
$"{expectedValues[x]} != {actualValues[x]} @ (Row:{i}, Col:{x})"); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
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<TKernelMap>( |
||||
|
TKernelMap kernelMap, |
||||
|
Func<TKernelMap, int> getDestinationSize, |
||||
|
Func<TKernelMap, int, ReferenceKernel> getKernel) |
||||
|
{ |
||||
|
var bld = new StringBuilder(); |
||||
|
|
||||
|
if (kernelMap is ResizeKernelMap actualMap) |
||||
|
{ |
||||
|
bld.AppendLine(actualMap.Info); |
||||
|
} |
||||
|
|
||||
|
int destinationSize = getDestinationSize(kernelMap); |
||||
|
|
||||
|
for (int i = 0; i < destinationSize; i++) |
||||
|
{ |
||||
|
ReferenceKernel kernel = getKernel(kernelMap, i); |
||||
|
bld.Append($"[{i:D3}] (L{kernel.Left:D3}) || "); |
||||
|
Span<float> span = kernel.Values; |
||||
|
|
||||
|
for (int j = 0; j < kernel.Length; j++) |
||||
|
{ |
||||
|
float value = span[j]; |
||||
|
bld.Append($"{value,8:F5}"); |
||||
|
bld.Append(" | "); |
||||
|
} |
||||
|
|
||||
|
bld.AppendLine(); |
||||
|
} |
||||
|
|
||||
|
return bld.ToString(); |
||||
|
} |
||||
|
|
||||
|
|
||||
|
private static TheoryData<string, int, int> GenerateImageResizeData() |
||||
|
{ |
||||
|
var result = new TheoryData<string, int, int>(); |
||||
|
|
||||
|
string[] resamplerNames = typeof(KnownResamplers).GetProperties(BindingFlags.Public | BindingFlags.Static) |
||||
|
.Select(p => p.Name) |
||||
|
.Where(name => name != nameof(KnownResamplers.NearestNeighbor)) |
||||
|
.ToArray(); |
||||
|
|
||||
|
int[] dimensionVals = |
||||
|
{ |
||||
|
// Arbitrary, small dimensions:
|
||||
|
9, 10, 11, 13, 49, 50, 53, 99, 100, 199, 200, 201, 299, 300, 301, |
||||
|
|
||||
|
// Typical image sizes:
|
||||
|
640, 480, 800, 600, 1024, 768, 1280, 960, 1536, 1180, 1600, 1200, 2048, 1536, 2240, 1680, 2560, |
||||
|
1920, 3032, 2008, 3072, 2304, 3264, 2448 |
||||
|
}; |
||||
|
|
||||
|
IOrderedEnumerable<(int s, int d)> source2Dest = dimensionVals |
||||
|
.SelectMany(s => dimensionVals.Select(d => (s, d))) |
||||
|
.OrderBy(x => x.s + x.d); |
||||
|
|
||||
|
foreach (string resampler in resamplerNames) |
||||
|
{ |
||||
|
foreach ((int s, int d) x in source2Dest) |
||||
|
{ |
||||
|
result.Add(resampler, x.s, x.d); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return result; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1 +1 @@ |
|||||
Subproject commit e7e0f1311d1d585ea8e38efb5a69fca98c44e8a4 |
Subproject commit 5b18d8c95acffb773012881870ba6f521ba13128 |
||||
Loading…
Reference in new issue