Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
b16301b
Simplified quantization table scaling in the encoder
Jul 17, 2021
51e1366
Added debug guard to the estimate quality
Jul 17, 2021
1d781da
Added comments to DQT marker parser, DQT exceptions now provide bette…
Jul 17, 2021
15d77dd
EstimateQuality now return if given tables are standard
Jul 17, 2021
664d7f3
Initial new quality estimator
Jul 17, 2021
7772ff1
Added comments, docs and improved estimation performances with intrin…
Jul 17, 2021
ca29541
Fixed standard quality logic
Jul 17, 2021
2fd703d
Fixed 75 default quality from the encoder
Jul 17, 2021
ee424d4
Revert "Simplified quantization table scaling in the encoder"
Jul 17, 2021
d50f4b5
Fixed compilation errors
Jul 17, 2021
54105e3
Added docs to the metadata properties
Jul 18, 2021
1746283
Added docs
Jul 18, 2021
4b20325
Commented obsolete attribute for quality
Jul 18, 2021
2494131
Fixed invalid quality estimation
Jul 18, 2021
961e3b1
Added tests for variance thresholds
Jul 18, 2021
d537ede
Updated thresholds
Jul 19, 2021
e40313c
Made quality metadata int, added tests for standard tables
Jul 19, 2021
81b5e7f
Removed obsolete attribute
Jul 19, 2021
cf1ad8e
(WIP) quality
Jul 20, 2021
eaeab7a
(WIP) quality
Jul 20, 2021
3fdefa7
Encoder now uses appropriate quality or provied quantization tables
Jul 20, 2021
376d190
Fixed encoder metadata usage
Jul 20, 2021
e9fddd0
Fixed subsample assignment in the encoder
Jul 20, 2021
a9a4f6f
Removed luma/chroma quality from the encoder
Jul 22, 2021
5bdc5a0
Added remarks
Jul 22, 2021
7453466
Fixed comments
Jul 22, 2021
45292fb
Fixed clamping
Jul 22, 2021
404639f
Final API improvements
Jul 23, 2021
0e1aa6a
Made Quality nullable
Jul 27, 2021
7a99d6f
[Rollback] Removed table inheritance for jpeg re-encoding
Jul 27, 2021
c64ac5f
Comments, quantization table scaling with clamp call
Jul 27, 2021
e205bea
Tests
Jul 27, 2021
99b24fe
Added images to quality estimation tests
Jul 27, 2021
906ac84
Fixed invalid chrominance quality estimation
Jul 27, 2021
df72d10
Merge branch 'master' into jpeg-quantization-metadata
Jul 27, 2021
7463b49
Used nint in Unsafe.Add calls
Jul 27, 2021
eb6888a
Fixed warnings
Jul 27, 2021
8b12623
Revert "Used nint in Unsafe.Add calls"
Jul 27, 2021
84e52e7
Moved quality debug check to a proper place
Jul 28, 2021
eaf40fc
nint usage
Jul 28, 2021
66604f4
Revert "nint usage"
Jul 28, 2021
1e6547b
Merge branch 'master' into jpeg-quantization-metadata
JimBobSquarePants Aug 4, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions src/ImageSharp/Formats/Jpeg/Components/Block8x8F.cs
Original file line number Diff line number Diff line change
Expand Up @@ -830,5 +830,46 @@ public void TransposeInto(ref Block8x8F d)
d.V7R.W = this.V7R.W;
}
}

/// <summary>
/// Compares entire 8x8 block to a single scalar value.
/// </summary>
/// <param name="value">Value to compare to.</param>
public bool EqualsToScalar(int value)
{
#if SUPPORTS_RUNTIME_INTRINSICS
if (Avx2.IsSupported)
{
const int equalityMask = unchecked((int)0b1111_1111_1111_1111_1111_1111_1111_1111);

var targetVector = Vector256.Create(value);
ref Vector256<float> blockStride = ref this.V0;

for (int i = 0; i < RowCount; i++)
{
Vector256<int> areEqual = Avx2.CompareEqual(Avx.ConvertToVector256Int32WithTruncation(Unsafe.Add(ref this.V0, i)), targetVector);
if (Avx2.MoveMask(areEqual.AsByte()) != equalityMask)
{
return false;
}
}

return true;
}
#endif
{
ref float scalars = ref Unsafe.As<Block8x8F, float>(ref this);

for (int i = 0; i < Size; i++)
{
if ((int)Unsafe.Add(ref scalars, i) != value)
{
return false;
}
}

return true;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public JpegComponent(MemoryAllocator memoryAllocator, JpegFrame frame, byte id,

if (quantizationTableIndex > 3)
{
JpegThrowHelper.ThrowBadQuantizationTable();
JpegThrowHelper.ThrowBadQuantizationTableIndex(quantizationTableIndex);
}

this.QuantizationTableIndex = quantizationTableIndex;
Expand Down
144 changes: 0 additions & 144 deletions src/ImageSharp/Formats/Jpeg/Components/Decoder/QualityEvaluator.cs

This file was deleted.

194 changes: 194 additions & 0 deletions src/ImageSharp/Formats/Jpeg/Components/Quantization.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.

using System;
using System.Runtime.CompilerServices;

namespace SixLabors.ImageSharp.Formats.Jpeg.Components
{
/// <summary>
/// Provides methods and properties related to jpeg quantization.
/// </summary>
internal static class Quantization
{
/// <summary>
/// Upper bound (inclusive) for jpeg quality setting.
/// </summary>
public const int MaxQualityFactor = 100;

/// <summary>
/// Lower bound (inclusive) for jpeg quality setting.
/// </summary>
public const int MinQualityFactor = 1;

/// <summary>
/// Default JPEG quality for both luminance and chominance tables.
/// </summary>
public const int DefaultQualityFactor = 75;

/// <summary>
/// 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.
/// </summary>
public const int QualityEstimationConfidenceLowerThreshold = 25;

/// <summary>
/// Represents highest quality setting which can be estimated with enough confidence.
/// </summary>
public const int QualityEstimationConfidenceUpperThreshold = 98;

/// <summary>
/// Gets the unscaled luminance quantization table in zig-zag order. Each
/// encoder copies and scales the tables according to its quality parameter.
/// The values are derived from ITU section K.1 after converting from natural to
/// zig-zag order.
/// </summary>
// The C# compiler emits this as a compile-time constant embedded in the PE file.
// This is effectively compiled down to: return new ReadOnlySpan<byte>(&data, length)
// More details can be found: https://github.com/dotnet/roslyn/pull/24621
public static ReadOnlySpan<byte> 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,
57, 51, 56, 55, 64, 72, 92, 78, 64, 68, 87, 69, 55, 56, 80,
109, 81, 87, 95, 98, 103, 104, 103, 62, 77, 113, 121, 112,
100, 120, 92, 101, 103, 99,
};

/// <summary>
/// Gets the unscaled chrominance quantization table in zig-zag order. Each
/// encoder copies and scales the tables according to its quality parameter.
/// The values are derived from ITU section K.1 after converting from natural to
/// zig-zag order.
/// </summary>
// The C# compiler emits this as a compile-time constant embedded in the PE file.
// This is effectively compiled down to: return new ReadOnlySpan<byte>(&data, length)
// More details can be found: https://github.com/dotnet/roslyn/pull/24621
public static ReadOnlySpan<byte> 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,
99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99,
99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99,
99, 99, 99, 99, 99, 99, 99, 99,
};

/// Ported from JPEGsnoop:
/// https://github.com/ImpulseAdventure/JPEGsnoop/blob/9732ee0961f100eb69bbff4a0c47438d5997abee/source/JfifDecode.cpp#L4570-L4694
/// <summary>
/// Estimates jpeg quality based on quantization table in zig-zag order.
/// </summary>
/// <remarks>
/// This technically can be used with any given table but internal decoder code uses ITU spec tables:
/// <see cref="UnscaledQuant_Luminance"/> and <see cref="UnscaledQuant_Chrominance"/>.
/// </remarks>
/// <param name="table">Input quantization table.</param>
/// <param name="target">Quantization to estimate against.</param>
/// <returns>Estimated quality</returns>
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;

// 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' which will affect result filesize drastically.
// Quality=100 shouldn't be used in usual use case.
return 100;
}

int quality;
for (int i = 0; i < Block8x8F.Size; i++)
{
float coeff = table[i];
int coeffInteger = (int)coeff;

// Coefficients are actually int16 casted to float numbers so there's no truncating error.
if (coeffInteger != 0)
{
comparePercent = 100.0 * (table[i] / target[i]);
}
else
{
// No 'valid' quantization table should contain zero at any position
// while this is okay to decode with, it will throw DivideByZeroException at encoding proces stage.
// Not sure what to do here, we can't throw as this technically correct
// but this will screw up the encoder.
comparePercent = 999.99;
}

sumPercent += comparePercent;
}

// Perform some statistical analysis of the quality factor
// to determine the likelihood of the current quantization
// table being a scaled version of the "standard" tables.
// If the variance is high, it is unlikely to be the case.
sumPercent /= 64.0;

// Generate the equivalent IJQ "quality" factor
if (sumPercent <= 100.0)
{
quality = (int)Math.Round((200 - sumPercent) / 2);
}
else
{
quality = (int)Math.Round(5000.0 / sumPercent);
}

return quality;
}

/// <summary>
/// Estimates jpeg quality based on quantization table in zig-zag order.
/// </summary>
/// <param name="luminanceTable">Luminance quantization table.</param>
/// <returns>Estimated quality</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int EstimateLuminanceQuality(ref Block8x8F luminanceTable)
=> EstimateQuality(ref luminanceTable, UnscaledQuant_Luminance);

/// <summary>
/// Estimates jpeg quality based on quantization table in zig-zag order.
/// </summary>
/// <param name="chrominanceTable">Chrominance quantization table.</param>
/// <returns>Estimated quality</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int EstimateChrominanceQuality(ref Block8x8F chrominanceTable)
=> EstimateQuality(ref chrominanceTable, UnscaledQuant_Chrominance);

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int QualityToScale(int quality)
{
DebugGuard.MustBeBetweenOrEqualTo(quality, MinQualityFactor, MaxQualityFactor, nameof(quality));

return quality < 50 ? (5000 / quality) : (200 - (quality * 2));
}

private static Block8x8F ScaleQuantizationTable(int scale, ReadOnlySpan<byte> unscaledTable)
{
Block8x8F table = default;
for (int j = 0; j < Block8x8F.Size; j++)
{
int x = ((unscaledTable[j] * scale) + 50) / 100;
table[j] = Numerics.Clamp(x, 1, 255);
}

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);
}
}
Loading