Browse Source

Document and correct HEIF quantization

pull/2633/head
James Jackson-South 1 week ago
parent
commit
726b559fcf
  1. 46
      src/ImageSharp/Formats/Heif/Av1/Pipeline/Quantification/Av1DeQuantizationContext.cs
  2. 26
      src/ImageSharp/Formats/Heif/Av1/Pipeline/Quantification/Av1InverseQuantizationLookup.cs
  3. 52
      src/ImageSharp/Formats/Heif/Av1/Pipeline/Quantification/Av1InverseQuantizer.cs
  4. 45
      src/ImageSharp/Formats/Heif/Av1/Pipeline/Quantification/Av1QuantizationLookup.cs

46
src/ImageSharp/Formats/Heif/Av1/Pipeline/Quantification/Av1DeQuantizationContext.cs

@ -5,14 +5,28 @@ using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Quantification;
/// <summary>
/// Stores the AV1 DC and AC dequantization values for every segment and color plane in a frame.
/// </summary>
internal class Av1DeQuantizationContext
{
/// <summary>
/// The DC dequantization values indexed by segment and then plane.
/// </summary>
private readonly short[][] dcContent;
/// <summary>
/// The AC dequantization values indexed by segment and then plane.
/// </summary>
private readonly short[][] acContent;
/// <remarks>
/// SVT: svt_aom_setup_segmentation_dequant
/// </remarks>
/// <summary>
/// Initializes a new instance of the <see cref="Av1DeQuantizationContext"/> class from the frame's base quantizer,
/// segment adjustments, plane deltas, and coded bit depth.
/// </summary>
/// <param name="sequenceHeader">The sequence header that supplies the coded bit depth.</param>
/// <param name="frameHeader">The frame header that supplies segmentation and quantization parameters.</param>
/// <remarks>SVT-AV1: <c>svt_aom_setup_segmentation_dequant</c>.</remarks>
public Av1DeQuantizationContext(ObuSequenceHeader sequenceHeader, ObuFrameHeader frameHeader)
{
Av1BitDepth bitDepth = sequenceHeader.ColorConfig.BitDepth;
@ -35,15 +49,39 @@ internal class Av1DeQuantizationContext
}
}
/// <summary>
/// Gets the DC dequantization value for a segment and color plane.
/// </summary>
/// <param name="segmentId">The zero-based AV1 segment identifier.</param>
/// <param name="plane">The color plane.</param>
/// <returns>The DC dequantization value.</returns>
public short GetDc(int segmentId, Av1Plane plane)
=> this.dcContent[segmentId][(int)plane];
/// <summary>
/// Gets the AC dequantization value for a segment and color plane.
/// </summary>
/// <param name="segmentId">The zero-based AV1 segment identifier.</param>
/// <param name="plane">The color plane.</param>
/// <returns>The AC dequantization value.</returns>
public short GetAc(int segmentId, Av1Plane plane)
=> this.acContent[segmentId][(int)plane];
/// <summary>
/// Sets the AC dequantization value for a segment and color plane.
/// </summary>
/// <param name="segmentId">The zero-based AV1 segment identifier.</param>
/// <param name="plane">The color plane.</param>
/// <param name="value">The AC dequantization value.</param>
public void SetAc(int segmentId, Av1Plane plane, short value)
=> this.dcContent[segmentId][(int)plane] = value;
=> this.acContent[segmentId][(int)plane] = value;
/// <summary>
/// Sets the DC dequantization value for a segment and color plane.
/// </summary>
/// <param name="segmentId">The zero-based AV1 segment identifier.</param>
/// <param name="plane">The color plane.</param>
/// <param name="value">The DC dequantization value.</param>
public void SetDc(int segmentId, Av1Plane plane, short value)
=> this.dcContent[segmentId][(int)plane] = value;
}

26
src/ImageSharp/Formats/Heif/Av1/Pipeline/Quantification/Av1InverseQuantizationLookup.cs

@ -6,22 +6,23 @@ using SixLabors.ImageSharp.Formats.Heif.Av1.Transform;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Quantification;
/// <summary>
/// Provides the normative AV1 inverse quantization matrices for each matrix level, plane class, and transform size.
/// </summary>
internal class Av1InverseQuantizationLookup
{
// AV1 reuses the adjusted matrix for 64-pixel transform dimensions, while the stored tables omit those duplicate entries.
/// <summary>
/// Maps each AV1 transform size to its stored matrix index; sizes with a 64-pixel dimension reuse the adjusted 32-pixel matrix.
/// </summary>
private static readonly byte[] TransformMatrixIndices =
[
0, 1, 2, 3, 3, 4, 5, 6, 7, 8, 9, 3, 3, 10, 11, 12, 13, 8, 9
];
/// <summary>
/// Gets 16 sets of quantization matrices for chroma and luma and each TX size.
/// Matrices for different TX sizes are in fact sub-sampled from the 32x32 and 16x16 sizes,
/// but explicitly defined here for convenience. Intra and inter matrix sets are the
/// same but changing DEFAULT_QM_INTER_OFFSET from zero allows for different matrices
/// for inter and intra blocks in the same frame.
/// Matrices for different QM levels have been rescaled in the frequency domain according
/// to different nominal viewing distances.
/// The inverse matrix weights indexed by matrix level, plane class, adjusted transform-size index, and raster coefficient.
/// Luma and chroma have separate matrices; U and V share the chroma set. Size-specific matrices are subsampled from the
/// normative 32x32 and 16x16 bases, and matrix levels represent different frequency-domain weighting strengths.
/// </summary>
private static readonly int[][][][] InverseWeightTable =
[
@ -6802,9 +6803,18 @@ internal class Av1InverseQuantizationLookup
]
];
/// <summary>
/// Gets the inverse quantization matrix for a matrix level, color plane, and transform size.
/// </summary>
/// <param name="level">The quantization-matrix level.</param>
/// <param name="plane">The color plane; U and V select the shared chroma matrix.</param>
/// <param name="transformSize">The transform size whose raster coefficient weights are requested.</param>
/// <returns>The inverse matrix weights in raster coefficient order.</returns>
public static ReadOnlySpan<int> GetQuantizationMatrix(int level, Av1Plane plane, Av1TransformSize transformSize)
{
int[][][] levelMatrices = InverseWeightTable[level];
// The table stores one luma plane class and one shared chroma plane class.
int[][] planeMatrices = levelMatrices[Math.Min(1, (int)plane)];
int transformMatrixIndex = TransformMatrixIndices[(int)transformSize];

52
src/ImageSharp/Formats/Heif/Av1/Pipeline/Quantification/Av1InverseQuantizer.cs

@ -7,12 +7,31 @@ using SixLabors.ImageSharp.Formats.Heif.Av1.Transform;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Quantification;
/// <summary>
/// Reconstructs AV1 transform coefficients from quantized coefficient levels.
/// </summary>
internal class Av1InverseQuantizer
{
/// <summary>
/// The sequence-level color configuration that determines coefficient precision.
/// </summary>
private readonly ObuSequenceHeader sequenceHeader;
/// <summary>
/// The frame-level segmentation and quantization configuration.
/// </summary>
private readonly ObuFrameHeader frameHeader;
/// <summary>
/// The current per-segment, per-plane dequantization values, including any superblock delta-Q update.
/// </summary>
private Av1DeQuantizationContext deQuantsDeltaQ;
/// <summary>
/// Initializes a new instance of the <see cref="Av1InverseQuantizer"/> class.
/// </summary>
/// <param name="sequenceHeader">The sequence header that supplies coded bit depth and color configuration.</param>
/// <param name="frameHeader">The frame header that supplies segmentation and quantization parameters.</param>
public Av1InverseQuantizer(ObuSequenceHeader sequenceHeader, ObuFrameHeader frameHeader)
{
this.sequenceHeader = sequenceHeader;
@ -20,6 +39,11 @@ internal class Av1InverseQuantizer
this.deQuantsDeltaQ = new(sequenceHeader, frameHeader);
}
/// <summary>
/// Updates the active dequantization context for a superblock, applying its delta-Q value when signaled.
/// </summary>
/// <param name="deQuants">The frame dequantization context to update and retain.</param>
/// <param name="superblockInfo">The superblock whose quantizer adjustment is applied.</param>
public void UpdateDequant(Av1DeQuantizationContext deQuants, Av1SuperblockInfo superblockInfo)
{
Av1BitDepth bitDepth = this.sequenceHeader.ColorConfig.BitDepth;
@ -44,19 +68,32 @@ internal class Av1InverseQuantizer
}
/// <summary>
/// SVT: svt_aom_inverse_quantize
/// Converts scan-ordered quantized levels into clamped, raster-ordered transform coefficients.
/// </summary>
/// <param name="mode">The block mode information containing the active segment identifier.</param>
/// <param name="level">The packed coefficient buffer: the first element is the coefficient count and the remaining elements are scan-ordered levels.</param>
/// <param name="qCoefficients">The destination for raster-ordered dequantized coefficients.</param>
/// <param name="transformType">The transform type that selects the coefficient scan and matrix class.</param>
/// <param name="transformSize">The transform dimensions and scale.</param>
/// <param name="plane">The color plane whose quantizer and matrix are used.</param>
/// <returns>The number of coefficient levels consumed.</returns>
/// <remarks>SVT-AV1: <c>svt_aom_inverse_quantize</c>.</remarks>
public int InverseQuantize(Av1BlockModeInfo mode, Span<int> level, Span<int> qCoefficients, Av1TransformType transformType, Av1TransformSize transformSize, Av1Plane plane)
{
Guard.NotNull(this.deQuantsDeltaQ);
Av1ScanOrder scanOrder = Av1ScanOrderConstants.GetScanOrder(transformSize, transformType);
ReadOnlySpan<short> scanIndices = scanOrder.Scan;
// AV1 bounds reconstructed coefficients to a signed range with seven headroom bits beyond pixel precision.
int maxValue = (1 << (7 + this.sequenceHeader.ColorConfig.BitDepth.GetBitCount())) - 1;
int minValue = -(1 << (7 + this.sequenceHeader.ColorConfig.BitDepth.GetBitCount()));
bool usingQuantizationMatrix = this.frameHeader.QuantizationParameters.IsUsingQMatrix;
bool lossless = this.frameHeader.LosslessArray[mode.SegmentId];
short dequantDc = this.deQuantsDeltaQ.GetDc(mode.SegmentId, plane);
short dequantAc = this.deQuantsDeltaQ.GetAc(mode.SegmentId, plane);
// The final matrix level is flat. Lossless blocks, frames without matrices, and one-dimensional transforms
// must use it so coefficient frequency does not change the signaled dequantization value.
int qmLevel = lossless || !usingQuantizationMatrix
? Av1ScanOrderConstants.QuantizationMatrixLevelCount - 1
: this.frameHeader.SegmentationParameters.QMLevel[(int)plane][mode.SegmentId];
@ -67,6 +104,7 @@ internal class Av1InverseQuantizer
int shift = transformSize.GetScale();
// Entropy decoding stores the populated coefficient count in the leading slot and the levels after it.
int coefficientCount = level[0];
level = level[1..];
int lev = level[0];
@ -74,6 +112,8 @@ internal class Av1InverseQuantizer
if (lev != 0)
{
int pos = scanIndices[0];
// Preserve the AV1 24-bit dequantization intermediate before removing transform-size scaling.
qCoefficient = (int)(((long)Math.Abs(lev) * GetDeQuantizedValue(dequantDc, pos, iqMatrix)) & 0xffffff);
qCoefficient >>= shift;
@ -91,6 +131,8 @@ internal class Av1InverseQuantizer
if (lev != 0)
{
int pos = scanIndices[i];
// AC levels arrive in entropy scan order but the inverse transform consumes raster positions.
qCoefficient = (int)(((long)Math.Abs(lev) * GetDeQuantizedValue(dequantAc, pos, iqMatrix)) & 0xffffff);
qCoefficient >>= shift;
@ -107,10 +149,16 @@ internal class Av1InverseQuantizer
}
/// <summary>
/// SVT: get_dqv
/// Applies an inverse quantization-matrix weight to a plane dequantization value.
/// </summary>
/// <param name="dequant">The unweighted DC or AC dequantization value.</param>
/// <param name="coefficientIndex">The raster coefficient index into the inverse matrix.</param>
/// <param name="iqMatrix">The inverse quantization matrix for the current level, plane, and transform size.</param>
/// <returns>The matrix-weighted dequantization value.</returns>
/// <remarks>SVT-AV1: <c>get_dqv</c>.</remarks>
private static int GetDeQuantizedValue(short dequant, int coefficientIndex, ReadOnlySpan<int> iqMatrix)
{
// Matrix elements use fixed-point precision; adding half a unit produces nearest-integer rounding on shift.
const int bias = 1 << (Av1Constants.QuantizationMatrixElementBitCount - 1);
int deQuantifiedValue = dequant;

45
src/ImageSharp/Formats/Heif/Av1/Pipeline/Quantification/Av1QuantizationLookup.cs

@ -5,6 +5,9 @@ using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Quantification;
/// <summary>
/// Provides the normative AV1 DC and AC dequantization values for each quantizer index and supported bit depth.
/// </summary>
internal class Av1QuantizationLookup
{
// Coefficient scaling and quantization with AV1 TX are tailored to
@ -35,6 +38,10 @@ internal class Av1QuantizationLookup
// expects quantizers to be larger for higher-bitdepth input. In
// addition, the minimum allowable quantizer is 4; smaller values will
// underflow to 0 in the actual quantization routines.
/// <summary>
/// The Q3 AC dequantization values for 8-bit samples, indexed by quantizer index.
/// </summary>
private static readonly short[] AcQlookup8 = [
4, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25,
26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44,
@ -52,6 +59,9 @@ internal class Av1QuantizationLookup
1567, 1597, 1628, 1660, 1692, 1725, 1759, 1793, 1828,
];
/// <summary>
/// The Q3 AC dequantization values for 10-bit samples, indexed by quantizer index.
/// </summary>
private static readonly short[] AcQlookup10 = [
4, 9, 11, 13, 16, 18, 21, 24, 27, 30, 33, 37, 40, 44, 48, 51, 55, 59, 63,
67, 71, 75, 79, 83, 88, 92, 96, 100, 105, 109, 114, 118, 122, 127, 131, 136, 140, 145,
@ -69,6 +79,9 @@ internal class Av1QuantizationLookup
6268, 6388, 6512, 6640, 6768, 6900, 7036, 7172, 7312,
];
/// <summary>
/// The Q3 AC dequantization values for 12-bit samples, indexed by quantizer index.
/// </summary>
private static readonly short[] AcQlookup12 = [
4, 13, 19, 27, 35, 44, 54, 64, 75, 87, 99, 112, 126, 139, 154, 168,
183, 199, 214, 230, 247, 263, 280, 297, 314, 331, 349, 366, 384, 402, 420, 438,
@ -88,6 +101,9 @@ internal class Av1QuantizationLookup
21902, 22334, 22766, 23214, 23662, 24126, 24590, 25070, 25551, 26047, 26559, 27071, 27599, 28143, 28687, 29247,
];
/// <summary>
/// The Q3 DC dequantization values for 8-bit samples, indexed by quantizer index.
/// </summary>
private static readonly short[] DcQlookup8 = [
4, 8, 8, 9, 10, 11, 12, 12, 13, 14, 15, 16, 17, 18, 19, 19, 20, 21, 22, 23,
24, 25, 26, 26, 27, 28, 29, 30, 31, 32, 32, 33, 34, 35, 36, 37, 38, 38, 39, 40,
@ -104,6 +120,9 @@ internal class Av1QuantizationLookup
796, 819, 843, 869, 896, 925, 955, 988, 1022, 1058, 1098, 1139, 1184, 1232, 1282, 1336,
];
/// <summary>
/// The Q3 DC dequantization values for 10-bit samples, indexed by quantizer index.
/// </summary>
private static readonly short[] DcQlookup10 = [
4, 9, 10, 13, 15, 17, 20, 22, 25, 28, 31, 34, 37, 40, 43, 47, 50, 53, 57,
60, 64, 68, 71, 75, 78, 82, 86, 90, 93, 97, 101, 105, 109, 113, 116, 120, 124, 128,
@ -121,6 +140,9 @@ internal class Av1QuantizationLookup
3953, 4089, 4236, 4394, 4559, 4737, 4929, 5130, 5347,
];
/// <summary>
/// The Q3 DC dequantization values for 12-bit samples, indexed by quantizer index.
/// </summary>
private static readonly short[] DcQlookup12 = [
4, 12, 18, 25, 33, 41, 50, 60, 70, 80, 91, 103, 115, 127, 140, 153,
166, 180, 194, 208, 222, 237, 251, 266, 281, 296, 312, 327, 343, 358, 374, 390,
@ -140,8 +162,16 @@ internal class Av1QuantizationLookup
12750, 13118, 13501, 13913, 14343, 14807, 15290, 15812, 16356, 16943, 17575, 18237, 18949, 19718, 20521, 21387,
];
/// <summary>
/// Gets the DC dequantization value after applying a plane delta to the frame quantizer index.
/// </summary>
/// <param name="qIndex">The frame or segment quantizer index.</param>
/// <param name="dcDeltaQ">The signed DC quantizer adjustment for the selected plane.</param>
/// <param name="bitDepth">The coded sample bit depth.</param>
/// <returns>The Q3 DC dequantization value.</returns>
public static short GetDcQuant(int qIndex, int dcDeltaQ, Av1BitDepth bitDepth)
{
// Plane deltas may move beyond the signaled 8-bit quantizer domain, where AV1 requires endpoint clamping.
int qClamped = Av1Math.Clamp(qIndex + dcDeltaQ, 0, Av1Constants.MaxQ);
switch (bitDepth)
{
@ -157,8 +187,16 @@ internal class Av1QuantizationLookup
}
}
/// <summary>
/// Gets the AC dequantization value after applying a plane delta to the frame quantizer index.
/// </summary>
/// <param name="qIndex">The frame or segment quantizer index.</param>
/// <param name="dcDeltaQ">The signed AC quantizer adjustment for the selected plane.</param>
/// <param name="bitDepth">The coded sample bit depth.</param>
/// <returns>The Q3 AC dequantization value.</returns>
public static short GetAcQuant(int qIndex, int dcDeltaQ, Av1BitDepth bitDepth)
{
// Plane deltas may move beyond the signaled 8-bit quantizer domain, where AV1 requires endpoint clamping.
int qClamped = Av1Math.Clamp(qIndex + dcDeltaQ, 0, Av1Constants.MaxQ);
switch (bitDepth)
{
@ -174,6 +212,13 @@ internal class Av1QuantizationLookup
}
}
/// <summary>
/// Gets the quantizer index for a segment, including its alternative-quantizer feature when active.
/// </summary>
/// <param name="segmentationParameters">The frame segmentation configuration.</param>
/// <param name="segmentId">The zero-based AV1 segment identifier.</param>
/// <param name="baseQIndex">The frame's base quantizer index.</param>
/// <returns>The segment quantizer index clamped to the AV1 quantizer domain.</returns>
public static int GetQIndex(ObuSegmentationParameters segmentationParameters, int segmentId, int baseQIndex)
{
if (segmentationParameters.IsFeatureActive(segmentId, ObuSegmentationLevelFeature.AlternativeQuantizer))

Loading…
Cancel
Save