Browse Source

Complete exhaustive luma transform search

pull/2633/head
James Jackson-South 4 weeks ago
parent
commit
cab9bc47cf
  1. 8
      HEIF_IMPLEMENTATION_PLAN.md
  2. 8
      src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1EncoderModeDecisionWorkspace.cs
  3. 10
      src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.ChromaModeDecision.cs
  4. 10
      src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.ChromaPaletteModeDecision.cs
  5. 46
      src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.IntraBlockCopyModeDecision.cs
  6. 311
      src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.ModeDecision.cs
  7. 14
      src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.PaletteModeDecision.cs
  8. 3
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EncoderFrameTests.cs

8
HEIF_IMPLEMENTATION_PLAN.md

File diff suppressed because one or more lines are too long

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

@ -64,15 +64,15 @@ internal readonly ref struct Av1EncoderModeDecisionWorkspace<TSample>
public Av1EncoderModeDecisionWorkspace(Span<int> storage) => this.storage = storage; public Av1EncoderModeDecisionWorkspace(Span<int> storage) => this.storage = storage;
/// <summary> /// <summary>
/// Gets the temporary prediction span used by filter-intra mode search. /// Gets the temporary prediction span shared by mutually exclusive mode searches.
/// </summary> /// </summary>
public Span<TSample> FilterPrediction public Span<TSample> Prediction
=> MemoryMarshal.Cast<int, TSample>(this.storage[TransientStorageOffset..])[..MaximumSampleCount]; => MemoryMarshal.Cast<int, TSample>(this.storage[TransientStorageOffset..])[..MaximumSampleCount];
/// <summary> /// <summary>
/// Gets the temporary residual span used by filter-intra mode search. /// Gets the temporary residual span shared by mutually exclusive mode searches.
/// </summary> /// </summary>
public Span<short> FilterResidual public Span<short> Residual
=> MemoryMarshal.Cast<int, short>( => MemoryMarshal.Cast<int, short>(
this.storage.Slice( this.storage.Slice(
TransientStorageOffset + (MaximumSampleCount * sizeof(ushort) / sizeof(int)), TransientStorageOffset + (MaximumSampleCount * sizeof(ushort) / sizeof(int)),

10
src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.ChromaModeDecision.cs

@ -200,6 +200,9 @@ internal static partial class Av1IntraSuperblockEncoder
Av1EncoderTransformBlockState candidateBlueState = default; Av1EncoderTransformBlockState candidateBlueState = default;
Av1EncoderTransformBlockState candidateRedState = default; Av1EncoderTransformBlockState candidateRedState = default;
// A chroma mode and angle are shared by U and V, so neither plane can replace the
// retained result independently. Their complete rate and distortion compete jointly.
long candidateCost = this.GetChromaCandidateCost( long candidateCost = this.GetChromaCandidateCost(
writer, writer,
modeInfo, modeInfo,
@ -395,6 +398,8 @@ internal static partial class Av1IntraSuperblockEncoder
if (chromaFromLumaSelected) if (chromaFromLumaSelected)
{ {
// The alpha tables retain only rate and distortion. Regenerate the two selected planes
// once here instead of copying reconstruction and coefficient blocks for all 66 trials.
Av1EncoderTransformBlockState candidateBlueState = default; Av1EncoderTransformBlockState candidateBlueState = default;
_ = this.GetChromaFromLumaPlaneCost( _ = this.GetChromaFromLumaPlaneCost(
writer, writer,
@ -561,6 +566,9 @@ internal static partial class Av1IntraSuperblockEncoder
{ {
const Av1BlockSize BlockSize = Av1BlockSize.Block8x8; const Av1BlockSize BlockSize = Av1BlockSize.Block8x8;
Av1PredictionMode predictionMode = chromaMode.ToLumaMode(); Av1PredictionMode predictionMode = chromaMode.ToLumaMode();
// Intra chroma derives one transform type from the shared UV prediction mode. The type is not
// signaled independently for either chroma plane, so U and V must use the same legal fallback.
Av1TransformType transformType = Av1SymbolContextHelper.GetDefaultIntraTransformType( Av1TransformType transformType = Av1SymbolContextHelper.GetDefaultIntraTransformType(
predictionMode, predictionMode,
transformSize, transformSize,
@ -608,6 +616,8 @@ internal static partial class Av1IntraSuperblockEncoder
this.bitDepth, this.bitDepth,
ref candidateRedState); ref candidateRedState);
// The mode and angle are written once for the UV pair; coefficient syntax remains independent
// because each plane has its own EOB, scan values, and neighboring coefficient context.
int rate = Av1TileWriter.GetChromaModeCost( int rate = Av1TileWriter.GetChromaModeCost(
writer, writer,
this.picture.Parent.FrameHeader, this.picture.Parent.FrameHeader,

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

@ -150,6 +150,8 @@ internal static partial class Av1IntraSuperblockEncoder
workspace.GetAlternateCentroids(1), workspace.GetAlternateCentroids(1),
workspace.AlternateIndices); workspace.AlternateIndices);
// Only U participates in the neighbor-color cache. Snap bounded U deltas before sorting,
// while preserving each U/V centroid as one paired palette entry.
for (int colorIndex = 0; colorIndex < paletteSize && !colorCache.IsEmpty; colorIndex++) for (int colorIndex = 0; colorIndex < paletteSize && !colorCache.IsEmpty; colorIndex++)
{ {
int minimumDifference = Math.Abs(candidateBlueCentroids[colorIndex] - colorCache[0]); int minimumDifference = Math.Abs(candidateBlueCentroids[colorIndex] - colorCache[0]);
@ -221,6 +223,8 @@ internal static partial class Av1IntraSuperblockEncoder
redPaletteColors[colorIndex] = (ushort)candidateRedCentroids[colorIndex]; redPaletteColors[colorIndex] = (ushort)candidateRedCentroids[colorIndex];
} }
// U and V share one color-index map but reconstruct through their own palette values and
// residuals. Both preparations remain valid until the next palette-size candidate.
TOperator.PreparePalette( TOperator.PreparePalette(
blueSource, blueSource,
chromaOrigin, chromaOrigin,
@ -239,6 +243,8 @@ internal static partial class Av1IntraSuperblockEncoder
redResidual[..sampleCount], redResidual[..sampleCount],
transformSize); transformSize);
// Intra chroma derives one transform type from the shared UV mode. Palette uses UV DC, so
// both planes use DCT while retaining independent coefficient contexts and end positions.
Av1EncoderTransformBlockState candidateBlueState = default; Av1EncoderTransformBlockState candidateBlueState = default;
long distortion = TOperator.EncodePredictionCandidate( long distortion = TOperator.EncodePredictionCandidate(
this.blockWorkspace, this.blockWorkspace,
@ -314,6 +320,8 @@ internal static partial class Av1IntraSuperblockEncoder
Av1FilterIntraMode.AllFilterIntraModes, Av1FilterIntraMode.AllFilterIntraModes,
usesInterTransformSet: false); usesInterTransformSet: false);
// Mode, palette, and color-map syntax is shared by the pair; coefficient syntax and
// distortion remain per plane before the joint chroma rate-distortion comparison.
rate += writer.GetCoefficientCost( rate += writer.GetCoefficientCost(
transformSize, transformSize,
Av1TransformType.DctDct, Av1TransformType.DctDct,
@ -329,6 +337,8 @@ internal static partial class Av1IntraSuperblockEncoder
long candidateCost = Av1RateDistortion.GetCost(this.rateMultiplier, rate, distortion); long candidateCost = Av1RateDistortion.GetCost(this.rateMultiplier, rate, distortion);
if (candidateCost < bestCost) if (candidateCost < bestCost)
{ {
// Every following palette size overwrites the shared maps and candidate spans, so a
// global improvement must retain reconstruction, coefficients, colors, and indices together.
Buffer2DRegion<TSample> blueReconstruction = this.reconstruction.GetPlane(Av1Plane.U); Buffer2DRegion<TSample> blueReconstruction = this.reconstruction.GetPlane(Av1Plane.U);
Buffer2DRegion<TSample> redReconstruction = this.reconstruction.GetPlane(Av1Plane.V); Buffer2DRegion<TSample> redReconstruction = this.reconstruction.GetPlane(Av1Plane.V);
CopyCandidate( CopyCandidate(

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

@ -102,6 +102,8 @@ internal static partial class Av1IntraSuperblockEncoder
return; return;
} }
// Mode-decision costs already contain the selected coefficient syntax but not the block's skip
// or IBC choice. When regular intra skips, replace its empty-coefficient rate with skip syntax.
int skipContext = Av1TileWriter.GetSkipContext(macroBlock); int skipContext = Av1TileWriter.GetSkipContext(macroBlock);
int regularRateAdjustment = writer.GetUseIntraBlockCopyCost(false) + int regularRateAdjustment = writer.GetUseIntraBlockCopyCost(false) +
writer.GetSkipCost(modeInfo.Block.Skip, skipContext); writer.GetSkipCost(modeInfo.Block.Skip, skipContext);
@ -128,6 +130,8 @@ internal static partial class Av1IntraSuperblockEncoder
BlockSize, BlockSize,
LumaTransformSize); LumaTransformSize);
// A coded IBC residual uses the unsplit transform root at this fixed block size. A skipped block
// omits both the transform-partition bit and coefficient syntax, so this rate is added only below.
int transformPartitionRate = 0; int transformPartitionRate = 0;
if (this.picture.Parent.FrameHeader.TransformMode == Av1TransformMode.Select) if (this.picture.Parent.FrameHeader.TransformMode == Av1TransformMode.Select)
{ {
@ -176,6 +180,8 @@ internal static partial class Av1IntraSuperblockEncoder
chromaTransformSize); chromaTransformSize);
} }
// Per-vector plane results reuse candidate scratch. Separate selected spans retain only a new
// global winner, allowing the complete search to finish before committed reconstruction changes.
for (int candidateIndex = 0; candidateIndex < uniqueCandidateCount; candidateIndex++) for (int candidateIndex = 0; candidateIndex < uniqueCandidateCount; candidateIndex++)
{ {
Av1MotionVector candidate = candidates[candidateIndex]; Av1MotionVector candidate = candidates[candidateIndex];
@ -291,6 +297,9 @@ internal static partial class Av1IntraSuperblockEncoder
long candidateDistortion = lumaDistortion + blueDistortion + redDistortion; long candidateDistortion = lumaDistortion + blueDistortion + redDistortion;
long candidateCost = Av1RateDistortion.GetCost(this.rateMultiplier, candidateRate, candidateDistortion); long candidateCost = Av1RateDistortion.GetCost(this.rateMultiplier, candidateRate, candidateDistortion);
bool candidateSkip = false; bool candidateSkip = false;
// The skip alternative is available only when every coded plane has an empty transform. Its
// distortion comes from prediction alone and its rate excludes the transform tree and coefficients.
if (hasEmptyLuma && hasEmptyBlue && hasEmptyRed) if (hasEmptyLuma && hasEmptyBlue && hasEmptyRed)
{ {
int skipRate = writer.GetUseIntraBlockCopyCost(true) + int skipRate = writer.GetUseIntraBlockCopyCost(true) +
@ -363,6 +372,8 @@ internal static partial class Av1IntraSuperblockEncoder
return; return;
} }
// Only the winning vector is now visible to later coding blocks. This single publication keeps
// rejected motion vectors from contaminating intra references or entropy contexts.
Span<int> retainedLumaCoefficients = this.coefficientBuffer.GetPlaneSpan(this.superblock.Index, Av1Plane.Y); Span<int> retainedLumaCoefficients = this.coefficientBuffer.GetPlaneSpan(this.superblock.Index, Av1Plane.Y);
Span<Av1EncoderTransformBlockState> retainedLumaTransformBlocks = Span<Av1EncoderTransformBlockState> retainedLumaTransformBlocks =
this.coefficientBuffer.GetTransformBlockSpan(this.superblock.Index, Av1Plane.Y); this.coefficientBuffer.GetTransformBlockSpan(this.superblock.Index, Av1Plane.Y);
@ -473,6 +484,8 @@ internal static partial class Av1IntraSuperblockEncoder
residual[..sampleCount], residual[..sampleCount],
transformSize); transformSize);
// Motion compensation and subtraction do not depend on transform type. Keep them outside the
// transform loop so exhaustive luma search traverses the source and reference blocks only once.
Av1TransformSetType transformSetType = Av1SymbolContextHelper.GetExtendedTransformSetType( Av1TransformSetType transformSetType = Av1SymbolContextHelper.GetExtendedTransformSetType(
transformSize, transformSize,
isInter: true, isInter: true,
@ -492,6 +505,15 @@ internal static partial class Av1IntraSuperblockEncoder
hasEmptyTransform = false; hasEmptyTransform = false;
emptyState = default; emptyState = default;
emptyDistortion = 0; emptyDistortion = 0;
// The candidate and best spans alternate ownership whenever a transform improves the result.
// This mirrors the reference's buffer-pointer swap and replaces a copy on every improvement
// with at most one normalization copy after the transform search.
Span<TSample> candidateReconstruction = transformReconstruction[..sampleCount];
Span<int> candidateCoefficients = transformCoefficients[..sampleCount];
Span<TSample> bestReconstruction = selectedReconstruction[..sampleCount];
Span<int> bestCoefficients = selectedCoefficients[..sampleCount];
bool bestUsesSelectedStorage = true;
for (Av1TransformType transformType = firstTransformType; for (Av1TransformType transformType = firstTransformType;
transformType < transformTypeLimit; transformType < transformTypeLimit;
transformType++) transformType++)
@ -508,9 +530,9 @@ internal static partial class Av1IntraSuperblockEncoder
planeOrigin, planeOrigin,
prediction[..sampleCount], prediction[..sampleCount],
residual[..sampleCount], residual[..sampleCount],
transformReconstruction[..sampleCount], candidateReconstruction,
transformSize.GetWidth(), transformSize.GetWidth(),
transformCoefficients[..sampleCount], candidateCoefficients,
transformSize, transformSize,
transformType, transformType,
plane, plane,
@ -524,7 +546,7 @@ internal static partial class Av1IntraSuperblockEncoder
transformSize, transformSize,
transformType, transformType,
Av1PredictionMode.DC, Av1PredictionMode.DC,
transformCoefficients[..sampleCount], candidateCoefficients,
componentType, componentType,
blockContext, blockContext,
candidateState.EndOfBlock, candidateState.EndOfBlock,
@ -539,8 +561,14 @@ internal static partial class Av1IntraSuperblockEncoder
if (candidateCost < bestCost) if (candidateCost < bestCost)
{ {
transformReconstruction[..sampleCount].CopyTo(selectedReconstruction); Span<TSample> previousBestReconstruction = bestReconstruction;
transformCoefficients[..sampleCount].CopyTo(selectedCoefficients); bestReconstruction = candidateReconstruction;
candidateReconstruction = previousBestReconstruction;
Span<int> previousBestCoefficients = bestCoefficients;
bestCoefficients = candidateCoefficients;
candidateCoefficients = previousBestCoefficients;
bestUsesSelectedStorage = !bestUsesSelectedStorage;
bestCost = candidateCost; bestCost = candidateCost;
selectedState = candidateState; selectedState = candidateState;
selectedRate = candidateRate; selectedRate = candidateRate;
@ -555,6 +583,14 @@ internal static partial class Av1IntraSuperblockEncoder
emptyDistortion = candidateDistortion; emptyDistortion = candidateDistortion;
} }
} }
// Callers retain the designated selected spans after this scratch workspace is reused by the
// next plane or motion vector, so normalize only when the final best result occupies scratch.
if (!bestUsesSelectedStorage)
{
bestReconstruction.CopyTo(selectedReconstruction);
bestCoefficients.CopyTo(selectedCoefficients);
}
} }
} }
} }

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

@ -207,6 +207,8 @@ internal static partial class Av1IntraSuperblockEncoder
block.FilterIntraMode = filterIntraMode; block.FilterIntraMode = filterIntraMode;
modeInfo.Block.TransformSize = lumaTransformSize; modeInfo.Block.TransformSize = lumaTransformSize;
// One 8x8 coding block retains either one 8x8 transform or four 4x4 transforms. The block-level
// skip decision is legal only when every transform selected by mode decision has an empty EOB.
int lumaTransformBlockCount = LumaTransformSize.GetSize2d() / lumaTransformSize.GetSize2d(); int lumaTransformBlockCount = LumaTransformSize.GetSize2d() / lumaTransformSize.GetSize2d();
bool lumaTransformEmpty = true; bool lumaTransformEmpty = true;
for (int transformIndex = 0; transformIndex < lumaTransformBlockCount; transformIndex++) for (int transformIndex = 0; transformIndex < lumaTransformBlockCount; transformIndex++)
@ -295,6 +297,8 @@ internal static partial class Av1IntraSuperblockEncoder
block.PredictionUnit.ChromaFromLumaIndex = chromaFromLumaIndex; block.PredictionUnit.ChromaFromLumaIndex = chromaFromLumaIndex;
block.PredictionUnit.ChromaFromLumaSigns = chromaFromLumaSigns; block.PredictionUnit.ChromaFromLumaSigns = chromaFromLumaSigns;
// Skip suppresses coefficient syntax for the entire coding block, not one plane independently.
// Preserve normal coefficient coding when any selected luma or chroma transform is nonempty.
bool allTransformsEmpty = lumaTransformEmpty && blueState.EndOfBlock == 0 && redState.EndOfBlock == 0; bool allTransformsEmpty = lumaTransformEmpty && blueState.EndOfBlock == 0 && redState.EndOfBlock == 0;
int regularEmptyTransformRate = 0; int regularEmptyTransformRate = 0;
if (allTransformsEmpty) if (allTransformsEmpty)
@ -590,10 +594,13 @@ internal static partial class Av1IntraSuperblockEncoder
Span<TSample> candidateReconstruction = workspace.GetCandidateReconstruction(0); Span<TSample> candidateReconstruction = workspace.GetCandidateReconstruction(0);
Span<int> candidateCoefficients = workspace.GetCandidateCoefficients(0); Span<int> candidateCoefficients = workspace.GetCandidateCoefficients(0);
Span<TSample> prediction = workspace.Prediction;
Span<short> residual = workspace.Residual;
long bestCost = long.MaxValue; long bestCost = long.MaxValue;
Av1PredictionMode bestMode = Av1PredictionMode.DC; Av1PredictionMode bestMode = Av1PredictionMode.DC;
selectedAngleDelta = 0; selectedAngleDelta = 0;
selectedFilterIntraMode = Av1FilterIntraMode.AllFilterIntraModes; selectedFilterIntraMode = Av1FilterIntraMode.AllFilterIntraModes;
selectedTransformSize = TransformSize;
int baseModeCount = LumaModeSearchOrder.Length; int baseModeCount = LumaModeSearchOrder.Length;
int deltaCount = AngleDeltaSearchOrder.Length; int deltaCount = AngleDeltaSearchOrder.Length;
int directionalModeCount = (int)Av1PredictionMode.Directional67Degrees - (int)Av1PredictionMode.Vertical + 1; int directionalModeCount = (int)Av1PredictionMode.Directional67Degrees - (int)Av1PredictionMode.Vertical + 1;
@ -611,6 +618,11 @@ internal static partial class Av1IntraSuperblockEncoder
TransformSize, TransformSize,
useReducedTransformSet); useReducedTransformSet);
// Transform type and transform size are separate search axes. Splitting their effort thresholds
// gives callers a useful intermediate tier without changing the fast default path.
bool searchEveryTransformType = this.effort >= 7;
bool searchEveryTransformSize = this.effort >= 8;
// Zero-angle modes precede groups of six nonzero adjustments for each directional mode. // Zero-angle modes precede groups of six nonzero adjustments for each directional mode.
// A single index preserves that tie-breaking order without duplicating candidate evaluation. // A single index preserves that tie-breaking order without duplicating candidate evaluation.
for (int candidateIndex = 0; candidateIndex < candidateCount; candidateIndex++) for (int candidateIndex = 0; candidateIndex < candidateCount; candidateIndex++)
@ -629,102 +641,196 @@ internal static partial class Av1IntraSuperblockEncoder
angleDelta = AngleDeltaSearchOrder[adjustedIndex % deltaCount]; angleDelta = AngleDeltaSearchOrder[adjustedIndex % deltaCount];
} }
Av1TransformType defaultTransformType = Av1SymbolContextHelper.GetDefaultIntraTransformType( // Prediction and subtraction do not depend on transform type. Preparing them once keeps
mode, // exhaustive transform search from repeating the same pixel traversal for every candidate.
TransformSize, TOperator.PrepareIntra(
useReducedTransformSet); this.blockWorkspace,
Av1EncoderTransformBlockState candidateState = default;
long candidateCost = this.GetLumaCandidateCost(
writer,
macroBlock,
sourcePlane, sourcePlane,
blockOrigin, blockOrigin,
prediction,
above, above,
left, left,
hasLeft, hasLeft,
hasAbove, hasAbove,
mode, mode,
angleDelta, angleDelta,
defaultTransformType, residual,
blockContext, TransformSize,
paletteDisabledCost, this.bitDepth);
largestTransformRate,
candidateReconstruction, // Transform types are visited in AV1 enumeration order. A strict cost comparison below keeps
candidateCoefficients, // the first legal type on ties, while lower efforts visit only the mode-derived default.
ref candidateState); Av1TransformType firstTransformType = searchEveryTransformType
? Av1TransformType.DctDct
: Av1SymbolContextHelper.GetDefaultIntraTransformType(
mode,
TransformSize,
useReducedTransformSet);
if (candidateCost < bestCost) Av1TransformType transformTypeLimit = searchEveryTransformType
? Av1TransformType.AllTransformTypes
: (Av1TransformType)((int)firstTransformType + 1);
for (Av1TransformType transformType = firstTransformType;
transformType < transformTypeLimit;
transformType++)
{ {
CopyCandidate( if (!transformType.IsExtendedSetUsed(transformSetType))
{
continue;
}
Av1EncoderTransformBlockState candidateState = default;
long candidateCost = this.GetLumaCandidateCost(
writer,
macroBlock,
sourcePlane,
blockOrigin,
prediction,
residual,
mode,
angleDelta,
transformType,
blockContext,
paletteDisabledCost,
largestTransformRate,
candidateReconstruction, candidateReconstruction,
candidateCoefficients, candidateCoefficients,
ref candidateState);
if (candidateCost < bestCost)
{
// The shared candidate spans are overwritten by the next transform. Copy only a
// global improvement into final block storage so no per-mode retained buffer is needed.
CopyCandidate(
candidateReconstruction,
candidateCoefficients,
reconstructionPlane,
blockOrigin,
retainedCoefficients,
TransformSize,
candidateState,
ref retainedStates[0]);
bestCost = candidateCost;
bestMode = mode;
selectedAngleDelta = angleDelta;
selectedTransformSize = TransformSize;
}
}
// At exhaustive effort, transform size belongs to this mode's RD result. Evaluate it
// before advancing so an 8x8-only preliminary result cannot discard a better split mode.
if (searchEveryTransformSize &&
this.picture.Parent.FrameHeader.TransformMode == Av1TransformMode.Select)
{
long splitCost = this.GetSplitLumaCandidateCost(
writer,
macroBlock,
sourcePlane,
reconstructionPlane, reconstructionPlane,
blockOrigin, blockOrigin,
retainedCoefficients, tileIndex,
TransformSize, mode,
candidateState, angleDelta,
ref retainedStates[0]); Av1FilterIntraMode.AllFilterIntraModes,
0,
ReadOnlySpan<ushort>.Empty,
0,
paletteDisabledCost,
transformSizeContext,
bestCost,
candidateReconstruction,
candidateCoefficients,
workspace.CandidateTransformBlocks);
bestCost = candidateCost; if (splitCost < bestCost)
bestMode = mode; {
selectedAngleDelta = angleDelta; CopySplitCandidate(
candidateReconstruction,
candidateCoefficients,
workspace.CandidateTransformBlocks,
reconstructionPlane,
blockOrigin,
retainedCoefficients,
retainedStates);
bestCost = splitCost;
bestMode = mode;
selectedAngleDelta = angleDelta;
selectedTransformSize = Av1TransformSize.Size4x4;
}
} }
} }
// Lower effort levels retain the mode-derived default transform. Higher levels refine only the winning // Midrange effort refines the preliminary mode only. Higher effort already searched every
// mode across the legal transform set, preserving search order without evaluating every mode-transform pair. // mode-transform pair above, so repeating the winning mode would add no candidates.
long bestTransformCost = bestCost; long bestTransformCost = bestCost;
for (Av1TransformType transformType = Av1TransformType.DctDct; if (this.effort >= 3 && !searchEveryTransformType)
this.effort >= 3 && transformType < Av1TransformType.AllTransformTypes;
transformType++)
{ {
if (!transformType.IsExtendedSetUsed(transformSetType)) // The shared spans now contain the last mode visited above, so rebuild the preliminary
{ // winner once before refining its transform types.
continue; TOperator.PrepareIntra(
} this.blockWorkspace,
Av1EncoderTransformBlockState candidateState = default;
long candidateCost = this.GetLumaCandidateCost(
writer,
macroBlock,
sourcePlane, sourcePlane,
blockOrigin, blockOrigin,
prediction,
above, above,
left, left,
hasLeft, hasLeft,
hasAbove, hasAbove,
bestMode, bestMode,
selectedAngleDelta, selectedAngleDelta,
transformType, residual,
blockContext, TransformSize,
paletteDisabledCost, this.bitDepth);
largestTransformRate,
candidateReconstruction,
candidateCoefficients,
ref candidateState);
if (candidateCost < bestTransformCost) for (Av1TransformType transformType = Av1TransformType.DctDct;
transformType < Av1TransformType.AllTransformTypes;
transformType++)
{ {
CopyCandidate( if (!transformType.IsExtendedSetUsed(transformSetType))
{
continue;
}
Av1EncoderTransformBlockState candidateState = default;
long candidateCost = this.GetLumaCandidateCost(
writer,
macroBlock,
sourcePlane,
blockOrigin,
prediction,
residual,
bestMode,
selectedAngleDelta,
transformType,
blockContext,
paletteDisabledCost,
largestTransformRate,
candidateReconstruction, candidateReconstruction,
candidateCoefficients, candidateCoefficients,
reconstructionPlane, ref candidateState);
blockOrigin,
retainedCoefficients,
TransformSize,
candidateState,
ref retainedStates[0]);
bestTransformCost = candidateCost; if (candidateCost < bestTransformCost)
{
CopyCandidate(
candidateReconstruction,
candidateCoefficients,
reconstructionPlane,
blockOrigin,
retainedCoefficients,
TransformSize,
candidateState,
ref retainedStates[0]);
bestTransformCost = candidateCost;
}
} }
} }
if (this.effort >= 4 && this.picture.Sequence.SequenceHeader.EnableFilterIntra) if (this.effort >= 4 && this.picture.Sequence.SequenceHeader.EnableFilterIntra)
{ {
Span<TSample> filterPrediction = workspace.FilterPrediction;
Span<short> filterResidual = workspace.FilterResidual;
// Each recursive filter prediction and its source residual are independent of transform type. // Each recursive filter prediction and its source residual are independent of transform type.
// Prepare them once per filter mode so all legal transforms reuse the same samples. // Prepare them once per filter mode so all legal transforms reuse the same samples.
for (Av1FilterIntraMode filterIntraMode = Av1FilterIntraMode.DC; for (Av1FilterIntraMode filterIntraMode = Av1FilterIntraMode.DC;
@ -735,10 +841,10 @@ internal static partial class Av1IntraSuperblockEncoder
this.blockWorkspace, this.blockWorkspace,
sourcePlane, sourcePlane,
blockOrigin, blockOrigin,
filterPrediction, prediction,
above, above,
left, left,
filterResidual, residual,
filterIntraMode, filterIntraMode,
TransformSize, TransformSize,
this.bitDepth); this.bitDepth);
@ -758,8 +864,8 @@ internal static partial class Av1IntraSuperblockEncoder
macroBlock, macroBlock,
sourcePlane, sourcePlane,
blockOrigin, blockOrigin,
filterPrediction, prediction,
filterResidual, residual,
filterIntraMode, filterIntraMode,
transformType, transformType,
blockContext, blockContext,
@ -785,12 +891,56 @@ internal static partial class Av1IntraSuperblockEncoder
bestMode = Av1PredictionMode.DC; bestMode = Av1PredictionMode.DC;
selectedAngleDelta = 0; selectedAngleDelta = 0;
selectedFilterIntraMode = filterIntraMode; selectedFilterIntraMode = filterIntraMode;
selectedTransformSize = TransformSize;
}
}
// Filter-intra mode and transform size form one candidate for RD comparison, just as
// ordinary spatial mode and transform size do in the exhaustive search above.
if (searchEveryTransformSize &&
this.picture.Parent.FrameHeader.TransformMode == Av1TransformMode.Select)
{
long splitCost = this.GetSplitLumaCandidateCost(
writer,
macroBlock,
sourcePlane,
reconstructionPlane,
blockOrigin,
tileIndex,
Av1PredictionMode.DC,
0,
filterIntraMode,
0,
ReadOnlySpan<ushort>.Empty,
0,
paletteDisabledCost,
transformSizeContext,
bestTransformCost,
candidateReconstruction,
candidateCoefficients,
workspace.CandidateTransformBlocks);
if (splitCost < bestTransformCost)
{
CopySplitCandidate(
candidateReconstruction,
candidateCoefficients,
workspace.CandidateTransformBlocks,
reconstructionPlane,
blockOrigin,
retainedCoefficients,
retainedStates);
bestTransformCost = splitCost;
bestMode = Av1PredictionMode.DC;
selectedAngleDelta = 0;
selectedFilterIntraMode = filterIntraMode;
selectedTransformSize = Av1TransformSize.Size4x4;
} }
} }
} }
} }
selectedTransformSize = TransformSize;
if (this.effort >= 5 && if (this.effort >= 5 &&
this.picture.Parent.FrameHeader.AllowScreenContentTools && this.picture.Parent.FrameHeader.AllowScreenContentTools &&
this.SelectLumaPalette( this.SelectLumaPalette(
@ -817,7 +967,10 @@ internal static partial class Av1IntraSuperblockEncoder
selectedFilterIntraMode = Av1FilterIntraMode.AllFilterIntraModes; selectedFilterIntraMode = Av1FilterIntraMode.AllFilterIntraModes;
} }
// Efforts six and seven save work by testing transform size only for the global non-palette
// winner. Effort eight and above already tested both sizes inside every candidate.
if (this.effort >= 6 && if (this.effort >= 6 &&
!searchEveryTransformSize &&
this.picture.Parent.FrameHeader.TransformMode == Av1TransformMode.Select && this.picture.Parent.FrameHeader.TransformMode == Av1TransformMode.Select &&
paletteInfo.PaletteSizes[0] == 0) paletteInfo.PaletteSizes[0] == 0)
{ {
@ -896,7 +1049,7 @@ internal static partial class Av1IntraSuperblockEncoder
TransformSampleCount); TransformSampleCount);
Span<int> transformCoefficients = workspace.GetCandidateCoefficients(1)[..TransformSampleCount]; Span<int> transformCoefficients = workspace.GetCandidateCoefficients(1)[..TransformSampleCount];
Span<short> residual = workspace.FilterResidual[..TransformSampleCount]; Span<short> residual = workspace.Residual[..TransformSampleCount];
Span<byte> contexts = workspace.TransformContexts; Span<byte> contexts = workspace.TransformContexts;
Span<byte> topContexts = contexts[..2]; Span<byte> topContexts = contexts[..2];
Span<byte> leftContexts = contexts[2..4]; Span<byte> leftContexts = contexts[2..4];
@ -912,6 +1065,8 @@ internal static partial class Av1IntraSuperblockEncoder
TransformSize, TransformSize,
useReducedTransformSet); useReducedTransformSet);
// Prediction-mode and transform-size symbols belong to the 8x8 coding block, while each
// 4x4 transform contributes its own coefficient rate below.
int rate = writer.GetTransformSizeCost(BlockSize, TransformSize, transformSizeContext); int rate = writer.GetTransformSizeCost(BlockSize, TransformSize, transformSizeContext);
if (paletteSize > 0) if (paletteSize > 0)
{ {
@ -941,6 +1096,9 @@ internal static partial class Av1IntraSuperblockEncoder
} }
long distortion = 0; long distortion = 0;
// Raster order is observable here: each retained 4x4 reconstruction supplies reference
// samples and coefficient context to transforms that follow it in the same coding block.
for (int transformRow = 0; transformRow < 2; transformRow++) for (int transformRow = 0; transformRow < 2; transformRow++)
{ {
for (int transformColumn = 0; transformColumn < 2; transformColumn++) for (int transformColumn = 0; transformColumn < 2; transformColumn++)
@ -1148,6 +1306,9 @@ internal static partial class Av1IntraSuperblockEncoder
int columnOffset = transformColumn * TransformWidth; int columnOffset = transformColumn * TransformWidth;
int modeInfoRow = blockOrigin.Y >> Av1Constants.ModeInfoSizeLog2; int modeInfoRow = blockOrigin.Y >> Av1Constants.ModeInfoSizeLog2;
int modeInfoColumn = blockOrigin.X >> Av1Constants.ModeInfoSizeLog2; int modeInfoColumn = blockOrigin.X >> Av1Constants.ModeInfoSizeLog2;
// Internal top and left edges come from the candidate mosaic built in raster order. Edges outside
// the 8x8 candidate continue to read committed reconstruction, keeping unsuccessful trials isolated.
hasAbove = transformRow > 0 || macroBlock.IsUpAvailable; hasAbove = transformRow > 0 || macroBlock.IsUpAvailable;
hasLeft = transformColumn > 0 || macroBlock.IsLeftAvailable; hasLeft = transformColumn > 0 || macroBlock.IsLeftAvailable;
bool rightAvailable = bool rightAvailable =
@ -1291,10 +1452,8 @@ internal static partial class Av1IntraSuperblockEncoder
Av1MacroBlockD macroBlock, Av1MacroBlockD macroBlock,
Buffer2DRegion<TSample> sourcePlane, Buffer2DRegion<TSample> sourcePlane,
Point blockOrigin, Point blockOrigin,
ReadOnlySpan<TSample> above, ReadOnlySpan<TSample> prediction,
ReadOnlySpan<TSample> left, ReadOnlySpan<short> residual,
bool hasLeft,
bool hasAbove,
Av1PredictionMode mode, Av1PredictionMode mode,
int angleDelta, int angleDelta,
Av1TransformType transformType, Av1TransformType transformType,
@ -1307,17 +1466,17 @@ internal static partial class Av1IntraSuperblockEncoder
{ {
const Av1BlockSize BlockSize = Av1BlockSize.Block8x8; const Av1BlockSize BlockSize = Av1BlockSize.Block8x8;
const Av1TransformSize TransformSize = Av1TransformSize.Size8x8; const Av1TransformSize TransformSize = Av1TransformSize.Size8x8;
long distortion = TOperator.EncodeCandidate(
// Prediction and subtraction were prepared by the owning mode loop. This stage performs only
// transform, quantization, reconstruction, and distortion for the requested transform type.
long distortion = TOperator.EncodePredictionCandidate(
this.blockWorkspace, this.blockWorkspace,
sourcePlane, sourcePlane,
blockOrigin, blockOrigin,
prediction,
residual,
candidateReconstruction, candidateReconstruction,
above, TransformSize.GetWidth(),
left,
hasLeft,
hasAbove,
mode,
angleDelta,
candidateCoefficients, candidateCoefficients,
TransformSize, TransformSize,
transformType, transformType,
@ -1328,6 +1487,8 @@ internal static partial class Av1IntraSuperblockEncoder
this.bitDepth, this.bitDepth,
ref candidateState); ref candidateState);
// Charge every block-level choice that distinguishes this spatial candidate before adding
// coefficient syntax derived from the live neighboring-transform context.
int rate = Av1TileWriter.GetLumaModeCost(writer, macroBlock, BlockSize, mode, angleDelta); int rate = Av1TileWriter.GetLumaModeCost(writer, macroBlock, BlockSize, mode, angleDelta);
rate += transformSizeRate; rate += transformSizeRate;
if (mode == Av1PredictionMode.DC) if (mode == Av1PredictionMode.DC)

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

@ -55,6 +55,9 @@ internal static partial class Av1IntraSuperblockEncoder
int uniqueColorCount = 0; int uniqueColorCount = 0;
short minimum = samples[0]; short minimum = samples[0];
short maximum = samples[0]; short maximum = samples[0];
// An 8x8 block has at most 64 distinct samples, so a compact workspace histogram avoids a
// dictionary allocation while collecting both frequency seeds and range endpoints.
foreach (short sample in samples) foreach (short sample in samples)
{ {
int colorIndex = uniqueColors[..uniqueColorCount].IndexOf(sample); int colorIndex = uniqueColors[..uniqueColorCount].IndexOf(sample);
@ -127,7 +130,9 @@ internal static partial class Av1IntraSuperblockEncoder
Span<short> centroids = workspace.GetCentroids(0); Span<short> centroids = workspace.GetCentroids(0);
bool paletteSelected = false; bool paletteSelected = false;
// Exhaustive ascending size search avoids the reference encoder's speed-dependent pruning. // Evaluate both frequency-seeded and range-seeded palette families for every legal size.
// Exhaustive ascending size order avoids speed-dependent pruning and gives smaller palettes
// deterministic precedence when complete rate-distortion costs tie.
for (int paletteSize = 2; paletteSize <= maximumPaletteSize; paletteSize++) for (int paletteSize = 2; paletteSize <= maximumPaletteSize; paletteSize++)
{ {
for (int index = 0; index < paletteSize; index++) for (int index = 0; index < paletteSize; index++)
@ -290,6 +295,9 @@ internal static partial class Av1IntraSuperblockEncoder
int bitDepth = this.bitDepth.GetBitCount(); int bitDepth = this.bitDepth.GetBitCount();
int cacheThreshold = 4 << (bitDepth - 8); int cacheThreshold = 4 << (bitDepth - 8);
// Nearby colors snap to a coded-neighbor cache entry when the quantization error is bounded.
// Snapping can merge centroids, so sorting and compaction below establish the final coded palette.
for (int colorIndex = 0; colorIndex < centroids.Length && !colorCache.IsEmpty; colorIndex++) for (int colorIndex = 0; colorIndex < centroids.Length && !colorCache.IsEmpty; colorIndex++)
{ {
int minimumDifference = Math.Abs(centroids[colorIndex] - colorCache[0]); int minimumDifference = Math.Abs(centroids[colorIndex] - colorCache[0]);
@ -376,6 +384,8 @@ internal static partial class Av1IntraSuperblockEncoder
columns, columns,
colorIndexMap); colorIndexMap);
// Palette prediction and subtraction are invariant for this color map. Reuse them across legal
// transform types, whose enumeration order also supplies deterministic tie precedence.
for (Av1TransformType transformType = Av1TransformType.DctDct; for (Av1TransformType transformType = Av1TransformType.DctDct;
transformType < Av1TransformType.AllTransformTypes; transformType < Av1TransformType.AllTransformTypes;
transformType++) transformType++)
@ -420,6 +430,8 @@ internal static partial class Av1IntraSuperblockEncoder
long candidateCost = Av1RateDistortion.GetCost(this.rateMultiplier, candidateRate, distortion); long candidateCost = Av1RateDistortion.GetCost(this.rateMultiplier, candidateRate, distortion);
if (candidateCost < bestCost) if (candidateCost < bestCost)
{ {
// Later palette sizes reuse every candidate span and the shared color map. Publish the
// complete palette state only when this candidate improves the global luma decision.
CopyCandidate( CopyCandidate(
candidateReconstruction, candidateReconstruction,
candidateCoefficients, candidateCoefficients,

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

@ -496,6 +496,9 @@ public class Av1EncoderFrameTests
[InlineData(4, true, false, false)] [InlineData(4, true, false, false)]
[InlineData(5, true, true, false)] [InlineData(5, true, true, false)]
[InlineData(6, true, true, true)] [InlineData(6, true, true, true)]
[InlineData(7, true, true, true)]
[InlineData(8, true, true, true)]
[InlineData(10, true, true, true)]
public void EncodeEffortControlsSearchFeatures( public void EncodeEffortControlsSearchFeatures(
int effort, int effort,
bool enableFilterIntra, bool enableFilterIntra,

Loading…
Cancel
Save