Browse Source

Integrate AV1 CDEF filtering

pull/2633/head
James Jackson-South 1 week ago
parent
commit
0b697cda44
  1. 3
      HEIF_IMPLEMENTATION_PLAN.md
  2. 5
      src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1FrameDecoder.cs
  3. 352
      src/ImageSharp/Formats/Heif/Av1/Pipeline/Cdef/Av1CdefDecoder.cs
  4. 321
      src/ImageSharp/Formats/Heif/Av1/Pipeline/Cdef/Av1CdefKernels.cs
  5. 2
      src/ImageSharp/Formats/Heif/Av1/Tiling/Av1FrameInfo.cs
  6. 6
      src/ImageSharp/Formats/Heif/Av1/Tiling/Av1PartitionInfo.cs
  7. 44
      src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileReader.cs

3
HEIF_IMPLEMENTATION_PLAN.md

@ -72,6 +72,7 @@ This snapshot pins or classifies the available references and failures; it does
| `Av1PredictionDecoder`, `Av1HighBitDepthPredictor`, `Av1ChromaFromLumaContext`, `Av1PartitionInfo`, and the scalar DC, directional, Paeth, smooth, filter-intra, and chroma-from-luma predictors | AV1 sections 7.11.2 and 7.11.2.3 intra prediction | libaom `aom_dsp/intrapred.c`, `av1/common/reconintra.c`, `av1/common/av1_common_int.h`, `av1/common/blockd.h`, `av1/common/cfl.c`, and `av1/common/cfl.h` at `03087864cf4bea6abb0d28f95cf7843511413d8f` | Behavioral oracle for luma/chroma mode-neighbor addressing, directional upsampling, Paeth selection, smooth normalization, filter-intra taps, high-bit-depth clipping, chroma-from-luma storage/subsampling, and chroma-from-luma row strides. Existing managed scalar tables and predictors remain the implementation base. The WIP rectangular byte-pipeline smooth digest expectations encode width/height-swapped weights and must be replaced only from an independently generated oracle, not regenerated from this implementation. |
| `Av1TileReader` palette mode/color-map parsing, `Av1SymbolDecoder` palette distributions, `Av1BlockModeInfo` palette state, and `Av1PredictionDecoder` palette reconstruction | AV1 sections 5.11.46, 5.11.49, and 7.11.2 palette prediction | libaom `av1/decoder/decodemv.c`, `av1/decoder/detokenize.c`, `av1/decoder/decoder.h`, `av1/common/pred_common.c`, `av1/common/pred_common.h`, and `av1/common/entropymode.c` at `03087864cf4bea6abb0d28f95cf7843511413d8f` | Reuse the existing tile range decoder and frame-owned mode map while porting the normative palette cache merge, high-bit-depth color deltas, tile-adaptive mode/size/index distributions, diagonal color-map traversal, edge padding, and direct palette-sample reconstruction. This is AV1 still-image compression syntax and does not add retained video reference state or any ISO BMFF surface. |
| `Av1LoopFilterKernels`, `Av1LoopFilterContext`, and `Av1LoopFilterDecoder` | AV1 section 7.14 deblocking loop filter | libaom `aom_dsp/loopfilter.c` and `av1/common/av1_loopfilter.c` at `03087864cf4bea6abb0d28f95cf7843511413d8f` | Port the scalar 4-, 6-, 8-, and 14-tap low/high-bit-depth filters, sharpness thresholds, still-frame intra filter-level derivation, transform-edge selection, and plane traversal before enabling the stage. Later SIMD must preserve the scalar result. This is normative AV1 image reconstruction and adds neither generic ISO BMFF models nor retained video reference state. |
| `Av1CdefDecoder`, `Av1CdefKernels`, and CDEF-unit strength storage | AV1 sections 7.15.2 through 7.15.4 constrained directional enhancement filtering | libaom `av1/common/cdef.c`, `av1/common/cdef_block.c`, `av1/common/cdef.h`, and `av1/common/cdef_block.h` at `03087864cf4bea6abb0d28f95cf7843511413d8f` | Port the scalar direction search, variance adjustment, constrained primary/secondary taps, subsampling direction conversion, skipped-8x8 selection, and frame-edge sentinel behavior. Use a frame-owned source snapshot so filtering never consumes already modified samples. This is normative AV1 still-image reconstruction and introduces no ISO BMFF, track, timing, or sequence-playback surface. |
| `Av1FrameInfo`, `Av1TileReader`, and `Av1BlockDecoder` transform/coefficient storage | AV1 section 5.11.39 coefficient syntax and section 7.11.2 reconstruction | libaom `av1/decoder/decodetxb.c` and `av1/decoder/decoder.h` at `03087864cf4bea6abb0d28f95cf7843511413d8f` | Preserve separate luma and chroma transform coefficients at monotonically advancing per-plane offsets within each superblock so reconstruction consumes the same transform-block order produced by tile parsing. |
| `Av1InverseQuantizer` and `Av1InverseQuantizationLookup` | AV1 section 7.12.3 inverse quantization | libaom `aom_dsp/aom_dsp_common.h`, `av1/common/quant_common.c`, and `av1/decoder/decodetxb.c` at `03087864cf4bea6abb0d28f95cf7843511413d8f` | Select the per-segment matrix level, alias 64-pixel transform dimensions to their adjusted matrices, retain a flat level-15 matrix, and apply the five-bit inverse-matrix weight scale. The large managed lookup remains a single process-wide table. |
| `Av1Inverse2dTransformer` and `Av1InverseTransformerFactory` | AV1 section 7.11.2 inverse transform and reconstruction | libaom `av1/common/av1_inv_txfm1d.c`, `av1/common/av1_inv_txfm2d.c`, and `av1/common/idct.c` at `03087864cf4bea6abb0d28f95cf7843511413d8f` | Scalar transform oracle for coefficient-row traversal, intermediate layout, stage ranges, clipping, and high-bit-depth sample addition. The managed 16-bit overload is also used as a parity oracle for the byte overload. |
@ -119,7 +120,7 @@ This assessment is based on the current source after the upstream ImageSharp mer
### AV1 decoder
- The single-still `Av1Decoder` path now parses tile state before allocating and reconstructing one independently decodable frame, and it disposes the reconstruction planes after pixel conversion. It deliberately does not retain animation/video reference frames or implement `show_existing_frame` playback state.
- The reconstruction pipeline now records plane-relative transform geometry, preserves tile-local delta-Q and delta-LF predictors, derives segmentation and reference-adjusted filter levels, and runs the exact scalar low/high-bit-depth AV1 4-, 6-, 8-, and 14-tap deblocking kernels in normative vertical-then-horizontal order. The available full-decode AV1 fixture has no internal edge, so independent low/high-bit-depth AVIF vectors with filtered transform boundaries remain required before deblocking has external pixel-level verification. CDEF, super-resolution, loop restoration, and padding remain disabled even though they are normative when signaled, not optional quality improvements.
- The reconstruction pipeline now records plane-relative transform geometry, preserves tile-local delta-Q and delta-LF predictors, derives segmentation and reference-adjusted filter levels, and runs the exact scalar low/high-bit-depth AV1 4-, 6-, 8-, and 14-tap deblocking kernels in normative vertical-then-horizontal order. It then applies scalar CDEF direction search, luma variance adjustment, primary and secondary constrained taps, chroma direction conversion, high-bit-depth scaling, skipped-block selection, and frame-edge sentinel handling from immutable per-plane snapshots. The available full-decode AV1 fixture does not exercise filtered transform edges or active CDEF strengths, so independent 8-, 10-, and 12-bit AVIF vectors remain required before either stage has external pixel-level verification. Super-resolution, loop restoration, and padding remain disabled even though they are normative when signaled, not optional quality improvements.
- Palette mode now reads the normative luma/chroma mode and size CDFs, neighbor color caches, high-bit-depth color syntax, diagonal color-index maps, clipped-edge padding, and direct sample prediction through the existing reconstruction pipeline. The scalar implementation matches the pinned libaom source, but an independently encoded palette AVIF fixture is still required before this path is independently verified.
- Loop restoration and other normative independently decodable still-image syntax paths still contain `NotImplementedException` or equivalent unsupported branches. Tile-local palette CDF adaptation is now present; the remaining still-image frame-context behavior requires a separate source audit without introducing sequence playback state.
- The frame buffer now establishes two-byte native sample storage, logical plane rows, and sample-unit block strides for 10/12-bit frames. The active intra-prediction, inverse-transform, and block-reconstruction path selects native 16-bit samples for 10/12-bit frames and has focused pipeline wiring coverage. Independently encoded high-bit-depth AVIF conformance files are still required; chroma-from-luma storage and the other normative reconstruction stages listed below remain incomplete.

5
src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1FrameDecoder.cs

@ -2,6 +2,7 @@
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Cdef;
using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.LoopFilter;
using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Quantification;
using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
@ -101,7 +102,9 @@ internal class Av1FrameDecoder : IAv1FrameDecoder
// LoopRestorationSaveBoundaryLines(false);
}
// DecodeCdef();
Av1CdefDecoder cdefDecoder = new(this.sequenceHeader, this.frameHeader, this.frameInfo, this.frameBuffer);
cdefDecoder.DecodeFrame();
// SuperResolutionUpscaling(doUpscale);
if (doLoopRestoration && doUpscale)
{

352
src/ImageSharp/Formats/Heif/Av1/Pipeline/Cdef/Av1CdefDecoder.cs

@ -0,0 +1,352 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Cdef;
/// <summary>
/// Applies AV1 constrained directional enhancement filtering to a reconstructed still-image frame.
/// </summary>
internal class Av1CdefDecoder
{
/// <summary>
/// The width and height of a CDEF unit in 4x4 luma mode-information units.
/// </summary>
private const int CdefUnitModeInfoSize = 16;
/// <summary>
/// The number of unavailable samples reserved on each source-plane edge.
/// </summary>
private const int SourceBorder = 2;
/// <summary>
/// The sequence-level superblock, bit-depth, and color configuration.
/// </summary>
private readonly ObuSequenceHeader sequenceHeader;
/// <summary>
/// The frame dimensions and CDEF strength table.
/// </summary>
private readonly ObuFrameHeader frameHeader;
/// <summary>
/// The decoded block skip state and CDEF-unit strength selections.
/// </summary>
private readonly Av1FrameInfo frameInfo;
/// <summary>
/// The reconstructed plane samples modified by CDEF.
/// </summary>
private readonly Av1FrameBuffer<byte> frameBuffer;
/// <summary>
/// Initializes a new instance of the <see cref="Av1CdefDecoder"/> class.
/// </summary>
/// <param name="sequenceHeader">The sequence header defining CDEF availability and the color layout.</param>
/// <param name="frameHeader">The frame header defining dimensions and CDEF strengths.</param>
/// <param name="frameInfo">The decoded block skip state and per-unit strength selections.</param>
/// <param name="frameBuffer">The deblocked frame samples to filter.</param>
public Av1CdefDecoder(
ObuSequenceHeader sequenceHeader,
ObuFrameHeader frameHeader,
Av1FrameInfo frameInfo,
Av1FrameBuffer<byte> frameBuffer)
{
this.sequenceHeader = sequenceHeader;
this.frameHeader = frameHeader;
this.frameInfo = frameInfo;
this.frameBuffer = frameBuffer;
}
/// <summary>
/// Filters every enabled color plane using directions derived from the deblocked luma plane.
/// </summary>
public void DecodeFrame()
{
if (!this.sequenceHeader.EnableCdef || this.frameHeader.CodedLossless || this.frameHeader.AllowIntraBlockCopy)
{
return;
}
ObuConstraintDirectionalEnhancementFilterParameters parameters = this.frameHeader.CdefParameters;
int strengthCount = 1 << parameters.BitCount;
bool hasNonZeroStrength = false;
for (int i = 0; i < strengthCount; i++)
{
if (parameters.YStrength[i] != 0 ||
(this.sequenceHeader.ColorConfig.PlaneCount > 1 && parameters.UvStrength[i] != 0))
{
hasNonZeroStrength = true;
break;
}
}
if (!hasNonZeroStrength)
{
return;
}
int lumaBlockColumnCount = this.frameHeader.ModeInfoColumnCount >> 1;
int lumaBlockRowCount = this.frameHeader.ModeInfoRowCount >> 1;
int[] directions = new int[lumaBlockColumnCount * lumaBlockRowCount];
int[] variances = new int[directions.Length];
ObuColorConfig colorConfig = this.sequenceHeader.ColorConfig;
// Luma must be processed first even when its strengths are zero because chroma CDEF
// consumes directions derived from the immutable, deblocked luma source.
for (int planeIndex = 0; planeIndex < colorConfig.PlaneCount; planeIndex++)
{
Av1Plane plane = (Av1Plane)planeIndex;
int subsamplingX = plane != Av1Plane.Y && colorConfig.SubSamplingX ? 1 : 0;
int subsamplingY = plane != Av1Plane.Y && colorConfig.SubSamplingY ? 1 : 0;
this.FilterPlane(plane, subsamplingX, subsamplingY, directions, variances, lumaBlockColumnCount);
}
}
/// <summary>
/// Filters one color plane from an immutable snapshot of its deblocked samples.
/// </summary>
/// <param name="plane">The color plane to filter.</param>
/// <param name="subsamplingX">The horizontal chroma subsampling shift.</param>
/// <param name="subsamplingY">The vertical chroma subsampling shift.</param>
/// <param name="directions">The frame-wide luma direction map in 8x8 block order.</param>
/// <param name="variances">The frame-wide luma directional-variance map in 8x8 block order.</param>
/// <param name="lumaBlockColumnCount">The number of 8x8 blocks in an aligned luma row.</param>
private void FilterPlane(
Av1Plane plane,
int subsamplingX,
int subsamplingY,
int[] directions,
int[] variances,
int lumaBlockColumnCount)
{
int planeWidth = this.frameHeader.ModeInfoColumnCount << (Av1Constants.ModeInfoSizeLog2 - subsamplingX);
int planeHeight = this.frameHeader.ModeInfoRowCount << (Av1Constants.ModeInfoSizeLog2 - subsamplingY);
int sourceStride = planeWidth + (SourceBorder * 2);
// CDEF output must never become input to a later block. The sentinel border also makes
// frame-edge taps follow AV1 without exposing the frame buffer's prediction padding.
ushort[] source = new ushort[(planeHeight + (SourceBorder * 2)) * sourceStride];
Array.Fill(source, Av1CdefKernels.VeryLarge);
Span<byte> lowBitDepthDestination = default;
Span<ushort> highBitDepthDestination = default;
int destinationStride;
if (this.frameBuffer.BytesPerSample == 2)
{
Span<short> signedDestination = this.frameBuffer.DeriveBlockPointer16(
plane,
Point.Empty,
subsamplingX,
subsamplingY,
out destinationStride);
highBitDepthDestination = MemoryMarshal.Cast<short, ushort>(signedDestination);
}
else
{
lowBitDepthDestination = this.frameBuffer.DeriveBlockPointer(
plane,
Point.Empty,
subsamplingX,
subsamplingY,
out destinationStride);
}
for (int row = 0; row < planeHeight; row++)
{
Span<ushort> sourceRow = source.AsSpan(
((row + SourceBorder) * sourceStride) + SourceBorder,
planeWidth);
int destinationOffset = destinationStride + (row * destinationStride);
if (this.frameBuffer.BytesPerSample == 2)
{
highBitDepthDestination.Slice(destinationOffset, planeWidth).CopyTo(sourceRow);
}
else
{
Span<byte> destinationRow = lowBitDepthDestination.Slice(destinationOffset, planeWidth);
for (int column = 0; column < planeWidth; column++)
{
sourceRow[column] = destinationRow[column];
}
}
}
ObuConstraintDirectionalEnhancementFilterParameters parameters = this.frameHeader.CdefParameters;
int coefficientShift = Math.Max(this.frameBuffer.BitDepth.GetBitCount() - 8, 0);
int blockWidth = 8 >> subsamplingX;
int blockHeight = 8 >> subsamplingY;
int unitColumnCount = (this.frameHeader.ModeInfoColumnCount + CdefUnitModeInfoSize - 1) / CdefUnitModeInfoSize;
int unitRowCount = (this.frameHeader.ModeInfoRowCount + CdefUnitModeInfoSize - 1) / CdefUnitModeInfoSize;
Span<ushort> filteredBlock = stackalloc ushort[8 * 8];
for (int unitRow = 0; unitRow < unitRowCount; unitRow++)
{
for (int unitColumn = 0; unitColumn < unitColumnCount; unitColumn++)
{
int unitModeInfoRow = unitRow * CdefUnitModeInfoSize;
int unitModeInfoColumn = unitColumn * CdefUnitModeInfoSize;
int strengthIndex = this.GetStrengthIndex(unitModeInfoColumn, unitModeInfoRow);
if (strengthIndex < 0)
{
continue;
}
int yStrength = parameters.YStrength[strengthIndex];
int uvStrength = parameters.UvStrength[strengthIndex];
bool unitNeedsDirections = yStrength != 0 ||
(this.sequenceHeader.ColorConfig.PlaneCount > 1 && uvStrength != 0);
if ((plane == Av1Plane.Y && !unitNeedsDirections) || (plane != Av1Plane.Y && uvStrength == 0))
{
continue;
}
int codedStrength = plane == Av1Plane.Y ? yStrength : uvStrength;
int primaryStrength = (codedStrength / 4) << coefficientShift;
int secondaryStrength = codedStrength % 4;
// The two-bit secondary field leaves value three unused and represents strength four instead.
secondaryStrength += secondaryStrength == 3 ? 1 : 0;
secondaryStrength <<= coefficientShift;
int damping = parameters.Damping + coefficientShift - (plane == Av1Plane.Y ? 0 : 1);
int unitModeInfoRowEnd = Math.Min(unitModeInfoRow + CdefUnitModeInfoSize, this.frameHeader.ModeInfoRowCount);
int unitModeInfoColumnEnd = Math.Min(unitModeInfoColumn + CdefUnitModeInfoSize, this.frameHeader.ModeInfoColumnCount);
for (int blockModeInfoRow = unitModeInfoRow; blockModeInfoRow < unitModeInfoRowEnd; blockModeInfoRow += 2)
{
for (int blockModeInfoColumn = unitModeInfoColumn; blockModeInfoColumn < unitModeInfoColumnEnd; blockModeInfoColumn += 2)
{
if (this.IsBlockSkipped(blockModeInfoColumn, blockModeInfoRow))
{
continue;
}
int lumaBlockRow = blockModeInfoRow >> 1;
int lumaBlockColumn = blockModeInfoColumn >> 1;
int directionIndex = (lumaBlockRow * lumaBlockColumnCount) + lumaBlockColumn;
int planeColumn = (blockModeInfoColumn << Av1Constants.ModeInfoSizeLog2) >> subsamplingX;
int planeRow = (blockModeInfoRow << Av1Constants.ModeInfoSizeLog2) >> subsamplingY;
int sourceOffset = ((planeRow + SourceBorder) * sourceStride) + planeColumn + SourceBorder;
if (plane == Av1Plane.Y)
{
directions[directionIndex] = Av1CdefKernels.FindDirection(
source,
sourceOffset,
sourceStride,
coefficientShift,
out variances[directionIndex]);
}
if (codedStrength == 0)
{
continue;
}
int filteredPrimaryStrength = plane == Av1Plane.Y
? Av1CdefKernels.AdjustStrength(primaryStrength, variances[directionIndex])
: primaryStrength;
if (filteredPrimaryStrength == 0 && secondaryStrength == 0)
{
continue;
}
// Secondary-only filtering uses direction zero; otherwise chroma remaps the
// luma direction into its asymmetrically subsampled sample grid when required.
int direction = primaryStrength != 0
? Av1CdefKernels.ConvertDirection(directions[directionIndex], subsamplingX, subsamplingY)
: 0;
Av1CdefKernels.FilterBlock(
source,
sourceOffset,
sourceStride,
filteredBlock,
0,
blockWidth,
filteredPrimaryStrength,
secondaryStrength,
direction,
damping,
damping,
coefficientShift,
blockWidth,
blockHeight);
int destinationOffset = destinationStride + (planeRow * destinationStride) + planeColumn;
for (int row = 0; row < blockHeight; row++)
{
ReadOnlySpan<ushort> filteredRow = filteredBlock.Slice(row * blockWidth, blockWidth);
if (this.frameBuffer.BytesPerSample == 2)
{
filteredRow.CopyTo(highBitDepthDestination.Slice(destinationOffset + (row * destinationStride), blockWidth));
}
else
{
Span<byte> destinationRow = lowBitDepthDestination.Slice(
destinationOffset + (row * destinationStride),
blockWidth);
for (int column = 0; column < blockWidth; column++)
{
destinationRow[column] = (byte)filteredRow[column];
}
}
}
}
}
}
}
}
/// <summary>
/// Gets the strength-table selection assigned to a 64x64 CDEF unit.
/// </summary>
/// <param name="modeInfoColumn">The unit's frame-relative column in 4x4 luma units.</param>
/// <param name="modeInfoRow">The unit's frame-relative row in 4x4 luma units.</param>
/// <returns>The strength-table index, or minus one when every block in the unit is skipped.</returns>
private int GetStrengthIndex(int modeInfoColumn, int modeInfoRow)
{
int superblockModeInfoSize = this.frameInfo.SuperblockModeInfoSize;
Point superblockPosition = new(
modeInfoColumn / superblockModeInfoSize,
modeInfoRow / superblockModeInfoSize);
int unitColumn = (modeInfoColumn % superblockModeInfoSize) / CdefUnitModeInfoSize;
int unitRow = (modeInfoRow % superblockModeInfoSize) / CdefUnitModeInfoSize;
// A 128x128 superblock stores four raster-ordered 64x64 selections; the same
// expression naturally resolves to index zero for a 64x64 superblock.
int unitIndex = unitColumn + (unitRow << 1);
return this.frameInfo.GetCdefStrength(superblockPosition)[unitIndex];
}
/// <summary>
/// Determines whether every 4x4 mode-information block covered by an 8x8 CDEF block is skipped.
/// </summary>
/// <param name="modeInfoColumn">The block's frame-relative column in 4x4 luma units.</param>
/// <param name="modeInfoRow">The block's frame-relative row in 4x4 luma units.</param>
/// <returns><see langword="true"/> when the complete 8x8 block is skipped; otherwise, <see langword="false"/>.</returns>
private bool IsBlockSkipped(int modeInfoColumn, int modeInfoRow)
{
for (int row = 0; row < 2; row++)
{
for (int column = 0; column < 2; column++)
{
if (!this.frameInfo.GetModeInfoAt(new Point(modeInfoColumn + column, modeInfoRow + row)).Skip)
{
return false;
}
}
}
return true;
}
}

321
src/ImageSharp/Formats/Heif/Av1/Pipeline/Cdef/Av1CdefKernels.cs

@ -0,0 +1,321 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Cdef;
/// <summary>
/// Provides the scalar constrained directional enhancement filter operations defined by AV1.
/// </summary>
internal static class Av1CdefKernels
{
/// <summary>
/// The sample value used for neighbors outside the coded frame.
/// </summary>
public const ushort VeryLarge = 0x4000;
/// <summary>
/// The horizontal offsets for the nearest primary or secondary taps in each direction.
/// </summary>
private static readonly int[] DirectionX1 = [1, 1, 1, 1, 1, 0, 0, 0];
/// <summary>
/// The vertical offsets for the nearest primary or secondary taps in each direction.
/// </summary>
private static readonly int[] DirectionY1 = [-1, 0, 0, 0, 1, 1, 1, 1];
/// <summary>
/// The horizontal offsets for the furthest primary or secondary taps in each direction.
/// </summary>
private static readonly int[] DirectionX2 = [2, 2, 2, 2, 2, 1, 0, -1];
/// <summary>
/// The vertical offsets for the furthest primary or secondary taps in each direction.
/// </summary>
private static readonly int[] DirectionY2 = [-2, -1, 0, 1, 2, 2, 2, 2];
/// <summary>
/// The primary-tap weights selected by the parity of the unscaled primary strength.
/// </summary>
private static readonly int[,] PrimaryTaps =
{
{ 4, 2 },
{ 3, 3 }
};
/// <summary>
/// The secondary-tap weights for the nearest and furthest samples.
/// </summary>
private static readonly int[] SecondaryTaps = [2, 1];
/// <summary>
/// The common multiples used to compare line variance without division.
/// </summary>
private static readonly int[] DivisionTable = [0, 840, 420, 280, 210, 168, 140, 120, 105];
/// <summary>
/// The direction mapping for horizontally subsampled, vertically full-resolution chroma.
/// </summary>
private static readonly int[] DirectionMap422 = [7, 0, 2, 4, 5, 6, 6, 6];
/// <summary>
/// The direction mapping for horizontally full-resolution, vertically subsampled chroma.
/// </summary>
private static readonly int[] DirectionMap440 = [1, 2, 2, 2, 3, 4, 6, 0];
/// <summary>
/// Finds the dominant direction of an 8x8 luma block and its directional variance.
/// </summary>
/// <param name="source">The bordered, deblocked source plane.</param>
/// <param name="sourceOffset">The offset of the block's top-left sample.</param>
/// <param name="sourceStride">The number of samples between adjacent source rows.</param>
/// <param name="coefficientShift">The number of bits above the eight-bit analysis precision.</param>
/// <param name="variance">Receives the variance difference between the selected and orthogonal directions.</param>
/// <returns>The zero-based AV1 direction index.</returns>
public static int FindDirection(
ReadOnlySpan<ushort> source,
int sourceOffset,
int sourceStride,
int coefficientShift,
out int variance)
{
Span<int> partial = stackalloc int[8 * 15];
Span<int> cost = stackalloc int[8];
partial.Clear();
cost.Clear();
for (int row = 0; row < 8; row++)
{
for (int column = 0; column < 8; column++)
{
// Direction analysis deliberately reduces every source to eight-bit precision so its
// strength selection is identical for 8-, 10-, and 12-bit coded images.
int value = (source[sourceOffset + (row * sourceStride) + column] >> coefficientShift) - 128;
partial[(0 * 15) + row + column] += value;
partial[(1 * 15) + row + (column / 2)] += value;
partial[(2 * 15) + row] += value;
partial[(3 * 15) + 3 + row - (column / 2)] += value;
partial[(4 * 15) + 7 + row - column] += value;
partial[(5 * 15) + 3 - (row / 2) + column] += value;
partial[(6 * 15) + column] += value;
partial[(7 * 15) + (row / 2) + column] += value;
}
}
for (int i = 0; i < 8; i++)
{
cost[2] += partial[(2 * 15) + i] * partial[(2 * 15) + i];
cost[6] += partial[(6 * 15) + i] * partial[(6 * 15) + i];
}
cost[2] *= DivisionTable[8];
cost[6] *= DivisionTable[8];
for (int i = 0; i < 7; i++)
{
cost[0] += ((partial[(0 * 15) + i] * partial[(0 * 15) + i]) +
(partial[(0 * 15) + 14 - i] * partial[(0 * 15) + 14 - i])) * DivisionTable[i + 1];
cost[4] += ((partial[(4 * 15) + i] * partial[(4 * 15) + i]) +
(partial[(4 * 15) + 14 - i] * partial[(4 * 15) + 14 - i])) * DivisionTable[i + 1];
}
cost[0] += partial[(0 * 15) + 7] * partial[(0 * 15) + 7] * DivisionTable[8];
cost[4] += partial[(4 * 15) + 7] * partial[(4 * 15) + 7] * DivisionTable[8];
for (int direction = 1; direction < 8; direction += 2)
{
for (int i = 0; i < 5; i++)
{
cost[direction] += partial[(direction * 15) + 3 + i] * partial[(direction * 15) + 3 + i];
}
cost[direction] *= DivisionTable[8];
for (int i = 0; i < 3; i++)
{
cost[direction] += ((partial[(direction * 15) + i] * partial[(direction * 15) + i]) +
(partial[(direction * 15) + 10 - i] * partial[(direction * 15) + 10 - i])) * DivisionTable[(2 * i) + 2];
}
}
int bestCost = 0;
int bestDirection = 0;
for (int direction = 0; direction < 8; direction++)
{
if (cost[direction] > bestCost)
{
bestCost = cost[direction];
bestDirection = direction;
}
}
// Both costs omit the same sum-of-squares term. Their scaled difference is the
// directional variance consumed by AV1's luma strength adjustment.
variance = (bestCost - cost[(bestDirection + 4) & 7]) >> 10;
return bestDirection;
}
/// <summary>
/// Adjusts a luma primary strength according to the directional variance of its 8x8 block.
/// </summary>
/// <param name="strength">The bit-depth-scaled primary strength.</param>
/// <param name="variance">The directional variance returned by <see cref="FindDirection"/>.</param>
/// <returns>The variance-adjusted primary strength.</returns>
public static int AdjustStrength(int strength, int variance)
{
int varianceClass = variance >> 6;
int adjustment = varianceClass != 0 ? Math.Min(Av1Math.MostSignificantBit((uint)varianceClass), 12) : 0;
return variance != 0 ? ((strength * (4 + adjustment)) + 8) >> 4 : 0;
}
/// <summary>
/// Converts a luma direction to the matching chroma direction for asymmetric subsampling.
/// </summary>
/// <param name="direction">The zero-based luma direction index.</param>
/// <param name="subsamplingX">The horizontal chroma subsampling shift.</param>
/// <param name="subsamplingY">The vertical chroma subsampling shift.</param>
/// <returns>The direction index in the chroma sample grid.</returns>
public static int ConvertDirection(int direction, int subsamplingX, int subsamplingY)
{
if (subsamplingX == subsamplingY)
{
return direction;
}
return subsamplingX != 0 ? DirectionMap422[direction] : DirectionMap440[direction];
}
/// <summary>
/// Filters one luma or chroma block from an immutable bordered source plane.
/// </summary>
/// <param name="source">The bordered, deblocked source plane.</param>
/// <param name="sourceOffset">The offset of the block's top-left source sample.</param>
/// <param name="sourceStride">The number of samples between adjacent source rows.</param>
/// <param name="destination">The unbordered filtered destination plane.</param>
/// <param name="destinationOffset">The offset of the block's top-left destination sample.</param>
/// <param name="destinationStride">The number of samples between adjacent destination rows.</param>
/// <param name="primaryStrength">The bit-depth-scaled primary strength.</param>
/// <param name="secondaryStrength">The bit-depth-scaled secondary strength.</param>
/// <param name="direction">The zero-based AV1 direction index.</param>
/// <param name="primaryDamping">The damping value applied to primary taps.</param>
/// <param name="secondaryDamping">The damping value applied to secondary taps.</param>
/// <param name="coefficientShift">The number of bits above eight-bit sample precision.</param>
/// <param name="blockWidth">The block width in plane samples.</param>
/// <param name="blockHeight">The block height in plane samples.</param>
public static void FilterBlock(
ReadOnlySpan<ushort> source,
int sourceOffset,
int sourceStride,
Span<ushort> destination,
int destinationOffset,
int destinationStride,
int primaryStrength,
int secondaryStrength,
int direction,
int primaryDamping,
int secondaryDamping,
int coefficientShift,
int blockWidth,
int blockHeight)
{
bool enablePrimary = primaryStrength != 0;
bool enableSecondary = secondaryStrength != 0;
bool clippingRequired = enablePrimary && enableSecondary;
int primaryTapSet = (primaryStrength >> coefficientShift) & 1;
for (int row = 0; row < blockHeight; row++)
{
for (int column = 0; column < blockWidth; column++)
{
int sourceIndex = sourceOffset + (row * sourceStride) + column;
int sample = source[sourceIndex];
int sum = 0;
int minimum = sample;
int maximum = sample;
for (int tap = 0; tap < 2; tap++)
{
if (enablePrimary)
{
int primaryDirectionOffset = GetDirectionOffset(direction, tap, sourceStride);
int neighbor0 = source[sourceIndex + primaryDirectionOffset];
int neighbor1 = source[sourceIndex - primaryDirectionOffset];
int weight = PrimaryTaps[primaryTapSet, tap];
sum += weight * Constrain(neighbor0 - sample, primaryStrength, primaryDamping);
sum += weight * Constrain(neighbor1 - sample, primaryStrength, primaryDamping);
if (clippingRequired)
{
maximum = neighbor0 != VeryLarge ? Math.Max(maximum, neighbor0) : maximum;
maximum = neighbor1 != VeryLarge ? Math.Max(maximum, neighbor1) : maximum;
minimum = Math.Min(minimum, neighbor0);
minimum = Math.Min(minimum, neighbor1);
}
}
if (enableSecondary)
{
int secondaryDirection0 = GetDirectionOffset((direction + 2) & 7, tap, sourceStride);
int secondaryDirection1 = GetDirectionOffset((direction + 6) & 7, tap, sourceStride);
int neighbor0 = source[sourceIndex + secondaryDirection0];
int neighbor1 = source[sourceIndex - secondaryDirection0];
int neighbor2 = source[sourceIndex + secondaryDirection1];
int neighbor3 = source[sourceIndex - secondaryDirection1];
int weight = SecondaryTaps[tap];
if (clippingRequired)
{
maximum = neighbor0 != VeryLarge ? Math.Max(maximum, neighbor0) : maximum;
maximum = neighbor1 != VeryLarge ? Math.Max(maximum, neighbor1) : maximum;
maximum = neighbor2 != VeryLarge ? Math.Max(maximum, neighbor2) : maximum;
maximum = neighbor3 != VeryLarge ? Math.Max(maximum, neighbor3) : maximum;
minimum = Math.Min(minimum, neighbor0);
minimum = Math.Min(minimum, neighbor1);
minimum = Math.Min(minimum, neighbor2);
minimum = Math.Min(minimum, neighbor3);
}
sum += weight * Constrain(neighbor0 - sample, secondaryStrength, secondaryDamping);
sum += weight * Constrain(neighbor1 - sample, secondaryStrength, secondaryDamping);
sum += weight * Constrain(neighbor2 - sample, secondaryStrength, secondaryDamping);
sum += weight * Constrain(neighbor3 - sample, secondaryStrength, secondaryDamping);
}
}
// The negative-sum correction preserves AV1's asymmetric signed rounding exactly.
int filtered = sample + ((8 + sum - (sum < 0 ? 1 : 0)) >> 4);
destination[destinationOffset + (row * destinationStride) + column] =
(ushort)(clippingRequired ? Av1Math.Clip3(minimum, maximum, filtered) : filtered);
}
}
}
/// <summary>
/// Converts a direction and tap number to a signed plane-buffer offset.
/// </summary>
/// <param name="direction">The zero-based AV1 direction index.</param>
/// <param name="tap">The zero-based distance index.</param>
/// <param name="stride">The number of samples between adjacent rows.</param>
/// <returns>The signed sample offset.</returns>
private static int GetDirectionOffset(int direction, int tap, int stride)
=> tap == 0
? (DirectionY1[direction] * stride) + DirectionX1[direction]
: (DirectionY2[direction] * stride) + DirectionX2[direction];
/// <summary>
/// Limits a neighbor difference according to a filter strength and damping value.
/// </summary>
/// <param name="difference">The signed difference from the current sample.</param>
/// <param name="threshold">The bit-depth-scaled filter strength.</param>
/// <param name="damping">The damping value.</param>
/// <returns>The signed constrained difference.</returns>
private static int Constrain(int difference, int threshold, int damping)
{
if (threshold == 0)
{
return 0;
}
// Stronger thresholds reduce the effective damping shift, matching the AV1 constrain function.
int shift = Math.Max(0, damping - Av1Math.MostSignificantBit((uint)threshold));
int magnitude = Math.Abs(difference);
int constrained = Av1Math.Clip3(0, magnitude, threshold - (magnitude >> shift));
return difference < 0 ? -constrained : constrained;
}
}

2
src/ImageSharp/Formats/Heif/Av1/Tiling/Av1FrameInfo.cs

@ -157,7 +157,7 @@ internal partial class Av1FrameInfo
this.quantizerIndices = new int[superblockCount];
// A 128x128 superblock contains four 64x64 CDEF filter blocks; a 64x64 superblock contains one.
this.cdefStrengthFactorLog2 = (superblockSizeLog2 - 6) << 2;
this.cdefStrengthFactorLog2 = (superblockSizeLog2 - 6) << 1;
this.cdefStrength = new int[superblockCount << this.cdefStrengthFactorLog2];
Array.Fill(this.cdefStrength, -1);
this.deltaLoopFilter = new int[superblockCount << this.deltaLoopFactorLog2];

6
src/ImageSharp/Formats/Heif/Av1/Tiling/Av1PartitionInfo.cs

@ -24,7 +24,6 @@ internal class Av1PartitionInfo
this.SuperblockInfo = superblockInfo;
this.IsChroma = isChroma;
this.Type = partitionType;
this.CdefStrength = [];
this.ReferenceFrame = [-1, -1];
this.WidthInPixels = new int[3];
this.HeightInPixels = new int[3];
@ -100,11 +99,6 @@ internal class Av1PartitionInfo
/// </summary>
public Av1BlockModeInfo? LeftModeInfoForChroma { get; set; }
/// <summary>
/// Gets or sets the constrained directional enhancement filter strengths associated with the block.
/// </summary>
public int[] CdefStrength { get; set; }
/// <summary>
/// Gets or sets the reference-frame identifiers selected for the block.
/// </summary>

44
src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileReader.cs

@ -1947,7 +1947,7 @@ internal class Av1TileReader : IAv1TileReader
/// </summary>
/// <param name="reader">The tile symbol decoder.</param>
/// <param name="partitionInfo">The current coding block.</param>
/// <remarks>Implements AV1 section 5.11.56 and corresponds to <c>read_cdef</c> in SVT-AV1.</remarks>
/// <remarks>Implements AV1 section 5.11.56 and corresponds to <c>read_cdef</c> in libaom.</remarks>
private void ReadCdef(ref Av1SymbolDecoder reader, Av1PartitionInfo partitionInfo)
{
if (partitionInfo.ModeInfo.Skip || this.FrameHeader.CodedLossless || !this.SequenceHeader.EnableCdef || this.FrameHeader.AllowIntraBlockCopy)
@ -1956,26 +1956,32 @@ internal class Av1TileReader : IAv1TileReader
}
int cdefSize4 = Av1BlockSize.Block64x64.Get4x4WideCount();
int row = partitionInfo.RowIndex & cdefSize4;
int col = partitionInfo.ColumnIndex & cdefSize4;
int index = this.SequenceHeader.SuperblockSize == Av1BlockSize.Block128x128 ? Math.Max(1, col) + (Math.Max(1, row) << 1) : 0;
if (partitionInfo.CdefStrength[index] == -1)
{
int cdfStrength = reader.ReadCdfStrength(this.FrameHeader.CdefParameters.BitCount);
partitionInfo.CdefStrength[index] = cdfStrength;
// A block in a 128x128 superblock can cover multiple 64x64 CDEF units. Replicate the
// first decoded strength so subsequent blocks in every covered unit observe it as assigned.
if (this.SequenceHeader.SuperblockSize == Av1BlockSize.Block128x128)
int superblockMask = this.SequenceHeader.SuperblockModeInfoSize - 1;
int rowInSuperblock = partitionInfo.RowIndex & superblockMask;
int columnInSuperblock = partitionInfo.ColumnIndex & superblockMask;
int unitRow = rowInSuperblock / cdefSize4;
int unitColumn = columnInSuperblock / cdefSize4;
int index = this.SequenceHeader.SuperblockSize == Av1BlockSize.Block128x128 ? unitColumn + (unitRow << 1) : 0;
Span<int> cdefStrength = partitionInfo.SuperblockInfo.CdefStrength;
if (cdefStrength[index] == -1)
{
int cdefStrengthIndex = reader.ReadCdfStrength(this.FrameHeader.CdefParameters.BitCount);
int blockWidth4 = partitionInfo.ModeInfo.BlockSize.Get4x4WideCount();
int blockHeight4 = partitionInfo.ModeInfo.BlockSize.Get4x4HighCount();
int lastUnitRow = (rowInSuperblock + blockHeight4 - 1) / cdefSize4;
int lastUnitColumn = (columnInSuperblock + blockWidth4 - 1) / cdefSize4;
// A coding block can cover the top-left cell of more than one 64x64 CDEF unit. libaom
// stores the index on shared mode information, so the frame-owned unit map must mirror it.
for (int coveredUnitRow = unitRow; coveredUnitRow <= lastUnitRow; coveredUnitRow++)
{
int w4 = partitionInfo.ModeInfo.BlockSize.Get4x4WideCount();
int h4 = partitionInfo.ModeInfo.BlockSize.Get4x4HighCount();
for (int i = row; i < row + h4; i += cdefSize4)
for (int coveredUnitColumn = unitColumn; coveredUnitColumn <= lastUnitColumn; coveredUnitColumn++)
{
for (int j = col; j < col + w4; j += cdefSize4)
{
partitionInfo.CdefStrength[Math.Max(1, j & cdefSize4) + (Math.Max(1, i & cdefSize4) << 1)] = cdfStrength;
}
int coveredIndex = this.SequenceHeader.SuperblockSize == Av1BlockSize.Block128x128
? coveredUnitColumn + (coveredUnitRow << 1)
: 0;
cdefStrength[coveredIndex] = cdefStrengthIndex;
}
}
}

Loading…
Cancel
Save