From 12776f003c7e7a4793bb2630bb80cfae6c27857f Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Sat, 23 Apr 2022 20:29:12 +0300 Subject: [PATCH] IDCT resizing modes --- .../Decoder/SpectralConverter{TPixel}.cs | 98 +++++++++----- .../Program.cs | 120 ++++++++++++++---- 2 files changed, 159 insertions(+), 59 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter{TPixel}.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter{TPixel}.cs index 497a2067c8..3e09e7c997 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter{TPixel}.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter{TPixel}.cs @@ -28,19 +28,19 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder /// /// Supported scaling factors for DCT jpeg scaling. /// - private static readonly int[] ScalingFactors = new int[] + private static readonly int[] ScaledBlockSizes = new int[] { - // 8 => 8, no scaling - 8, - - // 8 => 4, 1/2 of the original size - 4, + // 8 => 1, 1/8 of the original size + 1, // 8 => 2, 1/4 of the original size 2, - // 8 => 1, 1/8 of the original size - 1, + // 8 => 4, 1/2 of the original size + 4, + + // 8 => 8, no scaling + 8, }; /// @@ -132,45 +132,73 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder } /// - /// Calculates resulting image size and jpeg block scaling. + /// Calculates resulting image size. /// - /// Native size of the image. - /// Resulting jpeg block pixel size. + /// + /// If is null, unchanged is returned. + /// + /// Size of the image. + /// Target image size, can be null. + /// Scaled spectral block size if IDCT scaling should be applied /// Scaled jpeg image size. - private Size GetResultingImageSize(Size nativeSize, out int blockPixelSize) + // TODO: describe ALL outcomes of the built-in IDCT downscaling + public static Size GetResultingImageSize(Size size, Size? targetSize, out int outputBlockSize) { - if (this.targetSize == null) - { - blockPixelSize = 8; - return nativeSize; - } - else + const int jpegBlockPixelSize = 8; + + // must be at least 5% smaller than current IDCT scaled size + // to perform second pass resizing + // this is a highly experimental value + const float secondPassThresholdRatio = 0.95f; + + outputBlockSize = jpegBlockPixelSize; + if (targetSize != null) { - const uint jpegBlockPixelSize = 8; + Size tSize = targetSize.Value; - Size targetSize = this.targetSize.Value; - int outputWidth = nativeSize.Width; - int outputHeight = nativeSize.Height; - blockPixelSize = 1; + int widthInBlocks = (int)Numerics.DivideCeil((uint)size.Width, jpegBlockPixelSize); + int heightInBlocks = (int)Numerics.DivideCeil((uint)size.Height, jpegBlockPixelSize); - for (int i = 1; i < ScalingFactors.Length; i++) + for (int i = 0; i < ScaledBlockSizes.Length; i++) { - int scale = ScalingFactors[i]; - int scaledw = (int)Numerics.DivideCeil((uint)(nativeSize.Width * scale), jpegBlockPixelSize); - int scaledh = (int)Numerics.DivideCeil((uint)(nativeSize.Height * scale), jpegBlockPixelSize); + int blockSize = ScaledBlockSizes[i]; + int scaledWidth = widthInBlocks * blockSize; + int scaledHeight = heightInBlocks * blockSize; - if (scaledw < targetSize.Width || scaledh < targetSize.Height) + // skip to next IDCT scaling + if (scaledWidth < tSize.Width || scaledHeight < tSize.Height) { - blockPixelSize = ScalingFactors[i - 1]; - break; + // this if segment can be safely removed + continue; } - outputWidth = scaledw; - outputHeight = scaledh; - } + // exact match + if (scaledWidth == tSize.Width && scaledHeight == tSize.Height) + { + outputBlockSize = blockSize; + return new Size(scaledWidth, scaledHeight); + } - return new Size(outputWidth, outputHeight); + // center cropping + int widthDiff = Math.Abs(tSize.Width - scaledWidth); + int heightDiff = Math.Abs(tSize.Height - scaledHeight); + if (widthDiff < blockSize && heightDiff < blockSize) + { + throw new NotSupportedException($"Central cropping is not supported yet"); + } + + // small enough for second pass + float secondPassWidthRatio = (float)tSize.Width / scaledWidth; + float secondPassHeightRatio = (float)tSize.Height / scaledHeight; + if (secondPassWidthRatio <= secondPassThresholdRatio && secondPassHeightRatio <= secondPassThresholdRatio) + { + outputBlockSize = blockSize; + return new Size(scaledWidth, scaledHeight); + } + } } + + return size; } /// @@ -233,7 +261,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder this.colorConverter = converter; // Resulting image size - Size pixelSize = this.GetResultingImageSize(frame.PixelSize, out int blockPixelSize); + Size pixelSize = GetResultingImageSize(frame.PixelSize, this.targetSize, out int blockPixelSize); // iteration data int majorBlockWidth = frame.Components.Max((component) => component.SizeInBlocks.Width); diff --git a/tests/ImageSharp.Tests.ProfilingSandbox/Program.cs b/tests/ImageSharp.Tests.ProfilingSandbox/Program.cs index 20a45c4981..6063bd8a0e 100644 --- a/tests/ImageSharp.Tests.ProfilingSandbox/Program.cs +++ b/tests/ImageSharp.Tests.ProfilingSandbox/Program.cs @@ -6,9 +6,12 @@ using System.Diagnostics; using System.IO; using System.Reflection; using System.Threading; +using PhotoSauce.MagicScaler; +using PhotoSauce.MagicScaler.Interpolators; using SixLabors.ImageSharp.Formats.Jpeg; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; +using SixLabors.ImageSharp.Processing.Processors.Transforms; using SixLabors.ImageSharp.Tests.PixelFormats.PixelOperations; using SixLabors.ImageSharp.Tests.ProfilingBenchmarks; using Xunit.Abstractions; @@ -28,29 +31,60 @@ namespace SixLabors.ImageSharp.Tests.ProfilingSandbox public void WriteLine(string format, params object[] args) => Console.WriteLine(format, args); } - public static void Main(string[] args) - { - //ReEncodeImage("Calliphora"); + const string pathTemplate = ...""; - // DecodeImageResize__explicit("Calliphora", new Size(101, 150)); - // DecodeImageResize__experimental("Calliphora_aligned_size", new Size(101, 150)); - //DecodeImageResize__experimental("winter420_noninterleaved", new Size(80, 120)); + // Second pass - must be 5% smaller than appropriate IDCT scaled size + const float scale = 0.75f; + readonly IResampler resampler = KnownResamplers.Box; - // Decode-Resize-Encode w/ Mutate() - // Elapsed: 2504ms across 250 iterations - // Average: 10,016ms - BenchmarkResizingLoop__explicit("Calliphora", new Size(80, 120), 250); - - // Decode-Resize-Encode w/ downscaling decoder - // Elapsed: 1157ms across 250 iterations - // Average: 4,628ms - BenchmarkResizingLoop__experimental("Calliphora", new Size(80, 120), 250); + public static void Main(string[] args) + { + //ReEncodeImage("jpeg444"); + + //Size targetSize = new Size(808, 1200); + + // 808 x 1200 + // 404 x 600 + // 202 x 300 + // 101 x 150 + string imageName = "Calliphora_aligned_size"; + + // Exact matches for 8/4/2/1 scaling + //Size exactSizeX8 = new Size(808, 1200); + //Size exactSizeX4 = new Size(404, 600); + //Size exactSizeX2 = new Size(202, 300); + //Size exactSizeX1 = new Size(101, 150); + //ReencodeImageResize__experimental(imageName, exactSizeX8); + //ReencodeImageResize__experimental(imageName, exactSizeX4); + //ReencodeImageResize__experimental(imageName, exactSizeX2); + //ReencodeImageResize__experimental(imageName, exactSizeX1); + + Size secondPassSizeX8 = new Size((int)(808 * scale), (int)(1200 * scale)); + Size secondPassSizeX4 = new Size((int)(404 * scale), (int)(600 * scale)); + Size secondPassSizeX2 = new Size((int)(202 * scale), (int)(300 * scale)); + Size secondPassSizeX1 = new Size((int)(101 * scale), (int)(150 * scale)); + ReencodeImageResize__experimental(imageName, secondPassSizeX8); + ReencodeImageResize__experimental(imageName, secondPassSizeX4); + ReencodeImageResize__experimental(imageName, secondPassSizeX2); + ReencodeImageResize__experimental(imageName, secondPassSizeX1); + ReencodeImageResize__explicit(imageName, secondPassSizeX8, resampler); + ReencodeImageResize__explicit(imageName, secondPassSizeX4, resampler); + ReencodeImageResize__explicit(imageName, secondPassSizeX2, resampler); + ReencodeImageResize__explicit(imageName, secondPassSizeX1, resampler); + + // 'native' resizing - only jpeg dct downscaling + //ReencodeImageResize_Comparison("Calliphora_ratio1", targetSize, 100); + + // 'native' + software resizing - jpeg dct downscaling + postprocessing + //ReencodeImageResize_Comparison("Calliphora_aligned_size", new Size(269, 400), 99); + + //var benchmarkSize = new Size(404, 600); + //BenchmarkResizingLoop__explicit("jpeg_quality_100", benchmarkSize, 300); + //BenchmarkResizingLoop__experimental("jpeg_quality_100", benchmarkSize, 300); Console.WriteLine("Done."); } - const string pathTemplate = "C:\\Users\\pl4nu\\Downloads\\{0}.jpg"; - private static void BenchmarkEncoder(string fileName, int iterations, int quality, JpegColorType color) { string loadPath = String.Format(pathTemplate, fileName); @@ -174,10 +208,10 @@ namespace SixLabors.ImageSharp.Tests.ProfilingSandbox img.SaveAsJpeg(savePath, encoder); } - private static void DecodeImageResize__explicit(string fileName, Size targetSize, int? quality = null) + private static void ReencodeImageResize__explicit(string fileName, Size targetSize, IResampler sampler, int? quality = null) { string loadPath = String.Format(pathTemplate, fileName); - string savePath = String.Format(pathTemplate, $"q{quality}_test_{fileName}"); + string savePath = String.Format(pathTemplate, $"is_res_{sampler.GetType().Name}[{targetSize.Width}x{targetSize.Height}]_{fileName}"); var decoder = new JpegDecoder(); var encoder = new JpegEncoder() @@ -187,19 +221,18 @@ namespace SixLabors.ImageSharp.Tests.ProfilingSandbox }; using Image img = decoder.Decode(Configuration.Default, File.OpenRead(loadPath), CancellationToken.None); - img.Mutate(ctx => ctx.Resize(targetSize, KnownResamplers.Box, false)); + img.Mutate(ctx => ctx.Resize(targetSize, sampler, compand: false)); img.SaveAsJpeg(savePath, encoder); - } - private static void DecodeImageResize__experimental(string fileName, Size targetSize, int? quality = null) + private static void ReencodeImageResize__experimental(string fileName, Size targetSize, int? quality = null) { string loadPath = String.Format(pathTemplate, fileName); + string savePath = String.Format(pathTemplate, $"is_res_jpeg[{targetSize.Width}x{targetSize.Height}]_{fileName}"); - var decoder = new JpegDecoder(); + var decoder = new JpegDecoder { IgnoreMetadata = true }; using Image img = decoder.Experimental__DecodeInto(Configuration.Default, File.OpenRead(loadPath), targetSize, CancellationToken.None); - string savePath = String.Format(pathTemplate, $"q{quality}_test_{fileName}"); var encoder = new JpegEncoder() { Quality = quality, @@ -208,6 +241,45 @@ namespace SixLabors.ImageSharp.Tests.ProfilingSandbox img.SaveAsJpeg(savePath, encoder); } + private static void ReencodeImageResize__Netvips(string fileName, Size targetSize, int? quality) + { + string loadPath = String.Format(pathTemplate, fileName); + string savePath = String.Format(pathTemplate, $"netvips_resize_{fileName}"); + + using var thumb = NetVips.Image.Thumbnail(loadPath, targetSize.Width, targetSize.Height); + + // Save the results + thumb.Jpegsave(savePath, q: quality, strip: true, subsampleMode: NetVips.Enums.ForeignSubsample.Off); + } + + private static void ReencodeImageResize__MagicScaler(string fileName, Size targetSize, int quality) + { + string loadPath = String.Format(pathTemplate, fileName); + string savePath = String.Format(pathTemplate, $"magicscaler_resize_{fileName}"); + + var settings = new ProcessImageSettings() + { + Width = targetSize.Width, + Height = targetSize.Height, + SaveFormat = FileFormat.Jpeg, + JpegQuality = quality, + JpegSubsampleMode = ChromaSubsampleMode.Subsample444, + Sharpen = false, + ColorProfileMode = ColorProfileMode.Ignore, + HybridMode = HybridScaleMode.Turbo, + }; + + using var output = new FileStream(savePath, FileMode.Create); + MagicImageProcessor.ProcessImage(loadPath, output, settings); + } + + private static void ReencodeImageResize_Comparison(string fileName, Size targetSize, int quality) + { + ReencodeImageResize__experimental(fileName, targetSize, quality); + ReencodeImageResize__Netvips(fileName, targetSize, quality); + ReencodeImageResize__MagicScaler(fileName, targetSize, quality); + } + private static Version GetNetCoreVersion() { Assembly assembly = typeof(System.Runtime.GCSettings).GetTypeInfo().Assembly;