Browse Source

Document and correct AV1 prediction decoding

pull/2633/head
James Jackson-South 2 months ago
parent
commit
08bc4cd8ab
  1. 338
      src/ImageSharp/Formats/Heif/Av1/Prediction/Av1PredictionDecoder.cs
  2. 17
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ChromaFromLumaTests.cs

338
src/ImageSharp/Formats/Heif/Av1/Prediction/Av1PredictionDecoder.cs

@ -11,13 +11,34 @@ using SixLabors.ImageSharp.Formats.Heif.Av1.Transform;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction;
/// <summary>
/// Reconstructs AV1 intra-predicted transform blocks from neighboring samples and decoded mode information.
/// </summary>
/// <remarks>
/// This type implements the intra prediction portion of the AV1 reconstruction process for 8-, 10-, and 12-bit samples.
/// </remarks>
internal class Av1PredictionDecoder internal class Av1PredictionDecoder
{ {
/// <summary>
/// The largest edge length for which AV1 permits intra-edge upsampling.
/// </summary>
private const int MaxUpsampleSize = 16; private const int MaxUpsampleSize = 16;
/// <summary>
/// The sequence-level syntax that controls chroma sampling, bit depth, superblock size, and intra-edge filtering.
/// </summary>
private readonly ObuSequenceHeader sequenceHeader; private readonly ObuSequenceHeader sequenceHeader;
/// <summary>
/// The frame-level syntax that controls segment lossless state and prediction behavior.
/// </summary>
private readonly ObuFrameHeader frameHeader; private readonly ObuFrameHeader frameHeader;
/// <summary>
/// Initializes a new instance of the <see cref="Av1PredictionDecoder"/> class.
/// </summary>
/// <param name="sequenceHeader">The decoded sequence header for the current image.</param>
/// <param name="frameHeader">The decoded frame header for the current image.</param>
public Av1PredictionDecoder(ObuSequenceHeader sequenceHeader, ObuFrameHeader frameHeader) public Av1PredictionDecoder(ObuSequenceHeader sequenceHeader, ObuFrameHeader frameHeader)
{ {
this.sequenceHeader = sequenceHeader; this.sequenceHeader = sequenceHeader;
@ -25,8 +46,18 @@ internal class Av1PredictionDecoder
} }
/// <summary> /// <summary>
/// SVT: svt_av1_predict_intra /// Reconstructs an 8-bit intra-predicted transform block.
/// </summary> /// </summary>
/// <param name="partitionInfo">The decoded partition and mode state for the containing block.</param>
/// <param name="plane">The color plane being reconstructed.</param>
/// <param name="transformSize">The dimensions of the transform block.</param>
/// <param name="tileInfo">The tile boundaries used to determine neighboring-sample availability.</param>
/// <param name="pixelBuffer">The sample buffer beginning at the row above the destination block.</param>
/// <param name="pixelStride">The distance, in samples, between pixel rows.</param>
/// <param name="bitDepth">The bit depth of the reconstructed samples.</param>
/// <param name="blockModeInfoColumnOffset">The transform block's horizontal offset within the mode-information block.</param>
/// <param name="blockModeInfoRowOffset">The transform block's vertical offset within the mode-information block.</param>
/// <remarks>Corresponds to <c>svt_av1_predict_intra</c> in SVT-AV1.</remarks>
public void Decode( public void Decode(
Av1PartitionInfo partitionInfo, Av1PartitionInfo partitionInfo,
Av1Plane plane, Av1Plane plane,
@ -49,8 +80,18 @@ internal class Av1PredictionDecoder
blockModeInfoRowOffset); blockModeInfoRowOffset);
/// <summary> /// <summary>
/// AV1: 7.11.2 Reconstruct. /// Reconstructs a 10-bit or 12-bit intra-predicted transform block.
/// </summary> /// </summary>
/// <param name="partitionInfo">The decoded partition and mode state for the containing block.</param>
/// <param name="plane">The color plane being reconstructed.</param>
/// <param name="transformSize">The dimensions of the transform block.</param>
/// <param name="tileInfo">The tile boundaries used to determine neighboring-sample availability.</param>
/// <param name="pixelBuffer">The sample buffer beginning at the row above the destination block.</param>
/// <param name="pixelStride">The distance, in samples, between pixel rows.</param>
/// <param name="bitDepth">The bit depth of the reconstructed samples.</param>
/// <param name="blockModeInfoColumnOffset">The transform block's horizontal offset within the mode-information block.</param>
/// <param name="blockModeInfoRowOffset">The transform block's vertical offset within the mode-information block.</param>
/// <remarks>Implements the intra prediction portion of section 7.11.2 of the AV1 specification.</remarks>
public void Decode( public void Decode(
Av1PartitionInfo partitionInfo, Av1PartitionInfo partitionInfo,
Av1Plane plane, Av1Plane plane,
@ -72,6 +113,19 @@ internal class Av1PredictionDecoder
blockModeInfoColumnOffset, blockModeInfoColumnOffset,
blockModeInfoRowOffset); blockModeInfoRowOffset);
/// <summary>
/// Reconstructs an intra-predicted transform block in its native sample representation.
/// </summary>
/// <typeparam name="T">The 8-bit or high-bit-depth sample type.</typeparam>
/// <param name="partitionInfo">The decoded partition and mode state for the containing block.</param>
/// <param name="plane">The color plane being reconstructed.</param>
/// <param name="transformSize">The dimensions of the transform block.</param>
/// <param name="tileInfo">The tile boundaries used to determine neighboring-sample availability.</param>
/// <param name="pixelBuffer">The sample buffer beginning at the row above the destination block.</param>
/// <param name="pixelStride">The distance, in samples, between pixel rows.</param>
/// <param name="bitDepth">The bit depth of the reconstructed samples.</param>
/// <param name="blockModeInfoColumnOffset">The transform block's horizontal offset within the mode-information block.</param>
/// <param name="blockModeInfoRowOffset">The transform block's vertical offset within the mode-information block.</param>
private void DecodeCore<T>( private void DecodeCore<T>(
Av1PartitionInfo partitionInfo, Av1PartitionInfo partitionInfo,
Av1Plane plane, Av1Plane plane,
@ -86,7 +140,8 @@ internal class Av1PredictionDecoder
{ {
int stride = pixelStride; int stride = pixelStride;
// Deviation from SVT: Buffer starts at PREVIOUS row. // Unlike SVT's separate destination and reference pointers, this span begins at the
// previous row. That layout exposes the top, top-left, and strided left samples without copying.
Span<T> topNeighbor = pixelBuffer; Span<T> topNeighbor = pixelBuffer;
Span<T> leftNeighbor = pixelBuffer[(stride - 1)..]; Span<T> leftNeighbor = pixelBuffer[(stride - 1)..];
Span<T> startOfPixels = pixelBuffer[stride..]; Span<T> startOfPixels = pixelBuffer[stride..];
@ -137,6 +192,16 @@ internal class Av1PredictionDecoder
bitDepth); bitDepth);
} }
/// <summary>
/// Applies chroma-from-luma scaling to the DC prediction for one chroma transform block.
/// </summary>
/// <typeparam name="T">The 8-bit or high-bit-depth sample type.</typeparam>
/// <param name="partitionInfo">The decoded partition and mode state for the containing block.</param>
/// <param name="chromaFromLumaContext">The block-level luma prediction context shared by the chroma planes.</param>
/// <param name="pixelBuffer">The DC-predicted chroma samples that receive the luma-derived adjustment.</param>
/// <param name="stride">The distance, in samples, between pixel rows.</param>
/// <param name="transformSize">The dimensions of the chroma transform block.</param>
/// <param name="plane">The U or V plane being reconstructed.</param>
private void PredictChromaFromLumaBlock<T>(Av1PartitionInfo partitionInfo, Av1ChromaFromLumaContext? chromaFromLumaContext, Span<T> pixelBuffer, int stride, Av1TransformSize transformSize, Av1Plane plane) private void PredictChromaFromLumaBlock<T>(Av1PartitionInfo partitionInfo, Av1ChromaFromLumaContext? chromaFromLumaContext, Span<T> pixelBuffer, int stride, Av1TransformSize transformSize, Av1Plane plane)
where T : unmanaged, IBinaryInteger<T> where T : unmanaged, IBinaryInteger<T>
{ {
@ -149,14 +214,15 @@ internal class Av1PredictionDecoder
throw new InvalidOperationException("CFL context should have been defined already."); throw new InvalidOperationException("CFL context should have been defined already.");
} }
// U computes the shared subsampled-luma parameters first; V reuses them for the
// same block because both chroma planes have identical sampling geometry.
if (!chromaFromLumaContext.AreParametersComputed) if (!chromaFromLumaContext.AreParametersComputed)
{ {
chromaFromLumaContext.ComputeParameters(transformSize); chromaFromLumaContext.ComputeParameters(transformSize);
} }
int alphaQ3 = ChromaFromLumaIndexToAlpha(modeInfo.ChromaFromLumaAlphaIndex, modeInfo.ChromaFromLumaAlphaSign, (Av1Plane)((int)plane - 1)); int alphaQ3 = ChromaFromLumaIndexToAlpha(modeInfo.ChromaFromLumaAlphaIndex, modeInfo.ChromaFromLumaAlphaSign, plane);
// assert((transformSize.GetHeight() - 1) * CFL_BUF_LINE + transformSize.GetWidth() <= CFL_BUF_SQUARE);
Av1BitDepth bitDepth = this.sequenceHeader.ColorConfig.BitDepth; Av1BitDepth bitDepth = this.sequenceHeader.ColorConfig.BitDepth;
ChromaFromLumaPredict( ChromaFromLumaPredict(
chromaFromLumaContext.Q3Buffer, chromaFromLumaContext.Q3Buffer,
@ -170,11 +236,18 @@ internal class Av1PredictionDecoder
transformSize.GetHeight()); transformSize.GetHeight());
} }
/// <summary>
/// Determines whether chroma-from-luma prediction is permitted for the current block and frame state.
/// </summary>
/// <param name="partitionInfo">The decoded partition and mode state for the containing block.</param>
/// <param name="colorConfig">The sequence color configuration.</param>
/// <param name="frameHeader">The decoded frame header.</param>
/// <returns><see langword="true"/> when the block may use chroma-from-luma prediction; otherwise, <see langword="false"/>.</returns>
private static bool IsChromaFromLumaAllowedWithFrameHeader(Av1PartitionInfo partitionInfo, ObuColorConfig colorConfig, ObuFrameHeader frameHeader) private static bool IsChromaFromLumaAllowedWithFrameHeader(Av1PartitionInfo partitionInfo, ObuColorConfig colorConfig, ObuFrameHeader frameHeader)
{ {
Av1BlockModeInfo modeInfo = partitionInfo.ModeInfo; Av1BlockModeInfo modeInfo = partitionInfo.ModeInfo;
Av1BlockSize blockSize = modeInfo.BlockSize; Av1BlockSize blockSize = modeInfo.BlockSize;
DebugGuard.MustBeGreaterThan((int)blockSize, (int)Av1BlockSize.AllSizes, nameof(blockSize)); DebugGuard.MustBeLessThan((int)blockSize, (int)Av1BlockSize.AllSizes, nameof(blockSize));
if (frameHeader.LosslessArray[modeInfo.SegmentId]) if (frameHeader.LosslessArray[modeInfo.SegmentId])
{ {
// In lossless, CfL is available when the partition size is equal to the // In lossless, CfL is available when the partition size is equal to the
@ -185,11 +258,18 @@ internal class Av1PredictionDecoder
return planeBlockSize == Av1BlockSize.Block4x4; return planeBlockSize == Av1BlockSize.Block4x4;
} }
// Spec: CfL is available to luma partitions lesser than or equal to 32x32 // Outside lossless mode, AV1 limits CfL to luma partitions no larger than 32 by 32.
return blockSize.GetWidth() <= 32 && blockSize.GetHeight() <= 32; return blockSize.GetWidth() <= 32 && blockSize.GetHeight() <= 32;
} }
private static int ChromaFromLumaIndexToAlpha(int alphaIndex, int jointSign, Av1Plane plane) /// <summary>
/// Converts the packed chroma-from-luma magnitude and joint sign into a signed Q3 scaling factor.
/// </summary>
/// <param name="alphaIndex">The packed U and V alpha magnitudes.</param>
/// <param name="jointSign">The joint U and V alpha-sign symbol.</param>
/// <param name="plane">The U or V plane whose alpha value is selected.</param>
/// <returns>The signed Q3 alpha value for the selected chroma plane.</returns>
public static int ChromaFromLumaIndexToAlpha(int alphaIndex, int jointSign, Av1Plane plane)
{ {
int alphaSign = (plane == Av1Plane.U) ? Av1ChromaFromLumaMath.SignU(jointSign) : Av1ChromaFromLumaMath.SignV(jointSign); int alphaSign = (plane == Av1Plane.U) ? Av1ChromaFromLumaMath.SignU(jointSign) : Av1ChromaFromLumaMath.SignV(jointSign);
if (alphaSign == Av1ChromaFromLumaMath.SignZero) if (alphaSign == Av1ChromaFromLumaMath.SignZero)
@ -201,13 +281,32 @@ internal class Av1PredictionDecoder
return (alphaSign == Av1ChromaFromLumaMath.SignPositive) ? absAlphaQ3 + 1 : -absAlphaQ3 - 1; return (alphaSign == Av1ChromaFromLumaMath.SignPositive) ? absAlphaQ3 + 1 : -absAlphaQ3 - 1;
} }
/// <summary>
/// Multiplies a Q3 chroma-from-luma alpha by a Q3 luma residual and returns an integer sample adjustment.
/// </summary>
/// <param name="alphaQ3">The signed Q3 chroma scaling factor.</param>
/// <param name="predictedQ3">The signed Q3 luma residual.</param>
/// <returns>The signed integer sample adjustment.</returns>
private static int GetScaledLumaQ0(int alphaQ3, short predictedQ3) private static int GetScaledLumaQ0(int alphaQ3, short predictedQ3)
{ {
int scaledLumaQ6 = alphaQ3 * predictedQ3; int scaledLumaQ6 = alphaQ3 * predictedQ3;
return Av1Math.RoundPowerOf2Signed(scaledLumaQ6, 6); return Av1Math.RoundPowerOf2Signed(scaledLumaQ6, 6);
} }
internal static void ChromaFromLumaPredict<T>(Span<short> predictedBufferQ3, Span<T> predictedBuffer, int predictedStride, Span<T> destinationBuffer, int destinationStride, int alphaQ3, Av1BitDepth bitDepth, int width, int height) /// <summary>
/// Applies a chroma-from-luma residual to a chroma prediction block.
/// </summary>
/// <typeparam name="T">The 8-bit or high-bit-depth sample type.</typeparam>
/// <param name="predictedBufferQ3">The Q3 luma residuals stored with the normative 32-sample CfL row stride.</param>
/// <param name="predictedBuffer">The base chroma prediction samples.</param>
/// <param name="predictedStride">The distance, in samples, between base prediction rows.</param>
/// <param name="destinationBuffer">The buffer that receives the adjusted chroma samples.</param>
/// <param name="destinationStride">The distance, in samples, between destination rows.</param>
/// <param name="alphaQ3">The signed Q3 chroma scaling factor.</param>
/// <param name="bitDepth">The bit depth used to clamp reconstructed samples.</param>
/// <param name="width">The width of the prediction block in samples.</param>
/// <param name="height">The height of the prediction block in samples.</param>
public static void ChromaFromLumaPredict<T>(Span<short> predictedBufferQ3, Span<T> predictedBuffer, int predictedStride, Span<T> destinationBuffer, int destinationStride, int alphaQ3, Av1BitDepth bitDepth, int width, int height)
where T : unmanaged, IBinaryInteger<T> where T : unmanaged, IBinaryInteger<T>
{ {
// TODO: Make SIMD variant of this method. // TODO: Make SIMD variant of this method.
@ -223,10 +322,29 @@ internal class Av1PredictionDecoder
destinationBuffer = destinationBuffer[destinationStride..]; destinationBuffer = destinationBuffer[destinationStride..];
predictedBuffer = predictedBuffer[predictedStride..]; predictedBuffer = predictedBuffer[predictedStride..];
// The CfL scratch buffer has a fixed 32-sample row stride independent of transform width.
predictedBufferQ3 = predictedBufferQ3[32..]; predictedBufferQ3 = predictedBufferQ3[32..];
} }
} }
/// <summary>
/// Determines available reference samples and dispatches prediction for one transform block.
/// </summary>
/// <typeparam name="T">The 8-bit or high-bit-depth sample type.</typeparam>
/// <param name="partitionInfo">The decoded partition and mode state for the containing block.</param>
/// <param name="plane">The color plane being reconstructed.</param>
/// <param name="transformSize">The dimensions of the transform block.</param>
/// <param name="tileInfo">The tile boundaries used to determine neighboring-sample availability.</param>
/// <param name="pixelBuffer">The destination samples for the transform block.</param>
/// <param name="pixelBufferStride">The distance, in samples, between destination rows.</param>
/// <param name="topNeighbor">The reconstructed samples along the top edge.</param>
/// <param name="leftNeighbor">The reconstructed samples along the left edge.</param>
/// <param name="referenceStride">The distance, in samples, between consecutive left-edge references.</param>
/// <param name="mode">The intra prediction mode to apply.</param>
/// <param name="blockModeInfoColumnOffset">The transform block's horizontal offset within the mode-information block.</param>
/// <param name="blockModeInfoRowOffset">The transform block's vertical offset within the mode-information block.</param>
/// <param name="bitDepth">The bit depth of the reconstructed samples.</param>
private void PredictIntraBlock<T>( private void PredictIntraBlock<T>(
Av1PartitionInfo partitionInfo, Av1PartitionInfo partitionInfo,
Av1Plane plane, Av1Plane plane,
@ -260,6 +378,8 @@ internal class Av1PredictionDecoder
if (usePalette) if (usePalette)
{ {
// Palette blocks use their decoded color-index map rather than neighboring-sample
// prediction. The tile parser currently rejects palette syntax before this point.
return; return;
} }
@ -277,11 +397,11 @@ internal class Av1PredictionDecoder
int xrOffset = 0; int xrOffset = 0;
int ydOffset = 0; int ydOffset = 0;
// Distance between right edge of this pred block to frame right edge // These distances bound edge extension at the coded frame rather than allowing
// a transform to read padding that happens to exist beyond the visible image.
int xr = (partitionInfo.ModeBlockToRightEdge >> (3 + subX)) + (partitionInfo.WidthInPixels[(int)plane] - (blockModeInfoColumnOffset << Av1Constants.ModeInfoSizeLog2) - transformWidth) - int xr = (partitionInfo.ModeBlockToRightEdge >> (3 + subX)) + (partitionInfo.WidthInPixels[(int)plane] - (blockModeInfoColumnOffset << Av1Constants.ModeInfoSizeLog2) - transformWidth) -
xrOffset; xrOffset;
// Distance between bottom edge of this pred block to frame bottom edge
int yd = (partitionInfo.ModeBlockToBottomEdge >> (3 + subY)) + int yd = (partitionInfo.ModeBlockToBottomEdge >> (3 + subY)) +
(partitionInfo.HeightInPixels[(int)plane] - (blockModeInfoRowOffset << Av1Constants.ModeInfoSizeLog2) - transformHeight) - ydOffset; (partitionInfo.HeightInPixels[(int)plane] - (blockModeInfoRowOffset << Av1Constants.ModeInfoSizeLog2) - transformHeight) - ydOffset;
bool rightAvailable = modeInfoColumn + ((blockModeInfoColumnOffset + transformWidthInModeInfoUnits) << subX) < tileInfo.ModeInfoColumnEnd; bool rightAvailable = modeInfoColumn + ((blockModeInfoColumnOffset + transformWidthInModeInfoUnits) << subX) < tileInfo.ModeInfoColumnEnd;
@ -289,7 +409,7 @@ internal class Av1PredictionDecoder
Av1PartitionType partition = modeInfo.PartitionType; Av1PartitionType partition = modeInfo.PartitionType;
// force 4x4 chroma component block size. // Chroma prediction geometry cannot be smaller than 4 by 4 after subsampling.
blockSize = ScaleChromaBlockSize(blockSize, subX == 1, subY == 1); blockSize = ScaleChromaBlockSize(blockSize, subX == 1, subY == 1);
bool haveTopRight = IntraHasTopRight( bool haveTopRight = IntraHasTopRight(
@ -342,6 +462,13 @@ internal class Av1PredictionDecoder
bitDepth.GetBitCount()); bitDepth.GetBitCount());
} }
/// <summary>
/// Adjusts sub-8-by-8 luma block geometry to the minimum chroma prediction block size.
/// </summary>
/// <param name="blockSize">The luma block size.</param>
/// <param name="subX">A value indicating whether chroma is horizontally subsampled.</param>
/// <param name="subY">A value indicating whether chroma is vertically subsampled.</param>
/// <returns>The block size used to evaluate chroma reference availability.</returns>
private static Av1BlockSize ScaleChromaBlockSize(Av1BlockSize blockSize, bool subX, bool subY) private static Av1BlockSize ScaleChromaBlockSize(Av1BlockSize blockSize, bool subX, bool subY)
{ {
Av1BlockSize bs = blockSize; Av1BlockSize bs = blockSize;
@ -429,6 +556,22 @@ internal class Av1PredictionDecoder
return bs; return bs;
} }
/// <summary>
/// Determines whether every bottom-left reference sample required by a transform is already reconstructed.
/// </summary>
/// <param name="superblockSize">The sequence superblock size.</param>
/// <param name="blockSize">The containing block size in the current plane's geometry.</param>
/// <param name="modeInfoRow">The containing block row in 4-by-4 mode-information units.</param>
/// <param name="modeInfoColumn">The containing block column in 4-by-4 mode-information units.</param>
/// <param name="bottomAvailable">A value indicating whether the required rows remain inside the frame and tile.</param>
/// <param name="haveLeft">A value indicating whether reconstructed samples exist immediately to the left.</param>
/// <param name="partition">The partition type that determines reconstruction order.</param>
/// <param name="transformSize">The transform size whose extended edge is required.</param>
/// <param name="blockModeInfoRowOffset">The transform row offset within the containing block.</param>
/// <param name="blockModeInfoColumnOffset">The transform column offset within the containing block.</param>
/// <param name="subX">The horizontal chroma subsampling shift.</param>
/// <param name="subY">The vertical chroma subsampling shift.</param>
/// <returns><see langword="true"/> when the bottom-left reference extension is available; otherwise, <see langword="false"/>.</returns>
private static bool IntraHasBottomLeft(Av1BlockSize superblockSize, Av1BlockSize blockSize, int modeInfoRow, int modeInfoColumn, bool bottomAvailable, bool haveLeft, Av1PartitionType partition, Av1TransformSize transformSize, int blockModeInfoRowOffset, int blockModeInfoColumnOffset, int subX, int subY) private static bool IntraHasBottomLeft(Av1BlockSize superblockSize, Av1BlockSize blockSize, int modeInfoRow, int modeInfoColumn, bool bottomAvailable, bool haveLeft, Av1PartitionType partition, Av1TransformSize transformSize, int blockModeInfoRowOffset, int blockModeInfoColumnOffset, int subX, int subY)
{ {
if (!bottomAvailable || !haveLeft) if (!bottomAvailable || !haveLeft)
@ -436,9 +579,8 @@ internal class Av1PredictionDecoder
return false; return false;
} }
// Special case for 128x* blocks, when col_off is half the block width. // A 128-wide block is reconstructed as two 64-wide regions in raster order,
// This is needed because 128x* superblocks are divided into 64x* blocks in // so the right half can consume references that already belong to the left half.
// raster order
if (blockSize.GetWidth() > 64 && blockModeInfoColumnOffset > 0) if (blockSize.GetWidth() > 64 && blockModeInfoColumnOffset > 0)
{ {
int planeBlockWidthInUnits64 = 64 >> subX; int planeBlockWidthInUnits64 = 64 >> subX;
@ -504,6 +646,22 @@ internal class Av1PredictionDecoder
} }
} }
/// <summary>
/// Determines whether every top-right reference sample required by a transform is already reconstructed.
/// </summary>
/// <param name="superblockSize">The sequence superblock size.</param>
/// <param name="blockSize">The containing block size in the current plane's geometry.</param>
/// <param name="modeInfoRow">The containing block row in 4-by-4 mode-information units.</param>
/// <param name="modeInfoColumn">The containing block column in 4-by-4 mode-information units.</param>
/// <param name="haveTop">A value indicating whether reconstructed samples exist immediately above.</param>
/// <param name="rightAvailable">A value indicating whether the required columns remain inside the frame and tile.</param>
/// <param name="partition">The partition type that determines reconstruction order.</param>
/// <param name="transformSize">The transform size whose extended edge is required.</param>
/// <param name="blockModeInfoRowOffset">The transform row offset within the containing block.</param>
/// <param name="blockModeInfoColumnOffset">The transform column offset within the containing block.</param>
/// <param name="subX">The horizontal chroma subsampling shift.</param>
/// <param name="subY">The vertical chroma subsampling shift.</param>
/// <returns><see langword="true"/> when the top-right reference extension is available; otherwise, <see langword="false"/>.</returns>
private static bool IntraHasTopRight(Av1BlockSize superblockSize, Av1BlockSize blockSize, int modeInfoRow, int modeInfoColumn, bool haveTop, bool rightAvailable, Av1PartitionType partition, Av1TransformSize transformSize, int blockModeInfoRowOffset, int blockModeInfoColumnOffset, int subX, int subY) private static bool IntraHasTopRight(Av1BlockSize superblockSize, Av1BlockSize blockSize, int modeInfoRow, int modeInfoColumn, bool haveTop, bool rightAvailable, Av1PartitionType partition, Av1TransformSize transformSize, int blockModeInfoRowOffset, int blockModeInfoColumnOffset, int subX, int subY)
{ {
if (!haveTop || !rightAvailable) if (!haveTop || !rightAvailable)
@ -516,7 +674,9 @@ internal class Av1PredictionDecoder
int topRightUnitCount = transformSize.Get4x4WideCount(); int topRightUnitCount = transformSize.Get4x4WideCount();
if (blockModeInfoRowOffset > 0) if (blockModeInfoRowOffset > 0)
{ // Just need to check if enough pixels on the right. {
// Transforms below the first row obtain their top edge from the containing block,
// so only the reconstructed width to their right constrains availability.
if (blockSize.GetWidth() > 64) if (blockSize.GetWidth() > 64)
{ {
// Special case: For 128x128 blocks, the transform unit whose // Special case: For 128x128 blocks, the transform unit whose
@ -570,6 +730,27 @@ internal class Av1PredictionDecoder
} }
} }
/// <summary>
/// Prepares normative reference-edge samples and runs the selected intra predictor.
/// </summary>
/// <typeparam name="T">The 8-bit or high-bit-depth sample type.</typeparam>
/// <param name="partitionInfo">The decoded partition and neighboring mode state.</param>
/// <param name="aboveNeighbor">The reconstructed top and top-right reference samples.</param>
/// <param name="leftNeighbor">The reconstructed left and bottom-left reference samples.</param>
/// <param name="referenceStride">The distance, in samples, between consecutive left-edge references.</param>
/// <param name="destination">The buffer that receives the prediction block.</param>
/// <param name="destinationStride">The distance, in samples, between destination rows.</param>
/// <param name="mode">The intra prediction mode to apply.</param>
/// <param name="angleDelta">The coded directional angle adjustment.</param>
/// <param name="filterIntraMode">The selected filter intra mode, or the sentinel indicating that filter intra is disabled.</param>
/// <param name="transformSize">The dimensions of the prediction block.</param>
/// <param name="disableEdgeFilter">A value indicating whether intra-edge filtering and upsampling are disabled.</param>
/// <param name="topPixelCount">The number of available top samples.</param>
/// <param name="topRightPixelCount">The number of available top-right extension samples.</param>
/// <param name="leftPixelCount">The number of available left samples.</param>
/// <param name="bottomLeftPixelCount">The number of available bottom-left extension samples.</param>
/// <param name="plane">The color plane being reconstructed.</param>
/// <param name="bitDepth">The number of bits used to represent each sample.</param>
private void DecodeBuildIntraPredictors<T>( private void DecodeBuildIntraPredictors<T>(
Av1PartitionInfo partitionInfo, Av1PartitionInfo partitionInfo,
Span<T> aboveNeighbor, Span<T> aboveNeighbor,
@ -591,6 +772,9 @@ internal class Av1PredictionDecoder
where T : unmanaged, IBinaryInteger<T> where T : unmanaged, IBinaryInteger<T>
{ {
int baseValue = 128 << (bitDepth - 8); int baseValue = 128 << (bitDepth - 8);
// Prefix storage is required because AV1 addresses the shared top-left sample at -1
// and writes upsampled edge samples as far back as -2.
Span<T> aboveData = stackalloc T[(Av1Constants.MaxTransformSize * 2) + 32]; Span<T> aboveData = stackalloc T[(Av1Constants.MaxTransformSize * 2) + 32];
Span<T> leftData = stackalloc T[(Av1Constants.MaxTransformSize * 2) + 32]; Span<T> leftData = stackalloc T[(Av1Constants.MaxTransformSize * 2) + 32];
aboveData.Fill(T.CreateChecked(baseValue - 1)); aboveData.Fill(T.CreateChecked(baseValue - 1));
@ -644,6 +828,8 @@ internal class Av1PredictionDecoder
if ((!needAbove && leftPixelCount == 0) || (!needLeft && topPixelCount == 0)) if ((!needAbove && leftPixelCount == 0) || (!needLeft && topPixelCount == 0))
{ {
// Pure horizontal or vertical prediction with its sole required edge missing
// degenerates to the first perpendicular sample or the normative midpoint offset.
T value; T value;
if (needLeft) if (needLeft)
{ {
@ -662,7 +848,8 @@ internal class Av1PredictionDecoder
return; return;
} }
// NEED_LEFT // Copy the available left and bottom-left samples, then extend the final sample
// through any unavailable portion required by the selected predictor.
if (needLeft) if (needLeft)
{ {
bool needBottom = (need & Av1NeighborNeed.BottomLeft) == Av1NeighborNeed.BottomLeft; bool needBottom = (need & Av1NeighborNeed.BottomLeft) == Av1NeighborNeed.BottomLeft;
@ -712,7 +899,8 @@ internal class Av1PredictionDecoder
} }
} }
// NEED_ABOVE // Prepare the top edge by the same copy-and-extend rule. Unlike the left edge,
// these samples are contiguous in the reconstructed pixel buffer.
if (needAbove) if (needAbove)
{ {
bool needRight = (need & Av1NeighborNeed.AboveRight) == Av1NeighborNeed.AboveRight; bool needRight = (need & Av1NeighborNeed.AboveRight) == Av1NeighborNeed.AboveRight;
@ -758,6 +946,8 @@ internal class Av1PredictionDecoder
if (needAboveLeft) if (needAboveLeft)
{ {
// AV1 synthesizes the shared corner from the closest available edge when only
// one edge exists, and uses the bit-depth midpoint when neither edge exists.
ref T aboveLeft = ref Unsafe.Subtract(ref aboveRow[0], 1); ref T aboveLeft = ref Unsafe.Subtract(ref aboveRow[0], 1);
if (topPixelCount > 0 && leftPixelCount > 0) if (topPixelCount > 0 && leftPixelCount > 0)
{ {
@ -840,7 +1030,6 @@ internal class Av1PredictionDecoder
return; return;
} }
// predict
if (mode == Av1PredictionMode.DC) if (mode == Av1PredictionMode.DC)
{ {
DcPredictor(leftPixelCount > 0, topPixelCount > 0, transformSize, destination, destinationStride, aboveRow, leftColumn, bitDepth); DcPredictor(leftPixelCount > 0, topPixelCount > 0, transformSize, destination, destinationStride, aboveRow, leftColumn, bitDepth);
@ -851,9 +1040,23 @@ internal class Av1PredictionDecoder
} }
} }
/// <summary>
/// Dispatches DC prediction to the 8-bit or high-bit-depth implementation.
/// </summary>
/// <typeparam name="T">The byte or 16-bit sample type.</typeparam>
/// <param name="hasLeft">A value indicating whether reconstructed left samples are available.</param>
/// <param name="hasAbove">A value indicating whether reconstructed top samples are available.</param>
/// <param name="transformSize">The dimensions of the prediction block.</param>
/// <param name="destination">The buffer that receives the predicted samples.</param>
/// <param name="destinationStride">The distance, in samples, between destination rows.</param>
/// <param name="above">The prepared top reference samples.</param>
/// <param name="left">The prepared left reference samples.</param>
/// <param name="bitDepth">The number of bits used to represent each sample.</param>
private static void DcPredictor<T>(bool hasLeft, bool hasAbove, Av1TransformSize transformSize, Span<T> destination, nuint destinationStride, Span<T> above, Span<T> left, int bitDepth) private static void DcPredictor<T>(bool hasLeft, bool hasAbove, Av1TransformSize transformSize, Span<T> destination, nuint destinationStride, Span<T> above, Span<T> left, int bitDepth)
where T : unmanaged where T : unmanaged
{ {
// DecodeCore is reachable only through byte and short overloads, so this type
// dispatch permits shared reference preparation without boxing or allocating.
if (typeof(T) == typeof(byte)) if (typeof(T) == typeof(byte))
{ {
Av1PredictorFactory.DcPredictor( Av1PredictorFactory.DcPredictor(
@ -879,6 +1082,16 @@ internal class Av1PredictionDecoder
} }
} }
/// <summary>
/// Dispatches nondirectional prediction to the 8-bit or high-bit-depth implementation.
/// </summary>
/// <typeparam name="T">The byte or 16-bit sample type.</typeparam>
/// <param name="mode">The nondirectional prediction mode to apply.</param>
/// <param name="transformSize">The dimensions of the prediction block.</param>
/// <param name="destination">The buffer that receives the predicted samples.</param>
/// <param name="destinationStride">The distance, in samples, between destination rows.</param>
/// <param name="above">The prepared top reference samples.</param>
/// <param name="left">The prepared left reference samples.</param>
private static void GeneralPredictor<T>(Av1PredictionMode mode, Av1TransformSize transformSize, Span<T> destination, nuint destinationStride, Span<T> above, Span<T> left) private static void GeneralPredictor<T>(Av1PredictionMode mode, Av1TransformSize transformSize, Span<T> destination, nuint destinationStride, Span<T> above, Span<T> left)
where T : unmanaged where T : unmanaged
{ {
@ -904,6 +1117,19 @@ internal class Av1PredictionDecoder
} }
} }
/// <summary>
/// Dispatches directional prediction to the 8-bit or high-bit-depth implementation.
/// </summary>
/// <typeparam name="T">The byte or 16-bit sample type.</typeparam>
/// <param name="destination">The buffer that receives the predicted samples.</param>
/// <param name="destinationStride">The distance, in samples, between destination rows.</param>
/// <param name="transformSize">The dimensions of the prediction block.</param>
/// <param name="above">The prepared top reference samples.</param>
/// <param name="left">The prepared left reference samples.</param>
/// <param name="upsampleAbove">A value indicating whether the top edge was upsampled.</param>
/// <param name="upsampleLeft">A value indicating whether the left edge was upsampled.</param>
/// <param name="angle">The adjusted prediction angle in degrees.</param>
/// <param name="bitDepth">The number of bits used to represent each sample.</param>
private static void DirectionalPredictor<T>(Span<T> destination, nuint destinationStride, Av1TransformSize transformSize, Span<T> above, Span<T> left, bool upsampleAbove, bool upsampleLeft, int angle, int bitDepth) private static void DirectionalPredictor<T>(Span<T> destination, nuint destinationStride, Av1TransformSize transformSize, Span<T> above, Span<T> left, bool upsampleAbove, bool upsampleLeft, int angle, int bitDepth)
where T : unmanaged where T : unmanaged
{ {
@ -934,6 +1160,17 @@ internal class Av1PredictionDecoder
} }
} }
/// <summary>
/// Dispatches filter intra prediction to the 8-bit or high-bit-depth implementation.
/// </summary>
/// <typeparam name="T">The byte or 16-bit sample type.</typeparam>
/// <param name="destination">The buffer that receives the predicted samples.</param>
/// <param name="destinationStride">The distance, in samples, between destination rows.</param>
/// <param name="transformSize">The dimensions of the prediction block.</param>
/// <param name="above">The prepared top reference samples.</param>
/// <param name="left">The prepared left reference samples.</param>
/// <param name="mode">The filter intra mode whose coefficient set is applied.</param>
/// <param name="bitDepth">The number of bits used to represent each sample.</param>
private static void FilterIntraPredictor<T>(Span<T> destination, nuint destinationStride, Av1TransformSize transformSize, Span<T> above, Span<T> left, Av1FilterIntraMode mode, int bitDepth) private static void FilterIntraPredictor<T>(Span<T> destination, nuint destinationStride, Av1TransformSize transformSize, Span<T> above, Span<T> left, Av1FilterIntraMode mode, int bitDepth)
where T : unmanaged where T : unmanaged
{ {
@ -960,18 +1197,24 @@ internal class Av1PredictionDecoder
} }
} }
/// <summary>
/// Inserts half-sample positions into a prepared intra-prediction edge.
/// </summary>
/// <typeparam name="T">The 8-bit or high-bit-depth sample type.</typeparam>
/// <param name="buffer">The edge buffer, including writable prefix storage at indices -2 and -1.</param>
/// <param name="count">The number of original edge samples to upsample.</param>
/// <param name="bitDepth">The number of bits used to clamp interpolated samples.</param>
private static void UpsampleIntraEdge<T>(Span<T> buffer, int count, int bitDepth) private static void UpsampleIntraEdge<T>(Span<T> buffer, int count, int bitDepth)
where T : unmanaged, IBinaryInteger<T> where T : unmanaged, IBinaryInteger<T>
{ {
// TODO: Consider creating SIMD version // TODO: Consider creating SIMD version
// interpolate half-sample positions
DebugGuard.MustBeLessThanOrEqualTo(count, MaxUpsampleSize, nameof(count)); DebugGuard.MustBeLessThanOrEqualTo(count, MaxUpsampleSize, nameof(count));
Span<T> input = stackalloc T[MaxUpsampleSize + 3]; Span<T> input = stackalloc T[MaxUpsampleSize + 3];
T beforeBuffer = Unsafe.Subtract(ref buffer[0], 1); T beforeBuffer = Unsafe.Subtract(ref buffer[0], 1);
// copy p[-1..(sz-1)] and extend first and last samples // Duplicate both endpoints so the four-tap interpolation kernel can run at
// the edge without a separate boundary branch for its outer samples.
input[0] = beforeBuffer; input[0] = beforeBuffer;
input[1] = beforeBuffer; input[1] = beforeBuffer;
for (int i = 0; i < count; i++) for (int i = 0; i < count; i++)
@ -981,7 +1224,6 @@ internal class Av1PredictionDecoder
input[count + 2] = buffer[count - 1]; input[count + 2] = buffer[count - 1];
// interpolate half-sample edge positions
Unsafe.Subtract(ref buffer[0], 2) = input[0]; Unsafe.Subtract(ref buffer[0], 2) = input[0];
ref T output = ref buffer[0]; ref T output = ref buffer[0];
for (int i = 0; i < count; i++) for (int i = 0; i < count; i++)
@ -996,9 +1238,15 @@ internal class Av1PredictionDecoder
} }
/// <summary> /// <summary>
/// SVT: svt_aom_use_intra_edge_upsample /// Determines whether AV1 intra-edge upsampling applies to a directional prediction edge.
/// </summary> /// </summary>
private static bool UseIntraEdgeUpsample(int width, int height, int delta, bool type) /// <param name="width">The edge's primary block dimension.</param>
/// <param name="height">The edge's secondary block dimension.</param>
/// <param name="delta">The prediction angle relative to the edge's cardinal direction.</param>
/// <param name="filterType">A value indicating whether a neighboring smooth mode selects the alternate thresholds.</param>
/// <returns><see langword="true"/> when the edge must be upsampled; otherwise, <see langword="false"/>.</returns>
/// <remarks>Corresponds to <c>svt_aom_use_intra_edge_upsample</c> in SVT-AV1.</remarks>
private static bool UseIntraEdgeUpsample(int width, int height, int delta, bool filterType)
{ {
int d = Math.Abs(delta); int d = Math.Abs(delta);
if (d is <= 0 or >= 40) if (d is <= 0 or >= 40)
@ -1007,12 +1255,17 @@ internal class Av1PredictionDecoder
} }
int widthHeight = width + height; int widthHeight = width + height;
return type ? (widthHeight <= 8) : (widthHeight <= 16); return filterType ? (widthHeight <= 8) : (widthHeight <= 16);
} }
/// <summary> /// <summary>
/// SVT: svt_av1_filter_intra_edge_c /// Applies the AV1 intra-edge smoothing kernel at the requested strength.
/// </summary> /// </summary>
/// <typeparam name="T">The 8-bit or high-bit-depth sample type.</typeparam>
/// <param name="buffer">A reference to the first edge sample to filter.</param>
/// <param name="count">The number of edge samples.</param>
/// <param name="strength">The AV1 filter-strength index from zero through three.</param>
/// <remarks>Corresponds to <c>svt_av1_filter_intra_edge_c</c> in SVT-AV1.</remarks>
private static void FilterIntraEdge<T>(ref T buffer, int count, int strength) private static void FilterIntraEdge<T>(ref T buffer, int count, int strength)
where T : unmanaged, IBinaryInteger<T> where T : unmanaged, IBinaryInteger<T>
{ {
@ -1028,6 +1281,8 @@ internal class Av1PredictionDecoder
int filt = strength - 1; int filt = strength - 1;
Span<T> edge = stackalloc T[129]; Span<T> edge = stackalloc T[129];
// Filtering must read the original edge throughout the convolution, so retain
// a scratch copy rather than feeding earlier filtered samples into later outputs.
MemoryMarshal.CreateSpan(ref buffer, count).CopyTo(edge); MemoryMarshal.CreateSpan(ref buffer, count).CopyTo(edge);
for (int i = 1; i < count; i++) for (int i = 1; i < count; i++)
{ {
@ -1046,8 +1301,14 @@ internal class Av1PredictionDecoder
} }
/// <summary> /// <summary>
/// SVT: svt_aom_intra_edge_filter_strength /// Selects the AV1 intra-edge filter strength for the block dimensions and prediction angle.
/// </summary> /// </summary>
/// <param name="width">The edge's primary block dimension.</param>
/// <param name="height">The edge's secondary block dimension.</param>
/// <param name="delta">The prediction angle relative to the edge's cardinal direction.</param>
/// <param name="filterType">A value indicating whether a neighboring smooth mode selects the alternate thresholds.</param>
/// <returns>The filter strength from zero for no filtering through three for the strongest kernel.</returns>
/// <remarks>Corresponds to <c>svt_aom_intra_edge_filter_strength</c> in SVT-AV1.</remarks>
private static int IntraEdgeFilterStrength(int width, int height, int delta, bool filterType) private static int IntraEdgeFilterStrength(int width, int height, int delta, bool filterType)
{ {
int d = Math.Abs(delta); int d = Math.Abs(delta);
@ -1163,6 +1424,12 @@ internal class Av1PredictionDecoder
return strength; return strength;
} }
/// <summary>
/// Smooths the shared top-left reference sample where the prepared top and left edges meet.
/// </summary>
/// <typeparam name="T">The 8-bit or high-bit-depth sample type.</typeparam>
/// <param name="above">The prepared top edge with writable top-left prefix storage.</param>
/// <param name="left">The prepared left edge with writable top-left prefix storage.</param>
private static void FilterIntraEdgeCorner<T>(Span<T> above, Span<T> left) private static void FilterIntraEdgeCorner<T>(Span<T> above, Span<T> left)
where T : unmanaged, IBinaryInteger<T> where T : unmanaged, IBinaryInteger<T>
{ {
@ -1174,10 +1441,19 @@ internal class Av1PredictionDecoder
ref T leftPreviousRef = ref Unsafe.Subtract(ref leftRef, 1); ref T leftPreviousRef = ref Unsafe.Subtract(ref leftRef, 1);
int s = (int.CreateChecked(leftRef) * kernel[0]) + (int.CreateChecked(abovePreviousRef) * kernel[1]) + (int.CreateChecked(aboveRef) * kernel[2]); int s = (int.CreateChecked(leftRef) * kernel[0]) + (int.CreateChecked(abovePreviousRef) * kernel[1]) + (int.CreateChecked(aboveRef) * kernel[2]);
s = (s + 8) >> 4; s = (s + 8) >> 4;
// Both edge spans reserve their own prefix location for the same logical corner,
// so keep the two scratch representations synchronized after filtering.
abovePreviousRef = T.CreateChecked(s); abovePreviousRef = T.CreateChecked(s);
leftPreviousRef = T.CreateChecked(s); leftPreviousRef = T.CreateChecked(s);
} }
/// <summary>
/// Determines the directional edge-filter threshold class from neighboring prediction modes.
/// </summary>
/// <param name="partitionInfo">The decoded partition and neighboring mode state.</param>
/// <param name="plane">The color plane whose neighbors are inspected.</param>
/// <returns><see langword="true"/> when either relevant neighbor uses a smooth mode; otherwise, <see langword="false"/>.</returns>
private static bool GetFilterType(Av1PartitionInfo partitionInfo, Av1Plane plane) private static bool GetFilterType(Av1PartitionInfo partitionInfo, Av1Plane plane)
{ {
Av1BlockModeInfo? above; Av1BlockModeInfo? above;
@ -1198,6 +1474,12 @@ internal class Av1PredictionDecoder
return aboveIsSmooth || leftIsSmooth; return aboveIsSmooth || leftIsSmooth;
} }
/// <summary>
/// Determines whether a block uses any AV1 smooth intra prediction mode on the requested plane.
/// </summary>
/// <param name="modeInfo">The neighboring block's decoded mode state.</param>
/// <param name="plane">The luma or chroma plane class whose mode is inspected.</param>
/// <returns><see langword="true"/> for smooth, smooth-horizontal, or smooth-vertical prediction; otherwise, <see langword="false"/>.</returns>
private static bool IsSmooth(Av1BlockModeInfo modeInfo, Av1Plane plane) private static bool IsSmooth(Av1BlockModeInfo modeInfo, Av1Plane plane)
{ {
if (plane == Av1Plane.Y) if (plane == Av1Plane.Y)

17
tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ChromaFromLumaTests.cs

@ -121,6 +121,23 @@ public class Av1ChromaFromLumaTests
Assert.Equal(new short[] { 0, (short)((maximum / 2) - 16), maximum, maximum }, destination); Assert.Equal(new short[] { 0, (short)((maximum / 2) - 16), maximum, maximum }, destination);
} }
[Theory]
[InlineData((int)Av1Plane.U, 3)]
[InlineData((int)Av1Plane.V, 4)]
public void AlphaIndexSelectsMagnitudeForRequestedChromaPlane(int planeIndex, int expected)
{
// U occupies the high nibble and V occupies the low nibble in the packed AV1 alpha index.
const int alphaIndex = 0x23;
const int bothPositiveJointSign = 7;
int actual = Av1PredictionDecoder.ChromaFromLumaIndexToAlpha(
alphaIndex,
bothPositiveJointSign,
(Av1Plane)planeIndex);
Assert.Equal(expected, actual);
}
private static short[] GetBlock(short[] buffer, int width, int height) private static short[] GetBlock(short[] buffer, int width, int height)
{ {
short[] result = new short[width * height]; short[] result = new short[width * height];

Loading…
Cancel
Save