diff --git a/src/ImageSharp/Common/Helpers/Vector512Utilities.cs b/src/ImageSharp/Common/Helpers/Vector512Utilities.cs index 9a6df7b861..03ee4626cd 100644 --- a/src/ImageSharp/Common/Helpers/Vector512Utilities.cs +++ b/src/ImageSharp/Common/Helpers/Vector512Utilities.cs @@ -2,7 +2,6 @@ // Licensed under the Six Labors Split License. using System.Diagnostics.CodeAnalysis; -using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.Intrinsics; using System.Runtime.Intrinsics.X86; diff --git a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.PeriodicKernelMap.cs b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.PeriodicKernelMap.cs index b39f6de2a5..d606738abd 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.PeriodicKernelMap.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.PeriodicKernelMap.cs @@ -16,6 +16,8 @@ internal partial class ResizeKernelMap private readonly int cornerInterval; + private readonly int sourcePeriod; + public PeriodicKernelMap( MemoryAllocator memoryAllocator, int sourceLength, @@ -24,7 +26,8 @@ internal partial class ResizeKernelMap double scale, int radius, int period, - int cornerInterval) + int cornerInterval, + int sourcePeriod) : base( memoryAllocator, sourceLength, @@ -36,6 +39,7 @@ internal partial class ResizeKernelMap { this.cornerInterval = cornerInterval; this.period = period; + this.sourcePeriod = sourcePeriod; } internal override string Info => base.Info + $"|period:{this.period}|cornerInterval:{this.cornerInterval}"; @@ -54,10 +58,11 @@ internal partial class ResizeKernelMap int bottomStartDest = this.DestinationLength - this.cornerInterval; for (int i = startOfFirstRepeatedMosaic; i < bottomStartDest; i++) { - float center = (float)(((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); + + // Shift the kernel start index by the source-side period so the same weights align to the + // next repeated sampling window in the source image. + this.kernels[i] = kernel.AlterLeftValue(kernel.StartIndex + this.sourcePeriod); } // Build bottom corner data: diff --git a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.cs b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.cs index 1922110131..e5ac3381b1 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.cs @@ -144,9 +144,13 @@ internal partial class ResizeKernelMap : IDisposable int radius = (int)TolerantMath.Ceiling(scale * sampler.Radius); // 'ratio' is a rational number. - // Multiplying it by destSize/GCD(sourceSize, destSize) will result in a whole number "again". - // This value is determining the length of the periods in repeating kernel map rows. - int period = destinationSize / Numerics.GreatestCommonDivisor(sourceSize, destinationSize); + // Multiplying it by destSize/GCD(sourceSize, destinationSize) yields an integer, so every `period` rows + // the destination-space sampling centers repeat their fractional alignment. `period` is the repeat length + // in destination rows, while `sourcePeriod` is the corresponding integer offset in source pixels that + // must be added to the kernel's left index when we reuse the same weights from a previous period. + int gcd = Numerics.GreatestCommonDivisor(sourceSize, destinationSize); + int period = destinationSize / gcd; + int sourcePeriod = sourceSize / gcd; // the center position at i == 0: double center0 = (ratio - 1) * 0.5; @@ -178,7 +182,8 @@ internal partial class ResizeKernelMap : IDisposable scale, radius, period, - cornerInterval) + cornerInterval, + sourcePeriod) : new ResizeKernelMap( memoryAllocator, sourceSize, @@ -213,8 +218,8 @@ internal partial class ResizeKernelMap : IDisposable private ResizeKernel BuildKernel(in TResampler sampler, int destRowIndex, int dataRowIndex) where TResampler : struct, IResampler { - float center = (float)(((destRowIndex + .5) * this.ratio) - .5); - float scale = (float)this.scale; + double center = ((destRowIndex + .5) * this.ratio) - .5; + double scale = this.scale; // Keep inside bounds. int left = (int)TolerantMath.Ceiling(center - this.radius); @@ -236,7 +241,7 @@ internal partial class ResizeKernelMap : IDisposable for (int j = left; j <= right; j++) { - float value = sampler.GetValue((j - center) / scale); + float value = sampler.GetValue((float)((j - center) / scale)); sum += value; kernelStart = value; kernelStart = ref Unsafe.Add(ref kernelStart, 1); diff --git a/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeKernelMapTests.ReferenceKernelMap.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeKernelMapTests.ReferenceKernelMap.cs index 72142cbdc3..e38bf64c1b 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeKernelMapTests.ReferenceKernelMap.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeKernelMapTests.ReferenceKernelMap.cs @@ -39,10 +39,9 @@ public partial class ResizeKernelMapTests List result = []; - float scale = (float)scaleD; for (int i = 0; i < destinationSize; i++) { - float center = (float)(((i + .5) * ratio) - .5); + double center = ((i + .5) * ratio) - .5; // Keep inside bounds. int left = (int)tolerantMath.Ceiling(center - radius); @@ -63,7 +62,7 @@ public partial class ResizeKernelMapTests for (int j = left; j <= right; j++) { - float weight = sampler.GetValue((j - center) / scale); + float weight = sampler.GetValue((float)((j - center) / scaleD)); sum += weight; values[j - left] = weight; } diff --git a/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeKernelMapTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeKernelMapTests.cs index 8c200e983e..5f9a8055ff 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeKernelMapTests.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeKernelMapTests.cs @@ -13,10 +13,7 @@ public partial class ResizeKernelMapTests { private ITestOutputHelper Output { get; } - public ResizeKernelMapTests(ITestOutputHelper output) - { - this.Output = output; - } + public ResizeKernelMapTests(ITestOutputHelper output) => this.Output = output; /// /// resamplerName, srcSize, destSize @@ -24,71 +21,70 @@ public partial class ResizeKernelMapTests public static readonly TheoryData KernelMapData = new() { - { KnownResamplers.Bicubic, 15, 10 }, - { KnownResamplers.Bicubic, 10, 15 }, - { KnownResamplers.Bicubic, 20, 20 }, - { KnownResamplers.Bicubic, 50, 40 }, - { KnownResamplers.Bicubic, 40, 50 }, - { KnownResamplers.Bicubic, 500, 200 }, - { KnownResamplers.Bicubic, 200, 500 }, - { KnownResamplers.Bicubic, 3032, 400 }, - { KnownResamplers.Bicubic, 10, 25 }, - { KnownResamplers.Lanczos3, 16, 12 }, - { KnownResamplers.Lanczos3, 12, 16 }, - { KnownResamplers.Lanczos3, 12, 9 }, - { KnownResamplers.Lanczos3, 9, 12 }, - { KnownResamplers.Lanczos3, 6, 8 }, - { KnownResamplers.Lanczos3, 8, 6 }, - { KnownResamplers.Lanczos3, 20, 12 }, - { KnownResamplers.Lanczos3, 5, 25 }, - { KnownResamplers.Lanczos3, 5, 50 }, - { KnownResamplers.Lanczos3, 25, 5 }, - { KnownResamplers.Lanczos3, 50, 5 }, - { KnownResamplers.Lanczos3, 49, 5 }, - { KnownResamplers.Lanczos3, 31, 5 }, - { KnownResamplers.Lanczos8, 500, 200 }, - { KnownResamplers.Lanczos8, 100, 10 }, - { KnownResamplers.Lanczos8, 100, 80 }, - { KnownResamplers.Lanczos8, 10, 100 }, - - // Resize_WorksWithAllResamplers_Rgba32_CalliphoraPartial_Box-0.5: - { KnownResamplers.Box, 378, 149 }, - { KnownResamplers.Box, 349, 174 }, - - // Accuracy-related regression-test cases cherry-picked from GeneratedImageResizeData - { KnownResamplers.Box, 201, 100 }, - { KnownResamplers.Box, 199, 99 }, - { KnownResamplers.Box, 10, 299 }, - { KnownResamplers.Box, 299, 10 }, - { KnownResamplers.Box, 301, 300 }, - { KnownResamplers.Box, 1180, 480 }, - { KnownResamplers.Lanczos2, 3264, 3032 }, - { KnownResamplers.Bicubic, 1280, 2240 }, - { KnownResamplers.Bicubic, 1920, 1680 }, - { KnownResamplers.Bicubic, 3072, 2240 }, - { KnownResamplers.Welch, 300, 2008 }, - - // ResizeKernel.Length -related regression tests cherry-picked from GeneratedImageResizeData - { KnownResamplers.Bicubic, 10, 50 }, - { KnownResamplers.Bicubic, 49, 301 }, - { KnownResamplers.Bicubic, 301, 49 }, - { KnownResamplers.Bicubic, 1680, 1200 }, - { KnownResamplers.Box, 13, 299 }, - { KnownResamplers.Lanczos5, 3032, 600 }, - - // Large number. https://github.com/SixLabors/ImageSharp/issues/1616 - { KnownResamplers.Bicubic, 207773, 51943 } - }; - - public static TheoryData GeneratedImageResizeData = - GenerateImageResizeData(); + { KnownResamplers.Bicubic, 15, 10 }, + { KnownResamplers.Bicubic, 10, 15 }, + { KnownResamplers.Bicubic, 20, 20 }, + { KnownResamplers.Bicubic, 50, 40 }, + { KnownResamplers.Bicubic, 40, 50 }, + { KnownResamplers.Bicubic, 500, 200 }, + { KnownResamplers.Bicubic, 200, 500 }, + { KnownResamplers.Bicubic, 3032, 400 }, + { KnownResamplers.Bicubic, 10, 25 }, + { KnownResamplers.Lanczos3, 16, 12 }, + { KnownResamplers.Lanczos3, 12, 16 }, + { KnownResamplers.Lanczos3, 12, 9 }, + { KnownResamplers.Lanczos3, 9, 12 }, + { KnownResamplers.Lanczos3, 6, 8 }, + { KnownResamplers.Lanczos3, 8, 6 }, + { KnownResamplers.Lanczos3, 20, 12 }, + { KnownResamplers.Lanczos3, 5, 25 }, + { KnownResamplers.Lanczos3, 5, 50 }, + { KnownResamplers.Lanczos3, 25, 5 }, + { KnownResamplers.Lanczos3, 50, 5 }, + { KnownResamplers.Lanczos3, 49, 5 }, + { KnownResamplers.Lanczos3, 31, 5 }, + { KnownResamplers.Lanczos8, 500, 200 }, + { KnownResamplers.Lanczos8, 100, 10 }, + { KnownResamplers.Lanczos8, 100, 80 }, + { KnownResamplers.Lanczos8, 10, 100 }, + + // Resize_WorksWithAllResamplers_Rgba32_CalliphoraPartial_Box-0.5: + { KnownResamplers.Box, 378, 149 }, + { KnownResamplers.Box, 349, 174 }, + + // Accuracy-related regression-test cases cherry-picked from GeneratedImageResizeData + { KnownResamplers.Box, 201, 100 }, + { KnownResamplers.Box, 199, 99 }, + { KnownResamplers.Box, 10, 299 }, + { KnownResamplers.Box, 299, 10 }, + { KnownResamplers.Box, 301, 300 }, + { KnownResamplers.Box, 1180, 480 }, + { KnownResamplers.Lanczos2, 3264, 3032 }, + { KnownResamplers.Bicubic, 1280, 2240 }, + { KnownResamplers.Bicubic, 1920, 1680 }, + { KnownResamplers.Bicubic, 3072, 2240 }, + { KnownResamplers.Welch, 300, 2008 }, + + // ResizeKernel.Length -related regression tests cherry-picked from GeneratedImageResizeData + { KnownResamplers.Bicubic, 10, 50 }, + { KnownResamplers.Bicubic, 49, 301 }, + { KnownResamplers.Bicubic, 301, 49 }, + { KnownResamplers.Bicubic, 1680, 1200 }, + { KnownResamplers.Box, 13, 299 }, + { KnownResamplers.Lanczos5, 3032, 600 }, + + // Large number. https://github.com/SixLabors/ImageSharp/issues/1616 + { KnownResamplers.Bicubic, 207773, 51943 } + }; + + public static TheoryData GeneratedImageResizeData = GenerateImageResizeData(); [Theory(Skip = "Only for debugging and development")] [MemberData(nameof(KernelMapData))] public void PrintNonNormalizedKernelMap(TResampler resampler, int srcSize, int destSize) where TResampler : struct, IResampler { - ReferenceKernelMap kernelMap = ReferenceKernelMap.Calculate(in resampler, destSize, srcSize, false); + ReferenceKernelMap kernelMap = ReferenceKernelMap.Calculate(in resampler, destSize, srcSize, false); this.Output.WriteLine($"Actual KernelMap:\n{PrintKernelMap(kernelMap)}\n"); } @@ -97,9 +93,7 @@ public partial class ResizeKernelMapTests [MemberData(nameof(KernelMapData))] public void KernelMapContentIsCorrect(TResampler resampler, int srcSize, int destSize) where TResampler : struct, IResampler - { - this.VerifyKernelMapContentIsCorrect(resampler, srcSize, destSize); - } + => this.VerifyKernelMapContentIsCorrect(resampler, srcSize, destSize); // Comprehensive but expensive tests, for ResizeKernelMap. // Enabling them can kill you, but sometimes you have to wear the burden! @@ -125,6 +119,7 @@ public partial class ResizeKernelMapTests this.Output.WriteLine($"Actual KernelMap:\n{PrintKernelMap(kernelMap)}\n"); #endif + ApproximateFloatComparer comparer = new ApproximateFloatComparer(1e-6f); for (int i = 0; i < kernelMap.DestinationLength; i++) { ResizeKernel kernel = kernelMap.GetKernel((uint)i); @@ -140,15 +135,10 @@ public partial class ResizeKernelMapTests float[] expectedValues = referenceKernel.Values; Span actualValues; - ApproximateFloatComparer comparer; if (ResizeKernel.IsHardwareAccelerated) { - comparer = new ApproximateFloatComparer(1e-4f); - Assert.Equal(expectedValues.Length, kernel.Values.Length / 4); - int actualLength = referenceKernel.Length / 4; - actualValues = new float[expectedValues.Length]; for (int j = 0; j < expectedValues.Length; j++) @@ -158,7 +148,6 @@ public partial class ResizeKernelMapTests } else { - comparer = new ApproximateFloatComparer(1e-6f); actualValues = kernel.Values; } @@ -220,12 +209,13 @@ public partial class ResizeKernelMapTests int[] dimensionVals = [ + // Arbitrary, small dimensions: - 9, 10, 11, 13, 49, 50, 53, 99, 100, 199, 200, 201, 299, 300, 301, + 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 + // 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