Browse Source

Add AV1 luma transform-size search

pull/2633/head
James Jackson-South 4 weeks ago
parent
commit
339972b7fe
  1. 10
      HEIF_IMPLEMENTATION_PLAN.md
  2. 68
      src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolContextHelper.cs
  3. 26
      src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolEncoder.cs
  4. 25
      src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1EncoderModeDecisionWorkspace.cs
  5. 2
      src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1FrameEncoder.cs
  6. 2
      src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.ChromaPaletteModeDecision.cs
  7. 20
      src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.IntraBlockCopyModeDecision.cs
  8. 560
      src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.ModeDecision.cs
  9. 100
      src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.Operator.cs
  10. 7
      src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.PaletteModeDecision.cs
  11. 291
      src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1TransformBlockEncoder.cs
  12. 98
      src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileWriter.cs
  13. 55
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1CoefficientsEntropyTests.cs
  14. 93
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EncoderFrameTests.cs
  15. 9
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EntropyTests.cs
  16. 41
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1SymbolContextTests.cs

10
HEIF_IMPLEMENTATION_PLAN.md

File diff suppressed because one or more lines are too long

68
src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolContextHelper.cs

@ -288,6 +288,43 @@ internal static class Av1SymbolContextHelper
return TransformBlockSkipContexts[(topClass * 5) + leftClass];
}
/// <summary>
/// Derives the variable-transform partition context from the current node and its adjacent transform edges.
/// </summary>
/// <param name="aboveTransformWidth">The transform width retained immediately above the current node.</param>
/// <param name="leftTransformHeight">The transform height retained immediately left of the current node.</param>
/// <param name="blockSize">The containing coding-block size.</param>
/// <param name="transformSize">The transform size represented by the current partition node.</param>
/// <returns>The variable-transform partition context.</returns>
public static int GetTransformPartitionContext(
byte aboveTransformWidth,
byte leftTransformHeight,
Av1BlockSize blockSize,
Av1TransformSize transformSize)
{
if (transformSize <= Av1TransformSize.Size4x4)
{
return 0;
}
int above = aboveTransformWidth < transformSize.GetWidth() ? 1 : 0;
int left = leftTransformHeight < transformSize.GetHeight() ? 1 : 0;
int maximumDimension = Math.Max(blockSize.GetWidth(), blockSize.GetHeight());
Av1TransformSize maximumSquareTransform = maximumDimension switch
{
>= 64 => Av1TransformSize.Size64x64,
>= 32 => Av1TransformSize.Size32x32,
>= 16 => Av1TransformSize.Size16x16,
_ => Av1TransformSize.Size8x8
};
int category = (transformSize.GetSquareUpSize() != maximumSquareTransform &&
maximumSquareTransform > Av1TransformSize.Size8x8 ? 1 : 0) +
((((int)Av1TransformSize.SquareSizes - 1) - (int)maximumSquareTransform) * 2);
return (category * 3) + above + left;
}
/// <summary>
/// Reconstructs an end-of-block coefficient position from its token and extra offset.
/// </summary>
@ -668,6 +705,37 @@ internal static class Av1SymbolContextHelper
}
}
/// <summary>
/// Packs the magnitude class and DC sign retained by neighboring transform blocks.
/// </summary>
/// <param name="coefficients">The raster-ordered quantized coefficients.</param>
/// <param name="transformSize">The transform dimensions.</param>
/// <param name="transformType">The transform type selecting scan order.</param>
/// <param name="endOfBlock">The one-based final nonzero scan position.</param>
/// <returns>The packed coefficient context, or zero for an empty transform.</returns>
public static byte GetCoefficientContext(
ReadOnlySpan<int> coefficients,
Av1TransformSize transformSize,
Av1TransformType transformType,
ushort endOfBlock)
{
if (endOfBlock == 0)
{
return 0;
}
ReadOnlySpan<short> scan = Av1ScanOrderConstants.GetScanOrder(transformSize, transformType).Scan;
int culLevel = 0;
for (int scanIndex = 0; scanIndex < endOfBlock; scanIndex++)
{
culLevel += Math.Abs(coefficients[scan[scanIndex]]);
}
culLevel = Math.Min(Av1Constants.CoefficientContextMask, culLevel);
SetDcSign(ref culLevel, coefficients[0]);
return (byte)culLevel;
}
/// <summary>
/// Converts a one-based end-of-block position to its token and group offset.
/// </summary>

26
src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolEncoder.cs

@ -117,6 +117,11 @@ internal class Av1SymbolEncoder : IDisposable
/// </summary>
private readonly Av1Distribution[][] transformSize;
/// <summary>
/// The tile-adaptive variable-transform partition distributions.
/// </summary>
private readonly Av1Distribution[] transformPartition;
/// <summary>
/// The tile-adaptive spatial segment-identifier distributions.
/// </summary>
@ -200,6 +205,7 @@ internal class Av1SymbolEncoder : IDisposable
this.intraExtendedTransform = Av1DefaultDistributions.IntraExtendedTransform;
this.interExtendedTransform = Av1DefaultDistributions.InterExtendedTransform;
this.transformSize = Av1DefaultDistributions.TransformSize;
this.transformPartition = Av1DefaultDistributions.TransformPartition;
this.segmentId = Av1DefaultDistributions.SegmentId;
this.angleDelta = Av1DefaultDistributions.AngleDelta;
this.skip = Av1DefaultDistributions.Skip;
@ -1208,6 +1214,26 @@ internal class Av1SymbolEncoder : IDisposable
w.WriteSymbol(selectedDepth, this.transformSize[categoryDepth - 1][context]);
}
/// <summary>
/// Gets the current fixed-point cost of one variable-transform partition decision.
/// </summary>
/// <param name="split">Indicates whether the current transform node is split.</param>
/// <param name="context">The neighboring variable-transform context.</param>
/// <returns>The rate cost in 1/512-bit units.</returns>
public int GetTransformPartitionCost(bool split, int context)
=> Av1ProbabilityCost.GetSymbolCost(this.transformPartition[context], split ? 1 : 0);
/// <summary>
/// Writes one variable-transform partition decision.
/// </summary>
/// <param name="split">Indicates whether the current transform node is split.</param>
/// <param name="context">The neighboring variable-transform context.</param>
public void WriteTransformPartition(bool split, int context)
{
ref Av1SymbolWriter w = ref this.writer;
w.WriteSymbol(split ? 1 : 0, this.transformPartition[context]);
}
private static int GetTransformSizeDepth(
Av1BlockSize blockSize,
Av1TransformSize transformSize,

25
src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1EncoderModeDecisionWorkspace.cs

@ -4,6 +4,7 @@
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction;
using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.ChromaFromLuma;
using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline;
@ -19,6 +20,11 @@ internal readonly ref struct Av1EncoderModeDecisionWorkspace<TSample>
/// </summary>
public const int MaximumSampleCount = 8 * 8;
/// <summary>
/// The number of 4x4 transform blocks covering one 8x8 coding block.
/// </summary>
public const int CandidateTransformBlockCount = 4;
/// <summary>
/// The required workspace length in signed-integer storage elements.
/// </summary>
@ -31,7 +37,11 @@ internal readonly ref struct Av1EncoderModeDecisionWorkspace<TSample>
private const int CandidateSampleStorageLength = 2 * MaximumSampleCount * sizeof(ushort) / sizeof(int);
private const int CandidateCoefficientStorageOffset = CandidateSampleStorageOffset + CandidateSampleStorageLength;
private const int CandidateCoefficientStorageLength = 2 * MaximumSampleCount;
private const int TransientStorageOffset = CandidateCoefficientStorageOffset + CandidateCoefficientStorageLength;
private const int CandidateTransformBlockStorageOffset = CandidateCoefficientStorageOffset + CandidateCoefficientStorageLength;
private const int CandidateTransformBlockStorageLength = CandidateTransformBlockCount;
private const int TransformContextStorageOffset = CandidateTransformBlockStorageOffset + CandidateTransformBlockStorageLength;
private const int TransformContextStorageLength = 1;
private const int TransientStorageOffset = TransformContextStorageOffset + TransformContextStorageLength;
private const int ChromaFromLumaSampleCount = Av1ChromaFromLumaContext.BufferLine * 8;
private const int ChromaFromLumaSampleStorageLength = ChromaFromLumaSampleCount * sizeof(short) / sizeof(int);
private const int ChromaFromLumaBlueRateOffset = ChromaFromLumaSampleStorageLength;
@ -81,6 +91,19 @@ internal readonly ref struct Av1EncoderModeDecisionWorkspace<TSample>
public Av1EncoderPaletteWorkspace<TSample> Palette
=> new(this.storage[TransientStorageOffset..]);
/// <summary>
/// Gets the transform state retained while evaluating a uniform 4x4 luma layout.
/// </summary>
public Span<Av1EncoderTransformBlockState> CandidateTransformBlocks
=> MemoryMarshal.Cast<int, Av1EncoderTransformBlockState>(
this.storage.Slice(CandidateTransformBlockStorageOffset, CandidateTransformBlockStorageLength));
/// <summary>
/// Gets the two above and two left coefficient contexts used by a uniform 4x4 luma layout.
/// </summary>
public Span<byte> TransformContexts
=> MemoryMarshal.AsBytes(this.storage.Slice(TransformContextStorageOffset, TransformContextStorageLength));
/// <summary>
/// Gets one reference edge including its common-corner prefix.
/// </summary>

2
src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1FrameEncoder.cs

@ -119,7 +119,7 @@ internal static class Av1FrameEncoder
ErrorResilientMode = true,
RefreshFrameFlags = byte.MaxValue,
DisableFrameEndUpdateCdf = true,
TransformMode = Av1TransformMode.Largest,
TransformMode = effort >= 6 ? Av1TransformMode.Select : Av1TransformMode.Largest,
ModeInfoColumnCount = modeInfoColumnCount,
ModeInfoRowCount = modeInfoRowCount,
TilesInfo = tiles,

2
src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.ChromaPaletteModeDecision.cs

@ -247,6 +247,7 @@ internal static partial class Av1IntraSuperblockEncoder
bluePrediction,
blueResidual,
candidateBlueReconstruction,
transformSize.GetWidth(),
candidateBlueCoefficients,
transformSize,
Av1TransformType.DctDct,
@ -265,6 +266,7 @@ internal static partial class Av1IntraSuperblockEncoder
redPrediction,
redResidual,
candidateRedReconstruction,
transformSize.GetWidth(),
candidateRedCoefficients,
transformSize,
Av1TransformType.DctDct,

20
src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.IntraBlockCopyModeDecision.cs

@ -128,6 +128,23 @@ internal static partial class Av1IntraSuperblockEncoder
BlockSize,
LumaTransformSize);
int transformPartitionRate = 0;
if (this.picture.Parent.FrameHeader.TransformMode == Av1TransformMode.Select)
{
Av1NeighborArrayUnit<byte> transformContexts = this.picture.TransformFunctionContexts[tileIndex];
int topIndex = transformContexts.GetTopIndex(blockOrigin);
int leftIndex = transformContexts.GetLeftIndex(blockOrigin);
int transformPartitionContext = Av1SymbolContextHelper.GetTransformPartitionContext(
transformContexts.Top[topIndex],
transformContexts.Left[leftIndex],
BlockSize,
LumaTransformSize);
transformPartitionRate = writer.GetTransformPartitionCost(
false,
transformPartitionContext);
}
ObuColorConfig colorConfig = this.picture.Sequence.SequenceHeader.ColorConfig;
int subsamplingX = colorConfig.SubSamplingX ? 1 : 0;
int subsamplingY = colorConfig.SubSamplingY ? 1 : 0;
@ -250,6 +267,7 @@ internal static partial class Av1IntraSuperblockEncoder
int candidateRate = writer.GetUseIntraBlockCopyCost(true) +
displacementRate +
writer.GetSkipCost(false, skipContext) +
transformPartitionRate +
lumaRate +
blueRate +
redRate;
@ -385,6 +403,7 @@ internal static partial class Av1IntraSuperblockEncoder
modeInfo.Block.Mode = Av1PredictionMode.DC;
modeInfo.Block.UvMode = Av1ChromaPredictionMode.DC;
modeInfo.Block.TransformSize = LumaTransformSize;
modeInfo.Block.Skip = selectedSkip;
modeInfo.Block.UseIntraBlockCopy = true;
block.FilterIntraMode = Av1FilterIntraMode.AllFilterIntraModes;
@ -466,6 +485,7 @@ internal static partial class Av1IntraSuperblockEncoder
prediction[..sampleCount],
residual[..sampleCount],
transformReconstruction[..sampleCount],
transformSize.GetWidth(),
transformCoefficients[..sampleCount],
transformSize,
transformType,

