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> /// </summary>
public const int QualityEstimationConfidenceUpperThreshold = 98; 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> /// <summary>
/// Gets the unscaled luminance quantization table in zig-zag order. Each /// Gets the unscaled luminance quantization table in zig-zag order. Each
/// encoder copies and scales the tables according to its quality parameter. /// 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="target">Quantization to estimate against.</param>
/// <param name="quality">Estimated quality</param> /// <param name="quality">Estimated quality</param>
/// <returns><see cref="bool"/> indicating if given table is target-complient</returns> /// <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. // 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. // Or when we go to full-int16 spectral code implementation and inject both tables as Block8x8.
double comparePercent; double comparePercent;
double sumPercent = 0; double sumPercent = 0;
double sumPercentSqr = 0;
// Corner case - all 1's => 100 quality // Corner case - all 1's => 100 quality
// It would fail to deduce using algorithm below without this check // It would fail to deduce using algorithm below without this check
if (table.EqualsToScalar(1)) if (table.EqualsToScalar(1))
{ {
// While this is a 100% to be 100 quality, any given table can be scaled to all 1's. // 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 shouldn't be used in usual use case.
quality = 100; return 100;
return 0;
} }
int quality;
for (int i = 0; i < Block8x8F.Size; i++) for (int i = 0; i < Block8x8F.Size; i++)
{ {
float coeff = table[i]; float coeff = table[i];
@ -152,7 +127,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components
} }
sumPercent += comparePercent; sumPercent += comparePercent;
sumPercentSqr += comparePercent * comparePercent;
} }
// Perform some statistical analysis of the quality factor // 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. // table being a scaled version of the "standard" tables.
// If the variance is high, it is unlikely to be the case. // If the variance is high, it is unlikely to be the case.
sumPercent /= 64.0; sumPercent /= 64.0;
sumPercentSqr /= 64.0;
// Generate the equivalent IJQ "quality" factor // Generate the equivalent IJQ "quality" factor
if (sumPercent <= 100.0) if (sumPercent <= 100.0)
@ -172,7 +145,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components
quality = (int)Math.Round(5000.0 / sumPercent); quality = (int)Math.Round(5000.0 / sumPercent);
} }
return sumPercentSqr - (sumPercent * sumPercent); return quality;
} }
/// <summary> /// <summary>
@ -182,11 +155,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components
/// <param name="quality">Output jpeg quality.</param> /// <param name="quality">Output jpeg quality.</param>
/// <returns><see cref="bool"/> indicating if given table is ITU-complient.</returns> /// <returns><see cref="bool"/> indicating if given table is ITU-complient.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool EstimateLuminanceQuality(ref Block8x8F luminanceTable, out int quality) public static int EstimateLuminanceQuality(ref Block8x8F luminanceTable)
{ => EstimateQuality(ref luminanceTable, UnscaledQuant_Luminance);
double variance = EstimateQuality(ref luminanceTable, UnscaledQuant_Luminance, out quality);
return variance <= StandardLuminanceTableVarianceThreshold;
}
/// <summary> /// <summary>
/// Estimates jpeg quality based on quantization table in zig-zag order. /// 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> /// <param name="quality">Output jpeg quality.</param>
/// <returns><see cref="bool"/> indicating if given table is ITU-complient.</returns> /// <returns><see cref="bool"/> indicating if given table is ITU-complient.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool EstimateChrominanceQuality(ref Block8x8F chrominanceTable, out int quality) public static int EstimateChrominanceQuality(ref Block8x8F chrominanceTable)
{ => EstimateQuality(ref chrominanceTable, UnscaledQuant_Luminance);
double variance = EstimateQuality(ref chrominanceTable, UnscaledQuant_Chrominance, out quality);
return variance <= StandardChrominanceTableVarianceThreshold;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int QualityToScale(int quality) private static int QualityToScale(int quality)

20
src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs

@ -830,30 +830,14 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
// luminance table // luminance table
case 0: case 0:
{ {
// if quantization table is non-complient to stardard itu table jpegMetadata.LuminanceQuality = Quantization.EstimateLuminanceQuality(ref 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;
break; break;
} }
// chrominance table // chrominance table
case 1: case 1:
{ {
// if quantization table is non-complient to stardard itu table jpegMetadata.ChrominanceQuality = Quantization.EstimateChrominanceQuality(ref 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;
break; break;
} }
} }

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

@ -41,12 +41,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// <summary> /// <summary>
/// The quality, that will be used to encode the image. /// The quality, that will be used to encode the image.
/// </summary> /// </summary>
private readonly int? luminanceQuality; private readonly int? quality;
/// <summary>
/// The quality, that will be used to encode the image.
/// </summary>
private readonly int? chrominanceQuality;
/// <summary> /// <summary>
/// Gets or sets the subsampling method to use. /// Gets or sets the subsampling method to use.
@ -64,8 +59,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// <param name="options">The options</param> /// <param name="options">The options</param>
public JpegEncoderCore(IJpegEncoderOptions options) public JpegEncoderCore(IJpegEncoderOptions options)
{ {
this.luminanceQuality = options.Quality; this.quality = options.Quality;
this.chrominanceQuality = options.Quality;
this.subsample = options.Subsample; this.subsample = options.Subsample;
this.colorType = options.ColorType; this.colorType = options.ColorType;
} }
@ -654,30 +648,29 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// <param name="chrominanceQuantTable">Output chrominance quantization table.</param> /// <param name="chrominanceQuantTable">Output chrominance quantization table.</param>
private void InitQuantizationTables(int componentCount, JpegMetadata metadata, out Block8x8F luminanceQuantTable, out Block8x8F chrominanceQuantTable) 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); lumaQuality = this.quality.Value;
luminanceQuantTable = Quantization.ScaleLuminanceTable(lumaQuality); chromaQuality = this.quality.Value;
} }
else else
{ {
luminanceQuantTable = metadata.LuminanceQuantizationTable; lumaQuality = metadata.LuminanceQuality;
chromaQuality = metadata.ChrominanceQuality;
} }
// Luminance
lumaQuality = Numerics.Clamp(lumaQuality, 1, 100);
luminanceQuantTable = Quantization.ScaleLuminanceTable(lumaQuality);
// Chrominance
chrominanceQuantTable = default; chrominanceQuantTable = default;
if (componentCount > 1) if (componentCount > 1)
{ {
int chromaQuality; chromaQuality = Numerics.Clamp(chromaQuality, 1, 100);
if (this.chrominanceQuality.HasValue) chrominanceQuantTable = Quantization.ScaleChrominanceTable(chromaQuality);
{
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;
}
if (!this.subsample.HasValue) if (!this.subsample.HasValue)
{ {

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

@ -11,8 +11,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// </summary> /// </summary>
public class JpegMetadata : IDeepCloneable public class JpegMetadata : IDeepCloneable
{ {
private Block8x8F? lumaQuantTable; private int? luminanceQuality;
private Block8x8F? chromaQuantTable; private int? chrominanceQuality;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="JpegMetadata"/> class. /// Initializes a new instance of the <see cref="JpegMetadata"/> class.
@ -29,46 +29,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
{ {
this.ColorType = other.ColorType; this.ColorType = other.ColorType;
this.LuminanceQuantizationTable = other.LuminanceQuantizationTable; this.luminanceQuality = other.luminanceQuality;
this.ChromaQuantizationTable = other.ChromaQuantizationTable; this.chrominanceQuality = other.chrominanceQuality;
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;
} }
/// <summary> /// <summary>
@ -78,7 +40,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// This value might not be accurate if it was calculated during jpeg decoding /// This value might not be accurate if it was calculated during jpeg decoding
/// with non-complient ITU quantization tables. /// with non-complient ITU quantization tables.
/// </remarks> /// </remarks>
internal int? LuminanceQuality { get; set; } internal int LuminanceQuality
{
get => this.luminanceQuality ?? Quantization.DefaultQualityFactor;
set => this.luminanceQuality = value;
}
/// <summary> /// <summary>
/// Gets or sets the jpeg chrominance quality. /// 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 /// This value might not be accurate if it was calculated during jpeg decoding
/// with non-complient ITU quantization tables. /// with non-complient ITU quantization tables.
/// </remarks> /// </remarks>
internal int? ChrominanceQuality { get; set; } internal int ChrominanceQuality
{
get => this.chrominanceQuality ?? Quantization.DefaultQualityFactor;
set => this.chrominanceQuality = value;
}
/// <summary> /// <summary>
/// Gets or sets the encoded quality. /// 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. /// Note that jpeg image can have different quality for luminance and chrominance components.
/// This property returns maximum value of luma/chroma qualities. /// This property returns maximum value of luma/chroma qualities.
/// </remarks> /// </remarks>
public int? Quality public int Quality
{ {
get get
{ {
// Jpeg always has a luminance table thus it must have a luminance quality derived from it // 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 int lumaQuality = this.luminanceQuality.Value;
if (!this.ChrominanceQuality.HasValue)
// 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 // 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 // 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 set

60
tests/ImageSharp.Tests/Formats/Jpg/QuantizationTests.cs

@ -13,13 +13,6 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
[Trait("Format", "Jpg")] [Trait("Format", "Jpg")]
public class QuantizationTests public class QuantizationTests
{ {
public QuantizationTests(ITestOutputHelper output)
{
this.Output = output;
}
private ITestOutputHelper Output { get; }
[Fact] [Fact]
public void QualityEstimationFromStandardEncoderTables_Luminance() public void QualityEstimationFromStandardEncoderTables_Luminance()
{ {
@ -28,10 +21,9 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
for (int quality = firstIndex; quality <= lastIndex; quality++) for (int quality = firstIndex; quality <= lastIndex; quality++)
{ {
Block8x8F table = JpegQuantization.ScaleLuminanceTable(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.True(quality.Equals(estimatedQuality), $"Failed to estimate luminance quality for standard table at quality level {quality}");
Assert.Equal(quality, actualQuality);
} }
} }
@ -43,54 +35,10 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
for (int quality = firstIndex; quality <= lastIndex; quality++) for (int quality = firstIndex; quality <= lastIndex; quality++)
{ {
Block8x8F table = JpegQuantization.ScaleChrominanceTable(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.True(quality.Equals(estimatedQuality), $"Failed to estimate chrominance quality for standard table at quality level {quality}");
Assert.Equal(quality, actualQuality);
} }
} }
[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