mirror of https://github.com/SixLabors/ImageSharp
7 changed files with 705 additions and 28 deletions
@ -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; |
||||
|
} |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue