diff --git a/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs b/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs index fc602b7f8..7e528d056 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs @@ -40,30 +40,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components /// public const int QualityEstimationConfidenceUpperThreshold = 98; - /// - /// Threshold at which given luminance quantization table should be considered 'standard'. - /// Bigger the variance - more likely it to be a non-ITU complient table. - /// - /// - /// Jpeg does not define either 'quality' nor 'standard quantization table' properties - /// so this is purely a practical value derived from tests. - /// For actual variances output against standard table see tests at Formats.Jpg.QuantizationTests.PrintVariancesFromStandardTables_*. - /// Actual value is 2.3629059983706604, truncated unsignificant part. - /// - public const double StandardLuminanceTableVarianceThreshold = 2.36291; - - /// - /// Threshold at which given chrominance quantization table should be considered 'standard'. - /// Bigger the variance - more likely it to be a non-ITU complient table. - /// - /// - /// Jpeg does not define either 'quality' nor 'standard quantization table' properties - /// so this is purely a practical value derived from tests. - /// For actual variances output against standard table see tests at Formats.Jpg.QuantizationTests.PrintVariancesFromStandardTables_*. - /// Actual value is 0.8949631033036098, truncated unsignificant part. - /// - public const double StandardChrominanceTableVarianceThreshold = 0.894963; - /// /// Gets the unscaled luminance quantization table in zig-zag order. Each /// encoder copies and scales the tables according to its quality parameter. @@ -113,25 +89,24 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components /// Quantization to estimate against. /// Estimated quality /// indicating if given table is target-complient - public static double EstimateQuality(ref Block8x8F table, ReadOnlySpan target, out int quality) + public static int EstimateQuality(ref Block8x8F table, ReadOnlySpan target) { // 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. double comparePercent; double sumPercent = 0; - double sumPercentSqr = 0; // Corner case - all 1's => 100 quality // It would fail to deduce using algorithm below without this check if (table.EqualsToScalar(1)) { // While this is a 100% to be 100 quality, any given table can be scaled to all 1's. - // According to jpeg creators, top of the line quality is 99, 100 is just a technical 'limit' will affect result filesize drastically. + // According to jpeg creators, top of the line quality is 99, 100 is just a technical 'limit' which will affect result filesize drastically. // Quality=100 shouldn't be used in usual use case. - quality = 100; - return 0; + return 100; } + int quality; for (int i = 0; i < Block8x8F.Size; i++) { float coeff = table[i]; @@ -152,7 +127,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components } sumPercent += comparePercent; - sumPercentSqr += comparePercent * comparePercent; } // Perform some statistical analysis of the quality factor @@ -160,7 +134,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components // table being a scaled version of the "standard" tables. // If the variance is high, it is unlikely to be the case. sumPercent /= 64.0; - sumPercentSqr /= 64.0; // Generate the equivalent IJQ "quality" factor if (sumPercent <= 100.0) @@ -172,7 +145,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components quality = (int)Math.Round(5000.0 / sumPercent); } - return sumPercentSqr - (sumPercent * sumPercent); + return quality; } /// @@ -182,11 +155,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components /// Output jpeg quality. /// indicating if given table is ITU-complient. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool EstimateLuminanceQuality(ref Block8x8F luminanceTable, out int quality) - { - double variance = EstimateQuality(ref luminanceTable, UnscaledQuant_Luminance, out quality); - return variance <= StandardLuminanceTableVarianceThreshold; - } + public static int EstimateLuminanceQuality(ref Block8x8F luminanceTable) + => EstimateQuality(ref luminanceTable, UnscaledQuant_Luminance); /// /// Estimates jpeg quality based on quantization table in zig-zag order. @@ -195,11 +165,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components /// Output jpeg quality. /// indicating if given table is ITU-complient. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool EstimateChrominanceQuality(ref Block8x8F chrominanceTable, out int quality) - { - double variance = EstimateQuality(ref chrominanceTable, UnscaledQuant_Chrominance, out quality); - return variance <= StandardChrominanceTableVarianceThreshold; - } + public static int EstimateChrominanceQuality(ref Block8x8F chrominanceTable) + => EstimateQuality(ref chrominanceTable, UnscaledQuant_Luminance); [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int QualityToScale(int quality) diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs index cf21dd226..b6d5aafd1 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs @@ -830,30 +830,14 @@ namespace SixLabors.ImageSharp.Formats.Jpeg // luminance table case 0: { - // if quantization table is non-complient to stardard itu table - // we can't reacreate it later with calculated quality as this is an approximation - // so we save it in the metadata - if (!Quantization.EstimateLuminanceQuality(ref table, out int quality)) - { - jpegMetadata.LuminanceQuantizationTable = table; - } - - jpegMetadata.LuminanceQuality = quality; + jpegMetadata.LuminanceQuality = Quantization.EstimateLuminanceQuality(ref table); break; } // chrominance table case 1: { - // if quantization table is non-complient to stardard itu table - // we can't reacreate it later with calculated quality as this is an approximation - // so we save it in the metadata - if (!Quantization.EstimateChrominanceQuality(ref table, out int quality)) - { - jpegMetadata.ChromaQuantizationTable = table; - } - - jpegMetadata.ChrominanceQuality = quality; + jpegMetadata.ChrominanceQuality = Quantization.EstimateChrominanceQuality(ref table); break; } } diff --git a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs index 828e03de7..88d96f554 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs @@ -41,12 +41,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// /// The quality, that will be used to encode the image. /// - private readonly int? luminanceQuality; - - /// - /// The quality, that will be used to encode the image. - /// - private readonly int? chrominanceQuality; + private readonly int? quality; /// /// Gets or sets the subsampling method to use. @@ -64,8 +59,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// The options public JpegEncoderCore(IJpegEncoderOptions options) { - this.luminanceQuality = options.Quality; - this.chrominanceQuality = options.Quality; + this.quality = options.Quality; this.subsample = options.Subsample; this.colorType = options.ColorType; } @@ -654,30 +648,29 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// Output chrominance quantization table. private void InitQuantizationTables(int componentCount, JpegMetadata metadata, out Block8x8F luminanceQuantTable, out Block8x8F chrominanceQuantTable) { - if (this.luminanceQuality.HasValue) + int lumaQuality; + int chromaQuality; + if (this.quality.HasValue) { - int lumaQuality = Numerics.Clamp(this.luminanceQuality.Value, 1, 100); - luminanceQuantTable = Quantization.ScaleLuminanceTable(lumaQuality); + lumaQuality = this.quality.Value; + chromaQuality = this.quality.Value; } else { - luminanceQuantTable = metadata.LuminanceQuantizationTable; + lumaQuality = metadata.LuminanceQuality; + chromaQuality = metadata.ChrominanceQuality; } + // Luminance + lumaQuality = Numerics.Clamp(lumaQuality, 1, 100); + luminanceQuantTable = Quantization.ScaleLuminanceTable(lumaQuality); + + // Chrominance chrominanceQuantTable = default; if (componentCount > 1) { - int chromaQuality; - if (this.chrominanceQuality.HasValue) - { - chromaQuality = Numerics.Clamp(this.chrominanceQuality.Value, 1, 100); - chrominanceQuantTable = Quantization.ScaleLuminanceTable(chromaQuality); - } - else - { - chromaQuality = Numerics.Clamp(metadata.ChrominanceQuality ?? Quantization.DefaultQualityFactor, 1, 100); - chrominanceQuantTable = metadata.ChromaQuantizationTable; - } + chromaQuality = Numerics.Clamp(chromaQuality, 1, 100); + chrominanceQuantTable = Quantization.ScaleChrominanceTable(chromaQuality); if (!this.subsample.HasValue) { diff --git a/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs b/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs index 77d27ee93..1b17bdce7 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs @@ -11,8 +11,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// public class JpegMetadata : IDeepCloneable { - private Block8x8F? lumaQuantTable; - private Block8x8F? chromaQuantTable; + private int? luminanceQuality; + private int? chrominanceQuality; /// /// Initializes a new instance of the class. @@ -29,46 +29,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg { this.ColorType = other.ColorType; - this.LuminanceQuantizationTable = other.LuminanceQuantizationTable; - this.ChromaQuantizationTable = other.ChromaQuantizationTable; - this.LuminanceQuality = other.LuminanceQuality; - this.ChrominanceQuality = other.ChrominanceQuality; - } - - /// - /// Gets or sets luminance qunatization table for jpeg image. - /// - internal Block8x8F LuminanceQuantizationTable - { - get - { - if (this.lumaQuantTable.HasValue) - { - return this.lumaQuantTable.Value; - } - - return Quantization.ScaleLuminanceTable(this.LuminanceQuality ?? Quantization.DefaultQualityFactor); - } - - set => this.lumaQuantTable = value; - } - - /// - /// Gets or sets chrominance qunatization table for jpeg image. - /// - internal Block8x8F ChromaQuantizationTable - { - get - { - if (this.chromaQuantTable.HasValue) - { - return this.chromaQuantTable.Value; - } - - return Quantization.ScaleChrominanceTable(this.ChrominanceQuality ?? Quantization.DefaultQualityFactor); - } - - set => this.chromaQuantTable = value; + this.luminanceQuality = other.luminanceQuality; + this.chrominanceQuality = other.chrominanceQuality; } /// @@ -78,7 +40,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// This value might not be accurate if it was calculated during jpeg decoding /// with non-complient ITU quantization tables. /// - internal int? LuminanceQuality { get; set; } + internal int LuminanceQuality + { + get => this.luminanceQuality ?? Quantization.DefaultQualityFactor; + set => this.luminanceQuality = value; + } /// /// Gets or sets the jpeg chrominance quality. @@ -87,7 +53,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// This value might not be accurate if it was calculated during jpeg decoding /// with non-complient ITU quantization tables. /// - internal int? ChrominanceQuality { get; set; } + internal int ChrominanceQuality + { + get => this.chrominanceQuality ?? Quantization.DefaultQualityFactor; + set => this.chrominanceQuality = value; + } /// /// Gets or sets the encoded quality. @@ -96,25 +66,29 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// Note that jpeg image can have different quality for luminance and chrominance components. /// This property returns maximum value of luma/chroma qualities. /// - public int? Quality + public int Quality { get { // Jpeg always has a luminance table thus it must have a luminance quality derived from it - if (!this.LuminanceQuality.HasValue) + if (!this.luminanceQuality.HasValue) { - return null; + return Quantization.DefaultQualityFactor; } - // Jpeg might not have a chrominance table - if (!this.ChrominanceQuality.HasValue) + int lumaQuality = this.luminanceQuality.Value; + + // Jpeg might not have a chrominance table - return luminance quality (grayscale images) + if (!this.chrominanceQuality.HasValue) { - return this.LuminanceQuality.Value; + return lumaQuality; } + int chromaQuality = this.chrominanceQuality.Value; + // Theoretically, luma quality would always be greater or equal to chroma quality // But we've already encountered images which can have higher quality of chroma components - return Math.Max(this.LuminanceQuality.Value, this.ChrominanceQuality.Value); + return Math.Max(lumaQuality, chromaQuality); } set diff --git a/tests/ImageSharp.Tests/Formats/Jpg/QuantizationTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/QuantizationTests.cs index 8ed14bd81..2f673ef2f 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/QuantizationTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/QuantizationTests.cs @@ -13,13 +13,6 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg [Trait("Format", "Jpg")] public class QuantizationTests { - public QuantizationTests(ITestOutputHelper output) - { - this.Output = output; - } - - private ITestOutputHelper Output { get; } - [Fact] public void QualityEstimationFromStandardEncoderTables_Luminance() { @@ -28,10 +21,9 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg for (int quality = firstIndex; quality <= lastIndex; quality++) { Block8x8F table = JpegQuantization.ScaleLuminanceTable(quality); - bool isStrandard = JpegQuantization.EstimateLuminanceQuality(ref table, out int actualQuality); + int estimatedQuality = JpegQuantization.EstimateLuminanceQuality(ref table); - Assert.True(isStrandard, $"Standard table is estimated to be non-spec complient at quality level {quality}"); - Assert.Equal(quality, actualQuality); + Assert.True(quality.Equals(estimatedQuality), $"Failed to estimate luminance quality for standard table at quality level {quality}"); } } @@ -43,54 +35,10 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg for (int quality = firstIndex; quality <= lastIndex; quality++) { Block8x8F table = JpegQuantization.ScaleChrominanceTable(quality); - bool isStrandard = JpegQuantization.EstimateChrominanceQuality(ref table, out int actualQuality); + int estimatedQuality = JpegQuantization.EstimateChrominanceQuality(ref table); - Assert.True(isStrandard, $"Standard table is estimated to be non-spec complient at quality level {quality}"); - Assert.Equal(quality, actualQuality); + Assert.True(quality.Equals(estimatedQuality), $"Failed to estimate chrominance quality for standard table at quality level {quality}"); } } - - [Fact(Skip = "Debug only, enable manually!")] - 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.QualityEstimationConfidenceLowerThreshold; q <= JpegQuantization.MaxQualityFactor; q++) - { - Block8x8F table = JpegQuantization.ScaleLuminanceTable(q); - double variance = JpegQuantization.EstimateQuality(ref table, JpegQuantization.UnscaledQuant_Luminance, out int 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!")] - 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.QualityEstimationConfidenceLowerThreshold; q <= JpegQuantization.MaxQualityFactor; q++) - { - Block8x8F table = JpegQuantization.ScaleChrominanceTable(q); - double variance = JpegQuantization.EstimateQuality(ref table, JpegQuantization.UnscaledQuant_Chrominance, out int 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}"); - } } }