560
src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.ModeDecision.cs

@ -189,23 +189,31 @@ internal static partial class Av1IntraSuperblockEncoder
int lumaTransformIndex = this.codedAreaLuma /
Av1EncoderCoefficientBuffer.TransformBlockUnitCoefficientCount;
ref Av1EncoderTransformBlockState lumaState = ref lumaTransformBlocks[lumaTransformIndex];
Span<Av1EncoderTransformBlockState> retainedLumaStates = lumaTransformBlocks[lumaTransformIndex..];
modeInfo.Block.Mode = this.SelectLumaMode(
writer,
macroBlock,
blockOrigin,
tileIndex,
lumaCoefficients[this.codedAreaLuma..],
ref lumaState,
retainedLumaStates,
ref paletteInfo,
out int lumaAngleDelta,
out Av1FilterIntraMode filterIntraMode,
out Av1TransformSize lumaTransformSize,
out long lumaCost);
block.PredictionUnit.AngleDelta[(int)Av1PlaneType.Y] = (sbyte)lumaAngleDelta;
block.FilterIntraMode = filterIntraMode;
modeInfo.Block.TransformSize = lumaTransformSize;
int lumaTransformBlockCount = LumaTransformSize.GetSize2d() / lumaTransformSize.GetSize2d();
bool lumaTransformEmpty = true;
for (int transformIndex = 0; transformIndex < lumaTransformBlockCount; transformIndex++)
{
lumaTransformEmpty &= retainedLumaStates[transformIndex].EndOfBlock == 0;
}
bool lumaTransformEmpty = lumaState.EndOfBlock == 0;
if (this.source.IsMonochrome)
{
int emptyTransformRate = lumaTransformEmpty
@ -215,8 +223,7 @@ internal static partial class Av1IntraSuperblockEncoder
Av1ComponentType.Luminance,
blockOrigin,
BlockSize,
LumaTransformSize,
lumaState.TransformType,
lumaTransformSize,
modeInfo.Block.Mode,
block.FilterIntraMode)
: 0;
@ -302,8 +309,7 @@ internal static partial class Av1IntraSuperblockEncoder
Av1ComponentType.Luminance,
blockOrigin,
BlockSize,
LumaTransformSize,
lumaState.TransformType,
lumaTransformSize,
modeInfo.Block.Mode,
block.FilterIntraMode);
@ -314,7 +320,6 @@ internal static partial class Av1IntraSuperblockEncoder
chromaOrigin,
chromaBlockSize,
chromaTransformSize,
blueState.TransformType,
modeInfo.Block.Mode,
Av1FilterIntraMode.AllFilterIntraModes);
@ -325,7 +330,6 @@ internal static partial class Av1IntraSuperblockEncoder
chromaOrigin,
chromaBlockSize,
chromaTransformSize,
redState.TransformType,
modeInfo.Block.Mode,
Av1FilterIntraMode.AllFilterIntraModes);
@ -360,27 +364,73 @@ internal static partial class Av1IntraSuperblockEncoder
Point blockOrigin,
Av1BlockSize blockSize,
Av1TransformSize transformSize,
Av1TransformType transformType,
Av1PredictionMode lumaMode,
Av1FilterIntraMode filterIntraMode)
{
Av1TransformBlockContext blockContext = Av1TileWriter.GetTransformBlockContexts(
componentType,
coefficientNeighbors,
blockOrigin,
blockSize,
transformSize);
int blockWidth = blockSize.Get4x4WideCount();
int blockHeight = blockSize.Get4x4HighCount();
int transformWidth = transformSize.Get4x4WideCount();
int transformHeight = transformSize.Get4x4HighCount();
if (blockWidth == transformWidth && blockHeight == transformHeight)
{
Av1TransformBlockContext blockContext = Av1TileWriter.GetTransformBlockContexts(
componentType,
coefficientNeighbors,
blockOrigin,
blockSize,
transformSize);
return writer.GetCoefficientCost(
transformSize,
Av1TransformType.DctDct,
lumaMode,
ReadOnlySpan<int>.Empty,
componentType,
blockContext,
0,
this.picture.Parent.FrameHeader.UseReducedTransformSet,
filterIntraMode);
}
return writer.GetCoefficientCost(
transformSize,
transformType,
lumaMode,
ReadOnlySpan<int>.Empty,
componentType,
blockContext,
0,
this.picture.Parent.FrameHeader.UseReducedTransformSet,
filterIntraMode);
Span<byte> contexts = this.blockWorkspace
.GetModeDecisionWorkspace<TSample>()
.TransformContexts;
Span<byte> topContexts = contexts[..blockWidth];
Span<byte> leftContexts = contexts.Slice(blockWidth, blockHeight);
int topIndex = coefficientNeighbors.GetTopIndex(blockOrigin);
int leftIndex = coefficientNeighbors.GetLeftIndex(blockOrigin);
coefficientNeighbors.Top.Slice(topIndex, blockWidth).CopyTo(topContexts);
coefficientNeighbors.Left.Slice(leftIndex, blockHeight).CopyTo(leftContexts);
int rate = 0;
for (int blockRow = 0; blockRow < blockHeight; blockRow += transformHeight)
{
for (int blockColumn = 0; blockColumn < blockWidth; blockColumn += transformWidth)
{
Av1TransformBlockContext blockContext = Av1TileWriter.GetTransformBlockContexts(
componentType,
topContexts.Slice(blockColumn, transformWidth),
leftContexts.Slice(blockRow, transformHeight),
blockSize,
transformSize);
rate += writer.GetCoefficientCost(
transformSize,
Av1TransformType.DctDct,
lumaMode,
ReadOnlySpan<int>.Empty,
componentType,
blockContext,
0,
this.picture.Parent.FrameHeader.UseReducedTransformSet,
filterIntraMode);
topContexts.Slice(blockColumn, transformWidth).Clear();
leftContexts.Slice(blockRow, transformHeight).Clear();
}
}
return rate;
}
private Av1PredictionMode SelectLumaMode(
@ -389,10 +439,11 @@ internal static partial class Av1IntraSuperblockEncoder
Point blockOrigin,
ushort tileIndex,
Span<int> retainedCoefficients,
ref Av1EncoderTransformBlockState retainedState,
Span<Av1EncoderTransformBlockState> retainedStates,
ref Av1EncoderPaletteInfo paletteInfo,
out int selectedAngleDelta,
out Av1FilterIntraMode selectedFilterIntraMode,
out Av1TransformSize selectedTransformSize,
out long selectedCost)
{
const Av1BlockSize BlockSize = Av1BlockSize.Block8x8;
@ -509,6 +560,16 @@ internal static partial class Av1IntraSuperblockEncoder
BlockSize,
TransformSize);
int transformSizeContext = Av1TileWriter.GetTransformSizeContext(
this.picture.TransformFunctionContexts[tileIndex],
macroBlock,
blockOrigin,
BlockSize);
int largestTransformRate = this.picture.Parent.FrameHeader.TransformMode == Av1TransformMode.Select
? writer.GetTransformSizeCost(BlockSize, TransformSize, transformSizeContext)
: 0;
int paletteDisabledCost = 0;
if (this.picture.Parent.FrameHeader.AllowScreenContentTools)
{
@ -586,6 +647,7 @@ internal static partial class Av1IntraSuperblockEncoder
defaultTransformType,
blockContext,
paletteDisabledCost,
largestTransformRate,
candidateReconstruction,
candidateCoefficients,
ref candidateState);
@ -600,7 +662,7 @@ internal static partial class Av1IntraSuperblockEncoder
retainedCoefficients,
TransformSize,
candidateState,
ref retainedState);
ref retainedStates[0]);
bestCost = candidateCost;
bestMode = mode;
@ -635,6 +697,7 @@ internal static partial class Av1IntraSuperblockEncoder
transformType,
blockContext,
paletteDisabledCost,
largestTransformRate,
candidateReconstruction,
candidateCoefficients,
ref candidateState);
@ -649,7 +712,7 @@ internal static partial class Av1IntraSuperblockEncoder
retainedCoefficients,
TransformSize,
candidateState,
ref retainedState);
ref retainedStates[0]);
bestTransformCost = candidateCost;
}
@ -699,6 +762,7 @@ internal static partial class Av1IntraSuperblockEncoder
transformType,
blockContext,
paletteDisabledCost,
largestTransformRate,
candidateReconstruction,
candidateCoefficients,
ref candidateState);
@ -713,7 +777,7 @@ internal static partial class Av1IntraSuperblockEncoder
retainedCoefficients,
TransformSize,
candidateState,
ref retainedState);
ref retainedStates[0]);
bestTransformCost = candidateCost;
bestMode = Av1PredictionMode.DC;
@ -735,10 +799,11 @@ internal static partial class Av1IntraSuperblockEncoder
tileIndex,
transformSetType,
blockContext,
largestTransformRate,
candidateReconstruction,
candidateCoefficients,
retainedCoefficients,
ref retainedState,
ref retainedStates[0],
ref bestTransformCost,
ref paletteInfo))
{
@ -747,10 +812,413 @@ internal static partial class Av1IntraSuperblockEncoder
selectedFilterIntraMode = Av1FilterIntraMode.AllFilterIntraModes;
}
selectedTransformSize = TransformSize;
if (this.effort >= 6 &&
this.picture.Parent.FrameHeader.TransformMode == Av1TransformMode.Select &&
paletteInfo.PaletteSizes[0] == 0 &&
selectedFilterIntraMode == Av1FilterIntraMode.AllFilterIntraModes)
{
long splitCost = this.GetSplitLumaCandidateCost(
writer,
macroBlock,
sourcePlane,
reconstructionPlane,
blockOrigin,
tileIndex,
bestMode,
selectedAngleDelta,
paletteDisabledCost,
transformSizeContext,
bestTransformCost,
candidateReconstruction,
candidateCoefficients,
workspace.CandidateTransformBlocks);
if (splitCost < bestTransformCost)
{
CopySplitCandidate(
candidateReconstruction,
candidateCoefficients,
workspace.CandidateTransformBlocks,
reconstructionPlane,
blockOrigin,
retainedCoefficients,
retainedStates);
bestTransformCost = splitCost;
selectedTransformSize = Av1TransformSize.Size4x4;
}
}
selectedCost = bestTransformCost;
return bestMode;
}
private long GetSplitLumaCandidateCost(
Av1SymbolEncoder writer,
Av1MacroBlockD macroBlock,
Buffer2DRegion<TSample> sourcePlane,
Buffer2DRegion<TSample> reconstructionPlane,
Point blockOrigin,
ushort tileIndex,
Av1PredictionMode mode,
int angleDelta,
int paletteDisabledCost,
int transformSizeContext,
long costLimit,
Span<TSample> candidateReconstruction,
Span<int> candidateCoefficients,
Span<Av1EncoderTransformBlockState> candidateTransformBlocks)
{
const Av1BlockSize BlockSize = Av1BlockSize.Block8x8;
const Av1TransformSize TransformSize = Av1TransformSize.Size4x4;
const int BlockWidth = 8;
const int TransformWidth = 4;
const int TransformSampleCount = TransformWidth * TransformWidth;
Av1EncoderModeDecisionWorkspace<TSample> workspace =
this.blockWorkspace.GetModeDecisionWorkspace<TSample>();
Span<TSample> transformSamples = workspace.GetCandidateReconstruction(1);
Span<TSample> prediction = transformSamples[..TransformSampleCount];
Span<TSample> transformReconstruction = transformSamples.Slice(
TransformSampleCount,
TransformSampleCount);
Span<int> transformCoefficients = workspace.GetCandidateCoefficients(1)[..TransformSampleCount];
Span<short> residual = workspace.FilterResidual[..TransformSampleCount];
Span<byte> contexts = workspace.TransformContexts;
Span<byte> topContexts = contexts[..2];
Span<byte> leftContexts = contexts[2..4];
Av1NeighborArrayUnit<byte> coefficientNeighbors =
this.picture.LuminanceDcSignLevelCoefficientNeighbors[tileIndex];
int topIndex = coefficientNeighbors.GetTopIndex(blockOrigin);
int leftIndex = coefficientNeighbors.GetLeftIndex(blockOrigin);
coefficientNeighbors.Top.Slice(topIndex, 2).CopyTo(topContexts);
coefficientNeighbors.Left.Slice(leftIndex, 2).CopyTo(leftContexts);
bool useReducedTransformSet = this.picture.Parent.FrameHeader.UseReducedTransformSet;
Av1TransformSetType transformSetType = Av1SymbolContextHelper.GetExtendedTransformSetType(
TransformSize,
useReducedTransformSet);
int rate = Av1TileWriter.GetLumaModeCost(writer, macroBlock, BlockSize, mode, angleDelta);
rate += writer.GetTransformSizeCost(BlockSize, TransformSize, transformSizeContext);
if (mode == Av1PredictionMode.DC)
{
rate += paletteDisabledCost;
if (this.picture.Sequence.SequenceHeader.EnableFilterIntra)
{
rate += writer.GetFilterIntraModeCost(
Av1FilterIntraMode.AllFilterIntraModes,
BlockSize);
}
}
long distortion = 0;
for (int transformRow = 0; transformRow < 2; transformRow++)
{
for (int transformColumn = 0; transformColumn < 2; transformColumn++)
{
int transformIndex = (transformRow * 2) + transformColumn;
int reconstructionOffset =
(transformRow * TransformWidth * BlockWidth) + (transformColumn * TransformWidth);
Point transformOrigin = blockOrigin + new Size(
transformColumn * TransformWidth,
transformRow * TransformWidth);
Span<TSample> aboveStorage = workspace.GetReferenceSamples(0);
Span<TSample> leftStorage = workspace.GetReferenceSamples(1);
this.PrepareSplitLumaReferenceSamples(
reconstructionPlane,
blockOrigin,
macroBlock,
transformRow,
transformColumn,
candidateReconstruction,
aboveStorage,
leftStorage,
out bool hasLeft,
out bool hasAbove);
TOperator.PrepareIntra(
this.blockWorkspace,
sourcePlane,
transformOrigin,
prediction,
aboveStorage.Slice(1, TransformWidth * 2),
leftStorage.Slice(1, TransformWidth * 2),
hasLeft,
hasAbove,
mode,
angleDelta,
residual,
TransformSize,
this.bitDepth);
Av1TransformBlockContext blockContext = Av1TileWriter.GetTransformBlockContexts(
Av1ComponentType.Luminance,
topContexts.Slice(transformColumn, 1),
leftContexts.Slice(transformRow, 1),
BlockSize,
TransformSize);
long bestTransformCost = long.MaxValue;
Av1TransformType bestTransformType = Av1TransformType.DctDct;
int bestTransformRate = 0;
long bestTransformDistortion = 0;
Av1EncoderTransformBlockState bestTransformState = default;
Span<int> retainedTransformCoefficients = candidateCoefficients.Slice(
transformIndex * TransformSampleCount,
TransformSampleCount);
for (Av1TransformType transformType = Av1TransformType.DctDct;
transformType < Av1TransformType.AllTransformTypes;
transformType++)
{
if (!transformType.IsExtendedSetUsed(transformSetType))
{
continue;
}
Av1EncoderTransformBlockState candidateState = default;
long candidateDistortion = TOperator.EncodePredictionCandidate(
this.blockWorkspace,
sourcePlane,
transformOrigin,
prediction,
residual,
transformReconstruction,
TransformWidth,
transformCoefficients,
TransformSize,
transformType,
Av1Plane.Y,
this.quantization.QIndex[0],
this.quantization.DeltaQDc[(int)Av1Plane.Y],
this.quantization.DeltaQAc[(int)Av1Plane.Y],
this.bitDepth,
ref candidateState);
int candidateRate = writer.GetCoefficientCost(
TransformSize,
transformType,
mode,
transformCoefficients,
Av1ComponentType.Luminance,
blockContext,
candidateState.EndOfBlock,
useReducedTransformSet,
Av1FilterIntraMode.AllFilterIntraModes);
long candidateCost = Av1RateDistortion.GetCost(
this.rateMultiplier,
candidateRate,
candidateDistortion);
if (candidateCost < bestTransformCost)
{
// Preserve the improving 4x4 trial in the block mosaic. Later transform predictions
// consume that reconstruction, and copying the compact result avoids another transform.
transformCoefficients.CopyTo(retainedTransformCoefficients);
for (int row = 0; row < TransformWidth; row++)
{
transformReconstruction.Slice(row * TransformWidth, TransformWidth)
.CopyTo(
candidateReconstruction.Slice(
reconstructionOffset + (row * BlockWidth),
TransformWidth));
}
bestTransformCost = candidateCost;
bestTransformType = transformType;
bestTransformRate = candidateRate;
bestTransformDistortion = candidateDistortion;
bestTransformState = candidateState;
}
}
rate += bestTransformRate;
distortion += bestTransformDistortion;
candidateTransformBlocks[transformIndex] = bestTransformState;
byte coefficientContext = Av1SymbolContextHelper.GetCoefficientContext(
retainedTransformCoefficients,
TransformSize,
bestTransformType,
bestTransformState.EndOfBlock);
topContexts[transformColumn] = coefficientContext;
leftContexts[transformRow] = coefficientContext;
// Every remaining transform can only add nonnegative rate and distortion.
if (Av1RateDistortion.GetCost(this.rateMultiplier, rate, distortion) >= costLimit)
{
return long.MaxValue;
}
}
}
return Av1RateDistortion.GetCost(this.rateMultiplier, rate, distortion);
}
private void PrepareSplitLumaReferenceSamples(
Buffer2DRegion<TSample> reconstructionPlane,
Point blockOrigin,
Av1MacroBlockD macroBlock,
int transformRow,
int transformColumn,
ReadOnlySpan<TSample> candidateReconstruction,
Span<TSample> aboveStorage,
Span<TSample> leftStorage,
out bool hasLeft,
out bool hasAbove)
{
const Av1BlockSize BlockSize = Av1BlockSize.Block8x8;
const Av1TransformSize TransformSize = Av1TransformSize.Size4x4;
const int BlockWidth = 8;
const int TransformWidth = 4;
int rowOffset = transformRow * TransformWidth;
int columnOffset = transformColumn * TransformWidth;
int modeInfoRow = blockOrigin.Y >> Av1Constants.ModeInfoSizeLog2;
int modeInfoColumn = blockOrigin.X >> Av1Constants.ModeInfoSizeLog2;
hasAbove = transformRow > 0 || macroBlock.IsUpAvailable;
hasLeft = transformColumn > 0 || macroBlock.IsLeftAvailable;
bool rightAvailable =
modeInfoColumn + transformColumn + TransformSize.Get4x4WideCount() <
macroBlock.Tile.ModeInfoColumnEnd;
bool bottomAvailable =
modeInfoRow + transformRow + TransformSize.Get4x4HighCount() <
macroBlock.Tile.ModeInfoRowEnd;
bool hasTopRight = Av1IntraReferenceAvailability.HasTopRight(
this.picture.Sequence.SequenceHeader.SuperblockSize,
BlockSize,
modeInfoRow,
modeInfoColumn,
hasAbove,
rightAvailable,
Av1PartitionType.None,
TransformSize,
transformRow,
transformColumn,
0,
0);
bool hasBottomLeft = Av1IntraReferenceAvailability.HasBottomLeft(
this.picture.Sequence.SequenceHeader.SuperblockSize,
BlockSize,
modeInfoRow,
modeInfoColumn,
bottomAvailable,
hasLeft,
Av1PartitionType.None,
TransformSize,
transformRow,
transformColumn,
0,
0);
Span<TSample> above = aboveStorage.Slice(1, TransformWidth * 2);
Span<TSample> left = leftStorage.Slice(1, TransformWidth * 2);
if (hasAbove)
{
if (transformRow > 0)
{
candidateReconstruction
.Slice(((rowOffset - 1) * BlockWidth) + columnOffset, TransformWidth)
.CopyTo(above);
}
else
{
reconstructionPlane.DangerousGetRowSpan(blockOrigin.Y - 1)
.Slice(blockOrigin.X + columnOffset, TransformWidth)
.CopyTo(above);
}
}
if (hasLeft)
{
if (transformColumn > 0)
{
for (int row = 0; row < TransformWidth; row++)
{
left[row] = candidateReconstruction[((rowOffset + row) * BlockWidth) + columnOffset - 1];
}
}
else
{
for (int row = 0; row < TransformWidth; row++)
{
left[row] = reconstructionPlane
.DangerousGetRowSpan(blockOrigin.Y + rowOffset + row)[blockOrigin.X - 1];
}
}
}
int midpoint = 128 << (this.bitDepth.GetBitCount() - 8);
if (!hasAbove)
{
above[..TransformWidth].Fill(hasLeft ? left[0] : TOperator.CreateSample(midpoint - 1));
}
if (!hasLeft)
{
left[..TransformWidth].Fill(hasAbove ? above[0] : TOperator.CreateSample(midpoint + 1));
}
if (hasTopRight)
{
if (transformRow > 0)
{
candidateReconstruction
.Slice(
((rowOffset - 1) * BlockWidth) + columnOffset + TransformWidth,
TransformWidth)
.CopyTo(above[TransformWidth..]);
}
else
{
reconstructionPlane.DangerousGetRowSpan(blockOrigin.Y - 1)
.Slice(blockOrigin.X + columnOffset + TransformWidth, TransformWidth)
.CopyTo(above[TransformWidth..]);
}
}
else
{
above[TransformWidth..].Fill(above[TransformWidth - 1]);
}
if (hasBottomLeft)
{
for (int row = TransformWidth; row < TransformWidth * 2; row++)
{
left[row] = reconstructionPlane
.DangerousGetRowSpan(blockOrigin.Y + rowOffset + row)[blockOrigin.X - 1];
}
}
else
{
left[TransformWidth..].Fill(left[TransformWidth - 1]);
}
// Only an interior transform corner belongs to decision scratch. Boundary corners continue
// to read the already reconstructed neighboring block so candidate trials remain isolated.
TSample corner = hasAbove && hasLeft
? transformRow > 0 && transformColumn > 0
? candidateReconstruction[((rowOffset - 1) * BlockWidth) + columnOffset - 1]
: reconstructionPlane.DangerousGetRowSpan(blockOrigin.Y + rowOffset - 1)[
blockOrigin.X + columnOffset - 1]
: hasAbove
? above[0]
: hasLeft
? left[0]
: TOperator.CreateSample(midpoint);
aboveStorage[0] = corner;
leftStorage[0] = corner;
}
private long GetLumaCandidateCost(
Av1SymbolEncoder writer,
Av1MacroBlockD macroBlock,
@ -765,6 +1233,7 @@ internal static partial class Av1IntraSuperblockEncoder
Av1TransformType transformType,
Av1TransformBlockContext blockContext,
int paletteDisabledCost,
int transformSizeRate,
Span<TSample> candidateReconstruction,
Span<int> candidateCoefficients,
ref Av1EncoderTransformBlockState candidateState)
@ -793,6 +1262,7 @@ internal static partial class Av1IntraSuperblockEncoder
ref candidateState);
int rate = Av1TileWriter.GetLumaModeCost(writer, macroBlock, BlockSize, mode, angleDelta);
rate += transformSizeRate;
if (mode == Av1PredictionMode.DC)
{
rate += paletteDisabledCost;
@ -828,6 +1298,7 @@ internal static partial class Av1IntraSuperblockEncoder
Av1TransformType transformType,
Av1TransformBlockContext blockContext,
int paletteDisabledCost,
int transformSizeRate,
Span<TSample> candidateReconstruction,
Span<int> candidateCoefficients,
ref Av1EncoderTransformBlockState candidateState)
@ -841,6 +1312,7 @@ internal static partial class Av1IntraSuperblockEncoder
prediction,
residual,
candidateReconstruction,
TransformSize.GetWidth(),
candidateCoefficients,
TransformSize,
transformType,
@ -858,6 +1330,7 @@ internal static partial class Av1IntraSuperblockEncoder
Av1PredictionMode.DC,
0);
rate += transformSizeRate;
rate += paletteDisabledCost;
rate += writer.GetFilterIntraModeCost(filterIntraMode, BlockSize);
rate += writer.GetCoefficientCost(
@ -895,5 +1368,30 @@ internal static partial class Av1IntraSuperblockEncoder
retainedState = candidateState;
}
private static void CopySplitCandidate(
ReadOnlySpan<TSample> candidateReconstruction,
ReadOnlySpan<int> candidateCoefficients,
ReadOnlySpan<Av1EncoderTransformBlockState> candidateTransformBlocks,
Buffer2DRegion<TSample> reconstructionPlane,
Point blockOrigin,
Span<int> retainedCoefficients,
Span<Av1EncoderTransformBlockState> retainedTransformBlocks)
{
const int BlockWidth = 8;
const int SampleCount = BlockWidth * BlockWidth;
candidateCoefficients[..SampleCount].CopyTo(retainedCoefficients);
candidateTransformBlocks[..Av1EncoderModeDecisionWorkspace<TSample>.CandidateTransformBlockCount]
.CopyTo(retainedTransformBlocks);
for (int row = 0; row < BlockWidth; row++)
{
candidateReconstruction.Slice(row * BlockWidth, BlockWidth)
.CopyTo(
reconstructionPlane
.DangerousGetRowSpan(blockOrigin.Y + row)
.Slice(blockOrigin.X, BlockWidth));
}
}
}
}

