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