mirror of https://github.com/SixLabors/ImageSharp
2 changed files with 194 additions and 0 deletions
@ -0,0 +1,193 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using System.Runtime.CompilerServices; |
|||
using System.Runtime.InteropServices; |
|||
using System.Runtime.Intrinsics; |
|||
using SixLabors.ImageSharp.Common.Helpers; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.LoopRestoration; |
|||
|
|||
/// <summary>
|
|||
/// Applies the normative separable Wiener filter used by AV1 loop restoration.
|
|||
/// </summary>
|
|||
internal static class Av1WienerFilter |
|||
{ |
|||
/// <summary>
|
|||
/// The number of coefficients in the padded Wiener convolution kernel.
|
|||
/// </summary>
|
|||
private const int FilterTapCount = 8; |
|||
|
|||
/// <summary>
|
|||
/// The number of independent symmetric coefficients transmitted for each filter direction.
|
|||
/// </summary>
|
|||
private const int TransmittedCoefficientCount = 3; |
|||
|
|||
/// <summary>
|
|||
/// The number of fractional bits in the Wiener filter coefficients.
|
|||
/// </summary>
|
|||
private const int FilterBits = 7; |
|||
|
|||
/// <summary>
|
|||
/// The default first-pass rounding shift.
|
|||
/// </summary>
|
|||
private const int InitialHorizontalRoundBits = 3; |
|||
|
|||
/// <summary>
|
|||
/// The number of intermediate rows required beyond the destination stripe height.
|
|||
/// </summary>
|
|||
private const int IntermediateRowExtension = FilterTapCount - 1; |
|||
|
|||
/// <summary>
|
|||
/// Gets the number of intermediate samples required to filter a stripe.
|
|||
/// </summary>
|
|||
/// <param name="width">The destination stripe width.</param>
|
|||
/// <param name="height">The destination stripe height.</param>
|
|||
/// <returns>The required scratch-span length.</returns>
|
|||
public static int GetScratchLength(int width, int height) => width * (height + IntermediateRowExtension); |
|||
|
|||
/// <summary>
|
|||
/// Filters one restoration stripe from a source rectangle containing the required three-sample borders.
|
|||
/// </summary>
|
|||
/// <param name="source">The source rectangle beginning three samples above and left of the destination stripe.</param>
|
|||
/// <param name="sourceStride">The number of samples between source rows.</param>
|
|||
/// <param name="destination">The destination span beginning at the restored stripe origin.</param>
|
|||
/// <param name="destinationStride">The number of samples between destination rows.</param>
|
|||
/// <param name="width">The stripe width in plane samples.</param>
|
|||
/// <param name="height">The stripe height in plane samples.</param>
|
|||
/// <param name="bitDepth">The encoded sample bit depth.</param>
|
|||
/// <param name="horizontalCoefficients">The three transmitted horizontal coefficients.</param>
|
|||
/// <param name="verticalCoefficients">The three transmitted vertical coefficients.</param>
|
|||
/// <param name="scratch">Intermediate sample storage sized according to <see cref="GetScratchLength"/>.</param>
|
|||
public static void FilterStripe( |
|||
ReadOnlySpan<ushort> source, |
|||
int sourceStride, |
|||
Span<ushort> destination, |
|||
int destinationStride, |
|||
int width, |
|||
int height, |
|||
int bitDepth, |
|||
ReadOnlySpan<int> horizontalCoefficients, |
|||
ReadOnlySpan<int> verticalCoefficients, |
|||
Span<ushort> scratch) |
|||
{ |
|||
Span<short> horizontalFilter = stackalloc short[FilterTapCount]; |
|||
Span<short> verticalFilter = stackalloc short[FilterTapCount]; |
|||
PopulateFilter(horizontalCoefficients, horizontalFilter); |
|||
PopulateFilter(verticalCoefficients, verticalFilter); |
|||
|
|||
int horizontalRoundBits = InitialHorizontalRoundBits; |
|||
int intermediateBitCount = bitDepth + FilterBits - horizontalRoundBits + 2; |
|||
if (intermediateBitCount > 16) |
|||
{ |
|||
// Twelve-bit input would otherwise exceed the unsigned 16-bit intermediate used by
|
|||
// the normative two-pass convolution, so AV1 transfers those excess bits to pass two.
|
|||
horizontalRoundBits += intermediateBitCount - 16; |
|||
} |
|||
|
|||
int verticalRoundBits = (FilterBits * 2) - horizontalRoundBits; |
|||
int intermediateMaximum = (1 << (bitDepth + 1 + FilterBits - horizontalRoundBits)) - 1; |
|||
int intermediateHeight = height + IntermediateRowExtension; |
|||
int horizontalBias = 1 << (bitDepth + FilterBits - 1); |
|||
for (int row = 0; row < intermediateHeight; row++) |
|||
{ |
|||
int sourceRowOffset = row * sourceStride; |
|||
int intermediateRowOffset = row * width; |
|||
for (int column = 0; column < width; column++) |
|||
{ |
|||
int sourceOffset = sourceRowOffset + column; |
|||
int sum = DotProduct(source, sourceOffset, horizontalFilter); |
|||
|
|||
// The transmitted center coefficient excludes its implicit 128 contribution.
|
|||
// Adding the unfiltered center sample here reconstructs the complete kernel.
|
|||
sum += (source[sourceOffset + TransmittedCoefficientCount] << FilterBits) + horizontalBias; |
|||
int value = RoundPowerOfTwo(sum, horizontalRoundBits); |
|||
scratch[intermediateRowOffset + column] = (ushort)Av1Math.Clip3(0, intermediateMaximum, value); |
|||
} |
|||
} |
|||
|
|||
int maximumSample = (1 << bitDepth) - 1; |
|||
int verticalBias = 1 << (bitDepth + verticalRoundBits - 1); |
|||
for (int row = 0; row < height; row++) |
|||
{ |
|||
int destinationRowOffset = row * destinationStride; |
|||
for (int column = 0; column < width; column++) |
|||
{ |
|||
int sum = 0; |
|||
for (int tap = 0; tap < FilterTapCount; tap++) |
|||
{ |
|||
sum += scratch[((row + tap) * width) + column] * verticalFilter[tap]; |
|||
} |
|||
|
|||
int center = scratch[((row + TransmittedCoefficientCount) * width) + column]; |
|||
sum += (center << FilterBits) - verticalBias; |
|||
destination[destinationRowOffset + column] = |
|||
(ushort)Av1Math.Clip3(0, maximumSample, RoundPowerOfTwo(sum, verticalRoundBits)); |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Expands three transmitted symmetric coefficients into the padded eight-tap convolution kernel.
|
|||
/// </summary>
|
|||
/// <param name="coefficients">The transmitted outer-to-inner coefficients.</param>
|
|||
/// <param name="filter">The destination eight-tap kernel.</param>
|
|||
private static void PopulateFilter(ReadOnlySpan<int> coefficients, Span<short> filter) |
|||
{ |
|||
int outer = coefficients[0]; |
|||
int middle = coefficients[1]; |
|||
int inner = coefficients[2]; |
|||
filter[0] = (short)outer; |
|||
filter[1] = (short)middle; |
|||
filter[2] = (short)inner; |
|||
filter[3] = (short)(-2 * (outer + middle + inner)); |
|||
filter[4] = (short)inner; |
|||
filter[5] = (short)middle; |
|||
filter[6] = (short)outer; |
|||
|
|||
// libaom stores a seven-tap Wiener kernel in the shared eight-tap interpolation shape.
|
|||
filter[7] = 0; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Computes one signed eight-tap horizontal filter product.
|
|||
/// </summary>
|
|||
/// <param name="source">The source rectangle containing the requested samples.</param>
|
|||
/// <param name="sourceOffset">The first source sample consumed by the filter.</param>
|
|||
/// <param name="filter">The eight signed filter coefficients.</param>
|
|||
/// <returns>The unrounded signed filter sum.</returns>
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
private static int DotProduct(ReadOnlySpan<ushort> source, int sourceOffset, ReadOnlySpan<short> filter) |
|||
{ |
|||
if (Vector128.IsHardwareAccelerated) |
|||
{ |
|||
ref ushort sourceReference = ref MemoryMarshal.GetReference(source); |
|||
ref short filterReference = ref MemoryMarshal.GetReference(filter); |
|||
Vector128<short> samples = Vector128.LoadUnsafe(ref sourceReference, (nuint)sourceOffset).AsInt16(); |
|||
Vector128<short> coefficients = Vector128.LoadUnsafe(ref filterReference); |
|||
Vector128<int> pairSums = Vector128_.MultiplyAddAdjacent(samples, coefficients); |
|||
|
|||
// The shared helper provides the architecture-specific adjacent products; reducing its
|
|||
// four 32-bit lanes scalarly avoids an additional platform-specific shuffle sequence.
|
|||
return pairSums.GetElement(0) + pairSums.GetElement(1) + pairSums.GetElement(2) + pairSums.GetElement(3); |
|||
} |
|||
|
|||
int sum = 0; |
|||
for (int tap = 0; tap < FilterTapCount; tap++) |
|||
{ |
|||
sum += source[sourceOffset + tap] * filter[tap]; |
|||
} |
|||
|
|||
return sum; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Rounds a signed fixed-point value to the requested lower precision.
|
|||
/// </summary>
|
|||
/// <param name="value">The signed fixed-point value.</param>
|
|||
/// <param name="bitCount">The number of low bits to discard.</param>
|
|||
/// <returns>The rounded signed value.</returns>
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
private static int RoundPowerOfTwo(int value, int bitCount) |
|||
=> (value + (1 << (bitCount - 1))) >> bitCount; |
|||
} |
|||
Loading…
Reference in new issue