Browse Source

Add live AV1 chroma-from-luma search

pull/2633/head
James Jackson-South 1 month ago
parent
commit
bdb366458c
  1. 9
      HEIF_IMPLEMENTATION_PLAN.md
  2. 28
      src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolEncoder.cs
  3. 251
      src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.ChromaModeDecision.cs
  4. 6
      src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.ModeDecision.cs
  5. 217
      src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.Operator.cs
  6. 199
      src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1TransformBlockEncoder.cs
  7. 117
      src/ImageSharp/Formats/Heif/Av1/Prediction/ChromaFromLuma/Av1ChromaFromLumaContext.Operations.cs
  8. 24
      src/ImageSharp/Formats/Heif/Av1/Prediction/ChromaFromLuma/Av1ChromaFromLumaContext.cs
  9. 53
      src/ImageSharp/Formats/Heif/Av1/Prediction/ChromaFromLuma/Av1ChromaFromLumaMath.cs
  10. 32
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EntropyTests.cs
  11. 291
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1IntraSuperblockEncoderTests.cs

9
HEIF_IMPLEMENTATION_PLAN.md

File diff suppressed because one or more lines are too long

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

@ -1114,6 +1114,34 @@ internal class Av1SymbolEncoder : IDisposable
return Av1ProbabilityCost.GetSymbolCost(this.uvMode[cflAllowed][(int)lumaMode], (int)chromaMode);
}
/// <summary>
/// Gets the current fixed-point cost of joint chroma-from-luma alpha syntax.
/// </summary>
/// <param name="chromaFromLumaIndex">The packed U/V alpha-magnitude indices.</param>
/// <param name="joinedSign">The joint U/V sign symbol.</param>
/// <returns>The rate cost in 1/512-bit units.</returns>
public int GetChromaFromLumaCost(int chromaFromLumaIndex, int joinedSign)
{
int cost = Av1ProbabilityCost.GetSymbolCost(this.chromaFromLumaSign, joinedSign);
int signU = Av1ChromaFromLumaMath.SignU(joinedSign);
if (signU != Av1ChromaFromLumaMath.SignZero)
{
int contextU = Av1ChromaFromLumaMath.ContextU(joinedSign);
int indexU = Av1ChromaFromLumaMath.IndexU(chromaFromLumaIndex);
cost += Av1ProbabilityCost.GetSymbolCost(this.chromaFromLumaAlpha[contextU], indexU);
}
int signV = Av1ChromaFromLumaMath.SignV(joinedSign);
if (signV != Av1ChromaFromLumaMath.SignZero)
{
int contextV = Av1ChromaFromLumaMath.ContextV(joinedSign);
int indexV = Av1ChromaFromLumaMath.IndexV(chromaFromLumaIndex);
cost += Av1ProbabilityCost.GetSymbolCost(this.chromaFromLumaAlpha[contextV], indexV);
}
return cost;
}
/// <summary>
/// Writes a chroma intra prediction mode conditioned on the luma mode and chroma-from-luma availability.
/// </summary>

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

