From 2494131cfa6d37ac280b10965fea17b6d834d112 Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Sun, 18 Jul 2021 22:55:26 +0300 Subject: [PATCH] Fixed invalid quality estimation --- .../Formats/Jpeg/Components/Quantization.cs | 48 +++++++++++++++-- .../Formats/Jpeg/JpegDecoderCore.cs | 16 +++--- .../Formats/Jpeg/JpegEncoderCore.cs | 51 ++----------------- 3 files changed, 57 insertions(+), 58 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs b/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs index a7af76a65d..b87c538a93 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Runtime.CompilerServices; using System.Text; namespace SixLabors.ImageSharp.Formats.Jpeg.Components @@ -20,7 +21,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. /// - public const double StandardLuminanceTableVarianceThreshold = 10.0; + private const double StandardLuminanceTableVarianceThreshold = 10.0; /// /// Threshold at which given chrominance quantization table should be considered 'standard'. @@ -30,7 +31,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. /// - public const double StandardChrominanceTableVarianceThreshold = 10.0; + private const double StandardChrominanceTableVarianceThreshold = 10.0; /// /// Gets the unscaled luminance quantization table in zig-zag order. Each @@ -41,7 +42,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 - public static ReadOnlySpan UnscaledQuant_Luminance => new byte[] + private 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, @@ -59,7 +60,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 - public static ReadOnlySpan UnscaledQuant_Chrominance => new byte[] + private 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, @@ -82,7 +83,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components /// Variance threshold after which given table is considered non-complient. /// Estimated quality /// indicating if given table is target-complient - private static bool EstimateQuality(ref Block8x8F table, ReadOnlySpan target, double varianceThreshold, out double quality) + public static bool EstimateQuality(ref Block8x8F table, ReadOnlySpan target, double varianceThreshold, 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. @@ -151,6 +152,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components /// 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); @@ -160,7 +162,43 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components /// 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); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int QualityToScale(int quality) + => quality < 50 ? 5000 / quality : 200 - (quality * 2); + + private static Block8x8F ScaleQuantizationTable(int scale, ReadOnlySpan unscaledTable) + { + Block8x8F table = default; + for (int j = 0; j < Block8x8F.Size; j++) + { + int x = unscaledTable[j]; + x = ((x * scale) + 50) / 100; + if (x < 1) + { + x = 1; + } + + if (x > 255) + { + x = 255; + } + + table[j] = x; + } + + return table; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Block8x8F ScaleLuminanceTable(int quality) + => ScaleQuantizationTable(scale: QualityToScale(quality), UnscaledQuant_Luminance); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Block8x8F ScaleChrominanceTable(int quality) + => ScaleQuantizationTable(scale: QualityToScale(quality), UnscaledQuant_Chrominance); } } diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs index 8d8ad3dad3..09b40e09d4 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs @@ -830,26 +830,30 @@ namespace SixLabors.ImageSharp.Formats.Jpeg // luminance table case 0: { - Quantization.EstimateQuality(ref table, Quantization.UnscaledQuant_Luminance, out double quality, out double variance); - jpegMetadata.LuminanceQuality = quality; - if (variance <= Quantization.StandardLuminanceTableVarianceThreshold) + // 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 double quality)) { jpegMetadata.LumaQuantizationTable = table.RoundAsInt16Block(); } + jpegMetadata.LuminanceQuality = quality; break; } // chrominance table case 1: { - Quantization.EstimateQuality(ref table, Quantization.UnscaledQuant_Chrominance, out double quality, out double variance); - jpegMetadata.ChrominanceQuality = quality; - if (variance <= Quantization.StandardChrominanceTableVarianceThreshold) + // 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 double quality)) { jpegMetadata.ChromaQuantizationTable = table.RoundAsInt16Block(); } + jpegMetadata.ChrominanceQuality = quality; break; } } diff --git a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs index 8711483355..c829e0972a 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs @@ -94,27 +94,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg int qlty = Numerics.Clamp(this.quality ?? metadata.GetJpegMetadata().Quality, 1, 100); this.subsample ??= qlty >= 91 ? JpegSubsample.Ratio444 : JpegSubsample.Ratio420; - // Convert from a quality rating to a scaling factor. - int scale; - if (qlty < 50) - { - scale = 5000 / qlty; - } - else - { - scale = 200 - (qlty * 2); - } - // Initialize the quantization tables. // TODO: This looks ugly, should we write chrominance table for luminance-only images? // If not - this can code can be simplified - Block8x8F luminanceQuantTable = default; - Block8x8F chrominanceQuantTable = default; - InitQuantizationTable(0, scale, ref luminanceQuantTable); - if (componentCount > 1) - { - InitQuantizationTable(1, scale, ref chrominanceQuantTable); - } + Block8x8F luminanceQuantTable = Quantization.ScaleLuminanceTable(qlty); + Block8x8F chrominanceQuantTable = Quantization.ScaleChrominanceTable(qlty); // Write the Start Of Image marker. this.WriteApplicationHeader(metadata); @@ -138,10 +122,12 @@ namespace SixLabors.ImageSharp.Formats.Jpeg var scanEncoder = new HuffmanScanEncoder(stream); if (this.colorType == JpegColorType.Luminance) { + // luminance quantization table only scanEncoder.EncodeGrayscale(image, ref luminanceQuantTable, cancellationToken); } else { + // luminance and chrominance quantization tables switch (this.subsample) { case JpegSubsample.Ratio444: @@ -650,34 +636,5 @@ namespace SixLabors.ImageSharp.Formats.Jpeg this.buffer[3] = (byte)(length & 0xff); this.outputStream.Write(this.buffer, 0, 4); } - - /// - /// Initializes quantization table. - /// - /// The quantization index. - /// The scaling factor. - /// The quantization table. - private static void InitQuantizationTable(int i, int scale, ref Block8x8F quant) - { - DebugGuard.MustBeBetweenOrEqualTo(i, 0, 1, nameof(i)); - ReadOnlySpan unscaledQuant = (i == 0) ? Quantization.UnscaledQuant_Luminance : Quantization.UnscaledQuant_Chrominance; - - for (int j = 0; j < Block8x8F.Size; j++) - { - int x = unscaledQuant[j]; - x = ((x * scale) + 50) / 100; - if (x < 1) - { - x = 1; - } - - if (x > 255) - { - x = 255; - } - - quant[j] = x; - } - } } }