From 961e3b1d6471eef8cfebdb7113cccd28a9667390 Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Mon, 19 Jul 2021 01:14:14 +0300 Subject: [PATCH] Added tests for variance thresholds --- .../Formats/Jpeg/Components/Quantization.cs | 51 ++++++++++---- .../Formats/Jpg/QuantizationTests.cs | 68 +++++++++++++++++++ 2 files changed, 105 insertions(+), 14 deletions(-) create mode 100644 tests/ImageSharp.Tests/Formats/Jpg/QuantizationTests.cs diff --git a/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs b/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs index b87c538a9..3087551d0 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs @@ -13,6 +13,23 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components /// internal static class Quantization { + /// + /// Upper bound (inclusive) for jpeg quality setting. + /// + public const int MaxQualityFactor = 100; + + /// + /// Lower bound (inclusive) for jpeg quality setting. + /// + public const int MinQualityFactor = 1; + + /// + /// Represents lowest quality setting which can be estimated with enough confidence. + /// Any quality below it results in a highly compressed jpeg image + /// which shouldn't use standard itu quantization tables for re-encoding. + /// + public const int QualityEstimationConfidenceThreshold = 25; + /// /// Threshold at which given luminance quantization table should be considered 'standard'. /// Bigger the variance - more likely it to be a non-ITU complient table. @@ -21,7 +38,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components /// Jpeg does not define either 'quality' nor 'standard quantization table' properties /// so this is purely a practical value derived from tests. /// - private const double StandardLuminanceTableVarianceThreshold = 10.0; + public const double StandardLuminanceTableVarianceThreshold = 10.0; /// /// Threshold at which given chrominance quantization table should be considered 'standard'. @@ -31,7 +48,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components /// Jpeg does not define either 'quality' nor 'standard quantization table' properties /// so this is purely a practical value derived from tests. /// - private const double StandardChrominanceTableVarianceThreshold = 10.0; + public const double StandardChrominanceTableVarianceThreshold = 10.0; /// /// Gets the unscaled luminance quantization table in zig-zag order. Each @@ -42,7 +59,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components // The C# compiler emits this as a compile-time constant embedded in the PE file. // This is effectively compiled down to: return new ReadOnlySpan(&data, length) // More details can be found: https://github.com/dotnet/roslyn/pull/24621 - private static ReadOnlySpan UnscaledQuant_Luminance => new byte[] + public static ReadOnlySpan UnscaledQuant_Luminance => new byte[] { 16, 11, 12, 14, 12, 10, 16, 14, 13, 14, 18, 17, 16, 19, 24, 40, 26, 24, 22, 22, 24, 49, 35, 37, 29, 40, 58, 51, 61, 60, @@ -60,7 +77,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components // The C# compiler emits this as a compile-time constant embedded in the PE file. // This is effectively compiled down to: return new ReadOnlySpan(&data, length) // More details can be found: https://github.com/dotnet/roslyn/pull/24621 - private static ReadOnlySpan UnscaledQuant_Chrominance => new byte[] + public static ReadOnlySpan UnscaledQuant_Chrominance => new byte[] { 17, 18, 18, 24, 21, 24, 47, 26, 26, 47, 99, 66, 56, 66, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, @@ -72,7 +89,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components /// Ported from JPEGsnoop: /// https://github.com/ImpulseAdventure/JPEGsnoop/blob/9732ee0961f100eb69bbff4a0c47438d5997abee/source/JfifDecode.cpp#L4570-L4694 /// - /// Estimates jpeg quality based on quantization table. + /// Estimates jpeg quality based on quantization table in zig-zag order. /// /// /// This technically can be used with any given table but internal decoder code uses ITU spec tables: @@ -80,10 +97,9 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components /// /// Input quantization table. /// Quantization to estimate against. - /// Variance threshold after which given table is considered non-complient. /// Estimated quality /// indicating if given table is target-complient - public static bool EstimateQuality(ref Block8x8F table, ReadOnlySpan target, double varianceThreshold, out double quality) + public static double EstimateQuality(ref Block8x8F table, ReadOnlySpan target, out double quality) { // This method can be SIMD'ified if standard table is injected as Block8x8F. // Or when we go to full-int16 spectral code implementation and inject both tables as Block8x8. @@ -99,7 +115,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components // According to jpeg creators, top of the line quality is 99, 100 is just a technical 'limit' will affect result filesize drastically. // Quality=100 shouldn't be used in usual use case. quality = 100; - return true; + return 0; } for (int i = 0; i < Block8x8F.Size; i++) @@ -131,7 +147,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components // If the variance is high, it is unlikely to be the case. sumPercent /= 64.0; sumPercentSqr /= 64.0; - double variance = sumPercentSqr - (sumPercent * sumPercent); // Generate the equivalent IJQ "quality" factor if (sumPercent <= 100.0) @@ -143,28 +158,34 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components quality = 5000.0 / sumPercent; } - return variance <= varianceThreshold; + return sumPercentSqr - (sumPercent * sumPercent); } /// - /// Estimates jpeg luminance quality. + /// Estimates jpeg quality based on quantization table in zig-zag order. /// /// Luminance quantization table. /// Output jpeg quality. /// indicating if given table is ITU-complient. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool EstimateLuminanceQuality(ref Block8x8F luminanceTable, out double quality) - => EstimateQuality(ref luminanceTable, UnscaledQuant_Luminance, StandardLuminanceTableVarianceThreshold, out quality); + { + double variance = EstimateQuality(ref luminanceTable, UnscaledQuant_Luminance, out quality); + return variance <= StandardLuminanceTableVarianceThreshold; + } /// - /// Estimates jpeg chrominance quality. + /// Estimates jpeg quality based on quantization table in zig-zag order. /// /// Chrominance quantization table. /// Output jpeg quality. /// indicating if given table is ITU-complient. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool EstimateChrominanceQuality(ref Block8x8F chrominanceTable, out double quality) - => EstimateQuality(ref chrominanceTable, UnscaledQuant_Chrominance, StandardChrominanceTableVarianceThreshold, out quality); + { + double variance = EstimateQuality(ref chrominanceTable, UnscaledQuant_Chrominance, out quality); + return variance <= StandardChrominanceTableVarianceThreshold; + } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int QualityToScale(int quality) @@ -172,6 +193,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components private static Block8x8F ScaleQuantizationTable(int scale, ReadOnlySpan unscaledTable) { + DebugGuard.MustBeBetweenOrEqualTo(scale, MinQualityFactor, MaxQualityFactor, nameof(scale)); + Block8x8F table = default; for (int j = 0; j < Block8x8F.Size; j++) { diff --git a/tests/ImageSharp.Tests/Formats/Jpg/QuantizationTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/QuantizationTests.cs new file mode 100644 index 000000000..1870c39fa --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Jpg/QuantizationTests.cs @@ -0,0 +1,68 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using SixLabors.ImageSharp.Formats.Jpeg.Components; +using Xunit; +using Xunit.Abstractions; + +using JpegQuantization = SixLabors.ImageSharp.Formats.Jpeg.Components.Quantization; + +namespace SixLabors.ImageSharp.Tests.Formats.Jpg +{ + [Trait("Format", "Jpg")] + public class QuantizationTests + { + public QuantizationTests(ITestOutputHelper output) + { + this.Output = output; + } + + private ITestOutputHelper Output { get; } + + //[Fact(Skip = "Debug only, enable manually!")] + [Fact] + public void PrintVariancesFromStandardTables_Luminance() + { + this.Output.WriteLine("Variances for Luminance table.\nQuality levels 25-100:"); + + double minVariance = double.MaxValue; + double maxVariance = double.MinValue; + + for (int q = JpegQuantization.QualityEstimationConfidenceThreshold; q <= JpegQuantization.MaxQualityFactor; q++) + { + Block8x8F table = JpegQuantization.ScaleLuminanceTable(q); + double variance = JpegQuantization.EstimateQuality(ref table, JpegQuantization.UnscaledQuant_Luminance, out double quality); + + minVariance = Math.Min(minVariance, variance); + maxVariance = Math.Max(maxVariance, variance); + + this.Output.WriteLine($"q={q}\t{variance}\test. q: {quality}"); + } + + this.Output.WriteLine($"Min variance: {minVariance}\nMax variance: {maxVariance}"); + } + + //[Fact(Skip = "Debug only, enable manually!")] + [Fact] + public void PrintVariancesFromStandardTables_Chrominance() + { + this.Output.WriteLine("Variances for Chrominance table.\nQuality levels 25-100:"); + + double minVariance = double.MaxValue; + double maxVariance = double.MinValue; + for (int q = JpegQuantization.QualityEstimationConfidenceThreshold; q <= JpegQuantization.MaxQualityFactor; q++) + { + Block8x8F table = JpegQuantization.ScaleChrominanceTable(q); + double variance = JpegQuantization.EstimateQuality(ref table, JpegQuantization.UnscaledQuant_Chrominance, out double quality); + + minVariance = Math.Min(minVariance, variance); + maxVariance = Math.Max(maxVariance, variance); + + this.Output.WriteLine($"q={q}\t{variance}\test. q: {quality}"); + } + + this.Output.WriteLine($"Min variance: {minVariance}\nMax variance: {maxVariance}"); + } + } +}