@ -4,6 +4,7 @@
using SixLabors.ImageSharp.Formats.Heif.Av1.Entropy;
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction;
using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.ChromaFromLuma;
using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
using SixLabors.ImageSharp.Formats.Heif.Av1.Transform;
using SixLabors.ImageSharp.Memory;
@ -52,7 +53,9 @@ internal static partial class Av1IntraSuperblockEncoder
Span<int> retainedRedCoefficients,
ref Av1EncoderTransformBlockState retainedBlueState,
ref Av1EncoderTransformBlockState retainedRedState,
out int selectedAngleDelta)
out int selectedAngleDelta,
out byte selectedChromaFromLumaIndex,
out sbyte selectedChromaFromLumaSigns)
{
const Av1BlockSize BlockSize = Av1BlockSize.Block8x8;
const int MaximumSampleCount = 8 * 8;
@ -154,6 +157,8 @@ internal static partial class Av1IntraSuperblockEncoder
long bestCost = long.MaxValue;
Av1ChromaPredictionMode bestMode = Av1ChromaPredictionMode.DC;
selectedAngleDelta = 0;
selectedChromaFromLumaIndex = 0;
selectedChromaFromLumaSigns = 0;
int baseModeCount = ChromaModeSearchOrder.Length;
int deltaCount = AngleDeltaSearchOrder.Length;
int directionalModeCount = (int)Av1ChromaPredictionMode.Directional67Degrees - (int)Av1ChromaPredictionMode.Vertical + 1;
@ -232,9 +237,253 @@ internal static partial class Av1IntraSuperblockEncoder
}
}
bool chromaFromLumaAllowed = BlockSize.AllowsChromaFromLuma(
this.picture.Parent.FrameHeader.LosslessArray[modeInfo.Block.SegmentId],
colorConfig.SubSamplingX,
colorConfig.SubSamplingY);
if (chromaFromLumaAllowed)
{
Span<short> lumaQ3 = stackalloc short[Av1ChromaFromLumaContext.BufferLine * 8];
TOperator.PrepareChromaFromLuma(
this.reconstruction.GetPlane(Av1Plane.Y),
lumaOrigin,
lumaQ3,
transformSize,
colorConfig.SubSamplingX,
colorConfig.SubSamplingY);
// Every alpha candidate uses the same constant DC predictor, so compute each plane once and
// refill the candidate block from its sample instead of rebuilding the identical edge average.
TOperator.PrepareChromaFromLumaDc(
candidateBlueReconstruction[..sampleCount],
blueAbove,
blueLeft,
hasLeft,
hasAbove,
transformSize,
this.bitDepth);
TSample blueDc = candidateBlueReconstruction[0];
TOperator.PrepareChromaFromLumaDc(
candidateRedReconstruction[..sampleCount],
redAbove,
redLeft,
hasLeft,
hasAbove,
transformSize,
this.bitDepth);
TSample redDc = candidateRedReconstruction[0];
Span<int> blueRates = stackalloc int[Av1ChromaFromLumaMath.AlphaCandidateCount];
Span<int> redRates = stackalloc int[Av1ChromaFromLumaMath.AlphaCandidateCount];
Span<long> blueDistortions = stackalloc long[Av1ChromaFromLumaMath.AlphaCandidateCount];
Span<long> redDistortions = stackalloc long[Av1ChromaFromLumaMath.AlphaCandidateCount];
// Each plane has only 33 signed alpha values. Caching those complete transform results reduces
// the joint search from 1089 transform pairs to 66 transforms plus inexpensive rate combinations.
for (int alphaCandidateIndex = 0; alphaCandidateIndex < Av1ChromaFromLumaMath.AlphaCandidateCount; alphaCandidateIndex++)
{
int alphaQ3 = Av1ChromaFromLumaMath.CandidateIndexToAlpha(alphaCandidateIndex);
Av1EncoderTransformBlockState candidateBlueState = default;
blueDistortions[alphaCandidateIndex] = this.GetChromaFromLumaPlaneCost(
writer,
lumaMode,
Av1Plane.U,
chromaOrigin,
transformSize,
blueSource,
blueDc,
blueContext,
lumaQ3,
alphaQ3,
candidateBlueReconstruction[..sampleCount],
candidateBlueCoefficients[..sampleCount],
ref candidateBlueState,
out blueRates[alphaCandidateIndex]);
Av1EncoderTransformBlockState candidateRedState = default;
redDistortions[alphaCandidateIndex] = this.GetChromaFromLumaPlaneCost(
writer,
lumaMode,
Av1Plane.V,
chromaOrigin,
transformSize,
redSource,
redDc,
redContext,
lumaQ3,
alphaQ3,
candidateRedReconstruction[..sampleCount],
candidateRedCoefficients[..sampleCount],
ref candidateRedState,
out redRates[alphaCandidateIndex]);
}
int chromaFromLumaModeRate = Av1TileWriter.GetChromaModeCost(
writer,
this.picture.Parent.FrameHeader,
colorConfig,
modeInfo,
BlockSize,
lumaMode,
Av1ChromaPredictionMode.ChromaFromLuma,
0);
bool chromaFromLumaSelected = false;
int selectedBlueCandidateIndex = 0;
int selectedRedCandidateIndex = 0;
for (int blueCandidateIndex = 0; blueCandidateIndex < Av1ChromaFromLumaMath.AlphaCandidateCount; blueCandidateIndex++)
{
int alphaU = Av1ChromaFromLumaMath.CandidateIndexToAlpha(blueCandidateIndex);
int signU = Av1ChromaFromLumaMath.AlphaToSign(alphaU);
int indexU = Av1ChromaFromLumaMath.AlphaToMagnitudeIndex(alphaU);
for (int redCandidateIndex = 0; redCandidateIndex < Av1ChromaFromLumaMath.AlphaCandidateCount; redCandidateIndex++)
{
int alphaV = Av1ChromaFromLumaMath.CandidateIndexToAlpha(redCandidateIndex);
int signV = Av1ChromaFromLumaMath.AlphaToSign(alphaV);
if (signU == Av1ChromaFromLumaMath.SignZero && signV == Av1ChromaFromLumaMath.SignZero)
{
continue;
}
int indexV = Av1ChromaFromLumaMath.AlphaToMagnitudeIndex(alphaV);
int jointSign = Av1ChromaFromLumaMath.JointSign(signU, signV);
int packedIndex = Av1ChromaFromLumaMath.PackIndices(indexU, indexV);
int rate = chromaFromLumaModeRate
+ blueRates[blueCandidateIndex]
+ redRates[redCandidateIndex]
+ writer.GetChromaFromLumaCost(packedIndex, jointSign);
long distortion = blueDistortions[blueCandidateIndex] + redDistortions[redCandidateIndex];
long candidateCost = Av1RateDistortion.GetCost(this.rateMultiplier, rate, distortion);
bool winsSearchOrderTie = candidateCost == bestCost
&& !chromaFromLumaSelected
&& bestMode != Av1ChromaPredictionMode.DC;
// CfL follows DC and precedes every other chroma mode in the reference search order.
if (candidateCost < bestCost || winsSearchOrderTie)
{
bestCost = candidateCost;
bestMode = Av1ChromaPredictionMode.ChromaFromLuma;
selectedAngleDelta = 0;
selectedBlueCandidateIndex = blueCandidateIndex;
selectedRedCandidateIndex = redCandidateIndex;
selectedChromaFromLumaIndex = (byte)packedIndex;
selectedChromaFromLumaSigns = (sbyte)jointSign;
chromaFromLumaSelected = true;
}
}
}
if (chromaFromLumaSelected)
{
Av1EncoderTransformBlockState candidateBlueState = default;
_ = this.GetChromaFromLumaPlaneCost(
writer,
lumaMode,
Av1Plane.U,
chromaOrigin,
transformSize,
blueSource,
blueDc,
blueContext,
lumaQ3,
Av1ChromaFromLumaMath.CandidateIndexToAlpha(selectedBlueCandidateIndex),
candidateBlueReconstruction[..sampleCount],
candidateBlueCoefficients[..sampleCount],
ref candidateBlueState,
out _);
CopyCandidate(
candidateBlueReconstruction,
candidateBlueCoefficients,
blueReconstruction,
chromaOrigin,
retainedBlueCoefficients,
transformSize,
candidateBlueState,
ref retainedBlueState);
Av1EncoderTransformBlockState candidateRedState = default;
_ = this.GetChromaFromLumaPlaneCost(
writer,
lumaMode,
Av1Plane.V,
chromaOrigin,
transformSize,
redSource,
redDc,
redContext,
lumaQ3,
Av1ChromaFromLumaMath.CandidateIndexToAlpha(selectedRedCandidateIndex),
candidateRedReconstruction[..sampleCount],
candidateRedCoefficients[..sampleCount],
ref candidateRedState,
out _);
CopyCandidate(
candidateRedReconstruction,
candidateRedCoefficients,
redReconstruction,
chromaOrigin,
retainedRedCoefficients,
transformSize,
candidateRedState,
ref retainedRedState);
}
}
return bestMode;
}
private long GetChromaFromLumaPlaneCost(
Av1SymbolEncoder writer,
Av1PredictionMode lumaMode,
Av1Plane plane,
Point chromaOrigin,
Av1TransformSize transformSize,
Buffer2DRegion<TSample> source,
TSample dc,
Av1TransformBlockContext context,
ReadOnlySpan<short> lumaQ3,
int alphaQ3,
Span<TSample> reconstruction,
Span<int> coefficients,
ref Av1EncoderTransformBlockState state,
out int rate)
{
long distortion = TOperator.EncodeChromaFromLumaCandidate(
this.blockWorkspace,
source,
chromaOrigin,
reconstruction,
dc,
lumaQ3,
alphaQ3,
coefficients,
transformSize,
plane,
this.quantization.QIndex[0],
this.quantization.DeltaQDc[(int)plane],
this.quantization.DeltaQAc[(int)plane],
this.bitDepth,
ref state);
rate = writer.GetCoefficientCost(
transformSize,
Av1TransformType.DctDct,
lumaMode,
coefficients,
Av1ComponentType.Chroma,
context,
state.EndOfBlock,
this.picture.Parent.FrameHeader.UseReducedTransformSet,
Av1FilterIntraMode.AllFilterIntraModes);
return distortion;
}
private long GetChromaCandidateCost(
Av1SymbolEncoder writer,
Av1MacroBlockModeInfo modeInfo,

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

@ -237,9 +237,13 @@ internal static partial class Av1IntraSuperblockEncoder
redCoefficients[this.codedAreaChroma..],
ref blueState,
ref redState,
out int chromaAngleDelta);
out int chromaAngleDelta,
out byte chromaFromLumaIndex,
out sbyte chromaFromLumaSigns);
block.PredictionUnit.AngleDelta[(int)Av1PlaneType.Uv] = (sbyte)chromaAngleDelta;
block.PredictionUnit.ChromaFromLumaIndex = chromaFromLumaIndex;
block.PredictionUnit.ChromaFromLumaSigns = chromaFromLumaSigns;
// A block-level skip suppresses every coefficient symbol, so all coded planes must be empty.
modeInfo.Block.Skip = skipTransform && blueState.EndOfBlock == 0 && redState.EndOfBlock == 0;

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

