Browse Source

Fix AV1 tile coefficient contexts

pull/2633/head
James Jackson-South 1 month ago
parent
commit
d813ae7d72
  1. 2
      HEIF_IMPLEMENTATION_PLAN.md
  2. 75
      src/ImageSharp/Formats/Heif/Av1/Tiling/Av1NeighborArrayUnit.cs
  3. 325
      src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileWriter.cs
  4. 56
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1CoefficientsEntropyTests.cs

2
HEIF_IMPLEMENTATION_PLAN.md

@ -820,7 +820,7 @@ Encoder verification contract:
- [ ] Implement inter mode search for bounded sequences, including reference selection and the decoder-supported inter tools.
- [~] Current-libaom `av1_quantize_fp_no_qmatrix` arithmetic is implemented as a closed generic forward-quantizer family with Vector512, Vector256, Vector128, and scalar paths, raster-order output, coded 64-point coefficient limits, and scan-order EOB selection. Transform search, coefficient optimization, and lossless behavior remain.
- [ ] Implement real rate-distortion selection and make quality and effort change work, size, and output quality.
- [ ] Implement tile-local entropy coding and CDF update behavior.
- [~] The tile writer now publishes one packed coefficient context per covered 4x4 edge unit and derives luma/chroma skip plus DC-sign contexts from the complete transform edges using current-libaom units. Complete tile traversal, initialized picture state, and verified CDF update behavior remain.
- [ ] Implement legal deblocking, CDEF, restoration, super-resolution, and film-grain signaling decisions.
- [~] The coefficient symbol encoder now reuses tile-lifetime level and context workspaces instead of allocating per transform. Every remaining encoder fragment must be audited before it becomes active.
- [~] The planar conversion, forward transform, and forward quantizer use descending SIMD dispatch: Vector512, Vector256, Vector128, then scalar. Apply the same rule to every later hot-path family.

75
src/ImageSharp/Formats/Heif/Av1/Tiling/Av1NeighborArrayUnit.cs

@ -3,7 +3,6 @@
using System.Buffers;
using System.Numerics;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
@ -127,11 +126,6 @@ internal sealed class Av1NeighborArrayUnit<T> : IDisposable
/// </summary>
public required int GranularityTopLeftLog2 { get; set; }
/// <summary>
/// Gets the number of consecutive values stored for each neighbor-array unit.
/// </summary>
public int UnitSize { get; private set; }
/// <summary>
/// Gets the left-neighbor unit index for a sample position.
/// </summary>
@ -166,20 +160,12 @@ internal sealed class Av1NeighborArrayUnit<T> : IDisposable
/// <summary>
/// Writes one context unit across the selected block edges.
/// </summary>
/// <param name="value">The values that make up one context unit.</param>
/// <param name="value">The context value to publish.</param>
/// <param name="origin">The block origin in samples.</param>
/// <param name="blockSize">The block dimensions in samples.</param>
/// <param name="mask">The neighbor arrays to update.</param>
public void UnitModeWrite(ReadOnlySpan<T> value, Point origin, Size blockSize, UnitMask mask)
public void UnitModeWrite(T value, Point origin, Size blockSize, UnitMask mask)
{
int idx, j;
int count;
int na_offset;
int na_unit_size;
na_unit_size = this.UnitSize;
if ((mask & UnitMask.Top) == UnitMask.Top)
{
// Top Neighbor Array
@ -196,21 +182,12 @@ internal sealed class Av1NeighborArrayUnit<T> : IDisposable
// bottom row of the source block
//
// Index = org_x
na_offset = this.GetTopIndex(origin);
ref T dst_ptr = ref this.Top[na_offset * na_unit_size];
int offset = this.GetTopIndex(origin);
int count = blockSize.Width >> this.GranularityNormalLog2;
count = blockSize.Width >> this.GranularityNormalLog2;
for (idx = 0; idx < count; ++idx)
{
// Unit sizes are deliberately tiny, so direct ref copies avoid slicing for every neighbor position.
for (j = 0; j < na_unit_size; ++j)
{
dst_ptr = value[j];
dst_ptr = Unsafe.Add(ref dst_ptr, 1);
}
}
// One packed value represents each AV1 edge unit. Filling the covered range mirrors the
// contiguous above-context update without retaining a caller-owned span.
this.Top.Slice(offset, count).Fill(value);
}
if ((mask & UnitMask.Left) == UnitMask.Left)
@ -230,21 +207,9 @@ internal sealed class Av1NeighborArrayUnit<T> : IDisposable
// right column of the source block
//
// Index = org_y
na_offset = this.GetLeftIndex(origin);
ref T dst_ptr = ref this.Left[na_offset * na_unit_size];
count = blockSize.Height >> this.GranularityNormalLog2;
for (idx = 0; idx < count; ++idx)
{
// Unit sizes are deliberately tiny, so direct ref copies avoid slicing for every neighbor position.
for (j = 0; j < na_unit_size; ++j)
{
dst_ptr = value[j];
dst_ptr = Unsafe.Add(ref dst_ptr, 1);
}
}
int offset = this.GetLeftIndex(origin);
int count = blockSize.Height >> this.GranularityNormalLog2;
this.Left.Slice(offset, count).Fill(value);
}
if ((mask & UnitMask.TopLeft) == UnitMask.TopLeft)
@ -267,23 +232,9 @@ internal sealed class Av1NeighborArrayUnit<T> : IDisposable
// Index = org_x - org_y
Point topLeft = origin;
topLeft.Offset(0, blockSize.Height - 1);
na_offset = this.GetTopLeftIndex(topLeft);
// Copy bottom-row + right-column
// *Note - start from the bottom-left corner
ref T dst_ptr = ref this.TopLeft[na_offset * na_unit_size];
count = ((blockSize.Width + blockSize.Height) >> this.GranularityTopLeftLog2) - 1;
for (idx = 0; idx < count; ++idx)
{
// Unit sizes are deliberately tiny, so direct ref copies avoid slicing for every neighbor position.
for (j = 0; j < na_unit_size; ++j)
{
dst_ptr = value[j];
dst_ptr = Unsafe.Add(ref dst_ptr, 1);
}
}
int offset = this.GetTopLeftIndex(topLeft);
int count = ((blockSize.Width + blockSize.Height) >> this.GranularityTopLeftLog2) - 1;
this.TopLeft.Slice(offset, count).Fill(value);
}
}
}

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

@ -1,7 +1,6 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Formats.Heif.Av1.Entropy;
using SixLabors.ImageSharp.Formats.Heif.Av1.ModeDecision;
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
@ -747,9 +746,8 @@ internal partial class Av1TileWriter
PartitionContextLookup[(int)blockSize].Above,
PartitionContextLookup[(int)blockSize].Left);
Size size = new(blk_geom.BlockWidth, blk_geom.BlockHeight);
Span<Av1PartitionContext> partitionSpan = new(ref partition);
partition_context_na.UnitModeWrite(
partitionSpan,
partition,
blockOrigin,
size,
Av1NeighborArrayUnit<Av1PartitionContext>.UnitMask.Left | Av1NeighborArrayUnit<Av1PartitionContext>.UnitMask.Top);
@ -757,26 +755,26 @@ internal partial class Av1TileWriter
{
// A skipped block has an all-zero residual, so publish a zero sign/level context over its edges
// and advance coefficient positions without reading transform units.
byte dcSignLevelCoefficient = 0;
Span<byte> dcSignSpan = new(ref dcSignLevelCoefficient);
luma_dc_sign_level_coeff_na.UnitModeWrite(
dcSignSpan,
0,
blockOrigin,
size,
Av1NeighborArrayUnit<byte>.UnitMask.Left | Av1NeighborArrayUnit<byte>.UnitMask.Top);
if (blk_geom.HasUv)
{
Point chromaOrigin = RoundUv(blockOrigin) >> 1;
Size chromaSize = new(blk_geom.BlockWidthUv, blk_geom.BlockHeightUv);
cb_dc_sign_level_coeff_na.UnitModeWrite(
dcSignSpan,
((blockOrigin >> 3) << 3) >> 1,
size,
0,
chromaOrigin,
chromaSize,
Av1NeighborArrayUnit<byte>.UnitMask.Left | Av1NeighborArrayUnit<byte>.UnitMask.Top);
cr_dc_sign_level_coeff_na.UnitModeWrite(
dcSignSpan,
((blockOrigin >> 3) << 3) >> 1,
size,
0,
chromaOrigin,
chromaSize,
Av1NeighborArrayUnit<byte>.UnitMask.Left | Av1NeighborArrayUnit<byte>.UnitMask.Top);
entropyCodingContext.CodedAreaSuperblockUv += blk_geom.BlockWidthUv * blk_geom.BlockHeightUv;
}
@ -1043,16 +1041,13 @@ internal partial class Av1TileWriter
int coeff1d_offset = entropyCodingContext.CodedAreaSuperblock;
Span<int> coeff_buffer = coeff_ptr.GetPlaneBuffer(Av1Plane.Y).DangerousGetSingleSpan()[coeff1d_offset..];
Av1TransformBlockContext blockContext = default;
Point transformOrigin = blockGeometry.TransformOrigin[tx_depth][txb_itr];
GetTransformBlockContexts(
pcs,
Av1TransformBlockContext blockContext = GetTransformBlockContexts(
Av1ComponentType.Luminance,
luma_dc_sign_level_coeff_na,
blockOrigin + (Size)transformOrigin - (Size)blockGeometry.Origin,
plane_bsize,
tx_size,
blockContext);
tx_size);
Av1TransformType tx_type = blk_ptr.TransformBlocks[txb_itr].TransformType[(int)Av1ComponentType.Luminance];
int eob = blk_ptr.TransformBlocks[txb_itr].NzCoefficientCount[0];
@ -1063,7 +1058,7 @@ internal partial class Av1TileWriter
Guard.IsTrue(tx_type == Av1TransformType.DctDct, nameof(tx_type), string.Empty);
}
int cul_level_y = writer.WriteCoefficients(
int culLevelY = writer.WriteCoefficients(
tx_size,
tx_type,
intraLumaDir,
@ -1074,15 +1069,12 @@ internal partial class Av1TileWriter
frameHeader.UseReducedTransformSet,
blk_ptr.FilterIntraMode);
// WriteCoefficients packs the DC sign and cumulative level into one integer; publish its bytes
// across the transform edges so the next blocks derive identical entropy contexts.
Span<int> culLevelSpan = new(ref cul_level_y);
ReadOnlySpan<byte> dc_sign_level_coeff = MemoryMarshal.AsBytes(culLevelSpan);
// Only the packed low byte is the AV1 entropy context. Converting the value explicitly keeps
// the update independent of machine endianness and publishes one value per covered edge unit.
int transformWidth = blockGeometry.TransformSize[tx_depth].GetWidth();
int transformHeight = blockGeometry.TransformSize[tx_depth].GetHeight();
luma_dc_sign_level_coeff_na.UnitModeWrite(
dc_sign_level_coeff,
(byte)culLevelY,
blockOrigin + (Size)transformOrigin - (Size)blockGeometry.Origin,
new Size(transformWidth, transformHeight),
Av1NeighborArrayUnit<byte>.UnitMask.Top | Av1NeighborArrayUnit<byte>.UnitMask.Left);
@ -1128,81 +1120,69 @@ internal partial class Av1TileWriter
int tx_depth = mbmi.Block.TransformDepth;
uint txb_count = 1;
ObuFrameHeader frameHeader = pcs.Parent.FrameHeader;
int transformWidth = blockGeometry.TransformSize[tx_depth].GetWidth();
int transformHeight = blockGeometry.TransformSize[tx_depth].GetHeight();
for (uint tx_index = 0; tx_index < txb_count; ++tx_index)
{
Av1TransformSize chroma_tx_size = blockGeometry.TransformSizeUv[tx_depth];
Av1TransformSize chromaTransformSize = blockGeometry.TransformSizeUv[tx_depth];
int transformWidth = chromaTransformSize.GetWidth();
int transformHeight = chromaTransformSize.GetHeight();
Point transformOrigin = blockGeometry.TransformOrigin[tx_depth][tx_index];
Point chromaOrigin = RoundUv(blockOrigin + (Size)transformOrigin - (Size)blockGeometry.Origin) >> 1;
// Both chroma planes share transform geometry but retain independent coefficient contexts.
Span<int> coefficientBuffer = coeff_ptr.GetPlaneBuffer(Av1Plane.U).DangerousGetSingleSpan().Slice(entropyCodingContext.CodedAreaSuperblockUv);
Av1TransformBlockContext blockContext = GetTransformBlockContexts(
Av1ComponentType.Chroma,
cb_dc_sign_level_coeff_na,
chromaOrigin,
blockGeometry.BlockSizeUv,
chromaTransformSize);
Av1TransformType chromaTransformType = blk_ptr.TransformBlocks[tx_index].TransformType[(int)Av1ComponentType.Chroma];
int endOfBlockCb = blk_ptr.TransformBlocks[tx_index].NzCoefficientCount[1];
int culLevelCb = writer.WriteCoefficients(
chromaTransformSize,
chromaTransformType,
intraLumaDir,
coefficientBuffer,
Av1ComponentType.Chroma,
blockContext,
(ushort)endOfBlockCb,
frameHeader.UseReducedTransformSet,
blk_ptr.FilterIntraMode);
if (blockGeometry.HasUv)
{
// Both chroma planes share transform geometry but retain independent coefficient contexts.
Span<int> coeff_buffer = coeff_ptr.GetPlaneBuffer(Av1Plane.U).DangerousGetSingleSpan().Slice(entropyCodingContext.CodedAreaSuperblockUv);
Av1TransformBlockContext blockContext = default;
Point transformOrigin = blockGeometry.TransformOrigin[tx_depth][tx_index];
GetTransformBlockContexts(
pcs,
Av1ComponentType.Chroma,
cb_dc_sign_level_coeff_na,
RoundUv(blockOrigin + (Size)transformOrigin - (Size)blockGeometry.Origin) >> 1,
blockGeometry.BlockSizeUv,
chroma_tx_size,
blockContext);
Av1TransformType chroma_tx_type = blk_ptr.TransformBlocks[tx_index].TransformType[(int)Av1ComponentType.Chroma];
int endOfBlockCb = blk_ptr.TransformBlocks[tx_index].NzCoefficientCount[1];
int cul_level_cb = writer.WriteCoefficients(
chroma_tx_size,
chroma_tx_type,
intraLumaDir,
coeff_buffer,
Av1ComponentType.Chroma,
blockContext,
(ushort)endOfBlockCb,
frameHeader.UseReducedTransformSet,
blk_ptr.FilterIntraMode);
coeff_buffer = coeff_ptr.GetPlaneBuffer(Av1Plane.V).DangerousGetSingleSpan().Slice(entropyCodingContext.CodedAreaSuperblockUv);
blockContext = default;
int endOfBlockCr = blk_ptr.TransformBlocks[tx_index].NzCoefficientCount[2];
GetTransformBlockContexts(
pcs,
Av1ComponentType.Chroma,
cr_dc_sign_level_coeff_na,
RoundUv(blockOrigin + (Size)transformOrigin - (Size)blockGeometry.Origin) >> 1,
blockGeometry.BlockSizeUv,
chroma_tx_size,
blockContext);
int cul_level_cr = writer.WriteCoefficients(
chroma_tx_size,
chroma_tx_type,
intraLumaDir,
coeff_buffer,
Av1ComponentType.Chroma,
blockContext,
(ushort)endOfBlockCr,
frameHeader.UseReducedTransformSet,
blk_ptr.FilterIntraMode);
// Publish each plane's packed sign/level summary across its transform edges.
Span<int> culLevelCbSpan = new(ref cul_level_cb);
ReadOnlySpan<byte> dc_sign_level_coeff = MemoryMarshal.AsBytes(culLevelCbSpan);
cb_dc_sign_level_coeff_na.UnitModeWrite(
dc_sign_level_coeff,
RoundUv(transformOrigin) >> 1,
new Size(transformWidth, transformHeight),
Av1NeighborArrayUnit<byte>.UnitMask.Top | Av1NeighborArrayUnit<byte>.UnitMask.Left);
coefficientBuffer = coeff_ptr.GetPlaneBuffer(Av1Plane.V).DangerousGetSingleSpan().Slice(entropyCodingContext.CodedAreaSuperblockUv);
int endOfBlockCr = blk_ptr.TransformBlocks[tx_index].NzCoefficientCount[2];
Span<int> culLevelCrSpan = new(ref cul_level_cr);
dc_sign_level_coeff = MemoryMarshal.AsBytes(culLevelCrSpan);
cr_dc_sign_level_coeff_na.UnitModeWrite(
dc_sign_level_coeff,
RoundUv(transformOrigin) >> 1,
new Size(transformWidth, transformHeight),
Av1NeighborArrayUnit<byte>.UnitMask.Top | Av1NeighborArrayUnit<byte>.UnitMask.Left);
}
blockContext = GetTransformBlockContexts(
Av1ComponentType.Chroma,
cr_dc_sign_level_coeff_na,
chromaOrigin,
blockGeometry.BlockSizeUv,
chromaTransformSize);
int culLevelCr = writer.WriteCoefficients(
chromaTransformSize,
chromaTransformType,
intraLumaDir,
coefficientBuffer,
Av1ComponentType.Chroma,
blockContext,
(ushort)endOfBlockCr,
frameHeader.UseReducedTransformSet,
blk_ptr.FilterIntraMode);
// Each plane publishes its packed context across the complete chroma transform edges.
cb_dc_sign_level_coeff_na.UnitModeWrite(
(byte)culLevelCb,
chromaOrigin,
new Size(transformWidth, transformHeight),
Av1NeighborArrayUnit<byte>.UnitMask.Top | Av1NeighborArrayUnit<byte>.UnitMask.Left);
cr_dc_sign_level_coeff_na.UnitModeWrite(
(byte)culLevelCr,
chromaOrigin,
new Size(transformWidth, transformHeight),
Av1NeighborArrayUnit<byte>.UnitMask.Top | Av1NeighborArrayUnit<byte>.UnitMask.Left);
entropyCodingContext.CodedAreaSuperblockUv += transformWidth * transformHeight;
}
@ -1218,84 +1198,65 @@ internal partial class Av1TileWriter
/// <summary>
/// Derives coefficient skip and DC-sign contexts from the transform block's above and left neighbors.
/// </summary>
/// <param name="pcs">The picture coding state.</param>
/// <param name="plane">The luma or chroma component class.</param>
/// <param name="dcSignLevelCoefficientNeighborArray">The packed DC-sign and coefficient-level neighbor contexts.</param>
/// <param name="blockOrigin">The transform-block origin in samples of the target plane.</param>
/// <param name="planeBlockSize">The containing block size on the target plane.</param>
/// <param name="transformSize">The transform size.</param>
/// <param name="blockContext">The context object to populate.</param>
private static void GetTransformBlockContexts(
Av1PictureControlSet pcs,
/// <returns>The coefficient skip and DC-sign contexts selected by both transform edges.</returns>
public static Av1TransformBlockContext GetTransformBlockContexts(
Av1ComponentType plane,
Av1NeighborArrayUnit<byte> dcSignLevelCoefficientNeighborArray,
Point blockOrigin,
Av1BlockSize planeBlockSize,
Av1TransformSize transformSize,
Av1TransformBlockContext blockContext)
Av1TransformSize transformSize)
{
int dcSignLevelCoefficientLeftNeighborIndex = dcSignLevelCoefficientNeighborArray.GetLeftIndex(blockOrigin);
int dcSignLevelCoefficientTopNeighborIndex = dcSignLevelCoefficientNeighborArray.GetTopIndex(blockOrigin);
sbyte[] signs = [0, -1, 1];
int transformBlockWidth;
int transformBlockHeight;
if (plane != Av1ComponentType.Luminance)
{
transformBlockWidth = Math.Min(transformSize.GetWidth(), ((pcs.Parent.AlignedWidth / 2) - blockOrigin.X) >> 2);
transformBlockHeight = Math.Min(transformSize.GetHeight(), ((pcs.Parent.AlignedHeight / 2) - blockOrigin.Y) >> 2);
}
else
int leftIndex = dcSignLevelCoefficientNeighborArray.GetLeftIndex(blockOrigin);
int topIndex = dcSignLevelCoefficientNeighborArray.GetTopIndex(blockOrigin);
int transformBlockWidth = transformSize.Get4x4WideCount();
int transformBlockHeight = transformSize.Get4x4HighCount();
ReadOnlySpan<byte> topContexts = dcSignLevelCoefficientNeighborArray.Top.Slice(topIndex, transformBlockWidth);
ReadOnlySpan<byte> leftContexts = dcSignLevelCoefficientNeighborArray.Left.Slice(leftIndex, transformBlockHeight);
int dcSign = 0;
int top = 0;
int left = 0;
// Each context packs a coefficient-level class in the low bits and the DC sign class above it.
// Accumulating both values in one traversal supplies every luma and chroma context without scratch storage.
foreach (byte context in topContexts)
{
transformBlockWidth = Math.Min(transformSize.GetWidth(), (pcs.Parent.AlignedWidth - blockOrigin.X) >> 2);
transformBlockHeight = Math.Min(transformSize.GetHeight(), (pcs.Parent.AlignedHeight - blockOrigin.Y) >> 2);
}
short dc_sign = 0;
ushort k = 0;
byte sign;
// The high bits encode the DC sign class: zero, negative, or positive. Summing classes over
// both edges selects whether neighboring DC coefficients bias the current sign context.
if (dcSignLevelCoefficientNeighborArray.Top[dcSignLevelCoefficientTopNeighborIndex] != Av1NeighborArrayUnit<byte>.InvalidNeighborData)
{
do
byte sign = (byte)(context >> Av1Constants.CoefficientContextBitCount);
DebugGuard.MustBeLessThanOrEqualTo(sign, (byte)2, nameof(sign));
if (sign == 1)
{
dcSign--;
}
else if (sign == 2)
{
sign = (byte)(dcSignLevelCoefficientNeighborArray.Top[k + dcSignLevelCoefficientTopNeighborIndex] >>
Av1Constants.CoefficientContextBitCount);
Guard.MustBeLessThanOrEqualTo(sign, (byte)2, nameof(sign));
dc_sign += signs[sign];
dcSign++;
}
while (++k < transformBlockWidth);
top |= context;
}
if (dcSignLevelCoefficientNeighborArray.Left[dcSignLevelCoefficientLeftNeighborIndex] != Av1NeighborArrayUnit<byte>.InvalidNeighborData)
foreach (byte context in leftContexts)
{
k = 0;
do
byte sign = (byte)(context >> Av1Constants.CoefficientContextBitCount);
DebugGuard.MustBeLessThanOrEqualTo(sign, (byte)2, nameof(sign));
if (sign == 1)
{
sign = (byte)(dcSignLevelCoefficientNeighborArray.Left[k + dcSignLevelCoefficientLeftNeighborIndex] >>
Av1Constants.CoefficientContextBitCount);
Guard.MustBeLessThanOrEqualTo(sign, (byte)2, nameof(sign));
dc_sign += signs[sign];
dcSign--;
}
else if (sign == 2)
{
dcSign++;
}
while (++k < transformBlockHeight);
}
if (dc_sign > 0)
{
blockContext.DcSignContext = 2;
}
else if (dc_sign < 0)
{
blockContext.DcSignContext = 1;
}
else
{
blockContext.DcSignContext = 0;
left |= context;
}
Av1TransformBlockContext blockContext = default;
blockContext.DcSignContext = dcSign > 0 ? 2 : dcSign < 0 ? 1 : 0;
if (plane == Av1ComponentType.Luminance)
{
if (planeBlockSize == transformSize.ToBlockSize())
@ -1304,33 +1265,7 @@ internal partial class Av1TileWriter
}
else
{
int top = 0;
int left = 0;
k = 0;
if (dcSignLevelCoefficientNeighborArray.Top[dcSignLevelCoefficientTopNeighborIndex] !=
Av1NeighborArrayUnit<byte>.InvalidNeighborData)
{
do
{
top |= dcSignLevelCoefficientNeighborArray.Top[k + dcSignLevelCoefficientTopNeighborIndex];
}
while (++k < transformBlockWidth);
}
top &= Av1Constants.CoefficientContextMask;
if (dcSignLevelCoefficientNeighborArray.Left[dcSignLevelCoefficientLeftNeighborIndex] !=
Av1NeighborArrayUnit<byte>.InvalidNeighborData)
{
k = 0;
do
{
left |= dcSignLevelCoefficientNeighborArray.Left[k + dcSignLevelCoefficientLeftNeighborIndex];
}
while (++k < transformBlockHeight);
}
left &= Av1Constants.CoefficientContextMask;
blockContext.SkipContext = Av1SymbolContextHelper.GetTransformBlockSkipContext(top, left);
}
@ -1339,36 +1274,12 @@ internal partial class Av1TileWriter
{
// Chroma contexts use only the presence of nonzero levels on each edge, plus an offset
// that distinguishes a transform smaller than its containing plane block.
short ctx_base_left = 0;
short ctx_base_top = 0;
if (dcSignLevelCoefficientNeighborArray.Top[dcSignLevelCoefficientTopNeighborIndex] !=
Av1NeighborArrayUnit<byte>.InvalidNeighborData)
{
k = 0;
do
{
ctx_base_top +=
(dcSignLevelCoefficientNeighborArray.Top[k + dcSignLevelCoefficientTopNeighborIndex] != 0) ? (short)1 : (short)0;
}
while (++k < transformBlockWidth);
}
if (dcSignLevelCoefficientNeighborArray.Left[dcSignLevelCoefficientLeftNeighborIndex] !=
Av1NeighborArrayUnit<byte>.InvalidNeighborData)
{
k = 0;
do
{
ctx_base_left += dcSignLevelCoefficientNeighborArray.Left[k + dcSignLevelCoefficientLeftNeighborIndex] != 0 ? (short)1 : (short)0;
}
while (++k < transformBlockHeight);
}
int ctx_base = ((ctx_base_left != 0) ? 1 : 0) + ((ctx_base_top != 0) ? 1 : 0);
int ctx_offset = planeBlockSize.GetPelsLog2Count() > transformSize.ToBlockSize().GetPelsLog2Count() ? 10 : 7;
blockContext.SkipContext = (short)(ctx_base + ctx_offset);
int contextBase = (left != 0 ? 1 : 0) + (top != 0 ? 1 : 0);
int contextOffset = planeBlockSize.GetPelsLog2Count() > transformSize.ToBlockSize().GetPelsLog2Count() ? 10 : 7;
blockContext.SkipContext = contextBase + contextOffset;
}
return blockContext;
}
/// <summary>

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

@ -16,6 +16,62 @@ public class Av1CoefficientsEntropyTests
{
private const int BaseQIndex = 23;
[Fact]
public void NeighborArrayWritesEveryCoveredFourByFourEdgeUnit()
{
using Av1NeighborArrayUnit<byte> neighbors = new(
Configuration.Default,
leftSize: 8,
topSize: 8,
topLeftSize: 16)
{
GranularityNormalLog2 = 2,
GranularityTopLeftLog2 = 2
};
neighbors.UnitModeWrite(
37,
new Point(8, 4),
new Size(16, 8),
Av1NeighborArrayUnit<byte>.UnitMask.Top | Av1NeighborArrayUnit<byte>.UnitMask.Left);
Assert.Equal(new byte[] { 0, 0, 37, 37, 37, 37, 0, 0 }, neighbors.Top.ToArray());
Assert.Equal(new byte[] { 0, 37, 37, 0, 0, 0, 0, 0 }, neighbors.Left.ToArray());
}
[Theory]
[InlineData((int)Av1ComponentType.Luminance, 5)]
[InlineData((int)Av1ComponentType.Chroma, 12)]
public void WriterDerivesTransformContextFromCompleteFourByFourEdges(
int componentType,
int expectedSkipContext)
{
using Av1NeighborArrayUnit<byte> neighbors = new(
Configuration.Default,
leftSize: 8,
topSize: 8,
topLeftSize: 16)
{
GranularityNormalLog2 = 2,
GranularityTopLeftLog2 = 2
};
// The high bits carry positive, positive, and negative DC signs. The low bits select
// the high-above and low-left coefficient classes used by the luma skip-context table.
neighbors.Top[2] = (2 << Av1Constants.CoefficientContextBitCount) | 4;
neighbors.Top[3] = 2 << Av1Constants.CoefficientContextBitCount;
neighbors.Left[1] = (1 << Av1Constants.CoefficientContextBitCount) | 1;
Av1TransformBlockContext context = Av1TileWriter.GetTransformBlockContexts(
(Av1ComponentType)componentType,
neighbors,
new Point(8, 4),
Av1BlockSize.Block16x16,
Av1TransformSize.Size8x8);
Assert.Equal(2, context.DcSignContext);
Assert.Equal(expectedSkipContext, context.SkipContext);
}
[Fact]
public void RoundTripZeroEndOfBlock()
{

Loading…
Cancel
Save