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