@ -3,6 +3,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;
using SixLabors.ImageSharp.Formats.Heif.Av1.Transform;
using SixLabors.ImageSharp.Memory;
@ -36,6 +37,42 @@ internal static partial class Av1IntraSuperblockEncoder
/// <returns>The converted sample.</returns>
public static abstract TSample CreateSample(int value);
/// <summary>
/// Builds the zero-mean Q3 luma surface shared by chroma-from-luma candidates.
/// </summary>
/// <param name="reconstruction">The coded reconstructed luma plane.</param>
/// <param name="blockOrigin">The luma block origin in plane samples.</param>
/// <param name="lumaQ3">The fixed-stride Q3 predictor workspace.</param>
/// <param name="transformSize">The chroma transform dimensions.</param>
/// <param name="subsamplingX">Whether luma is subsampled horizontally for chroma.</param>
/// <param name="subsamplingY">Whether luma is subsampled vertically for chroma.</param>
public static abstract void PrepareChromaFromLuma(
Buffer2DRegion<TSample> reconstruction,
Point blockOrigin,
Span<short> lumaQ3,
Av1TransformSize transformSize,
bool subsamplingX,
bool subsamplingY);
/// <summary>
/// Computes the DC predictor shared by every chroma-from-luma alpha candidate.
/// </summary>
/// <param name="reconstruction">The contiguous candidate reconstruction.</param>
/// <param name="above">The top reference samples.</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="transformSize">The chroma transform dimensions.</param>
/// <param name="bitDepth">The coded sample bit depth.</param>
public static abstract void PrepareChromaFromLumaDc(
Span<TSample> reconstruction,
ReadOnlySpan<TSample> above,
ReadOnlySpan<TSample> left,
bool hasLeft,
bool hasAbove,
Av1TransformSize transformSize,
Av1BitDepth bitDepth);
/// <summary>
/// Encodes and reconstructs one DC intra transform block.
/// </summary>
@ -116,6 +153,42 @@ internal static partial class Av1IntraSuperblockEncoder
int acDeltaQ,
Av1BitDepth bitDepth,
ref Av1EncoderTransformBlockState state);
/// <summary>
/// Encodes one chroma-from-luma candidate into contiguous decision scratch.
/// </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="reconstruction">The contiguous candidate reconstruction.</param>
/// <param name="dc">The cached DC predictor sample shared by every alpha.</param>
/// <param name="lumaQ3">The zero-mean reconstructed-luma predictor surface.</param>
/// <param name="alphaQ3">The signed chroma-from-luma multiplier.</param>
/// <param name="quantizedCoefficients">The candidate entropy-coding coefficients.</param>
/// <param name="transformSize">The transform dimensions.</param>
/// <param name="plane">The component plane containing the block.</param>
/// <param name="qIndex">The effective segment quantizer index.</param>
/// <param name="dcDeltaQ">The plane DC quantizer adjustment.</param>
/// <param name="acDeltaQ">The plane AC quantizer adjustment.</param>
/// <param name="bitDepth">The coded sample bit depth.</param>
/// <param name="state">The candidate transform state.</param>
/// <returns>The normalized pixel-domain distortion in AV1 transform units.</returns>
public static abstract long EncodeChromaFromLumaCandidate(
Av1EncoderBlockWorkspace workspace,
Buffer2DRegion<TSample> source,
Point blockOrigin,
Span<TSample> reconstruction,
TSample dc,
ReadOnlySpan<short> lumaQ3,
int alphaQ3,
Span<int> quantizedCoefficients,
Av1TransformSize transformSize,
Av1Plane plane,
int qIndex,
int dcDeltaQ,
int acDeltaQ,
Av1BitDepth bitDepth,
ref Av1EncoderTransformBlockState state);
}
/// <summary>
@ -130,6 +203,44 @@ internal static partial class Av1IntraSuperblockEncoder
/// <inheritdoc/>
public static byte CreateSample(int value) => (byte)value;
/// <inheritdoc/>
public static void PrepareChromaFromLuma(
Buffer2DRegion<byte> reconstruction,
Point blockOrigin,
Span<short> lumaQ3,
Av1TransformSize transformSize,
bool subsamplingX,
bool subsamplingY)
=> Av1ChromaFromLumaContext.PrepareBlock(
Av1TransformBlockEncoder.GetPlaneSpan(reconstruction, blockOrigin),
reconstruction.Stride,
lumaQ3,
transformSize,
subsamplingX,
subsamplingY);
/// <inheritdoc/>
public static void PrepareChromaFromLumaDc(
Span<byte> reconstruction,
ReadOnlySpan<byte> above,
ReadOnlySpan<byte> left,
bool hasLeft,
bool hasAbove,
Av1TransformSize transformSize,
Av1BitDepth bitDepth)
{
int width = transformSize.GetWidth();
Av1DcIntraPredictor.Predict(
hasLeft,
hasAbove,
reconstruction,
width,
above,
left,
width,
transformSize.GetHeight());
}
/// <inheritdoc/>
public static void Encode(
Av1EncoderBlockWorkspace workspace,
@ -206,6 +317,39 @@ internal static partial class Av1IntraSuperblockEncoder
acDeltaQ,
plane,
ref state);
/// <inheritdoc/>
public static long EncodeChromaFromLumaCandidate(
Av1EncoderBlockWorkspace workspace,
Buffer2DRegion<byte> source,
Point blockOrigin,
Span<byte> reconstruction,
byte dc,
ReadOnlySpan<short> lumaQ3,
int alphaQ3,
Span<int> quantizedCoefficients,
Av1TransformSize transformSize,
Av1Plane plane,
int qIndex,
int dcDeltaQ,
int acDeltaQ,
Av1BitDepth bitDepth,
ref Av1EncoderTransformBlockState state)
=> Av1TransformBlockEncoder.EncodeChromaFromLumaLossyCandidate(
workspace,
source,
blockOrigin,
reconstruction,
dc,
lumaQ3,
alphaQ3,
quantizedCoefficients,
transformSize,
qIndex,
dcDeltaQ,
acDeltaQ,
plane,
ref state);
}
/// <summary>
@ -220,6 +364,45 @@ internal static partial class Av1IntraSuperblockEncoder
/// <inheritdoc/>
public static ushort CreateSample(int value) => (ushort)value;
/// <inheritdoc/>
public static void PrepareChromaFromLuma(
Buffer2DRegion<ushort> reconstruction,
Point blockOrigin,
Span<short> lumaQ3,
Av1TransformSize transformSize,
bool subsamplingX,
bool subsamplingY)
=> Av1ChromaFromLumaContext.PrepareBlock(
MemoryMarshal.Cast<ushort, short>(Av1TransformBlockEncoder.GetPlaneSpan(reconstruction, blockOrigin)),
reconstruction.Stride,
lumaQ3,
transformSize,
subsamplingX,
subsamplingY);
/// <inheritdoc/>
public static void PrepareChromaFromLumaDc(
Span<ushort> reconstruction,
ReadOnlySpan<ushort> above,
ReadOnlySpan<ushort> left,
bool hasLeft,
bool hasAbove,
Av1TransformSize transformSize,
Av1BitDepth bitDepth)
{
int width = transformSize.GetWidth();
Av1DcIntraPredictor.Predict(
hasLeft,
hasAbove,
MemoryMarshal.Cast<ushort, short>(reconstruction),
width,
MemoryMarshal.Cast<ushort, short>(above),
MemoryMarshal.Cast<ushort, short>(left),
width,
transformSize.GetHeight(),
bitDepth.GetBitCount());
}
/// <inheritdoc/>
public static void Encode(
Av1EncoderBlockWorkspace workspace,
@ -298,5 +481,39 @@ internal static partial class Av1IntraSuperblockEncoder
plane,
bitDepth,
ref state);
/// <inheritdoc/>
public static long EncodeChromaFromLumaCandidate(
Av1EncoderBlockWorkspace workspace,
Buffer2DRegion<ushort> source,
Point blockOrigin,
Span<ushort> reconstruction,
ushort dc,
ReadOnlySpan<short> lumaQ3,
int alphaQ3,
Span<int> quantizedCoefficients,
Av1TransformSize transformSize,
Av1Plane plane,
int qIndex,
int dcDeltaQ,
int acDeltaQ,
Av1BitDepth bitDepth,
ref Av1EncoderTransformBlockState state)
=> Av1TransformBlockEncoder.EncodeChromaFromLumaLossyCandidate(
workspace,
source,
blockOrigin,
reconstruction,
dc,
lumaQ3,
alphaQ3,
quantizedCoefficients,
transformSize,
qIndex,
dcDeltaQ,
acDeltaQ,
plane,
bitDepth,
ref state);
}
}

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