100
src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.Operator.cs

@ -191,6 +191,37 @@ internal static partial class Av1IntraSuperblockEncoder
Av1BitDepth bitDepth,
ref Av1EncoderTransformBlockState state);
/// <summary>
/// Builds one spatial intra prediction and its source residual for reuse across transform candidates.
/// </summary>
/// <param name="workspace">The reusable block workspace.</param>
/// <param name="source">The coded source plane.</param>
/// <param name="blockOrigin">The transform-block origin in plane samples.</param>
/// <param name="prediction">The contiguous prediction destination.</param>
/// <param name="above">The top reference samples, with prefix storage for the shared corner.</param>
/// <param name="left">The left reference samples.</param>
/// <param name="hasLeft">Whether the left reference is available.</param>
/// <param name="hasAbove">Whether the top reference is available.</param>
/// <param name="mode">The intra prediction mode.</param>
/// <param name="angleDelta">The signed directional-angle adjustment.</param>
/// <param name="residual">The contiguous source-minus-prediction destination.</param>
/// <param name="transformSize">The prediction dimensions.</param>
/// <param name="bitDepth">The coded sample bit depth.</param>
public static abstract void PrepareIntra(
Av1EncoderBlockWorkspace workspace,
Buffer2DRegion<TSample> source,
Point blockOrigin,
Span<TSample> prediction,
ReadOnlySpan<TSample> above,
ReadOnlySpan<TSample> left,
bool hasLeft,
bool hasAbove,
Av1PredictionMode mode,
int angleDelta,
Span<short> residual,
Av1TransformSize transformSize,
Av1BitDepth bitDepth);
/// <summary>
/// Builds one filter-intra prediction for reuse across transform candidates.
/// </summary>
@ -247,7 +278,8 @@ internal static partial class Av1IntraSuperblockEncoder
/// <param name="blockOrigin">The transform-block origin in plane samples.</param>
/// <param name="prediction">The contiguous prediction samples.</param>
/// <param name="residual">The contiguous source-minus-prediction samples.</param>
/// <param name="reconstruction">The contiguous candidate reconstruction.</param>
/// <param name="reconstruction">The candidate reconstruction.</param>
/// <param name="reconstructionStride">The number of reconstruction samples between rows.</param>
/// <param name="quantizedCoefficients">The candidate entropy-coding coefficients.</param>
/// <param name="transformSize">The transform dimensions.</param>
/// <param name="transformType">The compound transform applied to the residual.</param>
@ -265,6 +297,7 @@ internal static partial class Av1IntraSuperblockEncoder
ReadOnlySpan<TSample> prediction,
ReadOnlySpan<short> residual,
Span<TSample> reconstruction,
int reconstructionStride,
Span<int> quantizedCoefficients,
Av1TransformSize transformSize,
Av1TransformType transformType,
@ -704,6 +737,36 @@ internal static partial class Av1IntraSuperblockEncoder
plane,
ref state);
/// <inheritdoc/>
public static void PrepareIntra(
Av1EncoderBlockWorkspace workspace,
Buffer2DRegion<byte> source,
Point blockOrigin,
Span<byte> prediction,
ReadOnlySpan<byte> above,
ReadOnlySpan<byte> left,
bool hasLeft,
bool hasAbove,
Av1PredictionMode mode,
int angleDelta,
Span<short> residual,
Av1TransformSize transformSize,
Av1BitDepth bitDepth)
=> Av1TransformBlockEncoder.PrepareIntraPrediction(
workspace,
Av1TransformBlockEncoder.GetPlaneSpan(source, blockOrigin),
source.Stride,
prediction,
transformSize.GetWidth(),
above,
left,
hasLeft,
hasAbove,
mode,
angleDelta,
residual,
transformSize);
/// <inheritdoc/>
public static void PrepareFilterIntra(
Av1EncoderBlockWorkspace workspace,
@ -782,6 +845,7 @@ internal static partial class Av1IntraSuperblockEncoder
ReadOnlySpan<byte> prediction,
ReadOnlySpan<short> residual,
Span<byte> reconstruction,
int reconstructionStride,
Span<int> quantizedCoefficients,
Av1TransformSize transformSize,
Av1TransformType transformType,
@ -798,6 +862,7 @@ internal static partial class Av1IntraSuperblockEncoder
prediction,
residual,
reconstruction,
reconstructionStride,
quantizedCoefficients,
transformSize,
transformType,
@ -1213,6 +1278,37 @@ internal static partial class Av1IntraSuperblockEncoder
bitDepth,
ref state);
/// <inheritdoc/>
public static void PrepareIntra(
Av1EncoderBlockWorkspace workspace,
Buffer2DRegion<ushort> source,
Point blockOrigin,
Span<ushort> prediction,
ReadOnlySpan<ushort> above,
ReadOnlySpan<ushort> left,
bool hasLeft,
bool hasAbove,
Av1PredictionMode mode,
int angleDelta,
Span<short> residual,
Av1TransformSize transformSize,
Av1BitDepth bitDepth)
=> Av1TransformBlockEncoder.PrepareIntraPrediction(
workspace,
Av1TransformBlockEncoder.GetPlaneSpan(source, blockOrigin),
source.Stride,
prediction,
transformSize.GetWidth(),
above,
left,
hasLeft,
hasAbove,
mode,
angleDelta,
residual,
transformSize,
bitDepth);
/// <inheritdoc/>
public static void PrepareFilterIntra(
Av1EncoderBlockWorkspace workspace,
@ -1299,6 +1395,7 @@ internal static partial class Av1IntraSuperblockEncoder
ReadOnlySpan<ushort> prediction,
ReadOnlySpan<short> residual,
Span<ushort> reconstruction,
int reconstructionStride,
Span<int> quantizedCoefficients,
Av1TransformSize transformSize,
Av1TransformType transformType,
@ -1315,6 +1412,7 @@ internal static partial class Av1IntraSuperblockEncoder
prediction,
residual,
reconstruction,
reconstructionStride,
quantizedCoefficients,
transformSize,
transformType,

7
src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.PaletteModeDecision.cs

@ -28,6 +28,7 @@ internal static partial class Av1IntraSuperblockEncoder
ushort tileIndex,
Av1TransformSetType transformSetType,
Av1TransformBlockContext blockContext,
int transformSizeRate,
Span<TSample> candidateReconstruction,
Span<int> candidateCoefficients,
Span<int> retainedCoefficients,
@ -138,6 +139,7 @@ internal static partial class Av1IntraSuperblockEncoder
blockOrigin,
transformSetType,
blockContext,
transformSizeRate,
samples,
rows,
columns,
@ -167,6 +169,7 @@ internal static partial class Av1IntraSuperblockEncoder
blockOrigin,
transformSetType,
blockContext,
transformSizeRate,
samples,
rows,
columns,
@ -205,6 +208,7 @@ internal static partial class Av1IntraSuperblockEncoder
blockOrigin,
transformSetType,
blockContext,
transformSizeRate,
samples,
rows,
columns,
@ -243,6 +247,7 @@ internal static partial class Av1IntraSuperblockEncoder
Point blockOrigin,
Av1TransformSetType transformSetType,
Av1TransformBlockContext blockContext,
int transformSizeRate,
ReadOnlySpan<short> samples,
int rows,
int columns,
@ -345,6 +350,7 @@ internal static partial class Av1IntraSuperblockEncoder
Av1PredictionMode.DC,
0);
rate += transformSizeRate;
rate += writer.GetPaletteYModeCost(true, blockSizeContext, neighborContext);
rate += writer.GetPaletteSizeCost(paletteSize, blockSizeContext, Av1PlaneType.Y);
rate += Av1SymbolEncoder.GetPaletteYColorCost(colorCache, paletteColors, bitDepth);
@ -372,6 +378,7 @@ internal static partial class Av1IntraSuperblockEncoder
prediction,
residual,
candidateReconstruction,
TransformSize.GetWidth(),
candidateCoefficients,
TransformSize,
transformType,

291
src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1TransformBlockEncoder.cs

@ -166,7 +166,8 @@ internal static class Av1TransformBlockEncoder
/// <param name="blockOrigin">The block origin in plane samples.</param>
/// <param name="prediction">The contiguous prediction samples.</param>
/// <param name="residual">The contiguous source-minus-prediction samples.</param>
/// <param name="reconstruction">The contiguous candidate reconstruction.</param>
/// <param name="reconstruction">The candidate reconstruction.</param>
/// <param name="reconstructionStride">The number of reconstruction samples between rows.</param>
/// <param name="quantizedCoefficients">The candidate entropy-coding coefficients.</param>
/// <param name="transformSize">The selected transform dimensions.</param>
/// <param name="transformType">The selected compound transform type.</param>
@ -183,6 +184,7 @@ internal static class Av1TransformBlockEncoder
ReadOnlySpan<byte> prediction,
ReadOnlySpan<short> residual,
Span<byte> reconstruction,
int reconstructionStride,
Span<int> quantizedCoefficients,
Av1TransformSize transformSize,
Av1TransformType transformType,
@ -198,7 +200,12 @@ internal static class Av1TransformBlockEncoder
ReadOnlySpan<byte> sourceSamples = GetPlaneSpan(source, blockOrigin);
// Each transform trial mutates reconstruction and residual scratch, so restore both prepared inputs.
prediction[..sampleCount].CopyTo(reconstruction);
// Row copies preserve a larger candidate surface without materializing a second compact block.
for (int row = 0; row < height; row++)
{
prediction.Slice(row * width, width).CopyTo(reconstruction.Slice(row * reconstructionStride, width));
}
residual[..sampleCount].CopyTo(workspace.Residual);
EncodeLossy(
workspace,
@ -216,7 +223,7 @@ internal static class Av1TransformBlockEncoder
Av1InverseTransformer.Reconstruct8Bit(
workspace.DequantizedCoefficients,
reconstruction,
width,
reconstructionStride,
transformSize,
transformType,
(int)plane,
@ -229,7 +236,7 @@ internal static class Av1TransformBlockEncoder
sourceSamples,
source.Stride,
reconstruction,
width,
reconstructionStride,
workspace.Residual,
width,
width,
@ -490,7 +497,8 @@ internal static class Av1TransformBlockEncoder
/// <param name="blockOrigin">The block origin in plane samples.</param>
/// <param name="prediction">The contiguous prediction samples.</param>
/// <param name="residual">The contiguous source-minus-prediction samples.</param>
/// <param name="reconstruction">The contiguous candidate reconstruction.</param>
/// <param name="reconstruction">The candidate reconstruction.</param>
/// <param name="reconstructionStride">The number of reconstruction samples between rows.</param>
/// <param name="quantizedCoefficients">The candidate entropy-coding coefficients.</param>
/// <param name="transformSize">The selected transform dimensions.</param>
/// <param name="transformType">The selected compound transform type.</param>
@ -508,6 +516,7 @@ internal static class Av1TransformBlockEncoder
ReadOnlySpan<ushort> prediction,
ReadOnlySpan<short> residual,
Span<ushort> reconstruction,
int reconstructionStride,
Span<int> quantizedCoefficients,
Av1TransformSize transformSize,
Av1TransformType transformType,
@ -524,7 +533,12 @@ internal static class Av1TransformBlockEncoder
ReadOnlySpan<ushort> sourceSamples = GetPlaneSpan(source, blockOrigin);
// Each transform trial mutates reconstruction and residual scratch, so restore both prepared inputs.
prediction[..sampleCount].CopyTo(reconstruction);
// Row copies preserve a larger candidate surface without materializing a second compact block.
for (int row = 0; row < height; row++)
{
prediction.Slice(row * width, width).CopyTo(reconstruction.Slice(row * reconstructionStride, width));
}
residual[..sampleCount].CopyTo(workspace.Residual);
EncodeLossy(
workspace,
@ -542,7 +556,7 @@ internal static class Av1TransformBlockEncoder
Av1InverseTransformer.ReconstructHighBitDepth(
workspace.DequantizedCoefficients,
MemoryMarshal.Cast<ushort, short>(reconstruction),
width,
reconstructionStride,
transformSize,
transformType,
(int)plane,
@ -556,7 +570,7 @@ internal static class Av1TransformBlockEncoder
sourceSamples,
source.Stride,
reconstruction,
width,
reconstructionStride,
workspace.Residual,
width,
width,
@ -678,65 +692,54 @@ internal static class Av1TransformBlockEncoder
}
/// <summary>
/// Encodes and reconstructs one eight-bit lossy intra block.
/// Builds an eight-bit intra prediction and its compact source residual.
/// </summary>
/// <param name="workspace">The reusable residual, coefficient, and transform storage.</param>
/// <param name="workspace">The reusable prediction scratch.</param>
/// <param name="source">The source samples.</param>
/// <param name="sourceStride">The number of source samples between rows.</param>
/// <param name="reconstruction">The reconstructed frame samples and prediction destination.</param>
/// <param name="reconstructionStride">The number of reconstruction samples between rows.</param>
/// <param name="prediction">The prediction destination.</param>
/// <param name="predictionStride">The number of prediction samples between rows.</param>
/// <param name="above">The contiguous top reference samples, with prefix storage for the shared corner.</param>
/// <param name="left">The contiguous left reference samples.</param>
/// <param name="hasLeft">Whether the left reference is available.</param>
/// <param name="hasAbove">Whether the top reference is available.</param>
/// <param name="mode">The intra prediction mode.</param>
/// <param name="angleDelta">The signed directional-angle adjustment.</param>
/// <param name="quantizedCoefficients">The retained entropy-coding coefficients.</param>
/// <param name="transformSize">The selected transform dimensions.</param>
/// <param name="transformType">The selected compound transform type.</param>
/// <param name="qIndex">The segment quantizer index.</param>
/// <param name="dcDeltaQ">The plane DC quantizer adjustment.</param>
/// <param name="acDeltaQ">The plane AC quantizer adjustment.</param>
/// <param name="plane">The component plane containing the block.</param>
/// <param name="state">The retained transform type and end-of-block syntax.</param>
private static void EncodeIntraLossyContiguous(
/// <param name="residual">The compact source-minus-prediction destination.</param>
/// <param name="transformSize">The prediction dimensions.</param>
public static void PrepareIntraPrediction(
Av1EncoderBlockWorkspace workspace,
ReadOnlySpan<byte> source,
int sourceStride,
Span<byte> reconstruction,
int reconstructionStride,
Span<byte> prediction,
int predictionStride,
ReadOnlySpan<byte> above,
ReadOnlySpan<byte> left,
bool hasLeft,
bool hasAbove,
Av1PredictionMode mode,
int angleDelta,
Span<int> quantizedCoefficients,
Av1TransformSize transformSize,
Av1TransformType transformType,
int qIndex,
int dcDeltaQ,
int acDeltaQ,
Av1Plane plane,
ref Av1EncoderTransformBlockState state)
Span<short> residual,
Av1TransformSize transformSize)
{
int width = transformSize.GetWidth();
int height = transformSize.GetHeight();
// Prediction and subtraction stay in their SIMD-first operators while this method owns the required block-stage ordering.
// Prediction remains in the specialized SIMD-first kernels. This boundary only shares the prepared
// samples and residual across transform trials that differ in transform size or type.
if (mode == Av1PredictionMode.DC)
{
Av1DcIntraPredictor.Predict(hasLeft, hasAbove, reconstruction, reconstructionStride, above, left, width, height);
Av1DcIntraPredictor.Predict(hasLeft, hasAbove, prediction, predictionStride, above, left, width, height);
}
else if (mode.IsDirectional())
{
// The current encoder disables intra-edge filtering in sequence syntax. Reusing transform workspace for
// zone-three transposition keeps directional prediction allocation-free before the transform overwrites it.
// The current encoder disables intra-edge filtering in sequence syntax. Zone-three transposition
// borrows transform scratch because prediction completes before forward transformation starts.
Span<byte> directionalScratch = MemoryMarshal.AsBytes(workspace.TransformWorkspace)[..(width * height)];
Av1DirectionalIntraPredictor.Predict(
reconstruction,
reconstructionStride,
prediction,
predictionStride,
transformSize,
above,
left,
@ -748,10 +751,163 @@ internal static class Av1TransformBlockEncoder
else
{
Av1NonDirectionalIntraPredictorBase.GetPredictor(mode)
.Predict(reconstruction, reconstructionStride, above, left, width, height);
.Predict(prediction, predictionStride, above, left, width, height);
}
Av1ResidualBuilder.Subtract(source, sourceStride, reconstruction, reconstructionStride, workspace.Residual, width, width, height);
Av1ResidualBuilder.Subtract(
source,
sourceStride,
prediction,
predictionStride,
residual,
width,
width,
height);
}
/// <summary>
/// Builds a high-bit-depth intra prediction and its compact source residual.
/// </summary>
/// <param name="workspace">The reusable prediction scratch.</param>
/// <param name="source">The source samples.</param>
/// <param name="sourceStride">The number of source samples between rows.</param>
/// <param name="prediction">The prediction destination.</param>
/// <param name="predictionStride">The number of prediction samples between rows.</param>
/// <param name="above">The contiguous top reference samples, with prefix storage for the shared corner.</param>
/// <param name="left">The contiguous left reference samples.</param>
/// <param name="hasLeft">Whether the left reference is available.</param>
/// <param name="hasAbove">Whether the top reference is available.</param>
/// <param name="mode">The intra prediction mode.</param>
/// <param name="angleDelta">The signed directional-angle adjustment.</param>
/// <param name="residual">The compact source-minus-prediction destination.</param>
/// <param name="transformSize">The prediction dimensions.</param>
/// <param name="bitDepth">The coded sample bit depth.</param>
public static void PrepareIntraPrediction(
Av1EncoderBlockWorkspace workspace,
ReadOnlySpan<ushort> source,
int sourceStride,
Span<ushort> prediction,
int predictionStride,
ReadOnlySpan<ushort> above,
ReadOnlySpan<ushort> left,
bool hasLeft,
bool hasAbove,
Av1PredictionMode mode,
int angleDelta,
Span<short> residual,
Av1TransformSize transformSize,
Av1BitDepth bitDepth)
{
int width = transformSize.GetWidth();
int height = transformSize.GetHeight();
// Valid high-bit-depth samples remain below the sign bit, so the predictor kernels can share
// the unsigned frame storage with their signed transform-domain implementation.
Span<short> signedPrediction = MemoryMarshal.Cast<ushort, short>(prediction);
ReadOnlySpan<short> signedAbove = MemoryMarshal.Cast<ushort, short>(above);
ReadOnlySpan<short> signedLeft = MemoryMarshal.Cast<ushort, short>(left);
if (mode == Av1PredictionMode.DC)
{
Av1DcIntraPredictor.Predict(
hasLeft,
hasAbove,
signedPrediction,
predictionStride,
signedAbove,
signedLeft,
width,
height,
bitDepth.GetBitCount());
}
else if (mode.IsDirectional())
{
Span<short> directionalScratch = MemoryMarshal.Cast<int, short>(workspace.TransformWorkspace)[..(width * height)];
Av1DirectionalIntraPredictor.Predict(
signedPrediction,
predictionStride,
transformSize,
signedAbove,
signedLeft,
false,
false,
mode.ToAngle() + (angleDelta * Av1Constants.AngleStep),
directionalScratch);
}
else
{
Av1NonDirectionalIntraPredictorBase.GetPredictor(mode)
.Predict(signedPrediction, predictionStride, signedAbove, signedLeft, width, height);
}
Av1ResidualBuilder.Subtract(
source,
sourceStride,
prediction,
predictionStride,
residual,
width,
width,
height);
}
/// <summary>
/// Encodes and reconstructs one eight-bit lossy intra block.
/// </summary>
/// <param name="workspace">The reusable residual, coefficient, and transform storage.</param>
/// <param name="source">The source samples.</param>
/// <param name="sourceStride">The number of source samples between rows.</param>
/// <param name="reconstruction">The reconstructed frame samples and prediction destination.</param>
/// <param name="reconstructionStride">The number of reconstruction samples between rows.</param>
/// <param name="above">The contiguous top reference samples, with prefix storage for the shared corner.</param>
/// <param name="left">The contiguous left reference samples.</param>
/// <param name="hasLeft">Whether the left reference is available.</param>
/// <param name="hasAbove">Whether the top reference is available.</param>
/// <param name="mode">The intra prediction mode.</param>
/// <param name="angleDelta">The signed directional-angle adjustment.</param>
/// <param name="quantizedCoefficients">The retained entropy-coding coefficients.</param>
/// <param name="transformSize">The selected transform dimensions.</param>
/// <param name="transformType">The selected compound transform type.</param>
/// <param name="qIndex">The segment quantizer index.</param>
/// <param name="dcDeltaQ">The plane DC quantizer adjustment.</param>
/// <param name="acDeltaQ">The plane AC quantizer adjustment.</param>
/// <param name="plane">The component plane containing the block.</param>
/// <param name="state">The retained transform type and end-of-block syntax.</param>
private static void EncodeIntraLossyContiguous(
Av1EncoderBlockWorkspace workspace,
ReadOnlySpan<byte> source,
int sourceStride,
Span<byte> reconstruction,
int reconstructionStride,
ReadOnlySpan<byte> above,
ReadOnlySpan<byte> left,
bool hasLeft,
bool hasAbove,
Av1PredictionMode mode,
int angleDelta,
Span<int> quantizedCoefficients,
Av1TransformSize transformSize,
Av1TransformType transformType,
int qIndex,
int dcDeltaQ,
int acDeltaQ,
Av1Plane plane,
ref Av1EncoderTransformBlockState state)
{
PrepareIntraPrediction(
workspace,
source,
sourceStride,
reconstruction,
reconstructionStride,
above,
left,
hasLeft,
hasAbove,
mode,
angleDelta,
workspace.Residual,
transformSize);
EncodeLossy(
workspace,
@ -825,56 +981,21 @@ internal static class Av1TransformBlockEncoder
Av1BitDepth bitDepth,
ref Av1EncoderTransformBlockState state)
{
int width = transformSize.GetWidth();
int height = transformSize.GetHeight();
// Valid high-bit-depth samples remain below the sign bit, so signed transform lanes can share the unsigned frame storage.
Span<short> signedReconstruction = MemoryMarshal.Cast<ushort, short>(reconstruction);
ReadOnlySpan<short> signedAbove = MemoryMarshal.Cast<ushort, short>(above);
ReadOnlySpan<short> signedLeft = MemoryMarshal.Cast<ushort, short>(left);
if (mode == Av1PredictionMode.DC)
{
Av1DcIntraPredictor.Predict(
hasLeft,
hasAbove,
signedReconstruction,
reconstructionStride,
signedAbove,
signedLeft,
width,
height,
bitDepth.GetBitCount());
}
else if (mode.IsDirectional())
{
Span<short> directionalScratch = MemoryMarshal.Cast<int, short>(workspace.TransformWorkspace)[..(width * height)];
Av1DirectionalIntraPredictor.Predict(
signedReconstruction,
reconstructionStride,
transformSize,
signedAbove,
signedLeft,
false,
false,
mode.ToAngle() + (angleDelta * Av1Constants.AngleStep),
directionalScratch);
}
else
{
Av1NonDirectionalIntraPredictorBase.GetPredictor(mode)
.Predict(signedReconstruction, reconstructionStride, signedAbove, signedLeft, width, height);
}
Av1ResidualBuilder.Subtract(
PrepareIntraPrediction(
workspace,
source,
sourceStride,
reconstruction,
reconstructionStride,
above,
left,
hasLeft,
hasAbove,
mode,
angleDelta,
workspace.Residual,
width,
width,
height);
transformSize,
bitDepth);
EncodeLossy(
workspace,
@ -892,7 +1013,7 @@ internal static class Av1TransformBlockEncoder
// Reconstructing the quantized result makes later predictions use exactly the samples a decoder will reproduce.
Av1InverseTransformer.ReconstructHighBitDepth(
workspace.DequantizedCoefficients,
signedReconstruction,
MemoryMarshal.Cast<ushort, short>(reconstruction),
reconstructionStride,
transformSize,
transformType,

98
src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileWriter.cs

@ -910,6 +910,50 @@ internal partial class Av1TileWriter
UpdateNeighbors(pcs, entropyCodingContext, blockOrigin, ref blk_ptr, tile_idx, blockSize);
}
/// <summary>
/// Derives the uniform intra transform-size context from the current above and left edges.
/// </summary>
/// <param name="transformContexts">The retained transform widths and heights.</param>
/// <param name="macroBlock">The reusable macroblock edge and neighbor state.</param>
/// <param name="blockOrigin">The block origin in samples.</param>
/// <param name="blockSize">The block size defining the maximum transform.</param>
/// <returns>The uniform transform-size context.</returns>
public static int GetTransformSizeContext(
Av1NeighborArrayUnit<byte> transformContexts,
Av1MacroBlockD macroBlock,
Point blockOrigin,
Av1BlockSize blockSize)
{
Av1TransformSize maximumTransformSize = blockSize.GetMaximumTransformSize();
int above = transformContexts.Top[transformContexts.GetTopIndex(blockOrigin)] >= maximumTransformSize.GetWidth() ? 1 : 0;
int left = transformContexts.Left[transformContexts.GetLeftIndex(blockOrigin)] >= maximumTransformSize.GetHeight() ? 1 : 0;
// Inter neighbors contribute their coding-block extent, not their residual-transform extent.
if (macroBlock.IsUpAvailable)
{
ref Av1MacroBlockModeInfo aboveModeInfo =
ref macroBlock.GetRelativeModeInfo(-macroBlock.ModeInfoStride);
if (aboveModeInfo.Block.UseIntraBlockCopy)
{
above = aboveModeInfo.Block.BlockSize.GetWidth() >= maximumTransformSize.GetWidth() ? 1 : 0;
}
}
if (macroBlock.IsLeftAvailable)
{
ref Av1MacroBlockModeInfo leftModeInfo = ref macroBlock.GetRelativeModeInfo(-1);
if (leftModeInfo.Block.UseIntraBlockCopy)
{
left = leftModeInfo.Block.BlockSize.GetHeight() >= maximumTransformSize.GetHeight() ? 1 : 0;
}
}
return macroBlock.IsUpAvailable
? macroBlock.IsLeftAvailable ? above + left : above
: macroBlock.IsLeftAvailable ? left : 0;
}
/// <summary>
/// Writes or derives the block transform size and publishes its edge contexts.
/// </summary>
@ -931,28 +975,42 @@ internal partial class Av1TileWriter
{
ObuFrameHeader frameHeader = pcs.Parent.FrameHeader;
bool isLossless = frameHeader.LosslessArray[macroBlockModeInfo.Block.SegmentId];
bool writesTransformSize = !isLossless &&
bool isInter = macroBlockModeInfo.Block.UseIntraBlockCopy;
bool writesUniformTransformSize = !isLossless &&
frameHeader.TransformMode == Av1TransformMode.Select &&
!isInter &&
blockSize > Av1BlockSize.Block4x4;
bool writesVariableTransformSize = !isLossless &&
frameHeader.TransformMode == Av1TransformMode.Select &&
isInter &&
!macroBlockModeInfo.Block.Skip;
Av1TransformSize transformSize = isLossless
? Av1TransformSize.Size4x4
: writesTransformSize
: writesUniformTransformSize || writesVariableTransformSize
? macroBlockModeInfo.Block.TransformSize
: blockSize.GetMaximumTransformSize();
macroBlockModeInfo.Block.TransformSize = transformSize;
Av1NeighborArrayUnit<byte> transformContexts = pcs.TransformFunctionContexts[tileIndex];
if (writesTransformSize)
if (writesUniformTransformSize)
{
Av1TransformSize maximumTransformSize = blockSize.GetMaximumTransformSize();
int above = transformContexts.Top[transformContexts.GetTopIndex(blockOrigin)] >= maximumTransformSize.GetWidth() ? 1 : 0;
int left = transformContexts.Left[transformContexts.GetLeftIndex(blockOrigin)] >= maximumTransformSize.GetHeight() ? 1 : 0;
int context = macroBlock.IsUpAvailable
? macroBlock.IsLeftAvailable ? above + left : above
: macroBlock.IsLeftAvailable ? left : 0;
int context = GetTransformSizeContext(transformContexts, macroBlock, blockOrigin, blockSize);
writer.WriteTransformSize(blockSize, transformSize, context);
}
else if (writesVariableTransformSize)
{
int topIndex = transformContexts.GetTopIndex(blockOrigin);
int leftIndex = transformContexts.GetLeftIndex(blockOrigin);
int context = Av1SymbolContextHelper.GetTransformPartitionContext(
transformContexts.Top[topIndex],
transformContexts.Left[leftIndex],
blockSize,
blockSize.GetMaximumTransformSize());
// Intra-block copy currently retains the maximum transform, so its variable-transform tree has one unsplit root.
writer.WriteTransformPartition(false, context);
}
Size blockDimensions = new(blockSize.GetWidth(), blockSize.GetHeight());
@ -1863,6 +1921,26 @@ internal partial class Av1TileWriter
int transformBlockHeight = transformSize.Get4x4HighCount();
ReadOnlySpan<byte> topContexts = dcSignLevelCoefficientNeighborArray.Top.Slice(topIndex, transformBlockWidth);
ReadOnlySpan<byte> leftContexts = dcSignLevelCoefficientNeighborArray.Left.Slice(leftIndex, transformBlockHeight);
return GetTransformBlockContexts(plane, topContexts, leftContexts, planeBlockSize, transformSize);
}
/// <summary>
/// Derives coefficient skip and DC-sign contexts from explicit transform-edge contexts.
/// </summary>
/// <param name="plane">The luma or chroma component class.</param>
/// <param name="topContexts">The packed contexts immediately above the transform block.</param>
/// <param name="leftContexts">The packed contexts immediately left of the transform block.</param>
/// <param name="planeBlockSize">The containing block size on the target plane.</param>
/// <param name="transformSize">The transform size.</param>
/// <returns>The coefficient skip and DC-sign contexts selected by both transform edges.</returns>
public static Av1TransformBlockContext GetTransformBlockContexts(
Av1ComponentType plane,
ReadOnlySpan<byte> topContexts,
ReadOnlySpan<byte> leftContexts,
Av1BlockSize planeBlockSize,
Av1TransformSize transformSize)
{
int dcSign = 0;
int top = 0;
int left = 0;

55
tests/ImageSharp.Tests/Formats/Heif/Av1/Av1CoefficientsEntropyTests.cs

@ -594,6 +594,53 @@ public class Av1CoefficientsEntropyTests
}
}
[Fact]
public void TransformSizeContextUsesIntraBlockCopyNeighborExtents()
{
Av1PictureControlSet picture = CreateEncoderPicture(16, 16);
Point blockOrigin = new(16, 16);
Av1MacroBlockD macroBlock = new()
{
Tile = new Av1TileInfo(0, 0, picture.Parent.FrameHeader),
IsUpAvailable = true,
IsLeftAvailable = true
};
int modeInfoIndex =
((blockOrigin.Y >> Av1Constants.ModeInfoSizeLog2) * picture.ModeInfoStride) +
(blockOrigin.X >> Av1Constants.ModeInfoSizeLog2);
macroBlock.ModeInfoStride = picture.ModeInfoStride;
macroBlock.SetModeInfoGrid(picture.ModeInfoGrid, picture.ModeInfoAllocation, modeInfoIndex);
ref Av1MacroBlockModeInfo aboveModeInfo = ref macroBlock.GetRelativeModeInfo(-macroBlock.ModeInfoStride);
aboveModeInfo.Block.BlockSize = Av1BlockSize.Block16x8;
aboveModeInfo.Block.UseIntraBlockCopy = true;
ref Av1MacroBlockModeInfo leftModeInfo = ref macroBlock.GetRelativeModeInfo(-1);
leftModeInfo.Block.BlockSize = Av1BlockSize.Block8x16;
leftModeInfo.Block.UseIntraBlockCopy = true;
using Av1NeighborArrayUnit<byte> transforms = new(
Configuration.Default,
leftSize: 64,
topSize: 64)
{
GranularityNormalLog2 = Av1Constants.ModeInfoSizeLog2
};
// Residual contexts report 8x8, but libaom derives 16x16 availability from the IBC coding blocks.
transforms.Top[transforms.GetTopIndex(blockOrigin)] = 8;
transforms.Left[transforms.GetLeftIndex(blockOrigin)] = 8;
Assert.Equal(
2,
Av1TileWriter.GetTransformSizeContext(
transforms,
macroBlock,
blockOrigin,
Av1BlockSize.Block16x16));
}
[Fact]
public void SelectedTransformSizeRoundTripsAndPublishesRectangularEdgeContexts()
{
@ -611,6 +658,14 @@ public class Av1CoefficientsEntropyTests
IsLeftAvailable = true
};
int modeInfoIndex =
((blockOrigin.Y >> Av1Constants.ModeInfoSizeLog2) * picture.ModeInfoStride) +
(blockOrigin.X >> Av1Constants.ModeInfoSizeLog2);
// Uniform-size context substitutes coding-block extents for inter neighbors, so mirror production mode-info setup.
macroBlock.ModeInfoStride = picture.ModeInfoStride;
macroBlock.SetModeInfoGrid(picture.ModeInfoGrid, picture.ModeInfoAllocation, modeInfoIndex);
using Av1NeighborArrayUnit<byte> transforms = new(
Configuration.Default,
leftSize: 64,

93
tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EncoderFrameTests.cs

@ -7,6 +7,7 @@ using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline;
using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction;
using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
using SixLabors.ImageSharp.Formats.Heif.Av1.Transform;
using SixLabors.ImageSharp.Formats.Heif.Components;
using SixLabors.ImageSharp.Formats.Heif.Components.Alpha;
using SixLabors.ImageSharp.Memory;
@ -488,13 +489,18 @@ public class Av1EncoderFrameTests
}
[Theory]
[InlineData(0, false, false)]
[InlineData(1, false, false)]
[InlineData(2, false, false)]
[InlineData(3, false, false)]
[InlineData(4, true, false)]
[InlineData(5, true, true)]
public void EncodeEffortControlsSearchFeatures(int effort, bool enableFilterIntra, bool enableScreenContentTools)
[InlineData(0, false, false, false)]
[InlineData(1, false, false, false)]
[InlineData(2, false, false, false)]
[InlineData(3, false, false, false)]
[InlineData(4, true, false, false)]
[InlineData(5, true, true, false)]
[InlineData(6, true, true, true)]
public void EncodeEffortControlsSearchFeatures(
int effort,
bool enableFilterIntra,
bool enableScreenContentTools,
bool selectTransformSize)
{
const int width = 16;
const int height = 16;
@ -529,6 +535,9 @@ public class Av1EncoderFrameTests
Assert.Equal(enableFilterIntra, sequenceHeader.EnableFilterIntra);
Assert.Equal(enableScreenContentTools, frameHeader.AllowScreenContentTools);
Assert.Equal(enableScreenContentTools, frameHeader.AllowIntraBlockCopy);
Assert.Equal(
selectTransformSize ? Av1TransformMode.Select : Av1TransformMode.Largest,
frameHeader.TransformMode);
Assert.Equal(new Size(width, height), decoded.Size);
int modeCount = 0;
@ -573,7 +582,64 @@ public class Av1EncoderFrameTests
}
[Fact]
public void EncodeSelectsIntraBlockCopyForRepeatedScreenContent()
public void EncodeEffortSixSelectsFourByFourLumaTransforms()
{
const int Width = 16;
const int Height = 16;
using Image<Rgba32> source = new(Width, Height);
for (int row = 0; row < Height; row++)
{
Span<Rgba32> pixels = source.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(row);
for (int column = 0; column < Width; column++)
{
byte value = (byte)(16 + ((((row >> 2) * 4) + (column >> 2)) * 14));
pixels[column] = new Rgba32(value, value, value);
}
}
ObuColorConfig colorConfig = CreateColorConfig(Av1BitDepth.EightBit, Av1ColorFormat.Yuv400);
using MemoryStream stream = new();
_ = Av1FrameEncoder.Encode(
Configuration.Default,
source.Frames.RootFrame,
stream,
colorConfig,
qIndex: 37,
effort: 6);
byte[] payload = stream.ToArray();
using Av1Decoder decoder = new(Configuration.Default);
using Image<L8> decoded = decoder.Decode<L8>(payload);
Assert.NotNull(decoder.FrameHeader);
Assert.Equal(Av1TransformMode.Select, decoder.FrameHeader.TransformMode);
Assert.NotNull(decoder.FrameInfo);
bool foundSplitTransform = false;
foreach (Av1BlockModeInfo modeInfo in decoder.FrameInfo.GetSuperblock(Point.Empty).GetModeInfos())
{
foundSplitTransform |= modeInfo.GetTransformUnitCount(Av1Plane.Y) == 4;
}
Assert.True(foundSplitTransform);
Assert.Equal(new Size(Width, Height), decoded.Size);
string outputDirectory = Path.Combine(
TestEnvironment.ActualOutputDirectoryFullPath,
"Formats",
"Heif",
"Av1");
Directory.CreateDirectory(outputDirectory);
File.WriteAllBytes(
Path.Combine(outputDirectory, "encoder-frame-16x16-8b-400-transform-size-select.obu"),
payload);
}
[Theory]
[InlineData(5, false)]
[InlineData(6, true)]
public void EncodeSelectsIntraBlockCopyForRepeatedScreenContent(
int effort,
bool selectTransformSize)
{
const int Width = 328;
const int Height = 16;
@ -599,7 +665,7 @@ public class Av1EncoderFrameTests
stream,
colorConfig,
qIndex: 37,
effort: 5);
effort);
byte[] payload = stream.ToArray();
using Av1Decoder decoder = new(Configuration.Default);
@ -607,6 +673,9 @@ public class Av1EncoderFrameTests
Assert.NotNull(decoder.FrameHeader);
Assert.True(decoder.FrameHeader.AllowScreenContentTools);
Assert.True(decoder.FrameHeader.AllowIntraBlockCopy);
Assert.Equal(
selectTransformSize ? Av1TransformMode.Select : Av1TransformMode.Largest,
decoder.FrameHeader.TransformMode);
Assert.NotNull(decoder.FrameInfo);
Av1SuperblockInfo targetSuperblock = decoder.FrameInfo.GetSuperblock(new Point(5, 0));
bool usesIntraBlockCopy = false;
@ -625,7 +694,11 @@ public class Av1EncoderFrameTests
"Av1");
Directory.CreateDirectory(outputDirectory);
File.WriteAllBytes(Path.Combine(outputDirectory, "encoder-frame-328x16-8b-444-intrabc.obu"), payload);
string fileName = effort == 5
? "encoder-frame-328x16-8b-444-intrabc.obu"
: "encoder-frame-328x16-8b-444-intrabc-effort-6.obu";
File.WriteAllBytes(Path.Combine(outputDirectory, fileName), payload);
}
[Fact]

9
tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EntropyTests.cs

@ -134,6 +134,15 @@ public class Av1EntropyTests
Av1ProbabilityCost.GetSymbolCost(transformSize, 1),
encoder.GetTransformSizeCost(BlockSize, Av1TransformSize.Size4x4, SkipContext));
Av1Distribution transformPartition = Av1DefaultDistributions.TransformPartition[SkipContext];
Assert.Equal(
Av1ProbabilityCost.GetSymbolCost(transformPartition, 0),
encoder.GetTransformPartitionCost(false, SkipContext));
Assert.Equal(
Av1ProbabilityCost.GetSymbolCost(transformPartition, 1),
encoder.GetTransformPartitionCost(true, SkipContext));
Av1Distribution transformSkip = Av1DefaultDistributions
.GetTransformBlockSkip(BaseQIndex)[(int)TransformSize][SkipContext];

41
tests/ImageSharp.Tests/Formats/Heif/Av1/Av1SymbolContextTests.cs

@ -76,6 +76,47 @@ public class Av1SymbolContextTests
Assert.Equal((Av1TransformType)expectedValue, actual);
}
[Theory]
[InlineData(0, 0, 0)]
[InlineData(-1, 4, 12)]
[InlineData(1, 4, 20)]
public void CoefficientContextMatchesCurrentLibaom(int dcCoefficient, ushort endOfBlock, byte expected)
{
Span<int> coefficients = stackalloc int[16];
coefficients.Fill(1);
coefficients[0] = dcCoefficient;
byte actual = Av1SymbolContextHelper.GetCoefficientContext(
coefficients,
Av1TransformSize.Size4x4,
Av1TransformType.DctDct,
endOfBlock);
Assert.Equal(expected, actual);
}
[Theory]
[InlineData(8, 8, (int)Av1BlockSize.Block8x8, (int)Av1TransformSize.Size8x8, 18)]
[InlineData(4, 8, (int)Av1BlockSize.Block8x8, (int)Av1TransformSize.Size8x8, 19)]
[InlineData(4, 4, (int)Av1BlockSize.Block8x8, (int)Av1TransformSize.Size8x8, 20)]
[InlineData(4, 4, (int)Av1BlockSize.Block16x16, (int)Av1TransformSize.Size8x8, 17)]
[InlineData(0, 0, (int)Av1BlockSize.Block8x8, (int)Av1TransformSize.Size4x4, 0)]
public void TransformPartitionContextMatchesCurrentLibaom(
byte aboveWidth,
byte leftHeight,
int blockSizeValue,
int transformSizeValue,
int expected)
{
int actual = Av1SymbolContextHelper.GetTransformPartitionContext(
aboveWidth,
leftHeight,
(Av1BlockSize)blockSizeValue,
(Av1TransformSize)transformSizeValue);
Assert.Equal(expected, actual);
}
public static TheoryData<int, int, int> GetLowLevelContextEndOfBlockData()
{
TheoryData<int, int, int> result = [];

Loading…
Cancel
Save