Browse Source

[Rollback] Removed table inheritance for jpeg re-encoding

pull/1706/head
Dmitry Pentin 5 years ago
parent
commit
7a99d6f8a3
  1. 51
      src/ImageSharp/Formats/Jpeg/Components/Quantization.cs
  2. 20
      src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs
  3. 39
      src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs
  4. 76
      src/ImageSharp/Formats/Jpeg/JpegMetadata.cs
  5. 60
      tests/ImageSharp.Tests/Formats/Jpg/QuantizationTests.cs

51
src/ImageSharp/Formats/Jpeg/Components/Quantization.cs

@ -40,30 +40,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components
/// </summary>
public const int QualityEstimationConfidenceUpperThreshold = 98;
/// <summary>
/// Threshold at which given luminance quantization table should be considered 'standard'.
/// Bigger the variance - more likely it to be a non-ITU complient table.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
public const double StandardLuminanceTableVarianceThreshold = 2.36291;
/// <summary>
/// Threshold at which given chrominance quantization table should be considered 'standard'.
/// Bigger the variance - more likely it to be a non-ITU complient table.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
public const double StandardChrominanceTableVarianceThreshold = 0.894963;
/// <summary>
/// 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
/// <param name="target">Quantization to estimate against.</param>
/// <param name="quality">Estimated quality</param>
/// <returns><see cref="bool"/> indicating if given table is target-complient</returns>
public static double EstimateQuality(ref Block8x8F table, ReadOnlySpan<byte> target, out int quality)
public static int EstimateQuality(ref Block8x8F table, ReadOnlySpan<byte> 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;
}
/// <summary>
@ -182,11 +155,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components
/// <param name="quality">Output jpeg quality.</param>
/// <returns><see cref="bool"/> indicating if given table is ITU-complient.</returns>
[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);
/// <summary>
/// Estimates jpeg quality based on quantization table in zig-zag order.
@ -195,11 +165,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components
/// <param name="quality">Output jpeg quality.</param>
/// <returns><see cref="bool"/> indicating if given table is ITU-complient.</returns>
[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)

20
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;
}
}

39
src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs

@ -41,12 +41,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// <summary>
/// The quality, that will be used to encode the image.
/// </summary>
private readonly int? luminanceQuality;
/// <summary>
/// The quality, that will be used to encode the image.
/// </summary>
private readonly int? chrominanceQuality;
private readonly int? quality;
/// <summary>
/// Gets or sets the subsampling method to use.
@ -64,8 +59,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// <param name="options">The options</param>
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
/// <param name="chrominanceQuantTable">Output chrominance quantization table.</param>
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)
{

76
src/ImageSharp/Formats/Jpeg/JpegMetadata.cs

@ -11,8 +11,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// </summary>
public class JpegMetadata : IDeepCloneable
{
private Block8x8F? lumaQuantTable;
private Block8x8F? chromaQuantTable;
private int? luminanceQuality;
private int? chrominanceQuality;
/// <summary>
/// Initializes a new instance of the <see cref="JpegMetadata"/> 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;
}
/// <summary>
/// Gets or sets luminance qunatization table for jpeg image.
/// </summary>
internal Block8x8F LuminanceQuantizationTable
{
get
{
if (this.lumaQuantTable.HasValue)
{
return this.lumaQuantTable.Value;
}
return Quantization.ScaleLuminanceTable(this.LuminanceQuality ?? Quantization.DefaultQualityFactor);
}
set => this.lumaQuantTable = value;
}
/// <summary>
/// Gets or sets chrominance qunatization table for jpeg image.
/// </summary>
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;
}
/// <summary>
@ -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.
/// </remarks>
internal int? LuminanceQuality { get; set; }
internal int LuminanceQuality
{
get => this.luminanceQuality ?? Quantization.DefaultQualityFactor;
set => this.luminanceQuality = value;
}
/// <summary>
/// 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.
/// </remarks>
internal int? ChrominanceQuality { get; set; }
internal int ChrominanceQuality
{
get => this.chrominanceQuality ?? Quantization.DefaultQualityFactor;
set => this.chrominanceQuality = value;
}
/// <summary>
/// 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.
/// </remarks>
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

60
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}");
}
}
}

Loading…
Cancel
Save