@ -4,6 +4,7 @@
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Quantizers;
using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction;
using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.ChromaFromLuma;
using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
using SixLabors.ImageSharp.Formats.Heif.Av1.Transform;
using SixLabors.ImageSharp.Memory;
@ -157,6 +158,96 @@ internal static class Av1TransformBlockEncoder
return Av1ResidualBuilder.SumSquares(workspace.Residual[..transformSize.GetSize2d()]) << 4;
}
/// <summary>
/// Encodes one eight-bit chroma-from-luma candidate into contiguous decision scratch.
/// </summary>
/// <param name="workspace">The reusable residual, coefficient, and transform storage.</param>
/// <param name="source">The coded source plane.</param>
/// <param name="blockOrigin">The block origin in plane samples.</param>
/// <param name="reconstruction">The contiguous candidate reconstruction.</param>
/// <param name="dc">The cached DC predictor sample shared by every alpha.</param>
/// <param name="lumaQ3">The zero-mean reconstructed-luma predictor surface.</param>
/// <param name="alphaQ3">The signed chroma-from-luma multiplier.</param>
/// <param name="quantizedCoefficients">The candidate entropy-coding coefficients.</param>
/// <param name="transformSize">The selected chroma transform dimensions.</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 candidate transform type and end-of-block syntax.</param>
/// <returns>The normalized pixel-domain distortion in AV1 transform units.</returns>
public static long EncodeChromaFromLumaLossyCandidate(
Av1EncoderBlockWorkspace workspace,
Buffer2DRegion<byte> source,
Point blockOrigin,
Span<byte> reconstruction,
byte dc,
ReadOnlySpan<short> lumaQ3,
int alphaQ3,
Span<int> quantizedCoefficients,
Av1TransformSize transformSize,
int qIndex,
int dcDeltaQ,
int acDeltaQ,
Av1Plane plane,
ref Av1EncoderTransformBlockState state)
{
int width = transformSize.GetWidth();
int height = transformSize.GetHeight();
ReadOnlySpan<byte> sourceSamples = GetPlaneSpan(source, blockOrigin);
reconstruction[..transformSize.GetSize2d()].Fill(dc);
// CfL adds its scaled reconstructed-luma AC contribution to the cached DC predictor before residual coding.
Av1ChromaFromLumaPredictor.Predict(lumaQ3, reconstruction, width, alphaQ3, width, height);
Av1ResidualBuilder.Subtract(
sourceSamples,
source.Stride,
reconstruction,
width,
workspace.Residual,
width,
width,
height);
EncodeLossy(
workspace,
quantizedCoefficients,
transformSize,
Av1TransformType.DctDct,
qIndex,
dcDeltaQ,
acDeltaQ,
Av1BitDepth.EightBit,
ref state);
if (state.EndOfBlock > 0)
{
Av1InverseTransformer.Reconstruct8Bit(
workspace.DequantizedCoefficients,
reconstruction,
width,
transformSize,
Av1TransformType.DctDct,
(int)plane,
state.EndOfBlock,
false,
workspace.TransformWorkspace);
}
// Final distortion is measured against the samples a decoder reconstructs, not the unquantized predictor.
Av1ResidualBuilder.Subtract(
sourceSamples,
source.Stride,
reconstruction,
width,
workspace.Residual,
width,
width,
height);
return Av1ResidualBuilder.SumSquares(workspace.Residual[..transformSize.GetSize2d()]) << 4;
}
/// <summary>
/// Encodes and reconstructs one high-bit-depth lossy DC intra block in contiguous encoder planes.
/// </summary>
@ -311,6 +402,112 @@ internal static class Av1TransformBlockEncoder
return normalizedDistortion << 4;
}
/// <summary>
/// Encodes one high-bit-depth chroma-from-luma candidate into contiguous decision scratch.
/// </summary>
/// <param name="workspace">The reusable residual, coefficient, and transform storage.</param>
/// <param name="source">The coded source plane.</param>
/// <param name="blockOrigin">The block origin in plane samples.</param>
/// <param name="reconstruction">The contiguous candidate reconstruction.</param>
/// <param name="dc">The cached DC predictor sample shared by every alpha.</param>
/// <param name="lumaQ3">The zero-mean reconstructed-luma predictor surface.</param>
/// <param name="alphaQ3">The signed chroma-from-luma multiplier.</param>
/// <param name="quantizedCoefficients">The candidate entropy-coding coefficients.</param>
/// <param name="transformSize">The selected chroma transform dimensions.</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="bitDepth">The coded sample bit depth.</param>
/// <param name="state">The candidate transform type and end-of-block syntax.</param>
/// <returns>The normalized pixel-domain distortion in AV1 transform units.</returns>
public static long EncodeChromaFromLumaLossyCandidate(
Av1EncoderBlockWorkspace workspace,
Buffer2DRegion<ushort> source,
Point blockOrigin,
Span<ushort> reconstruction,
ushort dc,
ReadOnlySpan<short> lumaQ3,
int alphaQ3,
Span<int> quantizedCoefficients,
Av1TransformSize transformSize,
int qIndex,
int dcDeltaQ,
int acDeltaQ,
Av1Plane plane,
Av1BitDepth bitDepth,
ref Av1EncoderTransformBlockState state)
{
int width = transformSize.GetWidth();
int height = transformSize.GetHeight();
ReadOnlySpan<ushort> sourceSamples = GetPlaneSpan(source, blockOrigin);
Span<short> signedReconstruction = MemoryMarshal.Cast<ushort, short>(reconstruction);
reconstruction[..transformSize.GetSize2d()].Fill(dc);
Av1ChromaFromLumaPredictor.Predict(
lumaQ3,
signedReconstruction,
width,
alphaQ3,
bitDepth.GetBitCount(),
width,
height);
Av1ResidualBuilder.Subtract(
sourceSamples,
source.Stride,
reconstruction,
width,
workspace.Residual,
width,
width,
height);
EncodeLossy(
workspace,
quantizedCoefficients,
transformSize,
Av1TransformType.DctDct,
qIndex,
dcDeltaQ,
acDeltaQ,
bitDepth,
ref state);
if (state.EndOfBlock > 0)
{
Av1InverseTransformer.ReconstructHighBitDepth(
workspace.DequantizedCoefficients,
signedReconstruction,
width,
transformSize,
Av1TransformType.DctDct,
(int)plane,
state.EndOfBlock,
false,
bitDepth,
workspace.TransformWorkspace);
}
Av1ResidualBuilder.Subtract(
sourceSamples,
source.Stride,
reconstruction,
width,
workspace.Residual,
width,
width,
height);
long distortion = Av1ResidualBuilder.SumSquares(workspace.Residual[..transformSize.GetSize2d()]);
int shift = (bitDepth.GetBitCount() - 8) * 2;
long normalizedDistortion = shift == 0
? distortion
: (distortion + (1L << (shift - 1))) >> shift;
return normalizedDistortion << 4;
}
/// <summary>
/// Encodes and reconstructs one eight-bit lossy intra block.
/// </summary>
@ -591,7 +788,7 @@ internal static class Av1TransformBlockEncoder
state.TransformType = transformType;
}
private static Span<TSample> GetPlaneSpan<TSample>(Buffer2DRegion<TSample> plane, Point blockOrigin)
public static Span<TSample> GetPlaneSpan<TSample>(Buffer2DRegion<TSample> plane, Point blockOrigin)
where TSample : unmanaged
{
int offset =

117
src/ImageSharp/Formats/Heif/Av1/Prediction/ChromaFromLuma/Av1ChromaFromLumaContext.Operations.cs

@ -18,6 +18,52 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.ChromaFromLuma;
/// </content>
internal partial class Av1ChromaFromLumaContext
{
/// <summary>
/// Subsamples one reconstructed eight-bit luma block and removes its rounded Q3 mean.
/// </summary>
/// <param name="input">The reconstructed luma samples.</param>
/// <param name="inputStride">The distance, in samples, between input rows.</param>
/// <param name="output">The fixed-stride Q3 predictor workspace.</param>
/// <param name="transformSize">The chroma transform dimensions.</param>
/// <param name="subsamplingX">Whether two horizontal luma samples map to each chroma sample.</param>
/// <param name="subsamplingY">Whether two vertical luma samples map to each chroma sample.</param>
public static void PrepareBlock(
ReadOnlySpan<byte> input,
int inputStride,
Span<short> output,
Av1TransformSize transformSize,
bool subsamplingX,
bool subsamplingY)
{
int lumaWidth = transformSize.GetWidth() << (subsamplingX ? 1 : 0);
int lumaHeight = transformSize.GetHeight() << (subsamplingY ? 1 : 0);
StoreSamples(input, inputStride, 0, lumaWidth, lumaHeight, output, subsamplingX, subsamplingY);
SubtractAverage(output, transformSize);
}
/// <summary>
/// Subsamples one reconstructed high-bit-depth luma block and removes its rounded Q3 mean.
/// </summary>
/// <param name="input">The reconstructed luma samples.</param>
/// <param name="inputStride">The distance, in samples, between input rows.</param>
/// <param name="output">The fixed-stride Q3 predictor workspace.</param>
/// <param name="transformSize">The chroma transform dimensions.</param>
/// <param name="subsamplingX">Whether two horizontal luma samples map to each chroma sample.</param>
/// <param name="subsamplingY">Whether two vertical luma samples map to each chroma sample.</param>
public static void PrepareBlock(
ReadOnlySpan<short> input,
int inputStride,
Span<short> output,
Av1TransformSize transformSize,
bool subsamplingX,
bool subsamplingY)
{
int lumaWidth = transformSize.GetWidth() << (subsamplingX ? 1 : 0);
int lumaHeight = transformSize.GetHeight() << (subsamplingY ? 1 : 0);
StoreSamples(input, inputStride, 0, lumaWidth, lumaHeight, output, subsamplingX, subsamplingY);
SubtractAverage(output, transformSize);
}
/// <summary>
/// Stores 8-bit reconstructed luma samples in the Q3 predictor surface.
/// </summary>
@ -26,12 +72,23 @@ internal partial class Av1ChromaFromLumaContext
/// <param name="outputOffset">The first destination sample in the fixed-stride predictor buffer.</param>
/// <param name="width">The luma width in samples.</param>
/// <param name="height">The luma height in samples.</param>
private void StoreSamples(ReadOnlySpan<byte> input, int inputStride, int outputOffset, int width, int height)
/// <param name="output">The fixed-stride Q3 predictor workspace.</param>
/// <param name="subsamplingX">Whether horizontal luma pairs are subsampled.</param>
/// <param name="subsamplingY">Whether vertical luma pairs are subsampled.</param>
private static void StoreSamples(
ReadOnlySpan<byte> input,
int inputStride,
int outputOffset,
int width,
int height,
Span<short> output,
bool subsamplingX,
bool subsamplingY)
{
ref byte inputBase = ref MemoryMarshal.GetReference(input);
ref short outputBase = ref MemoryMarshal.GetReference(this.Q3Buffer);
ref short outputBase = ref MemoryMarshal.GetReference(output);
if (!this.subX)
if (!subsamplingX)
{
// One luma sample maps directly to one chroma sample, so multiplying by eight converts it to Q3.
for (int row = 0; row < height; row++)
@ -94,13 +151,13 @@ internal partial class Av1ChromaFromLumaContext
Vector256<sbyte> ones256 = Vector256.Create((sbyte)1);
Vector128<sbyte> ones128 = Vector128.Create((sbyte)1);
int rowStep = this.subY ? 2 : 1;
int outputShift = this.subY ? 1 : 2;
int rowStep = subsamplingY ? 2 : 1;
int outputShift = subsamplingY ? 1 : 2;
for (int row = 0; row < height; row += rowStep)
{
ref byte inputRow = ref Unsafe.Add(ref inputBase, row * inputStride);
ref byte nextInputRow = ref Unsafe.Add(ref inputRow, this.subY ? inputStride : 0);
ref short outputRow = ref Unsafe.Add(ref outputBase, outputOffset + ((row >> (this.subY ? 1 : 0)) * BufferLine));
ref byte nextInputRow = ref Unsafe.Add(ref inputRow, subsamplingY ? inputStride : 0);
ref short outputRow = ref Unsafe.Add(ref outputBase, outputOffset + ((row >> (subsamplingY ? 1 : 0)) * BufferLine));
int column = 0;
if (Avx2.IsSupported)
@ -109,7 +166,7 @@ internal partial class Av1ChromaFromLumaContext
for (; vectorCount > 0; vectorCount--, column += Vector256<byte>.Count)
{
Vector256<short> sum = Avx2.MultiplyAddAdjacent(Vector256.LoadUnsafe(ref inputRow, (nuint)column), ones256);
if (this.subY)
if (subsamplingY)
{
sum += Avx2.MultiplyAddAdjacent(Vector256.LoadUnsafe(ref nextInputRow, (nuint)column), ones256);
}
@ -124,7 +181,7 @@ internal partial class Av1ChromaFromLumaContext
for (; vectorCount > 0; vectorCount--, column += Vector128<byte>.Count)
{
Vector128<short> sum = PairSum(Vector128.LoadUnsafe(ref inputRow, (nuint)column), ones128);
if (this.subY)
if (subsamplingY)
{
sum += PairSum(Vector128.LoadUnsafe(ref nextInputRow, (nuint)column), ones128);
}
@ -139,7 +196,7 @@ internal partial class Av1ChromaFromLumaContext
? Unsafe.ReadUnaligned<uint>(ref Unsafe.Add(ref inputRow, column))
: Unsafe.ReadUnaligned<ulong>(ref Unsafe.Add(ref inputRow, column));
Vector128<short> sum = PairSum(Vector128.CreateScalarUnsafe(packed).AsByte(), ones128);
if (this.subY)
if (subsamplingY)
{
packed = remaining == 4
? Unsafe.ReadUnaligned<uint>(ref Unsafe.Add(ref nextInputRow, column))
@ -164,7 +221,7 @@ internal partial class Av1ChromaFromLumaContext
for (; column < width; column += 2)
{
int sum = Unsafe.Add(ref inputRow, column) + Unsafe.Add(ref inputRow, column + 1);
if (this.subY)
if (subsamplingY)
{
sum += Unsafe.Add(ref nextInputRow, column) + Unsafe.Add(ref nextInputRow, column + 1);
}
@ -182,12 +239,23 @@ internal partial class Av1ChromaFromLumaContext
/// <param name="outputOffset">The first destination sample in the fixed-stride predictor buffer.</param>
/// <param name="width">The luma width in samples.</param>
/// <param name="height">The luma height in samples.</param>
private void StoreSamples(ReadOnlySpan<short> input, int inputStride, int outputOffset, int width, int height)
/// <param name="output">The fixed-stride Q3 predictor workspace.</param>
/// <param name="subsamplingX">Whether horizontal luma pairs are subsampled.</param>
/// <param name="subsamplingY">Whether vertical luma pairs are subsampled.</param>
private static void StoreSamples(
ReadOnlySpan<short> input,
int inputStride,
int outputOffset,
int width,
int height,
Span<short> output,
bool subsamplingX,
bool subsamplingY)
{
ref short inputBase = ref MemoryMarshal.GetReference(input);
ref short outputBase = ref MemoryMarshal.GetReference(this.Q3Buffer);
ref short outputBase = ref MemoryMarshal.GetReference(output);
if (!this.subX)
if (!subsamplingX)
{
for (int row = 0; row < height; row++)
{
@ -232,13 +300,13 @@ internal partial class Av1ChromaFromLumaContext
Vector256<short> ones256 = Vector256.Create((short)1);
Vector128<short> ones128 = Vector128.Create((short)1);
int rowStep = this.subY ? 2 : 1;
int outputShift = this.subY ? 1 : 2;
int rowStep = subsamplingY ? 2 : 1;
int outputShift = subsamplingY ? 1 : 2;
for (int row = 0; row < height; row += rowStep)
{
ref short inputRow = ref Unsafe.Add(ref inputBase, row * inputStride);
ref short nextInputRow = ref Unsafe.Add(ref inputRow, this.subY ? inputStride : 0);
ref short outputRow = ref Unsafe.Add(ref outputBase, outputOffset + ((row >> (this.subY ? 1 : 0)) * BufferLine));
ref short nextInputRow = ref Unsafe.Add(ref inputRow, subsamplingY ? inputStride : 0);
ref short outputRow = ref Unsafe.Add(ref outputBase, outputOffset + ((row >> (subsamplingY ? 1 : 0)) * BufferLine));
int column = 0;
if (Vector256.IsHardwareAccelerated)
@ -247,7 +315,7 @@ internal partial class Av1ChromaFromLumaContext
for (; vectorCount > 0; vectorCount--, column += Vector256<short>.Count)
{
Vector256<int> sum = Vector256_.MultiplyAddAdjacent(Vector256.LoadUnsafe(ref inputRow, (nuint)column), ones256);
if (this.subY)
if (subsamplingY)
{
sum += Vector256_.MultiplyAddAdjacent(Vector256.LoadUnsafe(ref nextInputRow, (nuint)column), ones256);
}
@ -262,7 +330,7 @@ internal partial class Av1ChromaFromLumaContext
for (; vectorCount > 0; vectorCount--, column += Vector128<short>.Count)
{
Vector128<int> sum = Vector128_.MultiplyAddAdjacent(Vector128.LoadUnsafe(ref inputRow, (nuint)column), ones128);
if (this.subY)
if (subsamplingY)
{
sum += Vector128_.MultiplyAddAdjacent(Vector128.LoadUnsafe(ref nextInputRow, (nuint)column), ones128);
}
@ -275,7 +343,7 @@ internal partial class Av1ChromaFromLumaContext
Vector128<short> samples = Vector128.CreateScalarUnsafe(
Unsafe.ReadUnaligned<ulong>(ref Unsafe.As<short, byte>(ref Unsafe.Add(ref inputRow, column)))).AsInt16();
Vector128<int> sum = Vector128_.MultiplyAddAdjacent(samples, ones128);
if (this.subY)
if (subsamplingY)
{
samples = Vector128.CreateScalarUnsafe(
Unsafe.ReadUnaligned<ulong>(ref Unsafe.As<short, byte>(ref Unsafe.Add(ref nextInputRow, column)))).AsInt16();
@ -291,7 +359,7 @@ internal partial class Av1ChromaFromLumaContext
for (; column < width; column += 2)
{
int sum = Unsafe.Add(ref inputRow, column) + Unsafe.Add(ref inputRow, column + 1);
if (this.subY)
if (subsamplingY)
{
sum += Unsafe.Add(ref nextInputRow, column) + Unsafe.Add(ref nextInputRow, column + 1);
}
@ -304,8 +372,9 @@ internal partial class Av1ChromaFromLumaContext
/// <summary>
/// Subtracts the rounded Q3 average from each predictor sample, leaving the AC contribution used by CfL.
/// </summary>
/// <param name="buffer">The fixed-stride Q3 predictor workspace.</param>
/// <param name="transformSize">The populated predictor dimensions.</param>
private void SubtractAverage(Av1TransformSize transformSize)
private static void SubtractAverage(Span<short> buffer, Av1TransformSize transformSize)
{
int width = transformSize.GetWidth();
int height = transformSize.GetHeight();
@ -313,7 +382,7 @@ internal partial class Av1ChromaFromLumaContext
// Transform dimensions are powers of two, so division by the sample count is an exact right shift. Half
// the sample count is accumulated first to implement the normative nearest-integer rounding.
int sumQ3 = (width * height) >> 1;
ref short bufferBase = ref MemoryMarshal.GetReference(this.Q3Buffer);
ref short bufferBase = ref MemoryMarshal.GetReference(buffer);
if (Vector256.IsHardwareAccelerated && width >= Vector256<short>.Count)
{

24
src/ImageSharp/Formats/Heif/Av1/Prediction/ChromaFromLuma/Av1ChromaFromLumaContext.cs

@ -16,7 +16,7 @@ internal sealed partial class Av1ChromaFromLumaContext
/// <summary>
/// The fixed row stride and maximum dimension, in chroma samples, of the luma predictor buffer.
/// </summary>
private const int BufferLine = 32;
public const int BufferLine = 32;
/// <summary>
/// The number of samples in the fixed-stride chroma-from-luma workspace.
@ -145,11 +145,27 @@ internal sealed partial class Av1ChromaFromLumaContext
// here keeps sample conversion out of the row kernels and lets the JIT specialize both storage layouts.
if (typeof(T) == typeof(byte))
{
this.StoreSamples(MemoryMarshal.Cast<T, byte>(input), inputStride, outputOffset, width, height);
StoreSamples(
MemoryMarshal.Cast<T, byte>(input),
inputStride,
outputOffset,
width,
height,
this.Q3Buffer,
this.subX,
this.subY);
}
else
{
this.StoreSamples(MemoryMarshal.Cast<T, short>(input), inputStride, outputOffset, width, height);
StoreSamples(
MemoryMarshal.Cast<T, short>(input),
inputStride,
outputOffset,
width,
height,
this.Q3Buffer,
this.subX,
this.subY);
}
}
@ -161,7 +177,7 @@ internal sealed partial class Av1ChromaFromLumaContext
{
Guard.IsFalse(this.AreParametersComputed, nameof(this.AreParametersComputed), "Do not call cfl_compute_parameters multiple time on the same values.");
this.Pad(transformSize.GetWidth(), transformSize.GetHeight());
this.SubtractAverage(transformSize);
SubtractAverage(this.Q3Buffer, transformSize);
this.AreParametersComputed = true;
}

53
src/ImageSharp/Formats/Heif/Av1/Prediction/ChromaFromLuma/Av1ChromaFromLumaMath.cs

@ -18,6 +18,21 @@ internal static class Av1ChromaFromLumaMath
/// </summary>
private const int AlphabetSizeLog2 = 4;
/// <summary>
/// The number of nonzero alpha magnitudes represented by each plane's alphabet.
/// </summary>
public const int AlphaMagnitudeCount = 1 << AlphabetSizeLog2;
/// <summary>
/// The number of signed alpha candidates including zero.
/// </summary>
public const int AlphaCandidateCount = (AlphaMagnitudeCount * 2) + 1;
/// <summary>
/// The candidate index representing a zero alpha.
/// </summary>
public const int AlphaZeroIndex = AlphaMagnitudeCount;
/// <summary>
/// The alpha sign value representing a zero multiplier.
/// </summary>
@ -74,4 +89,42 @@ internal static class Av1ChromaFromLumaMath
/// <param name="jointSign">The coded joint U/V sign symbol.</param>
/// <returns>The V-plane alpha entropy context.</returns>
public static int ContextV(int jointSign) => (SignV(jointSign) * Signs) + SignU(jointSign) - Signs;
/// <summary>
/// Converts a signed-candidate index to its alpha value in Q3 units.
/// </summary>
/// <param name="candidateIndex">The candidate index in negative-to-positive order.</param>
/// <returns>The signed Q3 alpha value.</returns>
public static int CandidateIndexToAlpha(int candidateIndex) => candidateIndex - AlphaZeroIndex;
/// <summary>
/// Converts a signed Q3 alpha value to its coded sign state.
/// </summary>
/// <param name="alphaQ3">The signed alpha value.</param>
/// <returns>The zero, negative, or positive sign state.</returns>
public static int AlphaToSign(int alphaQ3)
=> alphaQ3 == 0 ? SignZero : alphaQ3 < 0 ? SignNegative : SignPositive;
/// <summary>
/// Converts a nonzero signed Q3 alpha value to its coded magnitude index.
/// </summary>
/// <param name="alphaQ3">The signed alpha value.</param>
/// <returns>The zero-based magnitude index, or zero for a zero alpha.</returns>
public static int AlphaToMagnitudeIndex(int alphaQ3) => alphaQ3 == 0 ? 0 : Math.Abs(alphaQ3) - 1;
/// <summary>
/// Combines the U and V sign states into the coded joint symbol.
/// </summary>
/// <param name="signU">The U-plane sign state.</param>
/// <param name="signV">The V-plane sign state.</param>
/// <returns>The joint sign symbol.</returns>
public static int JointSign(int signU, int signV) => (signU * Signs) + signV - 1;
/// <summary>
/// Packs the U and V alpha-magnitude indices into the coded byte.
/// </summary>
/// <param name="indexU">The U-plane magnitude index.</param>
/// <param name="indexV">The V-plane magnitude index.</param>
/// <returns>The packed magnitude indices.</returns>
public static int PackIndices(int indexU, int indexV) => (indexU << AlphabetSizeLog2) + indexV;
}

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

@ -7,6 +7,7 @@ using SixLabors.ImageSharp.Formats.Heif.Av1.Entropy;
using SixLabors.ImageSharp.Formats.Heif.Av1.Motion;
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction;
using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.ChromaFromLuma;
using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
using SixLabors.ImageSharp.Formats.Heif.Av1.Transform;
using SixLabors.ImageSharp.Memory;
@ -141,6 +142,37 @@ public class Av1EntropyTests
encoder.GetTransformBlockSkipCost(true, TransformSize, SkipContext));
}
[Theory]
[InlineData(-16, 16)]
[InlineData(0, 8)]
[InlineData(-4, 0)]
public void ChromaFromLumaCostMatchesCurrentDistributions(int alphaU, int alphaV)
{
int signU = Av1ChromaFromLumaMath.AlphaToSign(alphaU);
int signV = Av1ChromaFromLumaMath.AlphaToSign(alphaV);
int jointSign = Av1ChromaFromLumaMath.JointSign(signU, signV);
int indexU = Av1ChromaFromLumaMath.AlphaToMagnitudeIndex(alphaU);
int indexV = Av1ChromaFromLumaMath.AlphaToMagnitudeIndex(alphaV);
int packedIndex = Av1ChromaFromLumaMath.PackIndices(indexU, indexV);
int expected = Av1ProbabilityCost.GetSymbolCost(Av1DefaultDistributions.ChromaFromLumaSign, jointSign);
if (signU != Av1ChromaFromLumaMath.SignZero)
{
expected += Av1ProbabilityCost.GetSymbolCost(
Av1DefaultDistributions.ChromaFromLumaAlpha[Av1ChromaFromLumaMath.ContextU(jointSign)],
indexU);
}
if (signV != Av1ChromaFromLumaMath.SignZero)
{
expected += Av1ProbabilityCost.GetSymbolCost(
Av1DefaultDistributions.ChromaFromLumaAlpha[Av1ChromaFromLumaMath.ContextV(jointSign)],
indexV);
}
using Av1SymbolEncoder encoder = new(Configuration.Default, 64, BaseQIndex, updateCdf: false);
Assert.Equal(expected, encoder.GetChromaFromLumaCost(packedIndex, jointSign));
}
/// <summary>
/// Verifies that live luma rate accounting includes the selected signed directional adjustment.
/// </summary>

291
tests/ImageSharp.Tests/Formats/Heif/Av1/Av1IntraSuperblockEncoderTests.cs

@ -2,11 +2,13 @@
// Licensed under the Six Labors Split License.
using System.Buffers;
using System.Numerics;
using SixLabors.ImageSharp.Formats.Heif.Av1;
using SixLabors.ImageSharp.Formats.Heif.Av1.Entropy;
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.Prediction.ChromaFromLuma;
using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
using SixLabors.ImageSharp.Formats.Heif.Av1.Transform;
using SixLabors.ImageSharp.Memory;
@ -297,12 +299,12 @@ public class Av1IntraSuperblockEncoderTests
1,
1);
FillPlane(source.Frame.CodedView.GetPlane(Av1Plane.Y), 128);
FillPlane(source.Frame.CodedView.GetPlane(Av1Plane.Y), (byte)128);
ClearPlane(reconstruction.Luma);
if (!isMonochrome)
{
FillPlane(source.Frame.CodedView.GetPlane(Av1Plane.U), 128);
FillPlane(source.Frame.CodedView.GetPlane(Av1Plane.V), 128);
FillPlane(source.Frame.CodedView.GetPlane(Av1Plane.U), (byte)128);
FillPlane(source.Frame.CodedView.GetPlane(Av1Plane.V), (byte)128);
ClearPlane(Assert.IsType<Buffer2D<byte>>(reconstruction.ChromaBlue));
ClearPlane(Assert.IsType<Buffer2D<byte>>(reconstruction.ChromaRed));
}
@ -921,6 +923,277 @@ public class Av1IntraSuperblockEncoderTests
Assert.NotEqual(0, tileWriter.GetTileData(0).Length);
}
[Theory]
[InlineData((int)Av1ColorFormat.Yuv420)]
[InlineData((int)Av1ColorFormat.Yuv422)]
[InlineData((int)Av1ColorFormat.Yuv444)]
public void ProductionTileSelectsChromaFromReconstructedLuma(int colorFormatValue)
=> VerifyProductionTileSelectsChromaFromReconstructedLuma<byte>(
colorFormatValue,
8,
static (source, reconstruction, picture, coefficients, superblockWorkspace, blockWorkspace) =>
new(
Configuration.Default,
source,
reconstruction,
picture,
coefficients,
superblockWorkspace,
blockWorkspace,
initialSize: 512));
[Theory]
[InlineData((int)Av1ColorFormat.Yuv420, 10)]
[InlineData((int)Av1ColorFormat.Yuv420, 12)]
[InlineData((int)Av1ColorFormat.Yuv422, 10)]
[InlineData((int)Av1ColorFormat.Yuv422, 12)]
[InlineData((int)Av1ColorFormat.Yuv444, 10)]
[InlineData((int)Av1ColorFormat.Yuv444, 12)]
public void ProductionTileSelectsChromaFromReconstructedLumaHighBitDepth(
int colorFormatValue,
int bitDepth)
=> VerifyProductionTileSelectsChromaFromReconstructedLuma<ushort>(
colorFormatValue,
bitDepth,
static (source, reconstruction, picture, coefficients, superblockWorkspace, blockWorkspace) =>
new(
Configuration.Default,
source,
reconstruction,
picture,
coefficients,
superblockWorkspace,
blockWorkspace,
initialSize: 512));
private static void VerifyProductionTileSelectsChromaFromReconstructedLuma<TSample>(
int colorFormatValue,
int bitDepth,
TileWriterFactory<TSample> createWriter)
where TSample : unmanaged, IBinaryInteger<TSample>
{
const int Width = 16;
const int Height = 16;
const int QIndex = 1;
const int AlphaU = 16;
const int AlphaV = -16;
Av1ColorFormat colorFormat = (Av1ColorFormat)colorFormatValue;
bool subsamplingX = colorFormat is Av1ColorFormat.Yuv420 or Av1ColorFormat.Yuv422;
bool subsamplingY = colorFormat == Av1ColorFormat.Yuv420;
int chromaSubsamplingX = subsamplingX ? 1 : 0;
int chromaSubsamplingY = subsamplingY ? 1 : 0;
int sampleScale = 1 << (bitDepth - 8);
int midpoint = 1 << (bitDepth - 1);
int maxSample = (1 << bitDepth) - 1;
Av1TransformSize transformSize = Av1BlockSize.Block8x8.GetMaxUvTransformSize(
subsamplingX,
subsamplingY);
ObuColorConfig colorConfig = new()
{
IsMonochrome = false,
SubSamplingX = subsamplingX,
SubSamplingY = subsamplingY,
BitDepth = (Av1BitDepth)((bitDepth - 8) / 2)
};
using Av1EncoderFrameBuffer<TSample> pilotSource = new(
Configuration.Default,
Width,
Height,
bitDepth,
colorFormat,
chromaSubsamplingX,
chromaSubsamplingY);
using Av1EncoderFrameBuffer<TSample> pilotReconstruction = new(
Configuration.Default,
Width,
Height,
bitDepth,
colorFormat,
chromaSubsamplingX,
chromaSubsamplingY);
Buffer2DRegion<TSample> pilotLuma = pilotSource.Frame.CodedView.GetPlane(Av1Plane.Y);
for (int y = 0; y < pilotLuma.Height; y++)
{
Span<TSample> row = pilotLuma.DangerousGetRowSpan(y);
for (int x = 0; x < row.Length; x++)
{
row[x] = TSample.CreateChecked(
(96 + (((x * 29) + (y * 47) + (((x ^ y) & 1) * 53)) & 63)) * sampleScale);
}
}
FillPlane(pilotSource.Frame.CodedView.GetPlane(Av1Plane.U), TSample.CreateChecked(midpoint));
FillPlane(pilotSource.Frame.CodedView.GetPlane(Av1Plane.V), TSample.CreateChecked(midpoint));
ClearPlane(pilotReconstruction.Luma);
ClearPlane(Assert.IsType<Buffer2D<TSample>>(pilotReconstruction.ChromaBlue));
ClearPlane(Assert.IsType<Buffer2D<TSample>>(pilotReconstruction.ChromaRed));
using Av1EncoderModeInfoBuffer pilotModeInfo = new(Configuration.Default, Width, Height, disallow4x4AllFrames: true);
Av1PictureControlSet pilotTemplate = CreatePicture(pilotModeInfo, colorConfig, use128x128Superblock: false, QIndex);
using Av1EncoderPictureBuffer pilotPicture = new(
Configuration.Default,
pilotTemplate.Sequence.SequenceHeader,
pilotTemplate.Parent.FrameHeader,
Width,
Height);
using Av1EncoderCoefficientBuffer pilotCoefficients = new(
Configuration.Default,
pilotTemplate.Sequence.SequenceHeader,
Width,
Height);
using Av1EncoderSuperblockWorkspace pilotSuperblockWorkspace = new(Configuration.Default);
using Av1EncoderBlockWorkspace pilotBlockWorkspace = new(Configuration.Default);
using Av1IntraTileWriter pilotWriter = createWriter(
pilotSource.Frame,
pilotReconstruction.Frame,
pilotPicture.Picture,
pilotCoefficients,
pilotSuperblockWorkspace,
pilotBlockWorkspace);
Buffer2DRegion<TSample> reconstructedLuma = pilotReconstruction.Frame.CodedView.GetPlane(Av1Plane.Y);
int chromaWidth = transformSize.GetWidth();
int chromaHeight = transformSize.GetHeight();
int sampleCount = transformSize.GetSize2d();
int lumaScaleShift = 3 - chromaSubsamplingX - chromaSubsamplingY;
Span<short> lumaQ3 = stackalloc short[64];
int sumQ3 = sampleCount >> 1;
for (int row = 0; row < chromaHeight; row++)
{
for (int column = 0; column < chromaWidth; column++)
{
int lumaSum = 0;
int lumaX = 8 + (column << chromaSubsamplingX);
int lumaY = 8 + (row << chromaSubsamplingY);
for (int offsetY = 0; offsetY <= chromaSubsamplingY; offsetY++)
{
ReadOnlySpan<TSample> lumaRow = reconstructedLuma.DangerousGetRowSpan(lumaY + offsetY);
for (int offsetX = 0; offsetX <= chromaSubsamplingX; offsetX++)
{
lumaSum += int.CreateChecked(lumaRow[lumaX + offsetX]);
}
}
short sampleQ3 = (short)(lumaSum << lumaScaleShift);
lumaQ3[(row * chromaWidth) + column] = sampleQ3;
sumQ3 += sampleQ3;
}
}
int averageQ3 = sumQ3 >> (transformSize.GetBlockWidthLog2() + transformSize.GetBlockHeightLog2());
using Av1EncoderFrameBuffer<TSample> source = new(
Configuration.Default,
Width,
Height,
bitDepth,
colorFormat,
chromaSubsamplingX,
chromaSubsamplingY);
using Av1EncoderFrameBuffer<TSample> reconstruction = new(
Configuration.Default,
Width,
Height,
bitDepth,
colorFormat,
chromaSubsamplingX,
chromaSubsamplingY);
for (int y = 0; y < pilotLuma.Height; y++)
{
pilotLuma.DangerousGetRowSpan(y).CopyTo(source.Frame.CodedView.GetPlane(Av1Plane.Y).DangerousGetRowSpan(y));
}
Buffer2DRegion<TSample> blue = source.Frame.CodedView.GetPlane(Av1Plane.U);
Buffer2DRegion<TSample> red = source.Frame.CodedView.GetPlane(Av1Plane.V);
FillPlane(blue, TSample.CreateChecked(midpoint));
FillPlane(red, TSample.CreateChecked(midpoint));
for (int row = 0; row < chromaHeight; row++)
{
Span<TSample> blueRow = blue.DangerousGetRowSpan(chromaHeight + row);
Span<TSample> redRow = red.DangerousGetRowSpan(chromaHeight + row);
for (int column = 0; column < chromaWidth; column++)
{
int acQ3 = lumaQ3[(row * chromaWidth) + column] - averageQ3;
int blueProduct = AlphaU * acQ3;
int redProduct = AlphaV * acQ3;
int blueAdjustment = (blueProduct + 32 + (blueProduct >> 31)) >> 6;
int redAdjustment = (redProduct + 32 + (redProduct >> 31)) >> 6;
blueRow[chromaWidth + column] = TSample.CreateChecked(Math.Clamp(midpoint + blueAdjustment, 0, maxSample));
redRow[chromaWidth + column] = TSample.CreateChecked(Math.Clamp(midpoint + redAdjustment, 0, maxSample));
}
}
ClearPlane(reconstruction.Luma);
ClearPlane(Assert.IsType<Buffer2D<TSample>>(reconstruction.ChromaBlue));
ClearPlane(Assert.IsType<Buffer2D<TSample>>(reconstruction.ChromaRed));
using Av1EncoderModeInfoBuffer modeInfo = new(Configuration.Default, Width, Height, disallow4x4AllFrames: true);
Av1PictureControlSet pictureTemplate = CreatePicture(modeInfo, colorConfig, use128x128Superblock: false, QIndex);
using Av1EncoderPictureBuffer picture = new(
Configuration.Default,
pictureTemplate.Sequence.SequenceHeader,
pictureTemplate.Parent.FrameHeader,
Width,
Height);
using Av1EncoderCoefficientBuffer coefficients = new(
Configuration.Default,
pictureTemplate.Sequence.SequenceHeader,
Width,
Height);
using Av1EncoderSuperblockWorkspace superblockWorkspace = new(Configuration.Default);
using Av1EncoderBlockWorkspace blockWorkspace = new(Configuration.Default);
using Av1IntraTileWriter tileWriter = createWriter(
source.Frame,
reconstruction.Frame,
picture.Picture,
coefficients,
superblockWorkspace,
blockWorkspace);
Buffer2DRegion<TSample> actualLuma = reconstruction.Frame.CodedView.GetPlane(Av1Plane.Y);
for (int y = 0; y < reconstructedLuma.Height; y++)
{
Assert.Equal(reconstructedLuma.DangerousGetRowSpan(y), actualLuma.DangerousGetRowSpan(y));
}
ref Av1MacroBlockModeInfo targetBlock = ref picture.Picture.GetMacroBlockModeInfo(new Point(2, 2));
Assert.Equal(Av1ChromaPredictionMode.ChromaFromLuma, targetBlock.Block.UvMode);
Assert.Equal(
Av1ChromaFromLumaMath.JointSign(
Av1ChromaFromLumaMath.SignPositive,
Av1ChromaFromLumaMath.SignNegative),
superblockWorkspace.FinalBlocks[3].PredictionUnit.ChromaFromLumaSigns);
Assert.Equal(
Av1ChromaFromLumaMath.PackIndices(
Av1ChromaFromLumaMath.AlphaToMagnitudeIndex(AlphaU),
Av1ChromaFromLumaMath.AlphaToMagnitudeIndex(AlphaV)),
superblockWorkspace.FinalBlocks[3].PredictionUnit.ChromaFromLumaIndex);
int targetTransformIndex = (3 * sampleCount) /
Av1EncoderCoefficientBuffer.TransformBlockUnitCoefficientCount;
Av1EncoderTransformBlockState blueState =
coefficients.GetTransformBlockSpan(0, Av1Plane.U)[targetTransformIndex];
Av1EncoderTransformBlockState redState =
coefficients.GetTransformBlockSpan(0, Av1Plane.V)[targetTransformIndex];
Assert.Equal((ushort)0, blueState.EndOfBlock);
Assert.Equal((ushort)0, redState.EndOfBlock);
Assert.Equal(Av1TransformType.DctDct, blueState.TransformType);
Assert.Equal(Av1TransformType.DctDct, redState.TransformType);
Assert.NotEqual(0, pilotWriter.GetTileData(0).Length);
Assert.NotEqual(0, tileWriter.GetTileData(0).Length);
}
[Fact]
public void ProductionDirectionalModesConsumeAvailableExtendedEdges()
{
@ -1313,7 +1586,8 @@ public class Av1IntraSuperblockEncoderTests
}
}
private static void FillPlane(Buffer2DRegion<byte> plane, byte value)
private static void FillPlane<TSample>(Buffer2DRegion<TSample> plane, TSample value)
where TSample : unmanaged
{
for (int y = 0; y < plane.Height; y++)
{
@ -1321,6 +1595,15 @@ public class Av1IntraSuperblockEncoderTests
}
}
private delegate Av1IntraTileWriter TileWriterFactory<TSample>(
Av1EncoderFrame<TSample> source,
Av1EncoderFrame<TSample> reconstruction,
Av1PictureControlSet picture,
Av1EncoderCoefficientBuffer coefficients,
Av1EncoderSuperblockWorkspace superblockWorkspace,
Av1EncoderBlockWorkspace blockWorkspace)
where TSample : unmanaged;
private static void ClearPlane<TSample>(Buffer2D<TSample> plane)
where TSample : unmanaged
{

Loading…
Cancel
Save