mirror of https://github.com/SixLabors/ImageSharp
5 changed files with 2340 additions and 328 deletions
File diff suppressed because it is too large
@ -1,321 +0,0 @@ |
|||||
// 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; |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,627 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Six Labors Split License.
|
||||
|
|
||||
|
using System.Numerics; |
||||
|
using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Cdef; |
||||
|
using SixLabors.ImageSharp.Tests.TestUtilities; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Verifies AV1 constrained directional enhancement filtering across sample precision, block geometry, and intrinsic tiers.
|
||||
|
/// </summary>
|
||||
|
[Trait("Format", "Avif")] |
||||
|
public class Av1CdefFilterTests |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// The hardware configurations required to exercise packed filtering and the scalar fallback.
|
||||
|
/// </summary>
|
||||
|
private const HwIntrinsics Configurations = HwIntrinsics.AllowAll | HwIntrinsics.DisableHWIntrinsic; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The row stride of the bordered source plane used by the filter tests.
|
||||
|
/// </summary>
|
||||
|
private const int SourceStride = 16; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The number of unavailable samples surrounding the test image.
|
||||
|
/// </summary>
|
||||
|
private const int SourceBorder = 2; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Verifies direction selection and variance against an independent scalar definition.
|
||||
|
/// </summary>
|
||||
|
[Fact] |
||||
|
public void FindDirectionMatchesIndependentDefinitionAcrossIntrinsicTiers() |
||||
|
=> FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidateDirections, Configurations); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Verifies every CDEF block geometry and strength mode against an independent scalar definition.
|
||||
|
/// </summary>
|
||||
|
[Fact] |
||||
|
public void FilterBlockMatchesIndependentDefinitionAcrossIntrinsicTiers() |
||||
|
=> FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidateFilters, Configurations); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Verifies the complete asymmetric chroma direction mappings and the unchanged symmetric mappings.
|
||||
|
/// </summary>
|
||||
|
[Fact] |
||||
|
public void ConvertDirectionMatchesSubsamplingGeometry() |
||||
|
{ |
||||
|
int[] horizontalSubsampling = [7, 0, 2, 4, 5, 6, 6, 6]; |
||||
|
int[] verticalSubsampling = [1, 2, 2, 2, 3, 4, 6, 0]; |
||||
|
for (int direction = 0; direction < 8; direction++) |
||||
|
{ |
||||
|
Assert.Equal(horizontalSubsampling[direction], Av1CdefFilter.ConvertDirection(direction, 1, 0)); |
||||
|
Assert.Equal(verticalSubsampling[direction], Av1CdefFilter.ConvertDirection(direction, 0, 1)); |
||||
|
Assert.Equal(direction, Av1CdefFilter.ConvertDirection(direction, 0, 0)); |
||||
|
Assert.Equal(direction, Av1CdefFilter.ConvertDirection(direction, 1, 1)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Verifies luma strength adjustment at zero, logarithmic-class boundaries, and the capped variance class.
|
||||
|
/// </summary>
|
||||
|
[Fact] |
||||
|
public void AdjustStrengthMatchesIndependentDefinition() |
||||
|
{ |
||||
|
foreach (int strength in new[] { 0, 4, 15, 60 }) |
||||
|
{ |
||||
|
foreach (int variance in new[] { 0, 1, 63, 64, 255, 4096, 1 << 20 }) |
||||
|
{ |
||||
|
int varianceClass = variance >> 6; |
||||
|
int adjustment = varianceClass == 0 ? 0 : Math.Min(BitOperations.Log2((uint)varianceClass), 12); |
||||
|
int expected = variance == 0 ? 0 : ((strength * (4 + adjustment)) + 8) >> 4; |
||||
|
Assert.Equal(expected, Av1CdefFilter.AdjustStrength(strength, variance)); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Verifies that the deterministic direction corpus exercises every selected-direction branch.
|
||||
|
/// </summary>
|
||||
|
[Fact] |
||||
|
public void DirectionCorpusCoversEveryDirection() |
||||
|
{ |
||||
|
const int stride = 32; |
||||
|
const int sourceOffset = (4 * stride) + 5; |
||||
|
int secondSourceOffset = sourceOffset + 8; |
||||
|
HashSet<int> observedDirections = []; |
||||
|
for (int pattern = 0; pattern < 8; pattern++) |
||||
|
{ |
||||
|
ushort[] source = new ushort[stride * 16]; |
||||
|
PopulateDirectionSource(source, sourceOffset, stride, pattern, 8); |
||||
|
observedDirections.Add(FindDirectionReference(source, sourceOffset, stride, 0, out _)); |
||||
|
observedDirections.Add(FindDirectionReference(source, secondSourceOffset, stride, 0, out _)); |
||||
|
} |
||||
|
|
||||
|
Assert.Equal(Enumerable.Range(0, 8), observedDirections.Order()); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Exercises direction search with multiple source patterns at every supported sample precision.
|
||||
|
/// </summary>
|
||||
|
private static void ValidateDirections() |
||||
|
{ |
||||
|
const int stride = 32; |
||||
|
const int sourceOffset = (4 * stride) + 5; |
||||
|
|
||||
|
foreach (int bitDepth in new[] { 8, 10, 12 }) |
||||
|
{ |
||||
|
int coefficientShift = bitDepth - 8; |
||||
|
for (int pattern = 0; pattern < 8; pattern++) |
||||
|
{ |
||||
|
ushort[] source = new ushort[stride * 16]; |
||||
|
int secondSourceOffset = sourceOffset + 8; |
||||
|
PopulateDirectionSource(source, sourceOffset, stride, pattern, bitDepth); |
||||
|
|
||||
|
int expectedDirection = FindDirectionReference(source, sourceOffset, stride, coefficientShift, out int expectedVariance); |
||||
|
int actualDirection = Av1CdefFilter.FindDirection(source, sourceOffset, stride, coefficientShift, out int actualVariance); |
||||
|
Assert.Equal(expectedDirection, actualDirection); |
||||
|
Assert.Equal(expectedVariance, actualVariance); |
||||
|
|
||||
|
int secondExpectedDirection = FindDirectionReference(source, secondSourceOffset, stride, coefficientShift, out int secondExpectedVariance); |
||||
|
Av1CdefFilter.FindDirections( |
||||
|
source, |
||||
|
sourceOffset, |
||||
|
secondSourceOffset, |
||||
|
stride, |
||||
|
coefficientShift, |
||||
|
out int firstActualDirection, |
||||
|
out int firstActualVariance, |
||||
|
out int secondActualDirection, |
||||
|
out int secondActualVariance); |
||||
|
|
||||
|
Assert.Equal(expectedDirection, firstActualDirection); |
||||
|
Assert.Equal(expectedVariance, firstActualVariance); |
||||
|
Assert.Equal(secondExpectedDirection, secondActualDirection); |
||||
|
Assert.Equal(secondExpectedVariance, secondActualVariance); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Populates two adjacent 8x8 blocks with deterministic directional samples.
|
||||
|
/// </summary>
|
||||
|
/// <param name="source">The destination source plane.</param>
|
||||
|
/// <param name="sourceOffset">The first populated sample.</param>
|
||||
|
/// <param name="sourceStride">The source row stride.</param>
|
||||
|
/// <param name="pattern">The deterministic pattern index.</param>
|
||||
|
/// <param name="bitDepth">The sample precision.</param>
|
||||
|
private static void PopulateDirectionSource(Span<ushort> source, int sourceOffset, int sourceStride, int pattern, int bitDepth) |
||||
|
{ |
||||
|
int coefficientShift = bitDepth - 8; |
||||
|
int maximum = (1 << bitDepth) - 1; |
||||
|
for (int row = 0; row < 8; row++) |
||||
|
{ |
||||
|
for (int column = 0; column < 16; column++) |
||||
|
{ |
||||
|
int localColumn = column & 7; |
||||
|
int direction = column < 8 ? pattern : (pattern + 4) & 7; |
||||
|
int line = GetDirectionLineReference(direction, row, localColumn); |
||||
|
|
||||
|
// Samples are constant along the requested geometric line and vary between lines. Direction search
|
||||
|
// therefore minimizes reconstruction error in that direction while still exercising nonuniform values.
|
||||
|
int value = (24 + (line * 14)) << coefficientShift; |
||||
|
source[sourceOffset + (row * sourceStride) + column] = (ushort)(value & maximum); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Maps one source coordinate to its line in an AV1 direction independently of the production implementation.
|
||||
|
/// </summary>
|
||||
|
/// <param name="direction">The zero-based AV1 direction index.</param>
|
||||
|
/// <param name="row">The source row.</param>
|
||||
|
/// <param name="column">The source column.</param>
|
||||
|
/// <returns>The zero-based line index.</returns>
|
||||
|
private static int GetDirectionLineReference(int direction, int row, int column) => direction switch |
||||
|
{ |
||||
|
0 => row + column, |
||||
|
1 => row + (column / 2), |
||||
|
2 => row, |
||||
|
3 => 3 + row - (column / 2), |
||||
|
4 => 7 + row - column, |
||||
|
5 => 3 - (row / 2) + column, |
||||
|
6 => column, |
||||
|
_ => (row / 2) + column |
||||
|
}; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Exercises all normative block dimensions, directions, strength combinations, output types, and coded precisions.
|
||||
|
/// </summary>
|
||||
|
private static void ValidateFilters() |
||||
|
{ |
||||
|
(int Width, int Height)[] dimensions = [(4, 4), (4, 8), (8, 4), (8, 8)]; |
||||
|
foreach (int bitDepth in new[] { 8, 10, 12 }) |
||||
|
{ |
||||
|
int coefficientShift = bitDepth - 8; |
||||
|
int scale = 1 << coefficientShift; |
||||
|
ushort[] source = CreateBorderedSource(bitDepth); |
||||
|
int sourceOffset = (SourceBorder * SourceStride) + SourceBorder; |
||||
|
(int Primary, int Secondary)[] strengths = [(0, 0), (4 * scale, 0), (0, 2 * scale), (5 * scale, 2 * scale)]; |
||||
|
|
||||
|
foreach ((int blockWidth, int blockHeight) in dimensions) |
||||
|
{ |
||||
|
foreach (int direction in Enumerable.Range(0, 8)) |
||||
|
{ |
||||
|
foreach ((int primaryStrength, int secondaryStrength) in strengths) |
||||
|
{ |
||||
|
if (bitDepth == 8) |
||||
|
{ |
||||
|
AssertByteFilter( |
||||
|
source, |
||||
|
sourceOffset, |
||||
|
blockWidth, |
||||
|
blockHeight, |
||||
|
primaryStrength, |
||||
|
secondaryStrength, |
||||
|
direction, |
||||
|
coefficientShift); |
||||
|
} |
||||
|
|
||||
|
AssertUInt16Filter( |
||||
|
source, |
||||
|
sourceOffset, |
||||
|
blockWidth, |
||||
|
blockHeight, |
||||
|
primaryStrength, |
||||
|
secondaryStrength, |
||||
|
direction, |
||||
|
coefficientShift); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Creates a deterministic image whose top-left output block touches the unavailable-neighbor border.
|
||||
|
/// </summary>
|
||||
|
/// <param name="bitDepth">The source sample precision.</param>
|
||||
|
/// <returns>The bordered 16-bit source plane.</returns>
|
||||
|
private static ushort[] CreateBorderedSource(int bitDepth) |
||||
|
{ |
||||
|
ushort[] source = Enumerable.Repeat(Av1CdefFilter.VeryLarge, SourceStride * SourceStride).ToArray(); |
||||
|
int maximum = (1 << bitDepth) - 1; |
||||
|
int scale = 1 << (bitDepth - 8); |
||||
|
for (int row = SourceBorder; row < SourceStride - SourceBorder; row++) |
||||
|
{ |
||||
|
for (int column = SourceBorder; column < SourceStride - SourceBorder; column++) |
||||
|
{ |
||||
|
int localRow = row - SourceBorder; |
||||
|
int localColumn = column - SourceBorder; |
||||
|
int value = (72 + (localRow * 9) + (localColumn * 5) + ((localRow * localColumn) & 15)) * scale; |
||||
|
source[(row * SourceStride) + column] = (ushort)Math.Min(value, maximum); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return source; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Verifies one eight-bit output block while retaining untouched destination padding in the comparison.
|
||||
|
/// </summary>
|
||||
|
/// <param name="source">The bordered source plane.</param>
|
||||
|
/// <param name="sourceOffset">The first source sample in the block.</param>
|
||||
|
/// <param name="blockWidth">The output block width.</param>
|
||||
|
/// <param name="blockHeight">The output block height.</param>
|
||||
|
/// <param name="primaryStrength">The primary filter strength.</param>
|
||||
|
/// <param name="secondaryStrength">The secondary filter strength.</param>
|
||||
|
/// <param name="direction">The primary filter direction.</param>
|
||||
|
/// <param name="coefficientShift">The source precision shift.</param>
|
||||
|
private static void AssertByteFilter( |
||||
|
ushort[] source, |
||||
|
int sourceOffset, |
||||
|
int blockWidth, |
||||
|
int blockHeight, |
||||
|
int primaryStrength, |
||||
|
int secondaryStrength, |
||||
|
int direction, |
||||
|
int coefficientShift) |
||||
|
{ |
||||
|
const int destinationStride = 12; |
||||
|
const int destinationOffset = destinationStride + 1; |
||||
|
byte[] expected = Enumerable.Repeat((byte)231, destinationStride * 10).ToArray(); |
||||
|
byte[] actual = (byte[])expected.Clone(); |
||||
|
int damping = 5 + coefficientShift; |
||||
|
|
||||
|
FilterReference( |
||||
|
source, |
||||
|
sourceOffset, |
||||
|
SourceStride, |
||||
|
expected, |
||||
|
destinationOffset, |
||||
|
destinationStride, |
||||
|
primaryStrength, |
||||
|
secondaryStrength, |
||||
|
direction, |
||||
|
damping, |
||||
|
coefficientShift, |
||||
|
blockWidth, |
||||
|
blockHeight); |
||||
|
|
||||
|
Av1CdefFilter.FilterBlock( |
||||
|
source, |
||||
|
sourceOffset, |
||||
|
SourceStride, |
||||
|
actual, |
||||
|
destinationOffset, |
||||
|
destinationStride, |
||||
|
primaryStrength, |
||||
|
secondaryStrength, |
||||
|
direction, |
||||
|
damping, |
||||
|
damping, |
||||
|
coefficientShift, |
||||
|
blockWidth, |
||||
|
blockHeight); |
||||
|
|
||||
|
Assert.Equal(expected, actual); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Verifies one 16-bit output block while retaining untouched destination padding in the comparison.
|
||||
|
/// </summary>
|
||||
|
/// <param name="source">The bordered source plane.</param>
|
||||
|
/// <param name="sourceOffset">The first source sample in the block.</param>
|
||||
|
/// <param name="blockWidth">The output block width.</param>
|
||||
|
/// <param name="blockHeight">The output block height.</param>
|
||||
|
/// <param name="primaryStrength">The primary filter strength.</param>
|
||||
|
/// <param name="secondaryStrength">The secondary filter strength.</param>
|
||||
|
/// <param name="direction">The primary filter direction.</param>
|
||||
|
/// <param name="coefficientShift">The source precision shift.</param>
|
||||
|
private static void AssertUInt16Filter( |
||||
|
ushort[] source, |
||||
|
int sourceOffset, |
||||
|
int blockWidth, |
||||
|
int blockHeight, |
||||
|
int primaryStrength, |
||||
|
int secondaryStrength, |
||||
|
int direction, |
||||
|
int coefficientShift) |
||||
|
{ |
||||
|
const int destinationStride = 12; |
||||
|
const int destinationOffset = destinationStride + 1; |
||||
|
ushort[] expected = Enumerable.Repeat((ushort)60000, destinationStride * 10).ToArray(); |
||||
|
ushort[] actual = (ushort[])expected.Clone(); |
||||
|
int damping = 5 + coefficientShift; |
||||
|
|
||||
|
FilterReference( |
||||
|
source, |
||||
|
sourceOffset, |
||||
|
SourceStride, |
||||
|
expected, |
||||
|
destinationOffset, |
||||
|
destinationStride, |
||||
|
primaryStrength, |
||||
|
secondaryStrength, |
||||
|
direction, |
||||
|
damping, |
||||
|
coefficientShift, |
||||
|
blockWidth, |
||||
|
blockHeight); |
||||
|
|
||||
|
Av1CdefFilter.FilterBlock( |
||||
|
source, |
||||
|
sourceOffset, |
||||
|
SourceStride, |
||||
|
actual, |
||||
|
destinationOffset, |
||||
|
destinationStride, |
||||
|
primaryStrength, |
||||
|
secondaryStrength, |
||||
|
direction, |
||||
|
damping, |
||||
|
damping, |
||||
|
coefficientShift, |
||||
|
blockWidth, |
||||
|
blockHeight); |
||||
|
|
||||
|
Assert.Equal(expected, actual); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Finds one direction and variance using the scalar AV1 definition independently of the production layouts.
|
||||
|
/// </summary>
|
||||
|
/// <param name="source">The source plane.</param>
|
||||
|
/// <param name="sourceOffset">The first sample in the 8x8 block.</param>
|
||||
|
/// <param name="sourceStride">The source row stride.</param>
|
||||
|
/// <param name="coefficientShift">The source precision shift.</param>
|
||||
|
/// <param name="variance">Receives the directional variance.</param>
|
||||
|
/// <returns>The selected direction.</returns>
|
||||
|
private static int FindDirectionReference( |
||||
|
ReadOnlySpan<ushort> source, |
||||
|
int sourceOffset, |
||||
|
int sourceStride, |
||||
|
int coefficientShift, |
||||
|
out int variance) |
||||
|
{ |
||||
|
int[,] partials = new int[8, 15]; |
||||
|
int[] costs = new int[8]; |
||||
|
int[] divisions = [0, 840, 420, 280, 210, 168, 140, 120, 105]; |
||||
|
for (int row = 0; row < 8; row++) |
||||
|
{ |
||||
|
for (int column = 0; column < 8; column++) |
||||
|
{ |
||||
|
int value = (source[sourceOffset + (row * sourceStride) + column] >> coefficientShift) - 128; |
||||
|
partials[0, row + column] += value; |
||||
|
partials[1, row + (column / 2)] += value; |
||||
|
partials[2, row] += value; |
||||
|
partials[3, 3 + row - (column / 2)] += value; |
||||
|
partials[4, 7 + row - column] += value; |
||||
|
partials[5, 3 - (row / 2) + column] += value; |
||||
|
partials[6, column] += value; |
||||
|
partials[7, (row / 2) + column] += value; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
for (int line = 0; line < 8; line++) |
||||
|
{ |
||||
|
costs[2] += partials[2, line] * partials[2, line]; |
||||
|
costs[6] += partials[6, line] * partials[6, line]; |
||||
|
} |
||||
|
|
||||
|
costs[2] *= divisions[8]; |
||||
|
costs[6] *= divisions[8]; |
||||
|
for (int line = 0; line < 7; line++) |
||||
|
{ |
||||
|
costs[0] += ((partials[0, line] * partials[0, line]) + (partials[0, 14 - line] * partials[0, 14 - line])) * divisions[line + 1]; |
||||
|
costs[4] += ((partials[4, line] * partials[4, line]) + (partials[4, 14 - line] * partials[4, 14 - line])) * divisions[line + 1]; |
||||
|
} |
||||
|
|
||||
|
costs[0] += partials[0, 7] * partials[0, 7] * divisions[8]; |
||||
|
costs[4] += partials[4, 7] * partials[4, 7] * divisions[8]; |
||||
|
for (int direction = 1; direction < 8; direction += 2) |
||||
|
{ |
||||
|
for (int line = 0; line < 5; line++) |
||||
|
{ |
||||
|
costs[direction] += partials[direction, 3 + line] * partials[direction, 3 + line]; |
||||
|
} |
||||
|
|
||||
|
costs[direction] *= divisions[8]; |
||||
|
for (int line = 0; line < 3; line++) |
||||
|
{ |
||||
|
costs[direction] += ((partials[direction, line] * partials[direction, line]) |
||||
|
+ (partials[direction, 10 - line] * partials[direction, 10 - line])) * divisions[(2 * line) + 2]; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
int bestCost = 0; |
||||
|
int bestDirection = 0; |
||||
|
for (int direction = 0; direction < 8; direction++) |
||||
|
{ |
||||
|
if (costs[direction] > bestCost) |
||||
|
{ |
||||
|
bestCost = costs[direction]; |
||||
|
bestDirection = direction; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
variance = (bestCost - costs[(bestDirection + 4) & 7]) >> 10; |
||||
|
return bestDirection; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Applies the independent scalar filter definition to eight-bit output storage.
|
||||
|
/// </summary>
|
||||
|
private static void FilterReference( |
||||
|
ReadOnlySpan<ushort> source, |
||||
|
int sourceOffset, |
||||
|
int sourceStride, |
||||
|
Span<byte> destination, |
||||
|
int destinationOffset, |
||||
|
int destinationStride, |
||||
|
int primaryStrength, |
||||
|
int secondaryStrength, |
||||
|
int direction, |
||||
|
int damping, |
||||
|
int coefficientShift, |
||||
|
int blockWidth, |
||||
|
int blockHeight) |
||||
|
{ |
||||
|
for (int row = 0; row < blockHeight; row++) |
||||
|
{ |
||||
|
for (int column = 0; column < blockWidth; column++) |
||||
|
{ |
||||
|
destination[destinationOffset + (row * destinationStride) + column] = (byte)FilterSampleReference( |
||||
|
source, |
||||
|
sourceOffset + (row * sourceStride) + column, |
||||
|
sourceStride, |
||||
|
primaryStrength, |
||||
|
secondaryStrength, |
||||
|
direction, |
||||
|
damping, |
||||
|
coefficientShift); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Applies the independent scalar filter definition to 16-bit output storage.
|
||||
|
/// </summary>
|
||||
|
private static void FilterReference( |
||||
|
ReadOnlySpan<ushort> source, |
||||
|
int sourceOffset, |
||||
|
int sourceStride, |
||||
|
Span<ushort> destination, |
||||
|
int destinationOffset, |
||||
|
int destinationStride, |
||||
|
int primaryStrength, |
||||
|
int secondaryStrength, |
||||
|
int direction, |
||||
|
int damping, |
||||
|
int coefficientShift, |
||||
|
int blockWidth, |
||||
|
int blockHeight) |
||||
|
{ |
||||
|
for (int row = 0; row < blockHeight; row++) |
||||
|
{ |
||||
|
for (int column = 0; column < blockWidth; column++) |
||||
|
{ |
||||
|
destination[destinationOffset + (row * destinationStride) + column] = (ushort)FilterSampleReference( |
||||
|
source, |
||||
|
sourceOffset + (row * sourceStride) + column, |
||||
|
sourceStride, |
||||
|
primaryStrength, |
||||
|
secondaryStrength, |
||||
|
direction, |
||||
|
damping, |
||||
|
coefficientShift); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Computes one independently filtered sample from its primary and secondary neighbors.
|
||||
|
/// </summary>
|
||||
|
private static int FilterSampleReference( |
||||
|
ReadOnlySpan<ushort> source, |
||||
|
int sourceIndex, |
||||
|
int sourceStride, |
||||
|
int primaryStrength, |
||||
|
int secondaryStrength, |
||||
|
int direction, |
||||
|
int damping, |
||||
|
int coefficientShift) |
||||
|
{ |
||||
|
bool enablePrimary = primaryStrength != 0; |
||||
|
bool enableSecondary = secondaryStrength != 0; |
||||
|
bool clippingRequired = enablePrimary && enableSecondary; |
||||
|
int primaryTapSet = (primaryStrength >> coefficientShift) & 1; |
||||
|
int sample = source[sourceIndex]; |
||||
|
int sum = 0; |
||||
|
int minimum = sample; |
||||
|
int maximum = sample; |
||||
|
|
||||
|
for (int tap = 0; tap < 2; tap++) |
||||
|
{ |
||||
|
if (enablePrimary) |
||||
|
{ |
||||
|
int offset = GetDirectionOffsetReference(direction, tap, sourceStride); |
||||
|
int neighbor0 = source[sourceIndex + offset]; |
||||
|
int neighbor1 = source[sourceIndex - offset]; |
||||
|
int weight = primaryTapSet == 0 ? (tap == 0 ? 4 : 2) : 3; |
||||
|
sum += weight * ConstrainReference(neighbor0 - sample, primaryStrength, damping); |
||||
|
sum += weight * ConstrainReference(neighbor1 - sample, primaryStrength, damping); |
||||
|
|
||||
|
if (clippingRequired) |
||||
|
{ |
||||
|
maximum = neighbor0 != Av1CdefFilter.VeryLarge ? Math.Max(maximum, neighbor0) : maximum; |
||||
|
maximum = neighbor1 != Av1CdefFilter.VeryLarge ? Math.Max(maximum, neighbor1) : maximum; |
||||
|
minimum = Math.Min(minimum, Math.Min(neighbor0, neighbor1)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (enableSecondary) |
||||
|
{ |
||||
|
int offset0 = GetDirectionOffsetReference((direction + 2) & 7, tap, sourceStride); |
||||
|
int offset1 = GetDirectionOffsetReference((direction + 6) & 7, tap, sourceStride); |
||||
|
int neighbor0 = source[sourceIndex + offset0]; |
||||
|
int neighbor1 = source[sourceIndex - offset0]; |
||||
|
int neighbor2 = source[sourceIndex + offset1]; |
||||
|
int neighbor3 = source[sourceIndex - offset1]; |
||||
|
int weight = tap == 0 ? 2 : 1; |
||||
|
sum += weight * ConstrainReference(neighbor0 - sample, secondaryStrength, damping); |
||||
|
sum += weight * ConstrainReference(neighbor1 - sample, secondaryStrength, damping); |
||||
|
sum += weight * ConstrainReference(neighbor2 - sample, secondaryStrength, damping); |
||||
|
sum += weight * ConstrainReference(neighbor3 - sample, secondaryStrength, damping); |
||||
|
|
||||
|
if (clippingRequired) |
||||
|
{ |
||||
|
maximum = neighbor0 != Av1CdefFilter.VeryLarge ? Math.Max(maximum, neighbor0) : maximum; |
||||
|
maximum = neighbor1 != Av1CdefFilter.VeryLarge ? Math.Max(maximum, neighbor1) : maximum; |
||||
|
maximum = neighbor2 != Av1CdefFilter.VeryLarge ? Math.Max(maximum, neighbor2) : maximum; |
||||
|
maximum = neighbor3 != Av1CdefFilter.VeryLarge ? Math.Max(maximum, neighbor3) : maximum; |
||||
|
minimum = Math.Min(minimum, Math.Min(Math.Min(neighbor0, neighbor1), Math.Min(neighbor2, neighbor3))); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
int filtered = sample + ((8 + sum - (sum < 0 ? 1 : 0)) >> 4); |
||||
|
return clippingRequired ? Math.Clamp(filtered, minimum, maximum) : filtered; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Applies the scalar AV1 constrain equation independently of the production implementation.
|
||||
|
/// </summary>
|
||||
|
private static int ConstrainReference(int difference, int threshold, int damping) |
||||
|
{ |
||||
|
int shift = Math.Max(0, damping - BitOperations.Log2((uint)threshold)); |
||||
|
int magnitude = Math.Abs(difference); |
||||
|
int constrained = Math.Clamp(threshold - (magnitude >> shift), 0, magnitude); |
||||
|
return difference < 0 ? -constrained : constrained; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Converts a direction and tap to a signed source offset independently of the production implementation.
|
||||
|
/// </summary>
|
||||
|
private static int GetDirectionOffsetReference(int direction, int tap, int stride) |
||||
|
{ |
||||
|
int[] x0 = [1, 1, 1, 1, 1, 0, 0, 0]; |
||||
|
int[] y0 = [-1, 0, 0, 0, 1, 1, 1, 1]; |
||||
|
int[] x1 = [2, 2, 2, 2, 2, 1, 0, -1]; |
||||
|
int[] y1 = [-2, -1, 0, 1, 2, 2, 2, 2]; |
||||
|
return tap == 0 ? (y0[direction] * stride) + x0[direction] : (y1[direction] * stride) + x1[direction]; |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue